Compare commits

..

46 Commits

Author SHA1 Message Date
Piotr Osiewicz
3870eeff32 Merge branch 'main' into sumtree-v10000 2025-08-21 18:34:51 +02:00
Agus Zubiaga
4bee06e507 acp: Use ResourceLink for agents that don't support embedded context (#36687)
The completion provider was already limiting the mention kinds according
to `acp::PromptCapabilities`. However, it was still using
`ContentBlock::EmbeddedResource` when
`acp::PromptCapabilities::embedded_context` was `false`. We will now use
`ResourceLink` in that case making it more complaint with the
specification.

Release Notes:

- N/A
2025-08-21 14:57:46 +00:00
Ryan Drew
f23314bef4 editor: Use editorconfig's max_line_length for hard wrap (#36426)
PR #20198, "Do not alter soft wrap based on .editorconfig contents"
removed support for setting line lengths for both soft and hard wrap,
not just soft wrap. This causes the `max_line_length` property within a
`.editorconfig` file to be ignored by Zed. This commit restores allowing
for hard wrap limits to be set using `max_line_length` without impacting
soft wrap limits. This is done by merging the `max_line_length` property
from an editorconfig file into Zed's `preferred_line_length` property.

Release Notes:

- Added support for .editorconfig's `max_line_length` property

Signed-off-by: Ryan Drew <git@ry4n.me>
2025-08-21 17:55:43 +03:00
Smit Barmase
697a39c251 Fix issue where renaming a file would not update imports in related files if they are not open (#36681)
Closes #34445

Now we open a multi-buffer consisting of buffers that have updated,
renamed file imports.

Only local is handled, for now.

Release Notes:

- Fixed an issue where renaming a file would not update imports in
related files if they are not already open.
2025-08-21 20:19:17 +05:30
Conrad Irwin
d9ea97ee9c acp: Detect gemini auth errors and show a button (#36641)
Closes #ISSUE

Release Notes:

- N/A
2025-08-21 08:44:04 -06:00
Conrad Irwin
d8fc779a67 acp: Hide history unless in native agent (#36644)
Release Notes:

- N/A
2025-08-21 08:43:57 -06:00
Bennet Bo Fenner
001ec97c0e acp: Use file icons for edit tool cards when ToolCallLocation is known (#36684)
Release Notes:

- N/A
2025-08-21 14:18:22 +00:00
Marshall Bowers
2781a30971 collab: Add Orb subscription status and period to billing_subscriptions table (#36682)
This PR adds the following new columns to the `billing_subscriptions`
table:

- `orb_subscription_status`
- `orb_current_billing_period_start_date`
- `orb_current_billing_period_end_date`

Release Notes:

- N/A
2025-08-21 13:59:18 +00:00
David Kleingeld
e0613cbd0f Add Rodio audio pipeline as alternative to current LiveKit pipeline (#36607)
Rodio parts are well tested and need less configuration then the livekit
parts. I suspect there is a bug in the livekit configuration regarding
resampling. Rather then investigate that it seemed faster & easier to
swap in Rodio.

This opens the door to using other Rodio parts like:
 - Decibel based volume control
 - Limiter (prevents sound from becoming too loud)
 - Automatic gain control

To use this add to settings:
```
  "audio": {
    "experimental.rodio_audio": true
  }
```

Release Notes:

- N/A

Co-authored-by: Mikayla <mikayla@zed.dev>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-08-21 15:56:16 +02:00
Cole Miller
1dd237139c Fix more improper uses of the buffer_id field of Anchor (#36636)
Follow-up to #36524 

Release Notes:

- N/A
2025-08-21 09:24:34 -04:00
Cole Miller
f63d8e4c53 Show excerpt dividers in without_headers multibuffers (#36647)
Release Notes:

- Fixed diff cards in agent threads not showing dividers between
disjoint edited regions.
2025-08-21 13:23:56 +00:00
Bennet Bo Fenner
ad64a71f04 acp: Allow collapsing edit file tool calls (#36675)
Release Notes:

- N/A
2025-08-21 11:05:41 +00:00
Antonio Scandurra
f435af2fde acp: Use unstaged style for diffs (#36674)
Release Notes:

- N/A
2025-08-21 10:59:51 +00:00
Julia Ryan
c5ee3f3e2e Avoid suspending panicking thread while crashing (#36645)
On the latest build @maxbrunsfeld got a panic that hung zed. It appeared
that the hang occured after the minidump had been successfully written,
so our theory on what happened is that the `suspend_all_other_threads`
call in the crash handler suspended the panicking thread (due to the
signal from simulate_exception being received on a different thread),
and then when the crash handler returned everything was suspended so the
panic hook never made it to the `process::abort`.

This change makes the crash handler avoid _both_ the current and the
panicking thread which should avoid that scenario.

Release Notes:

- N/A
2025-08-21 10:33:45 +00:00
Piotr Osiewicz
7f1bd2f15e remote: Fix toolchain RPC messages not being handled because of the entity getting dropped (#36665)
Release Notes:

- N/A
2025-08-21 09:37:45 +00:00
Bennet Bo Fenner
62f2ef86dc agent2: Allow expanding terminals individually (#36670)
Release Notes:

- N/A
2025-08-21 11:25:00 +02:00
Antonio Scandurra
fda6eda3c2 Fix @-mentioning threads when their summary isn't ready yet (#36664)
Release Notes:

- N/A
2025-08-21 08:57:28 +00:00
Kirill Bulatov
ed84767c9d Fix overlooked Clippy lints (#36659)
Follow-up of https://github.com/zed-industries/zed/pull/36557 that is
needed after https://github.com/zed-industries/zed/pull/36652

Release Notes:

- N/A
2025-08-21 06:48:04 +00:00
Kirill Bulatov
cde0a5dd27 Add a non-style lint exclusion (#36658)
Follow-up of https://github.com/zed-industries/zed/pull/36651
Restores https://github.com/zed-industries/zed/pull/35955 footgun guard.

Release Notes:

- N/A
2025-08-21 06:36:57 +00:00
Sachith Shetty
68f97d6069 editor: Use highlight_text to highlight matching brackets, fix unnecessary inlay hint highlighting (#36540)
Closes #35981

Release Notes:

- Fixed bracket highlights overly including parts of inlays when
highlighting

Before -
<img width="1480" height="602" alt="Screenshot from 2025-08-19 17-15-06"
src="https://github.com/user-attachments/assets/8e6b5ed8-f133-4867-8352-ed93441fbd8b"
/>

After -
<img width="1480" height="602" alt="Screenshot from 2025-08-19 17-24-26"
src="https://github.com/user-attachments/assets/1314e54e-ecf9-4280-9d53-eed6e96e393f"
/>
2025-08-21 09:27:41 +03:00
Kirill Bulatov
5dcb90858e Stop waiting for part of LSP responses on remote Collab clients' part (#36557)
Instead of holding a connection for potentially long LSP queries (e.g.
rust-analyzer might take minutes to look up a definition), disconnect
right after sending the initial request and handle the follow-up
responses later.

As a bonus, this allows to cancel previously sent request on the local
Collab clients' side due to this, as instead of holding and serving the
old connection, local clients now can stop previous requests, if needed.

Current PR does not convert all LSP requests to the new paradigm, but
the problematic ones, deprecating `MultiLspQuery` and moving all its
requests to the new paradigm.

Release Notes:

- Improved resource usage when querying LSP over Collab

---------

Co-authored-by: David Kleingeld <git@davidsk.dev>
Co-authored-by: Mikayla Maki <mikayla@zed.dev>
Co-authored-by: David Kleingeld <davidsk@zed.dev>
2025-08-21 09:24:34 +03:00
Conrad Irwin
c731bb6d91 Re-add redundant clone (#36652)
Although I said I'd do this, I actually didn't...

Updates #36651

Release Notes:

- N/A
2025-08-20 21:08:49 -06:00
Conrad Irwin
4b03d791b5 Remove style lints for now (#36651)
Closes #36577

Release Notes:

- N/A
2025-08-20 20:38:30 -06:00
Agus Zubiaga
9a3e4c47d0 acp: Suggest upgrading to preview instead of latest (#36648)
A previous PR changed the install command from `@latest` to `@preview`,
but the upgrade command kept suggesting `@latest`.

Release Notes:

- N/A
2025-08-21 00:52:38 +00:00
Ben Brandt
568e1d0a42 acp: Add e2e test support for NativeAgent (#36635)
Release Notes:

- N/A
2025-08-21 00:36:50 +00:00
Agus Zubiaga
6f242772cc acp: Update to 0.0.30 (#36643)
See: https://github.com/zed-industries/agent-client-protocol/pull/20

Release Notes:

- N/A
2025-08-21 00:10:36 +00:00
张小白
8ef9ecc91f windows: Fix RevealInFileManager (#36592)
Closes #36314

This PR takes inspiration from [Electron’s
implementation](dd54e84a58/shell/common/platform_util_win.cc (L268-L314)).

Before and after:



https://github.com/user-attachments/assets/53eec5d3-23c7-4ee1-8477-e524b0538f60



Release Notes:

- N/A
2025-08-21 08:08:54 +08:00
Ben Kunkle
3dd362978a docs: Add table of all actions (#36642)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-08-20 23:41:06 +00:00
Agus Zubiaga
74c0ba980b acp: Reliably suppress gemini abort error (#36640)
https://github.com/zed-industries/zed/pull/36633 relied on the prompt
request responding before cancel, but that's not guaranteed


Release Notes:

- N/A
2025-08-20 23:32:17 +00:00
Marshall Bowers
c20233e0b4 agent_ui: Fix signed-in check in Zed provider configuration (#36639)
This PR fixes the check for if the user is signed in in the Agent panel
configuration.

Supersedes https://github.com/zed-industries/zed/pull/36634.

Release Notes:

- Fixed the user's plan badge near the Zed provider in the Agent panel
not showing despite being signed in.
2025-08-20 23:09:09 +00:00
Agus Zubiaga
ffb995181e acp: Supress gemini aborted errors (#36633)
This PR adds a temporary workaround to supress "Aborted" errors from
Gemini when cancelling generation. This won't be needed once
https://github.com/google-gemini/gemini-cli/pull/6656 is generally
available.

Release Notes:

- N/A
2025-08-20 22:30:25 +00:00
Conrad Irwin
5120b6b7f9 acp: Handle Gemini Auth Better (#36631)
Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-08-20 16:12:41 -06:00
Julia Ryan
c9c708ff08 nix: Re-enable nightly builds (#36632)
Release Notes:

- N/A
2025-08-20 21:43:53 +00:00
Agus Zubiaga
9e34bb3f05 acp: Hide feedback buttons for external agents (#36630)
Release Notes:

- N/A
2025-08-20 21:35:48 +00:00
Cole Miller
595cf1c6c3 acp: Rename assistant::QuoteSelection and support it in agent2 threads (#36628)
Release Notes:

- N/A
2025-08-20 21:31:25 +00:00
Agus Zubiaga
d1820b183a acp: Suggest installing gemini@preview instead of latest (#36629)
Release Notes:

- N/A
2025-08-20 21:26:07 +00:00
Danilo Leal
fb7edbfb46 thread_view: Add recent history entries & adjust empty state (#36625)
Release Notes:

- N/A
2025-08-20 18:01:22 -03:00
Agus Zubiaga
02dabbb9fa acp thread view: Do not go into editing mode if unsupported (#36623)
Release Notes:

- N/A
2025-08-20 20:05:53 +00:00
Joseph T. Lyons
fa8bef1496 Bump Zed to v0.202 (#36622)
Release Notes:

-N/A
2025-08-20 20:05:30 +00:00
Cole Miller
739e4551da Fix typo in Excerpt::contains (#36621)
Follow-up to #36524 

Release Notes:

- N/A
2025-08-20 19:30:11 +00:00
Ben Brandt
b0bef3a9a2 agent2: Clean up tool descriptions (#36619)
schemars was passing along the newlines from the doc comments. This
should make these closer to the markdown file versions we had in the old
agent.

Release Notes:

- N/A
2025-08-20 19:17:07 +00:00
Piotr Osiewicz
74a6c78fff debugs n stuff 2025-08-11 09:50:20 +02:00
Piotr Osiewicz
2be18a200f Warnings 2025-08-08 17:11:48 +02:00
Piotr Osiewicz
4e2907e296 Fitzery with cursor 2025-08-08 16:49:42 +02:00
Piotr Osiewicz
b43153a99f Make summaries a running sum 2025-08-08 16:49:30 +02:00
Piotr Osiewicz
57ea58c83e WIP: rip out summary 2025-08-08 16:06:47 +02:00
158 changed files with 6756 additions and 9003 deletions

View File

@@ -206,9 +206,6 @@ jobs:
runs-on: github-8vcpu-ubuntu-2404
needs: tests
name: Build Zed on FreeBSD
# env:
# MYTOKEN : ${{ secrets.MYTOKEN }}
# MYTOKEN2: "value2"
steps:
- uses: actions/checkout@v4
- name: Build FreeBSD remote-server
@@ -243,7 +240,6 @@ jobs:
bundle-nix:
name: Build and cache Nix package
if: false
needs: tests
secrets: inherit
uses: ./.github/workflows/nix.yml

100
Cargo.lock generated
View File

@@ -39,26 +39,6 @@ dependencies = [
"workspace-hack",
]
[[package]]
name = "acp_tools"
version = "0.1.0"
dependencies = [
"agent-client-protocol",
"collections",
"gpui",
"language",
"markdown",
"project",
"serde",
"serde_json",
"settings",
"theme",
"ui",
"util",
"workspace",
"workspace-hack",
]
[[package]]
name = "action_log"
version = "0.1.0"
@@ -191,12 +171,11 @@ dependencies = [
[[package]]
name = "agent-client-protocol"
version = "0.0.31"
version = "0.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "289eb34ee17213dadcca47eedadd386a5e7678094095414e475965d1bcca2860"
checksum = "5f792e009ba59b137ee1db560bc37e567887ad4b5af6f32181d381fff690e2d4"
dependencies = [
"anyhow",
"async-broadcast",
"futures 0.3.31",
"log",
"parking_lot",
@@ -265,7 +244,6 @@ dependencies = [
"terminal",
"text",
"theme",
"thiserror 2.0.12",
"tree-sitter-rust",
"ui",
"unindent",
@@ -285,10 +263,10 @@ name = "agent_servers"
version = "0.1.0"
dependencies = [
"acp_thread",
"acp_tools",
"action_log",
"agent-client-protocol",
"agent_settings",
"agentic-coding-protocol",
"anyhow",
"client",
"collections",
@@ -403,7 +381,6 @@ dependencies = [
"parking_lot",
"paths",
"picker",
"postage",
"pretty_assertions",
"project",
"prompt_store",
@@ -443,6 +420,24 @@ dependencies = [
"zed_actions",
]
[[package]]
name = "agentic-coding-protocol"
version = "0.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e6ae951b36fa2f8d9dd6e1af6da2fcaba13d7c866cf6a9e65deda9dc6c5fe4"
dependencies = [
"anyhow",
"chrono",
"derive_more 2.0.1",
"futures 0.3.31",
"log",
"parking_lot",
"schemars",
"semver",
"serde",
"serde_json",
]
[[package]]
name = "ahash"
version = "0.7.8"
@@ -858,7 +853,7 @@ dependencies = [
"anyhow",
"async-trait",
"collections",
"derive_more",
"derive_more 0.99.19",
"extension",
"futures 0.3.31",
"gpui",
@@ -921,7 +916,7 @@ dependencies = [
"clock",
"collections",
"ctor",
"derive_more",
"derive_more 0.99.19",
"gpui",
"icons",
"indoc",
@@ -958,7 +953,7 @@ dependencies = [
"cloud_llm_client",
"collections",
"component",
"derive_more",
"derive_more 0.99.19",
"diffy",
"editor",
"feature_flags",
@@ -1384,10 +1379,11 @@ version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"derive_more",
"gpui",
"parking_lot",
"rodio",
"schemars",
"serde",
"settings",
"util",
"workspace-hack",
]
@@ -3070,7 +3066,7 @@ dependencies = [
"cocoa 0.26.0",
"collections",
"credentials_provider",
"derive_more",
"derive_more 0.99.19",
"feature_flags",
"fs",
"futures 0.3.31",
@@ -3502,7 +3498,7 @@ name = "command_palette_hooks"
version = "0.1.0"
dependencies = [
"collections",
"derive_more",
"derive_more 0.99.19",
"gpui",
"workspace-hack",
]
@@ -4663,6 +4659,27 @@ dependencies = [
"syn 2.0.101",
]
[[package]]
name = "derive_more"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
dependencies = [
"derive_more-impl",
]
[[package]]
name = "derive_more-impl"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"unicode-xid",
]
[[package]]
name = "derive_refineable"
version = "0.1.0"
@@ -6403,7 +6420,7 @@ dependencies = [
"askpass",
"async-trait",
"collections",
"derive_more",
"derive_more 0.99.19",
"futures 0.3.31",
"git2",
"gpui",
@@ -7433,7 +7450,7 @@ dependencies = [
"core-video",
"cosmic-text",
"ctor",
"derive_more",
"derive_more 0.99.19",
"embed-resource",
"env_logger 0.11.8",
"etagere",
@@ -7521,7 +7538,6 @@ dependencies = [
name = "gpui_tokio"
version = "0.1.0"
dependencies = [
"anyhow",
"gpui",
"tokio",
"util",
@@ -7958,7 +7974,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"bytes 1.10.1",
"derive_more",
"derive_more 0.99.19",
"futures 0.3.31",
"http 1.3.1",
"http-body 1.0.1",
@@ -9606,6 +9622,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"audio",
"collections",
"core-foundation 0.10.0",
"core-video",
@@ -9628,6 +9645,7 @@ dependencies = [
"scap",
"serde",
"serde_json",
"settings",
"sha2",
"simplelog",
"smallvec",
@@ -14353,10 +14371,12 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984"
dependencies = [
"chrono",
"dyn-clone",
"indexmap",
"ref-cast",
"schemars_derive",
"semver",
"serde",
"serde_json",
]
@@ -16425,7 +16445,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"derive_more",
"derive_more 0.99.19",
"fs",
"futures 0.3.31",
"gpui",
@@ -19940,6 +19960,7 @@ dependencies = [
"rustix 1.0.7",
"rustls 0.23.26",
"rustls-webpki 0.103.1",
"schemars",
"scopeguard",
"sea-orm",
"sea-query-binder",
@@ -20373,9 +20394,8 @@ dependencies = [
[[package]]
name = "zed"
version = "0.201.4"
version = "0.202.0"
dependencies = [
"acp_tools",
"activity_indicator",
"agent",
"agent_servers",

View File

@@ -1,7 +1,6 @@
[workspace]
resolver = "2"
members = [
"crates/acp_tools",
"crates/acp_thread",
"crates/action_log",
"crates/activity_indicator",
@@ -227,7 +226,6 @@ edition = "2024"
# Workspace member crates
#
acp_tools = { path = "crates/acp_tools" }
acp_thread = { path = "crates/acp_thread" }
action_log = { path = "crates/action_log" }
agent = { path = "crates/agent" }
@@ -424,7 +422,8 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
agent-client-protocol = "0.0.31"
agentic-coding-protocol = "0.0.10"
agent-client-protocol = "0.0.30"
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14"
@@ -803,6 +802,12 @@ unexpected_cfgs = { level = "allow" }
dbg_macro = "deny"
todo = "deny"
# This is not a style lint, see https://github.com/rust-lang/rust-clippy/pull/15454
# Remove when the lint gets promoted to `suspicious`.
declare_interior_mutable_const = "deny"
redundant_clone = "deny"
# We currently do not restrict any style rules
# as it slows down shipping code to Zed.
#

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.37288 4.48506L7.43539 10.6638C7.43539 10.9365 7.54373 11.1981 7.73655 11.3909C7.92938 11.5837 8.19092 11.6921 8.46362 11.6921C8.73632 11.6921 8.99785 11.5837 9.19068 11.3909C9.38351 11.1981 9.49184 10.9366 9.49184 10.6638L9.42933 4.48506C9.42933 3.93975 9.2127 3.41678 8.82711 3.03119C8.44152 2.6456 7.91855 2.42898 7.37324 2.42898C6.82794 2.42898 6.30496 2.6456 5.91937 3.03119C5.53378 3.41678 5.31716 3.93975 5.31716 4.48506L5.37968 10.6384C5.37636 11.0455 5.45368 11.4492 5.60718 11.8263C5.76067 12.2034 5.98731 12.5463 6.27401 12.8354C6.56071 13.1244 6.9018 13.3538 7.27761 13.5104C7.65341 13.667 8.0565 13.7476 8.46362 13.7476C8.87073 13.7476 9.27382 13.667 9.64963 13.5104C10.0254 13.3538 10.3665 13.1244 10.6532 12.8354C10.9399 12.5463 11.1666 12.2034 11.3201 11.8263C11.4736 11.4492 11.5509 11.0455 11.5476 10.6384L11.485 4.48506" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,4 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.486 6.2H7.24795C6.66895 6.2 6.19995 6.669 6.19995 7.248V12.486C6.19995 13.064 6.66895 13.533 7.24795 13.533H12.486C13.064 13.533 13.533 13.064 13.533 12.486V7.248C13.533 6.669 13.064 6.2 12.486 6.2Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.91712 10.203C3.63951 10.2022 3.37351 10.0915 3.1773 9.89511C2.98109 9.69872 2.87064 9.43261 2.87012 9.155V3.917C2.87091 3.63956 2.98147 3.37371 3.17765 3.17753C3.37383 2.98135 3.63968 2.87079 3.91712 2.87H9.15512C9.43273 2.87053 9.69883 2.98097 9.89523 3.17718C10.0916 3.37339 10.2023 3.63939 10.2031 3.917" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M12.286 6H7.048C6.469 6 6 6.469 6 7.048v5.238c0 .578.469 1.047 1.048 1.047h5.238c.578 0 1.047-.469 1.047-1.047V7.048c0-.579-.469-1.048-1.047-1.048Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M3.714 10a1.05 1.05 0 0 1-1.047-1.048V3.714a1.05 1.05 0 0 1 1.047-1.047h5.238A1.05 1.05 0 0 1 10 3.714"/></svg>

Before

Width:  |  Height:  |  Size: 802 B

After

Width:  |  Height:  |  Size: 515 B

View File

@@ -1,6 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.5 5.50621L10.5941 3.41227C10.8585 3.14798 11.217 2.99953 11.5908 2.99957C11.9646 2.99962 12.3231 3.14816 12.5874 3.41252C12.8517 3.67688 13.0001 4.03541 13.0001 4.40922C13.0001 4.78304 12.8515 5.14152 12.5872 5.40582L10.493 7.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.50789 8.5L3.92098 10.0869C3.80488 10.2027 3.71903 10.3452 3.67097 10.5019L3.01047 12.678C2.99754 12.7212 2.99657 12.7672 3.00764 12.8109C3.01872 12.8547 3.04143 12.8946 3.07337 12.9265C3.1053 12.9584 3.14528 12.981 3.18905 12.992C3.23282 13.003 3.27875 13.002 3.32197 12.989L5.49849 12.329C5.65508 12.2813 5.79758 12.196 5.91349 12.0805L7.49184 10.5019" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 5L11 7" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 3L13 13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,4 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 12.375H13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 11.125L6.75003 7.375L3 3.62497" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 336 B

View File

@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.9526 10.2625C10.0833 9.62316 9.9526 9.60566 10.9315 8.47946C11.5008 7.82461 11.9105 7.07306 11.9105 6.242C11.9105 5.22472 11.4979 4.2491 10.7637 3.52978C10.0294 2.81046 9.03338 2.40634 7.99491 2.40634C6.95644 2.40634 5.96051 2.81046 5.22619 3.52978C4.49189 4.2491 4.07935 5.22472 4.07935 6.242C4.07935 6.88128 4.20987 7.64842 5.05825 8.47946C6.03714 9.62442 5.90666 9.62316 6.03718 10.2625M9.9526 10.2625V12.3893C9.9526 13.0544 9.40223 13.5937 8.72319 13.5937H7.26665C6.58761 13.5937 6.03714 13.0544 6.03714 12.3893L6.03718 10.2625M9.9526 10.2625H7.99491H6.03718" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.95231 10.2159C10.0803 9.58974 9.95231 9.57261 10.9111 8.46959C11.4686 7.82822 11.8699 7.09214 11.8699 6.27818C11.8699 5.28184 11.4658 4.32631 10.7467 3.62179C10.0275 2.91728 9.05201 2.52148 8.03492 2.52148C7.01782 2.52148 6.04239 2.91728 5.32319 3.62179C4.604 4.32631 4.19995 5.28184 4.19995 6.27818C4.19995 6.9043 4.32779 7.65565 5.1587 8.46959C6.11744 9.59098 5.98965 9.58974 6.11748 10.2159M9.95231 10.2159V12.2989C9.95231 12.9504 9.41327 13.4786 8.7482 13.4786H7.32165C6.65658 13.4786 6.11744 12.9504 6.11744 12.2989L6.11748 10.2159M9.95231 10.2159H8.03492H6.11748" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 768 B

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 176 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="160" height="61" fill="none"><g clip-path="url(#a)"><path fill="#000" d="M130.75.385c5.428 0 10.297 2.81 13.011 7.511l14.214 24.618-.013-.005c2.599 4.504 2.707 9.932.28 14.513-2.618 4.944-7.862 8.015-13.679 8.015h-31.811c-.452 0-.873-.242-1.103-.637a1.268 1.268 0 0 1 0-1.274l3.919-6.78c.223-.394.65-.636 1.102-.636h28.288a5.622 5.622 0 0 0 4.925-2.849 5.615 5.615 0 0 0 0-5.69l-14.214-24.617a5.621 5.621 0 0 0-4.925-2.848 5.621 5.621 0 0 0-4.925 2.848l-14.214 24.618a6.267 6.267 0 0 0-.319.643.998.998 0 0 1-.069.14L101.724 54.4l-.823 1.313-2.529 4.39a1.27 1.27 0 0 1-1.103.636h-7.83c-.452 0-.873-.242-1.102-.637-.23-.394-.23-.879 0-1.274l2.188-3.791H66.803c-3.32 0-6.454-1.122-8.818-3.167a17.141 17.141 0 0 1-3.394-3.96 1.261 1.261 0 0 1-.091-.137L34.2 12.573a5.622 5.622 0 0 0-4.925-2.849 5.621 5.621 0 0 0-4.924 2.85L10.137 37.19a5.615 5.615 0 0 0 0 5.69 5.63 5.63 0 0 0 4.925 2.841h29.862a1.276 1.276 0 0 1 1.102 1.912l-3.912 6.778a1.27 1.27 0 0 1-1.102.638H14.495c-3.32 0-6.454-1.128-8.817-3.173-5.906-5.104-7.36-12.883-3.62-19.363L16.267 7.89C18.872 3.385 23.517.583 28.697.39c.184-.006.356-.006.534-.006 5.378 0 10.45 3.007 13.246 7.85l12.986 22.372L68.58 7.891C71.186 3.385 75.83.582 81.01.39c.185-.006.358-.006.536-.006 4.453 0 8.71 2.039 11.672 5.588.337.407.388.98.127 1.446l-3.765 6.6a1.268 1.268 0 0 1-2.205.006l-.847-1.465a5.623 5.623 0 0 0-4.926-2.848 5.622 5.622 0 0 0-4.924 2.848L62.464 37.18a5.614 5.614 0 0 0 0 5.689 5.628 5.628 0 0 0 4.925 2.842H95.91L117.76 7.87c2.714-4.683 7.575-7.486 12.99-7.486Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 .385h160v60.36H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -93,7 +93,7 @@
"terminal.ansi.bright_cyan": "#4c806fff",
"terminal.ansi.dim_cyan": "#cbf2e4ff",
"terminal.ansi.white": "#bfbdb6ff",
"terminal.ansi.bright_white": "#fafafaff",
"terminal.ansi.bright_white": "#bfbdb6ff",
"terminal.ansi.dim_white": "#787876ff",
"link_text.hover": "#5ac1feff",
"conflict": "#feb454ff",
@@ -479,7 +479,7 @@
"terminal.ansi.bright_cyan": "#ace0cbff",
"terminal.ansi.dim_cyan": "#2a5f4aff",
"terminal.ansi.white": "#fcfcfcff",
"terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.bright_white": "#fcfcfcff",
"terminal.ansi.dim_white": "#bcbec0ff",
"link_text.hover": "#3b9ee5ff",
"conflict": "#f1ad49ff",
@@ -865,7 +865,7 @@
"terminal.ansi.bright_cyan": "#4c806fff",
"terminal.ansi.dim_cyan": "#cbf2e4ff",
"terminal.ansi.white": "#cccac2ff",
"terminal.ansi.bright_white": "#fafafaff",
"terminal.ansi.bright_white": "#cccac2ff",
"terminal.ansi.dim_white": "#898a8aff",
"link_text.hover": "#72cffeff",
"conflict": "#fecf72ff",

View File

@@ -94,7 +94,7 @@
"terminal.ansi.bright_cyan": "#45603eff",
"terminal.ansi.dim_cyan": "#c7dfbdff",
"terminal.ansi.white": "#fbf1c7ff",
"terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.bright_white": "#fbf1c7ff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#83a598ff",
"version_control.added": "#b7bb26ff",
@@ -494,7 +494,7 @@
"terminal.ansi.bright_cyan": "#45603eff",
"terminal.ansi.dim_cyan": "#c7dfbdff",
"terminal.ansi.white": "#fbf1c7ff",
"terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.bright_white": "#fbf1c7ff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#83a598ff",
"version_control.added": "#b7bb26ff",
@@ -894,7 +894,7 @@
"terminal.ansi.bright_cyan": "#45603eff",
"terminal.ansi.dim_cyan": "#c7dfbdff",
"terminal.ansi.white": "#fbf1c7ff",
"terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.bright_white": "#fbf1c7ff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#83a598ff",
"version_control.added": "#b7bb26ff",
@@ -1294,7 +1294,7 @@
"terminal.ansi.bright_cyan": "#9fbca8ff",
"terminal.ansi.dim_cyan": "#253e2eff",
"terminal.ansi.white": "#fbf1c7ff",
"terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.bright_white": "#fbf1c7ff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#0b6678ff",
"version_control.added": "#797410ff",
@@ -1694,7 +1694,7 @@
"terminal.ansi.bright_cyan": "#9fbca8ff",
"terminal.ansi.dim_cyan": "#253e2eff",
"terminal.ansi.white": "#f9f5d7ff",
"terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.bright_white": "#f9f5d7ff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#0b6678ff",
"version_control.added": "#797410ff",
@@ -2094,7 +2094,7 @@
"terminal.ansi.bright_cyan": "#9fbca8ff",
"terminal.ansi.dim_cyan": "#253e2eff",
"terminal.ansi.white": "#f2e5bcff",
"terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.bright_white": "#f2e5bcff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#0b6678ff",
"version_control.added": "#797410ff",

View File

@@ -93,7 +93,7 @@
"terminal.ansi.bright_cyan": "#3a565bff",
"terminal.ansi.dim_cyan": "#b9d9dfff",
"terminal.ansi.white": "#dce0e5ff",
"terminal.ansi.bright_white": "#fafafaff",
"terminal.ansi.bright_white": "#dce0e5ff",
"terminal.ansi.dim_white": "#575d65ff",
"link_text.hover": "#74ade8ff",
"version_control.added": "#27a657ff",
@@ -468,7 +468,7 @@
"terminal.bright_foreground": "#242529ff",
"terminal.dim_foreground": "#fafafaff",
"terminal.ansi.black": "#242529ff",
"terminal.ansi.bright_black": "#747579ff",
"terminal.ansi.bright_black": "#242529ff",
"terminal.ansi.dim_black": "#97979aff",
"terminal.ansi.red": "#d36151ff",
"terminal.ansi.bright_red": "#f0b0a4ff",
@@ -489,7 +489,7 @@
"terminal.ansi.bright_cyan": "#a3bedaff",
"terminal.ansi.dim_cyan": "#254058ff",
"terminal.ansi.white": "#fafafaff",
"terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.bright_white": "#fafafaff",
"terminal.ansi.dim_white": "#aaaaaaff",
"link_text.hover": "#5c78e2ff",
"version_control.added": "#27a657ff",

View File

@@ -183,15 +183,16 @@ impl ToolCall {
language_registry: Arc<LanguageRegistry>,
cx: &mut App,
) -> Self {
let title = if let Some((first_line, _)) = tool_call.title.split_once("\n") {
first_line.to_owned() + ""
} else {
tool_call.title
};
Self {
id: tool_call.id,
label: cx
.new(|cx| Markdown::new(title.into(), Some(language_registry.clone()), None, cx)),
label: cx.new(|cx| {
Markdown::new(
tool_call.title.into(),
Some(language_registry.clone()),
None,
cx,
)
}),
kind: tool_call.kind,
content: tool_call
.content
@@ -232,30 +233,15 @@ impl ToolCall {
if let Some(title) = title {
self.label.update(cx, |label, cx| {
if let Some((first_line, _)) = title.split_once("\n") {
label.replace(first_line.to_owned() + "", cx)
} else {
label.replace(title, cx);
}
label.replace(title, cx);
});
}
if let Some(content) = content {
let new_content_len = content.len();
let mut content = content.into_iter();
// Reuse existing content if we can
for (old, new) in self.content.iter_mut().zip(content.by_ref()) {
old.update_from_acp(new, language_registry.clone(), cx);
}
for new in content {
self.content.push(ToolCallContent::from_acp(
new,
language_registry.clone(),
cx,
))
}
self.content.truncate(new_content_len);
self.content = content
.into_iter()
.map(|chunk| ToolCallContent::from_acp(chunk, language_registry.clone(), cx))
.collect();
}
if let Some(locations) = locations {
@@ -512,7 +498,7 @@ impl ContentBlock {
"`Image`".into()
}
pub fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str {
fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str {
match self {
ContentBlock::Empty => "",
ContentBlock::Markdown { markdown } => markdown.read(cx).source(),
@@ -565,28 +551,6 @@ impl ToolCallContent {
}
}
pub fn update_from_acp(
&mut self,
new: acp::ToolCallContent,
language_registry: Arc<LanguageRegistry>,
cx: &mut App,
) {
let needs_update = match (&self, &new) {
(Self::Diff(old_diff), acp::ToolCallContent::Diff { diff: new_diff }) => {
old_diff.read(cx).needs_update(
new_diff.old_text.as_deref().unwrap_or(""),
&new_diff.new_text,
cx,
)
}
_ => true,
};
if needs_update {
*self = Self::from_acp(new, language_registry, cx);
}
}
pub fn to_markdown(&self, cx: &App) -> String {
match self {
Self::ContentBlock(content) => content.to_markdown(cx).to_string(),
@@ -759,8 +723,6 @@ pub struct AcpThread {
connection: Rc<dyn AgentConnection>,
session_id: acp::SessionId,
token_usage: Option<TokenUsage>,
prompt_capabilities: acp::PromptCapabilities,
_observe_prompt_capabilities: Task<anyhow::Result<()>>,
}
#[derive(Debug)]
@@ -775,12 +737,11 @@ pub enum AcpThreadEvent {
Stopped,
Error,
LoadError(LoadError),
PromptCapabilitiesUpdated,
}
impl EventEmitter<AcpThreadEvent> for AcpThread {}
#[derive(PartialEq, Eq, Debug)]
#[derive(PartialEq, Eq)]
pub enum ThreadStatus {
Idle,
WaitingForToolConfirmation,
@@ -789,10 +750,15 @@ pub enum ThreadStatus {
#[derive(Debug, Clone)]
pub enum LoadError {
NotInstalled,
NotInstalled {
error_message: SharedString,
install_message: SharedString,
install_command: String,
},
Unsupported {
command: SharedString,
current_version: SharedString,
error_message: SharedString,
upgrade_message: SharedString,
upgrade_command: String,
},
Exited {
status: ExitStatus,
@@ -803,12 +769,9 @@ pub enum LoadError {
impl Display for LoadError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
LoadError::NotInstalled => write!(f, "not installed"),
LoadError::Unsupported {
command: path,
current_version,
} => {
write!(f, "version {current_version} from {path} is not supported")
LoadError::NotInstalled { error_message, .. }
| LoadError::Unsupported { error_message, .. } => {
write!(f, "{error_message}")
}
LoadError::Exited { status } => write!(f, "Server exited with status {status}"),
LoadError::Other(msg) => write!(f, "{}", msg),
@@ -825,20 +788,7 @@ impl AcpThread {
project: Entity<Project>,
action_log: Entity<ActionLog>,
session_id: acp::SessionId,
mut prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
cx: &mut Context<Self>,
) -> Self {
let prompt_capabilities = *prompt_capabilities_rx.borrow();
let task = cx.spawn::<_, anyhow::Result<()>>(async move |this, cx| {
loop {
let caps = prompt_capabilities_rx.recv().await?;
this.update(cx, |this, cx| {
this.prompt_capabilities = caps;
cx.emit(AcpThreadEvent::PromptCapabilitiesUpdated);
})?;
}
});
Self {
action_log,
shared_buffers: Default::default(),
@@ -850,15 +800,9 @@ impl AcpThread {
connection,
session_id,
token_usage: None,
prompt_capabilities,
_observe_prompt_capabilities: task,
}
}
pub fn prompt_capabilities(&self) -> acp::PromptCapabilities {
self.prompt_capabilities
}
pub fn connection(&self) -> &Rc<dyn AgentConnection> {
&self.connection
}
@@ -1043,19 +987,10 @@ impl AcpThread {
cx.emit(AcpThreadEvent::NewEntry);
}
pub fn can_set_title(&mut self, cx: &mut Context<Self>) -> bool {
self.connection.set_title(&self.session_id, cx).is_some()
}
pub fn set_title(&mut self, title: SharedString, cx: &mut Context<Self>) -> Task<Result<()>> {
if title != self.title {
self.title = title.clone();
cx.emit(AcpThreadEvent::TitleUpdated);
if let Some(set_title) = self.connection.set_title(&self.session_id, cx) {
return set_title.run(title, cx);
}
}
Task::ready(Ok(()))
pub fn update_title(&mut self, title: SharedString, cx: &mut Context<Self>) -> Result<()> {
self.title = title;
cx.emit(AcpThreadEvent::TitleUpdated);
Ok(())
}
pub fn update_token_usage(&mut self, usage: Option<TokenUsage>, cx: &mut Context<Self>) {
@@ -1358,7 +1293,11 @@ impl AcpThread {
};
let git_store = self.project.read(cx).git_store().clone();
let message_id = if self.connection.truncate(&self.session_id, cx).is_some() {
let message_id = if self
.connection
.session_editor(&self.session_id, cx)
.is_some()
{
Some(UserMessageId::new())
} else {
None
@@ -1396,10 +1335,6 @@ impl AcpThread {
})
}
pub fn can_resume(&self, cx: &App) -> bool {
self.connection.resume(&self.session_id, cx).is_some()
}
pub fn resume(&mut self, cx: &mut Context<Self>) -> BoxFuture<'static, Result<()>> {
self.run_turn(cx, async move |this, cx| {
this.update(cx, |this, cx| {
@@ -1508,7 +1443,7 @@ impl AcpThread {
/// Rewinds this thread to before the entry at `index`, removing it and all
/// subsequent entries while reverting any changes made from that point.
pub fn rewind(&mut self, id: UserMessageId, cx: &mut Context<Self>) -> Task<Result<()>> {
let Some(truncate) = self.connection.truncate(&self.session_id, cx) else {
let Some(session_editor) = self.connection.session_editor(&self.session_id, cx) else {
return Task::ready(Err(anyhow!("not supported")));
};
let Some(message) = self.user_message(&id) else {
@@ -1528,7 +1463,8 @@ impl AcpThread {
.await?;
}
cx.update(|cx| truncate.run(id.clone(), cx))?.await?;
cx.update(|cx| session_editor.truncate(id.clone(), cx))?
.await?;
this.update(cx, |this, cx| {
if let Some((ix, _)) = this.user_message_mut(&id) {
let range = ix..this.entries.len();
@@ -2622,19 +2558,13 @@ mod tests {
.into(),
);
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let thread = cx.new(|cx| {
let thread = cx.new(|_cx| {
AcpThread::new(
"Test",
self.clone(),
project,
action_log,
session_id.clone(),
watch::Receiver::constant(acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
}),
cx,
)
});
self.sessions.lock().insert(session_id, thread.downgrade());
@@ -2668,6 +2598,14 @@ mod tests {
}
}
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
}
}
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
let sessions = self.sessions.lock();
let thread = sessions.get(session_id).unwrap().clone();
@@ -2681,11 +2619,11 @@ mod tests {
.detach();
}
fn truncate(
fn session_editor(
&self,
session_id: &acp::SessionId,
_cx: &App,
) -> Option<Rc<dyn AgentSessionTruncate>> {
_cx: &mut App,
) -> Option<Rc<dyn AgentSessionEditor>> {
Some(Rc::new(FakeAgentSessionEditor {
_session_id: session_id.clone(),
}))
@@ -2700,8 +2638,8 @@ mod tests {
_session_id: acp::SessionId,
}
impl AgentSessionTruncate for FakeAgentSessionEditor {
fn run(&self, _message_id: UserMessageId, _cx: &mut App) -> Task<Result<()>> {
impl AgentSessionEditor for FakeAgentSessionEditor {
fn truncate(&self, _message_id: UserMessageId, _cx: &mut App) -> Task<Result<()>> {
Task::ready(Ok(()))
}
}

View File

@@ -38,29 +38,23 @@ pub trait AgentConnection {
cx: &mut App,
) -> Task<Result<acp::PromptResponse>>;
fn prompt_capabilities(&self) -> acp::PromptCapabilities;
fn resume(
&self,
_session_id: &acp::SessionId,
_cx: &App,
_cx: &mut App,
) -> Option<Rc<dyn AgentSessionResume>> {
None
}
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App);
fn truncate(
fn session_editor(
&self,
_session_id: &acp::SessionId,
_cx: &App,
) -> Option<Rc<dyn AgentSessionTruncate>> {
None
}
fn set_title(
&self,
_session_id: &acp::SessionId,
_cx: &App,
) -> Option<Rc<dyn AgentSessionSetTitle>> {
_cx: &mut App,
) -> Option<Rc<dyn AgentSessionEditor>> {
None
}
@@ -85,18 +79,14 @@ impl dyn AgentConnection {
}
}
pub trait AgentSessionTruncate {
fn run(&self, message_id: UserMessageId, cx: &mut App) -> Task<Result<()>>;
pub trait AgentSessionEditor {
fn truncate(&self, message_id: UserMessageId, cx: &mut App) -> Task<Result<()>>;
}
pub trait AgentSessionResume {
fn run(&self, cx: &mut App) -> Task<Result<acp::PromptResponse>>;
}
pub trait AgentSessionSetTitle {
fn run(&self, title: SharedString, cx: &mut App) -> Task<Result<()>>;
}
pub trait AgentTelemetry {
/// The name of the agent used for telemetry.
fn agent_name(&self) -> String;
@@ -327,19 +317,13 @@ mod test_support {
) -> Task<gpui::Result<Entity<AcpThread>>> {
let session_id = acp::SessionId(self.sessions.lock().len().to_string().into());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let thread = cx.new(|cx| {
let thread = cx.new(|_cx| {
AcpThread::new(
"Test",
self.clone(),
project,
action_log,
session_id.clone(),
watch::Receiver::constant(acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
}),
cx,
)
});
self.sessions.lock().insert(
@@ -352,6 +336,14 @@ mod test_support {
Task::ready(Ok(thread))
}
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
}
}
fn authenticate(
&self,
_method_id: acp::AuthMethodId,
@@ -432,11 +424,11 @@ mod test_support {
}
}
fn truncate(
fn session_editor(
&self,
_session_id: &agent_client_protocol::SessionId,
_cx: &App,
) -> Option<Rc<dyn AgentSessionTruncate>> {
_cx: &mut App,
) -> Option<Rc<dyn AgentSessionEditor>> {
Some(Rc::new(StubAgentSessionEditor))
}
@@ -447,8 +439,8 @@ mod test_support {
struct StubAgentSessionEditor;
impl AgentSessionTruncate for StubAgentSessionEditor {
fn run(&self, _: UserMessageId, _: &mut App) -> Task<Result<()>> {
impl AgentSessionEditor for StubAgentSessionEditor {
fn truncate(&self, _: UserMessageId, _: &mut App) -> Task<Result<()>> {
Task::ready(Ok(()))
}
}

View File

@@ -28,12 +28,10 @@ impl Diff {
cx: &mut Context<Self>,
) -> Self {
let multibuffer = cx.new(|_cx| MultiBuffer::without_headers(Capability::ReadOnly));
let new_buffer = cx.new(|cx| Buffer::local(new_text, cx));
let base_text = old_text.clone().unwrap_or(String::new()).into();
let buffer = cx.new(|cx| Buffer::local(new_text, cx));
let task = cx.spawn({
let multibuffer = multibuffer.clone();
let path = path.clone();
let buffer = new_buffer.clone();
async move |_, cx| {
let language = language_registry
.language_for_file_path(&path)
@@ -78,26 +76,32 @@ impl Diff {
Self::Finalized(FinalizedDiff {
multibuffer,
path,
base_text,
new_buffer,
_update_diff: task,
})
}
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_snapshot = buffer.read(cx).snapshot();
let base_text = buffer_snapshot.text();
let language_registry = buffer.read(cx).language_registry();
let text_snapshot = buffer.read(cx).text_snapshot();
let buffer_diff = cx.new(|cx| {
let mut diff = BufferDiff::new_unchanged(&buffer_text_snapshot, base_text_snapshot);
let mut diff = BufferDiff::new(&text_snapshot, cx);
let _ = diff.set_base_text(
buffer_snapshot.clone(),
language_registry,
text_snapshot,
cx,
);
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);
let mut diff = BufferDiff::new(&buffer_snapshot, cx);
diff.set_snapshot(snapshot, &buffer_snapshot, cx);
diff
});
diff.set_secondary_diff(secondary_diff);
diff
});
@@ -115,7 +119,7 @@ impl Diff {
diff.update(cx);
}
}),
new_buffer: buffer,
buffer,
diff: buffer_diff,
revealed_ranges: Vec::new(),
update_diff: Task::ready(Ok(())),
@@ -150,9 +154,9 @@ impl Diff {
.map(|buffer| buffer.read(cx).text())
.join("\n");
let path = match self {
Diff::Pending(PendingDiff {
new_buffer: buffer, ..
}) => buffer.read(cx).file().map(|file| file.path().as_ref()),
Diff::Pending(PendingDiff { buffer, .. }) => {
buffer.read(cx).file().map(|file| file.path().as_ref())
}
Diff::Finalized(FinalizedDiff { path, .. }) => Some(path.as_path()),
};
format!(
@@ -165,33 +169,12 @@ impl Diff {
pub fn has_revealed_range(&self, cx: &App) -> bool {
self.multibuffer().read(cx).excerpt_paths().next().is_some()
}
pub fn needs_update(&self, old_text: &str, new_text: &str, cx: &App) -> bool {
match self {
Diff::Pending(PendingDiff {
base_text,
new_buffer,
..
}) => {
base_text.as_str() != old_text
|| !new_buffer.read(cx).as_rope().chunks().equals_str(new_text)
}
Diff::Finalized(FinalizedDiff {
base_text,
new_buffer,
..
}) => {
base_text.as_str() != old_text
|| !new_buffer.read(cx).as_rope().chunks().equals_str(new_text)
}
}
}
}
pub struct PendingDiff {
multibuffer: Entity<MultiBuffer>,
base_text: Arc<String>,
new_buffer: Entity<Buffer>,
buffer: Entity<Buffer>,
diff: Entity<BufferDiff>,
revealed_ranges: Vec<Range<Anchor>>,
_subscription: Subscription,
@@ -200,7 +183,7 @@ pub struct PendingDiff {
impl PendingDiff {
pub fn update(&mut self, cx: &mut Context<Diff>) {
let buffer = self.new_buffer.clone();
let buffer = self.buffer.clone();
let buffer_diff = self.diff.clone();
let base_text = self.base_text.clone();
self.update_diff = cx.spawn(async move |diff, cx| {
@@ -238,10 +221,10 @@ impl PendingDiff {
fn finalize(&self, cx: &mut Context<Diff>) -> FinalizedDiff {
let ranges = self.excerpt_ranges(cx);
let base_text = self.base_text.clone();
let language_registry = self.new_buffer.read(cx).language_registry();
let language_registry = self.buffer.read(cx).language_registry();
let path = self
.new_buffer
.buffer
.read(cx)
.file()
.map(|file| file.path().as_ref())
@@ -250,12 +233,12 @@ impl PendingDiff {
// Replace the buffer in the multibuffer with the snapshot
let buffer = cx.new(|cx| {
let language = self.new_buffer.read(cx).language().cloned();
let language = self.buffer.read(cx).language().cloned();
let buffer = TextBuffer::new_normalized(
0,
cx.entity_id().as_non_zero_u64().into(),
self.new_buffer.read(cx).line_ending(),
self.new_buffer.read(cx).as_rope().clone(),
self.buffer.read(cx).line_ending(),
self.buffer.read(cx).as_rope().clone(),
);
let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite);
buffer.set_language(language, cx);
@@ -291,9 +274,7 @@ impl PendingDiff {
FinalizedDiff {
path,
base_text: self.base_text.clone(),
multibuffer: self.multibuffer.clone(),
new_buffer: self.new_buffer.clone(),
_update_diff: update_diff,
}
}
@@ -302,8 +283,8 @@ impl PendingDiff {
let ranges = self.excerpt_ranges(cx);
self.multibuffer.update(cx, |multibuffer, cx| {
multibuffer.set_excerpts_for_path(
PathKey::for_buffer(&self.new_buffer, cx),
self.new_buffer.clone(),
PathKey::for_buffer(&self.buffer, cx),
self.buffer.clone(),
ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
cx,
@@ -315,7 +296,7 @@ impl PendingDiff {
}
fn excerpt_ranges(&self, cx: &App) -> Vec<Range<Point>> {
let buffer = self.new_buffer.read(cx);
let buffer = self.buffer.read(cx);
let diff = self.diff.read(cx);
let mut ranges = diff
.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx)
@@ -349,8 +330,6 @@ impl PendingDiff {
pub struct FinalizedDiff {
path: PathBuf,
base_text: Arc<String>,
new_buffer: Entity<Buffer>,
multibuffer: Entity<MultiBuffer>,
_update_diff: Task<Result<()>>,
}
@@ -404,21 +383,3 @@ async fn build_buffer_diff(
diff
})
}
#[cfg(test)]
mod tests {
use gpui::{AppContext as _, TestAppContext};
use language::Buffer;
use crate::Diff;
#[gpui::test]
async fn test_pending_diff(cx: &mut TestAppContext) {
let buffer = cx.new(|cx| Buffer::local("hello!", cx));
let _diff = cx.new(|cx| Diff::new(buffer.clone(), cx));
buffer.update(cx, |buffer, cx| {
buffer.set_text("HELLO!", cx);
});
cx.run_until_parked();
}
}

View File

@@ -5,7 +5,7 @@ use prompt_store::{PromptId, UserPromptId};
use serde::{Deserialize, Serialize};
use std::{
fmt,
ops::RangeInclusive,
ops::Range,
path::{Path, PathBuf},
str::FromStr,
};
@@ -17,14 +17,13 @@ pub enum MentionUri {
File {
abs_path: PathBuf,
},
PastedImage,
Directory {
abs_path: PathBuf,
},
Symbol {
abs_path: PathBuf,
path: PathBuf,
name: String,
line_range: RangeInclusive<u32>,
line_range: Range<u32>,
},
Thread {
id: acp::SessionId,
@@ -39,9 +38,8 @@ pub enum MentionUri {
name: String,
},
Selection {
#[serde(default, skip_serializing_if = "Option::is_none")]
abs_path: Option<PathBuf>,
line_range: RangeInclusive<u32>,
path: PathBuf,
line_range: Range<u32>,
},
Fetch {
url: Url,
@@ -50,44 +48,36 @@ pub enum MentionUri {
impl MentionUri {
pub fn parse(input: &str) -> Result<Self> {
fn parse_line_range(fragment: &str) -> Result<RangeInclusive<u32>> {
let range = fragment
.strip_prefix("L")
.context("Line range must start with \"L\"")?;
let (start, end) = range
.split_once(":")
.context("Line range must use colon as separator")?;
let range = start
.parse::<u32>()
.context("Parsing line range start")?
.checked_sub(1)
.context("Line numbers should be 1-based")?
..=end
.parse::<u32>()
.context("Parsing line range end")?
.checked_sub(1)
.context("Line numbers should be 1-based")?;
Ok(range)
}
let url = url::Url::parse(input)?;
let path = url.path();
match url.scheme() {
"file" => {
let path = url.to_file_path().ok().context("Extracting file path")?;
if let Some(fragment) = url.fragment() {
let line_range = parse_line_range(fragment)?;
let range = fragment
.strip_prefix("L")
.context("Line range must start with \"L\"")?;
let (start, end) = range
.split_once(":")
.context("Line range must use colon as separator")?;
let line_range = start
.parse::<u32>()
.context("Parsing line range start")?
.checked_sub(1)
.context("Line numbers should be 1-based")?
..end
.parse::<u32>()
.context("Parsing line range end")?
.checked_sub(1)
.context("Line numbers should be 1-based")?;
if let Some(name) = single_query_param(&url, "symbol")? {
Ok(Self::Symbol {
name,
abs_path: path,
path,
line_range,
})
} else {
Ok(Self::Selection {
abs_path: Some(path),
line_range,
})
Ok(Self::Selection { path, line_range })
}
} else if input.ends_with("/") {
Ok(Self::Directory { abs_path: path })
@@ -115,17 +105,6 @@ impl MentionUri {
id: rule_id.into(),
name,
})
} else if path.starts_with("/agent/pasted-image") {
Ok(Self::PastedImage)
} else if path.starts_with("/agent/untitled-buffer") {
let fragment = url
.fragment()
.context("Missing fragment for untitled buffer selection")?;
let line_range = parse_line_range(fragment)?;
Ok(Self::Selection {
abs_path: None,
line_range,
})
} else {
bail!("invalid zed url: {:?}", input);
}
@@ -142,16 +121,13 @@ impl MentionUri {
.unwrap_or_default()
.to_string_lossy()
.into_owned(),
MentionUri::PastedImage => "Image".to_string(),
MentionUri::Symbol { name, .. } => name.clone(),
MentionUri::Thread { name, .. } => name.clone(),
MentionUri::TextThread { name, .. } => name.clone(),
MentionUri::Rule { name, .. } => name.clone(),
MentionUri::Selection {
abs_path: path,
line_range,
..
} => selection_name(path.as_deref(), line_range),
path, line_range, ..
} => selection_name(path, line_range),
MentionUri::Fetch { url } => url.to_string(),
}
}
@@ -161,7 +137,6 @@ impl MentionUri {
MentionUri::File { abs_path } => {
FileIcons::get_icon(abs_path, cx).unwrap_or_else(|| IconName::File.path().into())
}
MentionUri::PastedImage => IconName::Image.path().into(),
MentionUri::Directory { .. } => FileIcons::get_folder_icon(false, cx)
.unwrap_or_else(|| IconName::Folder.path().into()),
MentionUri::Symbol { .. } => IconName::Code.path().into(),
@@ -182,40 +157,29 @@ impl MentionUri {
MentionUri::File { abs_path } => {
Url::from_file_path(abs_path).expect("mention path should be absolute")
}
MentionUri::PastedImage => Url::parse("zed:///agent/pasted-image").unwrap(),
MentionUri::Directory { abs_path } => {
Url::from_directory_path(abs_path).expect("mention path should be absolute")
}
MentionUri::Symbol {
abs_path,
path,
name,
line_range,
} => {
let mut url =
Url::from_file_path(abs_path).expect("mention path should be absolute");
let mut url = Url::from_file_path(path).expect("mention path should be absolute");
url.query_pairs_mut().append_pair("symbol", name);
url.set_fragment(Some(&format!(
"L{}:{}",
line_range.start() + 1,
line_range.end() + 1
line_range.start + 1,
line_range.end + 1
)));
url
}
MentionUri::Selection {
abs_path: path,
line_range,
} => {
let mut url = if let Some(path) = path {
Url::from_file_path(path).expect("mention path should be absolute")
} else {
let mut url = Url::parse("zed:///").unwrap();
url.set_path("/agent/untitled-buffer");
url
};
MentionUri::Selection { path, line_range } => {
let mut url = Url::from_file_path(path).expect("mention path should be absolute");
url.set_fragment(Some(&format!(
"L{}:{}",
line_range.start() + 1,
line_range.end() + 1
line_range.start + 1,
line_range.end + 1
)));
url
}
@@ -227,10 +191,7 @@ impl MentionUri {
}
MentionUri::TextThread { path, name } => {
let mut url = Url::parse("zed:///").unwrap();
url.set_path(&format!(
"/agent/text-thread/{}",
path.to_string_lossy().trim_start_matches('/')
));
url.set_path(&format!("/agent/text-thread/{}", path.to_string_lossy()));
url.query_pairs_mut().append_pair("name", name);
url
}
@@ -276,14 +237,12 @@ fn single_query_param(url: &Url, name: &'static str) -> Result<Option<String>> {
}
}
pub fn selection_name(path: Option<&Path>, line_range: &RangeInclusive<u32>) -> String {
pub fn selection_name(path: &Path, line_range: &Range<u32>) -> String {
format!(
"{} ({}:{})",
path.and_then(|path| path.file_name())
.unwrap_or("Untitled".as_ref())
.display(),
*line_range.start() + 1,
*line_range.end() + 1
path.file_name().unwrap_or_default().display(),
line_range.start + 1,
line_range.end + 1
)
}
@@ -343,14 +302,14 @@ mod tests {
let parsed = MentionUri::parse(symbol_uri).unwrap();
match &parsed {
MentionUri::Symbol {
abs_path: path,
path,
name,
line_range,
} => {
assert_eq!(path.to_str().unwrap(), path!("/path/to/file.rs"));
assert_eq!(name, "MySymbol");
assert_eq!(line_range.start(), &9);
assert_eq!(line_range.end(), &19);
assert_eq!(line_range.start, 9);
assert_eq!(line_range.end, 19);
}
_ => panic!("Expected Symbol variant"),
}
@@ -362,39 +321,16 @@ mod tests {
let selection_uri = uri!("file:///path/to/file.rs#L5:15");
let parsed = MentionUri::parse(selection_uri).unwrap();
match &parsed {
MentionUri::Selection {
abs_path: path,
line_range,
} => {
assert_eq!(
path.as_ref().unwrap().to_str().unwrap(),
path!("/path/to/file.rs")
);
assert_eq!(line_range.start(), &4);
assert_eq!(line_range.end(), &14);
MentionUri::Selection { path, line_range } => {
assert_eq!(path.to_str().unwrap(), path!("/path/to/file.rs"));
assert_eq!(line_range.start, 4);
assert_eq!(line_range.end, 14);
}
_ => panic!("Expected Selection variant"),
}
assert_eq!(parsed.to_uri().to_string(), selection_uri);
}
#[test]
fn test_parse_untitled_selection_uri() {
let selection_uri = uri!("zed:///agent/untitled-buffer#L1:10");
let parsed = MentionUri::parse(selection_uri).unwrap();
match &parsed {
MentionUri::Selection {
abs_path: None,
line_range,
} => {
assert_eq!(line_range.start(), &0);
assert_eq!(line_range.end(), &9);
}
_ => panic!("Expected Selection variant without path"),
}
assert_eq!(parsed.to_uri().to_string(), selection_uri);
}
#[test]
fn test_parse_thread_uri() {
let thread_uri = "zed:///agent/thread/session123?name=Thread+name";

View File

@@ -1,30 +0,0 @@
[package]
name = "acp_tools"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/acp_tools.rs"
doctest = false
[dependencies]
agent-client-protocol.workspace = true
collections.workspace = true
gpui.workspace = true
language.workspace= true
markdown.workspace = true
project.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
workspace-hack.workspace = true
workspace.workspace = true

View File

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

View File

@@ -1,494 +0,0 @@
use std::{
cell::RefCell,
collections::HashSet,
fmt::Display,
rc::{Rc, Weak},
sync::Arc,
};
use agent_client_protocol as acp;
use collections::HashMap;
use gpui::{
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::prelude::*;
use util::ResultExt as _;
use workspace::{Item, Workspace};
actions!(dev, [OpenAcpLogs]);
pub fn init(cx: &mut App) {
cx.observe_new(
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
workspace.register_action(|workspace, _: &OpenAcpLogs, window, cx| {
let acp_tools =
Box::new(cx.new(|cx| AcpTools::new(workspace.project().clone(), cx)));
workspace.add_item_to_active_pane(acp_tools, None, true, window, cx);
});
},
)
.detach();
}
struct GlobalAcpConnectionRegistry(Entity<AcpConnectionRegistry>);
impl Global for GlobalAcpConnectionRegistry {}
#[derive(Default)]
pub struct AcpConnectionRegistry {
active_connection: RefCell<Option<ActiveConnection>>,
}
struct ActiveConnection {
server_name: SharedString,
connection: Weak<acp::ClientSideConnection>,
}
impl AcpConnectionRegistry {
pub fn default_global(cx: &mut App) -> Entity<Self> {
if cx.has_global::<GlobalAcpConnectionRegistry>() {
cx.global::<GlobalAcpConnectionRegistry>().0.clone()
} else {
let registry = cx.new(|_cx| AcpConnectionRegistry::default());
cx.set_global(GlobalAcpConnectionRegistry(registry.clone()));
registry
}
}
pub fn set_active_connection(
&self,
server_name: impl Into<SharedString>,
connection: &Rc<acp::ClientSideConnection>,
cx: &mut Context<Self>,
) {
self.active_connection.replace(Some(ActiveConnection {
server_name: server_name.into(),
connection: Rc::downgrade(connection),
}));
cx.notify();
}
}
struct AcpTools {
project: Entity<Project>,
focus_handle: FocusHandle,
expanded: HashSet<usize>,
watched_connection: Option<WatchedConnection>,
connection_registry: Entity<AcpConnectionRegistry>,
_subscription: Subscription,
}
struct WatchedConnection {
server_name: SharedString,
messages: Vec<WatchedConnectionMessage>,
list_state: ListState,
connection: Weak<acp::ClientSideConnection>,
incoming_request_methods: HashMap<i32, Arc<str>>,
outgoing_request_methods: HashMap<i32, Arc<str>>,
_task: Task<()>,
}
impl AcpTools {
fn new(project: Entity<Project>, cx: &mut Context<Self>) -> Self {
let connection_registry = AcpConnectionRegistry::default_global(cx);
let subscription = cx.observe(&connection_registry, |this, _, cx| {
this.update_connection(cx);
cx.notify();
});
let mut this = Self {
project,
focus_handle: cx.focus_handle(),
expanded: HashSet::default(),
watched_connection: None,
connection_registry,
_subscription: subscription,
};
this.update_connection(cx);
this
}
fn update_connection(&mut self, cx: &mut Context<Self>) {
let active_connection = self.connection_registry.read(cx).active_connection.borrow();
let Some(active_connection) = active_connection.as_ref() else {
return;
};
if let Some(watched_connection) = self.watched_connection.as_ref() {
if Weak::ptr_eq(
&watched_connection.connection,
&active_connection.connection,
) {
return;
}
}
if let Some(connection) = active_connection.connection.upgrade() {
let mut receiver = connection.subscribe();
let task = cx.spawn(async move |this, cx| {
while let Ok(message) = receiver.recv().await {
this.update(cx, |this, cx| {
this.push_stream_message(message, cx);
})
.ok();
}
});
self.watched_connection = Some(WatchedConnection {
server_name: active_connection.server_name.clone(),
messages: vec![],
list_state: ListState::new(0, ListAlignment::Bottom, px(2048.)),
connection: active_connection.connection.clone(),
incoming_request_methods: HashMap::default(),
outgoing_request_methods: HashMap::default(),
_task: task,
});
}
}
fn push_stream_message(&mut self, stream_message: acp::StreamMessage, cx: &mut Context<Self>) {
let Some(connection) = self.watched_connection.as_mut() else {
return;
};
let language_registry = self.project.read(cx).languages().clone();
let index = connection.messages.len();
let (request_id, method, message_type, params) = match stream_message.message {
acp::StreamMessageContent::Request { id, method, params } => {
let method_map = match stream_message.direction {
acp::StreamMessageDirection::Incoming => {
&mut connection.incoming_request_methods
}
acp::StreamMessageDirection::Outgoing => {
&mut connection.outgoing_request_methods
}
};
method_map.insert(id, method.clone());
(Some(id), method.into(), MessageType::Request, Ok(params))
}
acp::StreamMessageContent::Response { id, result } => {
let method_map = match stream_message.direction {
acp::StreamMessageDirection::Incoming => {
&mut connection.outgoing_request_methods
}
acp::StreamMessageDirection::Outgoing => {
&mut connection.incoming_request_methods
}
};
if let Some(method) = method_map.remove(&id) {
(Some(id), method.into(), MessageType::Response, result)
} else {
(
Some(id),
"[unrecognized response]".into(),
MessageType::Response,
result,
)
}
}
acp::StreamMessageContent::Notification { method, params } => {
(None, method.into(), MessageType::Notification, Ok(params))
}
};
let message = WatchedConnectionMessage {
name: method,
message_type,
request_id,
direction: stream_message.direction,
collapsed_params_md: match params.as_ref() {
Ok(params) => params
.as_ref()
.map(|params| collapsed_params_md(params, &language_registry, cx)),
Err(err) => {
if let Ok(err) = &serde_json::to_value(err) {
Some(collapsed_params_md(&err, &language_registry, cx))
} else {
None
}
}
},
expanded_params_md: None,
params,
};
connection.messages.push(message);
connection.list_state.splice(index..index, 1);
cx.notify();
}
fn render_message(
&mut self,
index: usize,
window: &mut Window,
cx: &mut Context<Self>,
) -> AnyElement {
let Some(connection) = self.watched_connection.as_ref() else {
return Empty.into_any();
};
let Some(message) = connection.messages.get(index) else {
return Empty.into_any();
};
let base_size = TextSize::Editor.rems(cx);
let theme_settings = ThemeSettings::get_global(cx);
let text_style = window.text_style();
let colors = cx.theme().colors();
let expanded = self.expanded.contains(&index);
v_flex()
.w_full()
.px_4()
.py_3()
.border_color(colors.border)
.border_b_1()
.gap_2()
.items_start()
.font_buffer(cx)
.text_size(base_size)
.id(index)
.group("message")
.hover(|this| this.bg(colors.element_background.opacity(0.5)))
.on_click(cx.listener(move |this, _, _, cx| {
if this.expanded.contains(&index) {
this.expanded.remove(&index);
} else {
this.expanded.insert(index);
let Some(connection) = &mut this.watched_connection else {
return;
};
let Some(message) = connection.messages.get_mut(index) else {
return;
};
message.expanded(this.project.read(cx).languages().clone(), cx);
connection.list_state.scroll_to_reveal_item(index);
}
cx.notify()
}))
.child(
h_flex()
.w_full()
.gap_2()
.items_center()
.flex_shrink_0()
.child(match message.direction {
acp::StreamMessageDirection::Incoming => {
ui::Icon::new(ui::IconName::ArrowDown).color(Color::Error)
}
acp::StreamMessageDirection::Outgoing => {
ui::Icon::new(ui::IconName::ArrowUp).color(Color::Success)
}
})
.child(
Label::new(message.name.clone())
.buffer_font(cx)
.color(Color::Muted),
)
.child(div().flex_1())
.child(
div()
.child(ui::Chip::new(message.message_type.to_string()))
.visible_on_hover("message"),
)
.children(
message
.request_id
.map(|req_id| div().child(ui::Chip::new(req_id.to_string()))),
),
)
// I'm aware using markdown is a hack. Trying to get something working for the demo.
// Will clean up soon!
.when_some(
if expanded {
message.expanded_params_md.clone()
} else {
message.collapsed_params_md.clone()
},
|this, params| {
this.child(
div().pl_6().w_full().child(
MarkdownElement::new(
params,
MarkdownStyle {
base_text_style: text_style,
selection_background_color: colors.element_selection_background,
syntax: cx.theme().syntax().clone(),
code_block_overflow_x_scroll: true,
code_block: StyleRefinement {
text: Some(TextStyleRefinement {
font_family: Some(
theme_settings.buffer_font.family.clone(),
),
font_size: Some((base_size * 0.8).into()),
..Default::default()
}),
..Default::default()
},
..Default::default()
},
)
.code_block_renderer(
CodeBlockRenderer::Default {
copy_button: false,
copy_button_on_hover: expanded,
border: false,
},
),
),
)
},
)
.into_any()
}
}
struct WatchedConnectionMessage {
name: SharedString,
request_id: Option<i32>,
direction: acp::StreamMessageDirection,
message_type: MessageType,
params: Result<Option<serde_json::Value>, acp::Error>,
collapsed_params_md: Option<Entity<Markdown>>,
expanded_params_md: Option<Entity<Markdown>>,
}
impl WatchedConnectionMessage {
fn expanded(&mut self, language_registry: Arc<LanguageRegistry>, cx: &mut App) {
let params_md = match &self.params {
Ok(Some(params)) => Some(expanded_params_md(params, &language_registry, cx)),
Err(err) => {
if let Some(err) = &serde_json::to_value(err).log_err() {
Some(expanded_params_md(&err, &language_registry, cx))
} else {
None
}
}
_ => None,
};
self.expanded_params_md = params_md;
}
}
fn collapsed_params_md(
params: &serde_json::Value,
language_registry: &Arc<LanguageRegistry>,
cx: &mut App,
) -> Entity<Markdown> {
let params_json = serde_json::to_string(params).unwrap_or_default();
let mut spaced_out_json = String::with_capacity(params_json.len() + params_json.len() / 4);
for ch in params_json.chars() {
match ch {
'{' => spaced_out_json.push_str("{ "),
'}' => spaced_out_json.push_str(" }"),
':' => spaced_out_json.push_str(": "),
',' => spaced_out_json.push_str(", "),
c => spaced_out_json.push(c),
}
}
let params_md = format!("```json\n{}\n```", spaced_out_json);
cx.new(|cx| Markdown::new(params_md.into(), Some(language_registry.clone()), None, cx))
}
fn expanded_params_md(
params: &serde_json::Value,
language_registry: &Arc<LanguageRegistry>,
cx: &mut App,
) -> Entity<Markdown> {
let params_json = serde_json::to_string_pretty(params).unwrap_or_default();
let params_md = format!("```json\n{}\n```", params_json);
cx.new(|cx| Markdown::new(params_md.into(), Some(language_registry.clone()), None, cx))
}
enum MessageType {
Request,
Response,
Notification,
}
impl Display for MessageType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MessageType::Request => write!(f, "Request"),
MessageType::Response => write!(f, "Response"),
MessageType::Notification => write!(f, "Notification"),
}
}
}
enum AcpToolsEvent {}
impl EventEmitter<AcpToolsEvent> for AcpTools {}
impl Item for AcpTools {
type Event = AcpToolsEvent;
fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
format!(
"ACP: {}",
self.watched_connection
.as_ref()
.map_or("Disconnected", |connection| &connection.server_name)
)
.into()
}
fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
Some(ui::Icon::new(IconName::Thread))
}
}
impl Focusable for AcpTools {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for AcpTools {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.track_focus(&self.focus_handle)
.size_full()
.bg(cx.theme().colors().editor_background)
.child(match self.watched_connection.as_ref() {
Some(connection) => {
if connection.messages.is_empty() {
h_flex()
.size_full()
.justify_center()
.items_center()
.child("No messages recorded yet")
.into_any()
} else {
list(
connection.list_state.clone(),
cx.processor(Self::render_message),
)
.with_sizing_behavior(gpui::ListSizingBehavior::Auto)
.flex_grow()
.into_any()
}
}
None => h_flex()
.size_full()
.justify_center()
.items_center()
.child("No active connection")
.into_any(),
})
}
}

View File

@@ -893,19 +893,8 @@ impl ThreadsDatabase {
let needs_migration_from_heed = mdb_path.exists();
let connection = if *ZED_STATELESS {
let connection = if *ZED_STATELESS || cfg!(any(feature = "test-support", test)) {
Connection::open_memory(Some("THREAD_FALLBACK_DB"))
} else if cfg!(any(feature = "test-support", test)) {
// rust stores the name of the test on the current thread.
// We use this to automatically create a database that will
// be shared within the test (for the test_retrieve_old_thread)
// but not with concurrent tests.
let thread = std::thread::current();
let test_name = thread.name();
Connection::open_memory(Some(&format!(
"THREAD_FALLBACK_{}",
test_name.unwrap_or_default()
)))
} else {
Connection::open_file(&sqlite_path.to_string_lossy())
};

View File

@@ -61,7 +61,6 @@ sqlez.workspace = true
task.workspace = true
telemetry.workspace = true
terminal.workspace = true
thiserror.workspace = true
text.workspace = true
ui.workspace = true
util.workspace = true

View File

@@ -2,7 +2,7 @@ use crate::{
ContextServerRegistry, Thread, ThreadEvent, ThreadsDatabase, ToolCallAuthorization,
UserMessageContent, templates::Templates,
};
use crate::{HistoryStore, TitleUpdated, TokenUsageUpdated};
use crate::{HistoryStore, TokenUsageUpdated};
use acp_thread::{AcpThread, AgentModelSelector};
use action_log::ActionLog;
use agent_client_protocol as acp;
@@ -61,19 +61,16 @@ pub struct LanguageModels {
model_list: acp_thread::AgentModelList,
refresh_models_rx: watch::Receiver<()>,
refresh_models_tx: watch::Sender<()>,
_authenticate_all_providers_task: Task<()>,
}
impl LanguageModels {
fn new(cx: &mut App) -> Self {
fn new(cx: &App) -> Self {
let (refresh_models_tx, refresh_models_rx) = watch::channel(());
let mut this = Self {
models: HashMap::default(),
model_list: acp_thread::AgentModelList::Grouped(IndexMap::default()),
refresh_models_rx,
refresh_models_tx,
_authenticate_all_providers_task: Self::authenticate_all_language_model_providers(cx),
};
this.refresh_list(cx);
this
@@ -153,52 +150,6 @@ impl LanguageModels {
fn model_id(model: &Arc<dyn LanguageModel>) -> acp_thread::AgentModelId {
acp_thread::AgentModelId(format!("{}/{}", model.provider_id().0, model.id().0).into())
}
fn authenticate_all_language_model_providers(cx: &mut App) -> Task<()> {
let authenticate_all_providers = LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.iter()
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
.collect::<Vec<_>>();
cx.background_spawn(async move {
for (provider_id, provider_name, authenticate_task) in authenticate_all_providers {
if let Err(err) = authenticate_task.await {
if matches!(err, language_model::AuthenticateError::CredentialsNotFound) {
// Since we're authenticating these providers in the
// background for the purposes of populating the
// language selector, we don't care about providers
// where the credentials are not found.
} else {
// Some providers have noisy failure states that we
// don't want to spam the logs with every time the
// language model selector is initialized.
//
// Ideally these should have more clear failure modes
// that we know are safe to ignore here, like what we do
// with `CredentialsNotFound` above.
match provider_id.0.as_ref() {
"lmstudio" | "ollama" => {
// LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated".
//
// These fail noisily, so we don't log them.
}
"copilot_chat" => {
// Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors.
}
_ => {
log::error!(
"Failed to authenticate provider: {}: {err}",
provider_name.0
);
}
}
}
}
}
})
}
}
pub struct NativeAgent {
@@ -229,7 +180,7 @@ impl NativeAgent {
fs: Arc<dyn Fs>,
cx: &mut AsyncApp,
) -> Result<Entity<NativeAgent>> {
log::debug!("Creating new NativeAgent");
log::info!("Creating new NativeAgent");
let project_context = cx
.update(|cx| Self::build_project_context(&project, prompt_store.as_ref(), cx))?
@@ -289,23 +240,19 @@ impl NativeAgent {
let title = thread.title();
let project = thread.project.clone();
let action_log = thread.action_log.clone();
let prompt_capabilities_rx = thread.prompt_capabilities_rx.clone();
let acp_thread = cx.new(|cx| {
let acp_thread = cx.new(|_cx| {
acp_thread::AcpThread::new(
title,
connection,
project.clone(),
action_log.clone(),
session_id.clone(),
prompt_capabilities_rx,
cx,
)
});
let subscriptions = vec![
cx.observe_release(&acp_thread, |this, acp_thread, _cx| {
this.sessions.remove(acp_thread.session_id());
}),
cx.subscribe(&thread_handle, Self::handle_thread_title_updated),
cx.subscribe(&thread_handle, Self::handle_thread_token_usage_updated),
cx.observe(&thread_handle, move |this, thread, cx| {
this.save_thread(thread, cx)
@@ -494,26 +441,6 @@ impl NativeAgent {
})
}
fn handle_thread_title_updated(
&mut self,
thread: Entity<Thread>,
_: &TitleUpdated,
cx: &mut Context<Self>,
) {
let session_id = thread.read(cx).id();
let Some(session) = self.sessions.get(session_id) else {
return;
};
let thread = thread.downgrade();
let acp_thread = session.acp_thread.clone();
cx.spawn(async move |_, cx| {
let title = thread.read_with(cx, |thread, _| thread.title())?;
let task = acp_thread.update(cx, |acp_thread, cx| acp_thread.set_title(title, cx))?;
task.await
})
.detach_and_log_err(cx);
}
fn handle_thread_token_usage_updated(
&mut self,
thread: Entity<Thread>,
@@ -790,6 +717,10 @@ impl NativeAgentConnection {
thread.update_tool_call(update, cx)
})??;
}
ThreadEvent::TitleUpdate(title) => {
acp_thread
.update(cx, |thread, cx| thread.update_title(title, cx))??;
}
ThreadEvent::Retry(status) => {
acp_thread.update(cx, |thread, cx| {
thread.update_retry_status(status, cx)
@@ -808,7 +739,7 @@ impl NativeAgentConnection {
}
}
log::debug!("Response stream completed");
log::info!("Response stream completed");
anyhow::Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
})
@@ -833,7 +764,7 @@ impl AgentModelSelector for NativeAgentConnection {
model_id: acp_thread::AgentModelId,
cx: &mut App,
) -> Task<Result<()>> {
log::debug!("Setting model for session {}: {}", session_id, model_id);
log::info!("Setting model for session {}: {}", session_id, model_id);
let Some(thread) = self
.0
.read(cx)
@@ -904,11 +835,12 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
cx: &mut App,
) -> Task<Result<Entity<acp_thread::AcpThread>>> {
let agent = self.0.clone();
log::debug!("Creating new thread for project at: {:?}", cwd);
log::info!("Creating new thread for project at: {:?}", cwd);
cx.spawn(async move |cx| {
log::debug!("Starting thread creation in async context");
let action_log = cx.new(|_cx| ActionLog::new(project.clone()))?;
// Create Thread
let thread = agent.update(
cx,
@@ -924,16 +856,20 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
.models
.model_from_id(&LanguageModels::model_id(&default_model.model))
});
Ok(cx.new(|cx| {
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
agent.project_context.clone(),
agent.context_server_registry.clone(),
action_log.clone(),
agent.templates.clone(),
default_model,
cx,
)
}))
});
Ok(thread)
},
)??;
agent.update(cx, |agent, cx| agent.register_session(thread, cx))
@@ -969,7 +905,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
.into_iter()
.map(Into::into)
.collect::<Vec<_>>();
log::debug!("Converted prompt to message: {} chars", content.len());
log::info!("Converted prompt to message: {} chars", content.len());
log::debug!("Message id: {:?}", id);
log::debug!("Message content: {:?}", content);
@@ -977,10 +913,18 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
})
}
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
acp::PromptCapabilities {
image: true,
audio: false,
embedded_context: true,
}
}
fn resume(
&self,
session_id: &acp::SessionId,
_cx: &App,
_cx: &mut App,
) -> Option<Rc<dyn acp_thread::AgentSessionResume>> {
Some(Rc::new(NativeAgentSessionResume {
connection: self.clone(),
@@ -997,12 +941,12 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
});
}
fn truncate(
fn session_editor(
&self,
session_id: &agent_client_protocol::SessionId,
cx: &App,
) -> Option<Rc<dyn acp_thread::AgentSessionTruncate>> {
self.0.read_with(cx, |agent, _cx| {
cx: &mut App,
) -> Option<Rc<dyn acp_thread::AgentSessionEditor>> {
self.0.update(cx, |agent, _cx| {
agent.sessions.get(session_id).map(|session| {
Rc::new(NativeAgentSessionEditor {
thread: session.thread.clone(),
@@ -1012,17 +956,6 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
})
}
fn set_title(
&self,
session_id: &acp::SessionId,
_cx: &App,
) -> Option<Rc<dyn acp_thread::AgentSessionSetTitle>> {
Some(Rc::new(NativeAgentSessionSetTitle {
connection: self.clone(),
session_id: session_id.clone(),
}) as _)
}
fn telemetry(&self) -> Option<Rc<dyn acp_thread::AgentTelemetry>> {
Some(Rc::new(self.clone()) as Rc<dyn acp_thread::AgentTelemetry>)
}
@@ -1058,8 +991,8 @@ struct NativeAgentSessionEditor {
acp_thread: WeakEntity<AcpThread>,
}
impl acp_thread::AgentSessionTruncate for NativeAgentSessionEditor {
fn run(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task<Result<()>> {
impl acp_thread::AgentSessionEditor for NativeAgentSessionEditor {
fn truncate(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task<Result<()>> {
match self.thread.update(cx, |thread, cx| {
thread.truncate(message_id.clone(), cx)?;
Ok(thread.latest_token_usage())
@@ -1091,22 +1024,6 @@ impl acp_thread::AgentSessionResume for NativeAgentSessionResume {
}
}
struct NativeAgentSessionSetTitle {
connection: NativeAgentConnection,
session_id: acp::SessionId,
}
impl acp_thread::AgentSessionSetTitle for NativeAgentSessionSetTitle {
fn run(&self, title: SharedString, cx: &mut App) -> Task<Result<()>> {
let Some(session) = self.connection.0.read(cx).sessions.get(&self.session_id) else {
return Task::ready(Err(anyhow!("session not found")));
};
let thread = session.thread.clone();
thread.update(cx, |thread, cx| thread.set_title(title, cx));
Task::ready(Ok(()))
}
}
#[cfg(test)]
mod tests {
use crate::HistoryEntryId;
@@ -1406,8 +1323,6 @@ mod tests {
)
});
cx.run_until_parked();
// Drop the ACP thread, which should cause the session to be dropped as well.
cx.update(|_| {
drop(thread);
@@ -1450,9 +1365,10 @@ mod tests {
history: &Entity<HistoryStore>,
cx: &mut TestAppContext,
) -> Vec<(HistoryEntryId, String)> {
history.read_with(cx, |history, _| {
history.read_with(cx, |history, cx| {
history
.entries()
.entries(cx)
.iter()
.map(|e| (e.id(), e.title().to_string()))
.collect::<Vec<_>>()
})

View File

@@ -266,19 +266,8 @@ impl ThreadsDatabase {
}
pub fn new(executor: BackgroundExecutor) -> Result<Self> {
let connection = if *ZED_STATELESS {
let connection = if *ZED_STATELESS || cfg!(any(feature = "test-support", test)) {
Connection::open_memory(Some("THREAD_FALLBACK_DB"))
} else if cfg!(any(feature = "test-support", test)) {
// rust stores the name of the test on the current thread.
// We use this to automatically create a database that will
// be shared within the test (for the test_retrieve_old_thread)
// but not with concurrent tests.
let thread = std::thread::current();
let test_name = thread.name();
Connection::open_memory(Some(&format!(
"THREAD_FALLBACK_{}",
test_name.unwrap_or_default()
)))
} else {
let threads_dir = paths::data_dir().join("threads");
std::fs::create_dir_all(&threads_dir)?;

View File

@@ -86,7 +86,6 @@ enum SerializedRecentOpen {
pub struct HistoryStore {
threads: Vec<DbThreadMetadata>,
entries: Vec<HistoryEntry>,
context_store: Entity<assistant_context::ContextStore>,
recently_opened_entries: VecDeque<HistoryEntryId>,
_subscriptions: Vec<gpui::Subscription>,
@@ -98,7 +97,7 @@ impl HistoryStore {
context_store: Entity<assistant_context::ContextStore>,
cx: &mut Context<Self>,
) -> Self {
let subscriptions = vec![cx.observe(&context_store, |this, _, cx| this.update_entries(cx))];
let subscriptions = vec![cx.observe(&context_store, |_, _, cx| cx.notify())];
cx.spawn(async move |this, cx| {
let entries = Self::load_recently_opened_entries(cx).await;
@@ -117,7 +116,6 @@ impl HistoryStore {
context_store,
recently_opened_entries: VecDeque::default(),
threads: Vec::default(),
entries: Vec::default(),
_subscriptions: subscriptions,
_save_recently_opened_entries_task: Task::ready(()),
}
@@ -183,18 +181,20 @@ impl HistoryStore {
}
}
this.threads = threads;
this.update_entries(cx);
cx.notify();
})
})
.detach_and_log_err(cx);
}
fn update_entries(&mut self, cx: &mut Context<Self>) {
pub fn entries(&self, cx: &App) -> Vec<HistoryEntry> {
let mut history_entries = Vec::new();
#[cfg(debug_assertions)]
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
return;
return history_entries;
}
let mut history_entries = Vec::new();
history_entries.extend(self.threads.iter().cloned().map(HistoryEntry::AcpThread));
history_entries.extend(
self.context_store
@@ -205,12 +205,17 @@ impl HistoryStore {
);
history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at()));
self.entries = history_entries;
cx.notify()
history_entries
}
pub fn is_empty(&self, _cx: &App) -> bool {
self.entries.is_empty()
pub fn is_empty(&self, cx: &App) -> bool {
self.threads.is_empty()
&& self
.context_store
.read(cx)
.unordered_contexts()
.next()
.is_none()
}
pub fn recently_opened_entries(&self, cx: &App) -> Vec<HistoryEntry> {
@@ -351,7 +356,7 @@ impl HistoryStore {
self.save_recently_opened_entries(cx);
}
pub fn entries(&self) -> impl Iterator<Item = HistoryEntry> {
self.entries.iter().cloned()
pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
self.entries(cx).into_iter().take(limit).collect()
}
}

View File

@@ -3,7 +3,7 @@ use std::{any::Any, path::Path, rc::Rc, sync::Arc};
use agent_servers::AgentServer;
use anyhow::Result;
use fs::Fs;
use gpui::{App, Entity, SharedString, Task};
use gpui::{App, Entity, Task};
use project::Project;
use prompt_store::PromptStore;
@@ -22,37 +22,29 @@ impl NativeAgentServer {
}
impl AgentServer for NativeAgentServer {
fn telemetry_id(&self) -> &'static str {
"zed"
fn name(&self) -> &'static str {
"Native Agent"
}
fn name(&self) -> SharedString {
"Zed Agent".into()
fn empty_state_headline(&self) -> &'static str {
"Welcome to the Agent Panel"
}
fn empty_state_headline(&self) -> SharedString {
self.name()
}
fn empty_state_message(&self) -> SharedString {
"".into()
fn empty_state_message(&self) -> &'static str {
""
}
fn logo(&self) -> ui::IconName {
ui::IconName::ZedAgent
}
fn install_command(&self) -> Option<&'static str> {
None
}
fn connect(
&self,
_root_dir: &Path,
project: &Entity<Project>,
cx: &mut App,
) -> Task<Result<Rc<dyn acp_thread::AgentConnection>>> {
log::debug!(
log::info!(
"NativeAgentServer::connect called for path: {:?}",
_root_dir
);
@@ -71,7 +63,7 @@ impl AgentServer for NativeAgentServer {
// Create the connection wrapper
let connection = NativeAgentConnection(agent);
log::debug!("NativeAgentServer connection established successfully");
log::info!("NativeAgentServer connection established successfully");
Ok(Rc::new(connection) as Rc<dyn acp_thread::AgentConnection>)
})

File diff suppressed because it is too large Load Diff

View File

@@ -16,11 +16,11 @@ impl AgentTool for EchoTool {
type Input = EchoToolInput;
type Output = String;
fn name() -> &'static str {
"echo"
fn name(&self) -> SharedString {
"echo".into()
}
fn kind() -> acp::ToolKind {
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Other
}
@@ -51,8 +51,8 @@ impl AgentTool for DelayTool {
type Input = DelayToolInput;
type Output = String;
fn name() -> &'static str {
"delay"
fn name(&self) -> SharedString {
"delay".into()
}
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
@@ -63,7 +63,7 @@ impl AgentTool for DelayTool {
}
}
fn kind() -> acp::ToolKind {
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Other
}
@@ -92,11 +92,11 @@ impl AgentTool for ToolRequiringPermission {
type Input = ToolRequiringPermissionInput;
type Output = String;
fn name() -> &'static str {
"tool_requiring_permission"
fn name(&self) -> SharedString {
"tool_requiring_permission".into()
}
fn kind() -> acp::ToolKind {
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Other
}
@@ -127,11 +127,11 @@ impl AgentTool for InfiniteTool {
type Input = InfiniteToolInput;
type Output = String;
fn name() -> &'static str {
"infinite"
fn name(&self) -> SharedString {
"infinite".into()
}
fn kind() -> acp::ToolKind {
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Other
}
@@ -178,11 +178,11 @@ impl AgentTool for WordListTool {
type Input = WordListInput;
type Output = String;
fn name() -> &'static str {
"word_list"
fn name(&self) -> SharedString {
"word_list".into()
}
fn kind() -> acp::ToolKind {
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Other
}

File diff suppressed because it is too large Load Diff

View File

@@ -16,29 +16,6 @@ mod terminal_tool;
mod thinking_tool;
mod web_search_tool;
/// A list of all built in tool names, for use in deduplicating MCP tool names
pub fn default_tool_names() -> impl Iterator<Item = &'static str> {
[
CopyPathTool::name(),
CreateDirectoryTool::name(),
DeletePathTool::name(),
DiagnosticsTool::name(),
EditFileTool::name(),
FetchTool::name(),
FindPathTool::name(),
GrepTool::name(),
ListDirectoryTool::name(),
MovePathTool::name(),
NowTool::name(),
OpenTool::name(),
ReadFileTool::name(),
TerminalTool::name(),
ThinkingTool::name(),
WebSearchTool::name(),
]
.into_iter()
}
pub use context_server_registry::*;
pub use copy_path_tool::*;
pub use create_directory_tool::*;
@@ -56,5 +33,3 @@ pub use read_file_tool::*;
pub use terminal_tool::*;
pub use thinking_tool::*;
pub use web_search_tool::*;
use crate::AgentTool;

View File

@@ -1,7 +1,7 @@
use crate::{AgentTool, ToolCallEventStream};
use agent_client_protocol::ToolKind;
use anyhow::{Context as _, Result, anyhow};
use gpui::{App, AppContext, Entity, Task};
use gpui::{App, AppContext, Entity, SharedString, Task};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -50,11 +50,11 @@ impl AgentTool for CopyPathTool {
type Input = CopyPathToolInput;
type Output = String;
fn name() -> &'static str {
"copy_path"
fn name(&self) -> SharedString {
"copy_path".into()
}
fn kind() -> ToolKind {
fn kind(&self) -> ToolKind {
ToolKind::Move
}

View File

@@ -41,11 +41,11 @@ impl AgentTool for CreateDirectoryTool {
type Input = CreateDirectoryToolInput;
type Output = String;
fn name() -> &'static str {
"create_directory"
fn name(&self) -> SharedString {
"create_directory".into()
}
fn kind() -> ToolKind {
fn kind(&self) -> ToolKind {
ToolKind::Read
}

View File

@@ -44,11 +44,11 @@ impl AgentTool for DeletePathTool {
type Input = DeletePathToolInput;
type Output = String;
fn name() -> &'static str {
"delete_path"
fn name(&self) -> SharedString {
"delete_path".into()
}
fn kind() -> ToolKind {
fn kind(&self) -> ToolKind {
ToolKind::Delete
}

View File

@@ -63,11 +63,11 @@ impl AgentTool for DiagnosticsTool {
type Input = DiagnosticsToolInput;
type Output = String;
fn name() -> &'static str {
"diagnostics"
fn name(&self) -> SharedString {
"diagnostics".into()
}
fn kind() -> acp::ToolKind {
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Read
}

View File

@@ -186,11 +186,11 @@ impl AgentTool for EditFileTool {
type Input = EditFileToolInput;
type Output = EditFileToolOutput;
fn name() -> &'static str {
"edit_file"
fn name(&self) -> SharedString {
"edit_file".into()
}
fn kind() -> acp::ToolKind {
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Edit
}
@@ -273,13 +273,6 @@ impl AgentTool for EditFileTool {
let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?;
event_stream.update_diff(diff.clone());
let _finalize_diff = util::defer({
let diff = diff.downgrade();
let mut cx = cx.clone();
move || {
diff.update(&mut cx, |diff, cx| diff.finalize(cx)).ok();
}
});
let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let old_text = cx
@@ -396,6 +389,8 @@ impl AgentTool for EditFileTool {
})
.await;
diff.update(cx, |diff, cx| diff.finalize(cx)).ok();
let input_path = input.path.display();
if unified_diff.is_empty() {
anyhow::ensure!(
@@ -522,6 +517,7 @@ fn resolve_path(
mod tests {
use super::*;
use crate::{ContextServerRegistry, Templates};
use action_log::ActionLog;
use client::TelemetrySettings;
use fs::Fs;
use gpui::{TestAppContext, UpdateGlobal};
@@ -539,6 +535,7 @@ mod tests {
fs.insert_tree("/root", json!({})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
@@ -547,6 +544,7 @@ mod tests {
project,
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
action_log,
Templates::new(),
Some(model),
cx,
@@ -737,6 +735,7 @@ mod tests {
}
});
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
@@ -745,6 +744,7 @@ mod tests {
project,
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
action_log.clone(),
Templates::new(),
Some(model.clone()),
cx,
@@ -801,9 +801,7 @@ mod tests {
"Code should be formatted when format_on_save is enabled"
);
let stale_buffer_count = thread
.read_with(cx, |thread, _cx| thread.action_log.clone())
.read_with(cx, |log, cx| log.stale_buffers(cx).count());
let stale_buffer_count = action_log.read_with(cx, |log, cx| log.stale_buffers(cx).count());
assert_eq!(
stale_buffer_count, 0,
@@ -881,12 +879,14 @@ mod tests {
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project,
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
action_log.clone(),
Templates::new(),
Some(model.clone()),
cx,
@@ -1008,12 +1008,14 @@ mod tests {
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project,
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
action_log.clone(),
Templates::new(),
Some(model.clone()),
cx,
@@ -1144,12 +1146,14 @@ mod tests {
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project,
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
action_log.clone(),
Templates::new(),
Some(model.clone()),
cx,
@@ -1250,6 +1254,7 @@ mod tests {
)
.await;
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
@@ -1258,6 +1263,7 @@ mod tests {
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry.clone(),
action_log.clone(),
Templates::new(),
Some(model.clone()),
cx,
@@ -1330,6 +1336,7 @@ mod tests {
.await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
@@ -1338,6 +1345,7 @@ mod tests {
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry.clone(),
action_log.clone(),
Templates::new(),
Some(model.clone()),
cx,
@@ -1413,6 +1421,7 @@ mod tests {
.await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
@@ -1421,6 +1430,7 @@ mod tests {
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry.clone(),
action_log.clone(),
Templates::new(),
Some(model.clone()),
cx,
@@ -1493,6 +1503,7 @@ mod tests {
let fs = project::FakeFs::new(cx.executor());
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
@@ -1501,6 +1512,7 @@ mod tests {
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
action_log.clone(),
Templates::new(),
Some(model.clone()),
cx,
@@ -1550,100 +1562,6 @@ mod tests {
);
}
#[gpui::test]
async fn test_diff_finalization(cx: &mut TestAppContext) {
init_test(cx);
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree("/", json!({"main.rs": ""})).await;
let project = Project::test(fs.clone(), [path!("/").as_ref()], cx).await;
let languages = project.read_with(cx, |project, _cx| project.languages().clone());
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry.clone(),
Templates::new(),
Some(model.clone()),
cx,
)
});
// Ensure the diff is finalized after the edit completes.
{
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
let edit = cx.update(|cx| {
tool.run(
EditFileToolInput {
display_description: "Edit file".into(),
path: path!("/main.rs").into(),
mode: EditFileMode::Edit,
},
stream_tx,
cx,
)
});
stream_rx.expect_update_fields().await;
let diff = stream_rx.expect_diff().await;
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
cx.run_until_parked();
model.end_last_completion_stream();
edit.await.unwrap();
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
}
// Ensure the diff is finalized if an error occurs while editing.
{
model.forbid_requests();
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
let edit = cx.update(|cx| {
tool.run(
EditFileToolInput {
display_description: "Edit file".into(),
path: path!("/main.rs").into(),
mode: EditFileMode::Edit,
},
stream_tx,
cx,
)
});
stream_rx.expect_update_fields().await;
let diff = stream_rx.expect_diff().await;
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
edit.await.unwrap_err();
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
model.allow_requests();
}
// Ensure the diff is finalized if the tool call gets dropped.
{
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
let edit = cx.update(|cx| {
tool.run(
EditFileToolInput {
display_description: "Edit file".into(),
path: path!("/main.rs").into(),
mode: EditFileMode::Edit,
},
stream_tx,
cx,
)
});
stream_rx.expect_update_fields().await;
let diff = stream_rx.expect_diff().await;
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
drop(edit);
cx.run_until_parked();
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
}
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);

View File

@@ -118,11 +118,11 @@ impl AgentTool for FetchTool {
type Input = FetchToolInput;
type Output = String;
fn name() -> &'static str {
"fetch"
fn name(&self) -> SharedString {
"fetch".into()
}
fn kind() -> acp::ToolKind {
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Fetch
}
@@ -136,17 +136,12 @@ impl AgentTool for FetchTool {
fn run(
self: Arc<Self>,
input: Self::Input,
event_stream: ToolCallEventStream,
_event_stream: ToolCallEventStream,
cx: &mut App,
) -> Task<Result<Self::Output>> {
let authorize = event_stream.authorize(input.url.clone(), cx);
let text = cx.background_spawn({
let http_client = self.http_client.clone();
async move {
authorize.await?;
Self::build_message(http_client, &input.url).await
}
async move { Self::build_message(http_client, &input.url).await }
});
cx.foreground_executor().spawn(async move {

View File

@@ -85,11 +85,11 @@ impl AgentTool for FindPathTool {
type Input = FindPathToolInput;
type Output = FindPathToolOutput;
fn name() -> &'static str {
"find_path"
fn name(&self) -> SharedString {
"find_path".into()
}
fn kind() -> acp::ToolKind {
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Search
}
@@ -165,17 +165,16 @@ fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Resu
.collect();
cx.background_spawn(async move {
let mut results = Vec::new();
for snapshot in snapshots {
for entry in snapshot.entries(false, 0) {
Ok(snapshots
.iter()
.flat_map(|snapshot| {
let root_name = PathBuf::from(snapshot.root_name());
if path_matcher.is_match(root_name.join(&entry.path)) {
results.push(snapshot.abs_path().join(entry.path.as_ref()));
}
}
}
Ok(results)
snapshot
.entries(false, 0)
.map(move |entry| root_name.join(&entry.path))
.filter(|path| path_matcher.is_match(&path))
})
.collect())
})
}
@@ -216,8 +215,8 @@ mod test {
assert_eq!(
matches,
&[
PathBuf::from(path!("/root/apple/banana/carrot")),
PathBuf::from(path!("/root/apple/bandana/carbonara"))
PathBuf::from("root/apple/banana/carrot"),
PathBuf::from("root/apple/bandana/carbonara")
]
);
@@ -228,8 +227,8 @@ mod test {
assert_eq!(
matches,
&[
PathBuf::from(path!("/root/apple/banana/carrot")),
PathBuf::from(path!("/root/apple/bandana/carbonara"))
PathBuf::from("root/apple/banana/carrot"),
PathBuf::from("root/apple/bandana/carbonara")
]
);
}

View File

@@ -67,11 +67,11 @@ impl AgentTool for GrepTool {
type Input = GrepToolInput;
type Output = String;
fn name() -> &'static str {
"grep"
fn name(&self) -> SharedString {
"grep".into()
}
fn kind() -> acp::ToolKind {
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Search
}

View File

@@ -51,11 +51,11 @@ impl AgentTool for ListDirectoryTool {
type Input = ListDirectoryToolInput;
type Output = String;
fn name() -> &'static str {
"list_directory"
fn name(&self) -> SharedString {
"list_directory".into()
}
fn kind() -> ToolKind {
fn kind(&self) -> ToolKind {
ToolKind::Read
}

View File

@@ -52,11 +52,11 @@ impl AgentTool for MovePathTool {
type Input = MovePathToolInput;
type Output = String;
fn name() -> &'static str {
"move_path"
fn name(&self) -> SharedString {
"move_path".into()
}
fn kind() -> ToolKind {
fn kind(&self) -> ToolKind {
ToolKind::Move
}

View File

@@ -32,11 +32,11 @@ impl AgentTool for NowTool {
type Input = NowToolInput;
type Output = String;
fn name() -> &'static str {
"now"
fn name(&self) -> SharedString {
"now".into()
}
fn kind() -> acp::ToolKind {
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Other
}

View File

@@ -37,11 +37,11 @@ impl AgentTool for OpenTool {
type Input = OpenToolInput;
type Output = String;
fn name() -> &'static str {
"open"
fn name(&self) -> SharedString {
"open".into()
}
fn kind() -> ToolKind {
fn kind(&self) -> ToolKind {
ToolKind::Execute
}

View File

@@ -10,8 +10,7 @@ use project::{AgentLocation, ImageItem, Project, WorktreeSettings, image_store};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
use std::{path::Path, sync::Arc};
use util::markdown::MarkdownCodeBlock;
use std::sync::Arc;
use crate::{AgentTool, ToolCallEventStream};
@@ -60,21 +59,36 @@ impl AgentTool for ReadFileTool {
type Input = ReadFileToolInput;
type Output = LanguageModelToolResultContent;
fn name() -> &'static str {
"read_file"
fn name(&self) -> SharedString {
"read_file".into()
}
fn kind() -> acp::ToolKind {
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Read
}
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
input
.ok()
.as_ref()
.and_then(|input| Path::new(&input.path).file_name())
.map(|file_name| file_name.to_string_lossy().to_string().into())
.unwrap_or_default()
if let Ok(input) = input {
let path = &input.path;
match (input.start_line, input.end_line) {
(Some(start), Some(end)) => {
format!(
"[Read file `{}` (lines {}-{})](@selection:{}:({}-{}))",
path, start, end, path, start, end
)
}
(Some(start), None) => {
format!(
"[Read file `{}` (from line {})](@selection:{}:({}-{}))",
path, start, path, start, start
)
}
_ => format!("[Read file `{}`](@file:{})", path, path),
}
.into()
} else {
"Read file".into()
}
}
fn run(
@@ -244,19 +258,6 @@ impl AgentTool for ReadFileTool {
}]),
..Default::default()
});
if let Ok(LanguageModelToolResultContent::Text(text)) = &result {
let markdown = MarkdownCodeBlock {
tag: &input.path,
text,
}
.to_string();
event_stream.update_fields(ToolCallUpdateFields {
content: Some(vec![acp::ToolCallContent::Content {
content: markdown.into(),
}]),
..Default::default()
})
}
}
})?;

View File

@@ -63,11 +63,11 @@ impl AgentTool for TerminalTool {
type Input = TerminalToolInput;
type Output = String;
fn name() -> &'static str {
"terminal"
fn name(&self) -> SharedString {
"terminal".into()
}
fn kind() -> acp::ToolKind {
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Execute
}

View File

@@ -21,11 +21,11 @@ impl AgentTool for ThinkingTool {
type Input = ThinkingToolInput;
type Output = String;
fn name() -> &'static str {
"thinking"
fn name(&self) -> SharedString {
"thinking".into()
}
fn kind() -> acp::ToolKind {
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Think
}

View File

@@ -40,11 +40,11 @@ impl AgentTool for WebSearchTool {
type Input = WebSearchToolInput;
type Output = WebSearchToolOutput;
fn name() -> &'static str {
"web_search"
fn name(&self) -> SharedString {
"web_search".into()
}
fn kind() -> acp::ToolKind {
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Fetch
}

View File

@@ -17,11 +17,11 @@ path = "src/agent_servers.rs"
doctest = false
[dependencies]
acp_tools.workspace = true
acp_thread.workspace = true
action_log.workspace = true
agent-client-protocol.workspace = true
agent_settings.workspace = true
agentic-coding-protocol.workspace = true
anyhow.workspace = true
client = { workspace = true, optional = true }
collections.workspace = true

View File

@@ -1,418 +1,34 @@
use std::{path::Path, rc::Rc};
use crate::AgentServerCommand;
use acp_thread::AgentConnection;
use acp_tools::AcpConnectionRegistry;
use action_log::ActionLog;
use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
use anyhow::anyhow;
use collections::HashMap;
use futures::AsyncBufReadExt as _;
use futures::channel::oneshot;
use futures::io::BufReader;
use project::Project;
use serde::Deserialize;
use std::{any::Any, cell::RefCell};
use std::{path::Path, rc::Rc};
use anyhow::Result;
use gpui::AsyncApp;
use thiserror::Error;
use anyhow::{Context as _, Result};
use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString, Task, WeakEntity};
use acp_thread::{AcpThread, AuthRequired, LoadError};
mod v0;
mod v1;
#[derive(Debug, Error)]
#[error("Unsupported version")]
pub struct UnsupportedVersion;
pub struct AcpConnection {
server_name: SharedString,
connection: Rc<acp::ClientSideConnection>,
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
auth_methods: Vec<acp::AuthMethod>,
prompt_capabilities: acp::PromptCapabilities,
_io_task: Task<Result<()>>,
}
pub struct AcpSession {
thread: WeakEntity<AcpThread>,
suppress_abort_err: bool,
}
pub async fn connect(
server_name: SharedString,
server_name: &'static str,
command: AgentServerCommand,
root_dir: &Path,
cx: &mut AsyncApp,
) -> Result<Rc<dyn AgentConnection>> {
let conn = AcpConnection::stdio(server_name, command.clone(), root_dir, cx).await?;
Ok(Rc::new(conn) as _)
}
let conn = v1::AcpConnection::stdio(server_name, command.clone(), root_dir, cx).await;
const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1;
impl AcpConnection {
pub async fn stdio(
server_name: SharedString,
command: AgentServerCommand,
root_dir: &Path,
cx: &mut AsyncApp,
) -> Result<Self> {
let mut child = util::command::new_smol_command(command.path)
.args(command.args.iter().map(|arg| arg.as_str()))
.envs(command.env.iter().flatten())
.current_dir(root_dir)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.kill_on_drop(true)
.spawn()?;
let stdout = child.stdout.take().context("Failed to take stdout")?;
let stdin = child.stdin.take().context("Failed to take stdin")?;
let stderr = child.stderr.take().context("Failed to take stderr")?;
log::trace!("Spawned (pid: {})", child.id());
let sessions = Rc::new(RefCell::new(HashMap::default()));
let client = ClientDelegate {
sessions: sessions.clone(),
cx: cx.clone(),
};
let (connection, io_task) = acp::ClientSideConnection::new(client, stdin, stdout, {
let foreground_executor = cx.foreground_executor().clone();
move |fut| {
foreground_executor.spawn(fut).detach();
}
});
let io_task = cx.background_spawn(io_task);
cx.background_spawn(async move {
let mut stderr = BufReader::new(stderr);
let mut line = String::new();
while let Ok(n) = stderr.read_line(&mut line).await
&& n > 0
{
log::warn!("agent stderr: {}", &line);
line.clear();
}
})
.detach();
cx.spawn({
let sessions = sessions.clone();
async move |cx| {
let status = child.status().await?;
for session in sessions.borrow().values() {
session
.thread
.update(cx, |thread, cx| {
thread.emit_load_error(LoadError::Exited { status }, cx)
})
.ok();
}
anyhow::Ok(())
}
})
.detach();
let connection = Rc::new(connection);
cx.update(|cx| {
AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| {
registry.set_active_connection(server_name.clone(), &connection, cx)
});
})?;
let response = connection
.initialize(acp::InitializeRequest {
protocol_version: acp::VERSION,
client_capabilities: acp::ClientCapabilities {
fs: acp::FileSystemCapability {
read_text_file: true,
write_text_file: true,
},
},
})
.await?;
if response.protocol_version < MINIMUM_SUPPORTED_VERSION {
return Err(UnsupportedVersion.into());
match conn {
Ok(conn) => Ok(Rc::new(conn) as _),
Err(err) if err.is::<UnsupportedVersion>() => {
// Consider re-using initialize response and subprocess when adding another version here
let conn: Rc<dyn AgentConnection> =
Rc::new(v0::AcpConnection::stdio(server_name, command, root_dir, cx).await?);
Ok(conn)
}
Ok(Self {
auth_methods: response.auth_methods,
connection,
server_name,
sessions,
prompt_capabilities: response.agent_capabilities.prompt_capabilities,
_io_task: io_task,
})
}
pub fn prompt_capabilities(&self) -> &acp::PromptCapabilities {
&self.prompt_capabilities
}
}
impl AgentConnection for AcpConnection {
fn new_thread(
self: Rc<Self>,
project: Entity<Project>,
cwd: &Path,
cx: &mut App,
) -> Task<Result<Entity<AcpThread>>> {
let conn = self.connection.clone();
let sessions = self.sessions.clone();
let cwd = cwd.to_path_buf();
let context_server_store = project.read(cx).context_server_store().read(cx);
let mcp_servers = context_server_store
.configured_server_ids()
.iter()
.filter_map(|id| {
let configuration = context_server_store.configuration_for_server(id)?;
let command = configuration.command();
Some(acp::McpServer {
name: id.0.to_string(),
command: command.path.clone(),
args: command.args.clone(),
env: if let Some(env) = command.env.as_ref() {
env.iter()
.map(|(name, value)| acp::EnvVariable {
name: name.clone(),
value: value.clone(),
})
.collect()
} else {
vec![]
},
})
})
.collect();
cx.spawn(async move |cx| {
let response = conn
.new_session(acp::NewSessionRequest { mcp_servers, cwd })
.await
.map_err(|err| {
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
let mut error = AuthRequired::new();
if err.message != acp::ErrorCode::AUTH_REQUIRED.message {
error = error.with_description(err.message);
}
anyhow!(error)
} else {
anyhow!(err)
}
})?;
let session_id = response.session_id;
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
let thread = cx.new(|cx| {
AcpThread::new(
self.server_name.clone(),
self.clone(),
project,
action_log,
session_id.clone(),
// ACP doesn't currently support per-session prompt capabilities or changing capabilities dynamically.
watch::Receiver::constant(self.prompt_capabilities),
cx,
)
})?;
let session = AcpSession {
thread: thread.downgrade(),
suppress_abort_err: false,
};
sessions.borrow_mut().insert(session_id, session);
Ok(thread)
})
}
fn auth_methods(&self) -> &[acp::AuthMethod] {
&self.auth_methods
}
fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
let conn = self.connection.clone();
cx.foreground_executor().spawn(async move {
let result = conn
.authenticate(acp::AuthenticateRequest {
method_id: method_id.clone(),
})
.await?;
Ok(result)
})
}
fn prompt(
&self,
_id: Option<acp_thread::UserMessageId>,
params: acp::PromptRequest,
cx: &mut App,
) -> Task<Result<acp::PromptResponse>> {
let conn = self.connection.clone();
let sessions = self.sessions.clone();
let session_id = params.session_id.clone();
cx.foreground_executor().spawn(async move {
let result = conn.prompt(params).await;
let mut suppress_abort_err = false;
if let Some(session) = sessions.borrow_mut().get_mut(&session_id) {
suppress_abort_err = session.suppress_abort_err;
session.suppress_abort_err = false;
}
match result {
Ok(response) => Ok(response),
Err(err) => {
if err.code != ErrorCode::INTERNAL_ERROR.code {
anyhow::bail!(err)
}
let Some(data) = &err.data else {
anyhow::bail!(err)
};
// Temporary workaround until the following PR is generally available:
// https://github.com/google-gemini/gemini-cli/pull/6656
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct ErrorDetails {
details: Box<str>,
}
match serde_json::from_value(data.clone()) {
Ok(ErrorDetails { details }) => {
if suppress_abort_err
&& (details.contains("This operation was aborted")
|| details.contains("The user aborted a request"))
{
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Cancelled,
})
} else {
Err(anyhow!(details))
}
}
Err(_) => Err(anyhow!(err)),
}
}
}
})
}
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) {
session.suppress_abort_err = true;
}
let conn = self.connection.clone();
let params = acp::CancelNotification {
session_id: session_id.clone(),
};
cx.foreground_executor()
.spawn(async move { conn.cancel(params).await })
.detach();
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
}
struct ClientDelegate {
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
cx: AsyncApp,
}
impl acp::Client for ClientDelegate {
async fn request_permission(
&self,
arguments: acp::RequestPermissionRequest,
) -> Result<acp::RequestPermissionResponse, acp::Error> {
let cx = &mut self.cx.clone();
let rx = self
.sessions
.borrow()
.get(&arguments.session_id)
.context("Failed to get session")?
.thread
.update(cx, |thread, cx| {
thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx)
})?;
let result = rx?.await;
let outcome = match result {
Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option },
Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled,
};
Ok(acp::RequestPermissionResponse { outcome })
}
async fn write_text_file(
&self,
arguments: acp::WriteTextFileRequest,
) -> Result<(), acp::Error> {
let cx = &mut self.cx.clone();
let task = self
.sessions
.borrow()
.get(&arguments.session_id)
.context("Failed to get session")?
.thread
.update(cx, |thread, cx| {
thread.write_text_file(arguments.path, arguments.content, cx)
})?;
task.await?;
Ok(())
}
async fn read_text_file(
&self,
arguments: acp::ReadTextFileRequest,
) -> Result<acp::ReadTextFileResponse, acp::Error> {
let cx = &mut self.cx.clone();
let task = self
.sessions
.borrow()
.get(&arguments.session_id)
.context("Failed to get session")?
.thread
.update(cx, |thread, cx| {
thread.read_text_file(arguments.path, arguments.line, arguments.limit, false, cx)
})?;
let content = task.await?;
Ok(acp::ReadTextFileResponse { content })
}
async fn session_notification(
&self,
notification: acp::SessionNotification,
) -> Result<(), acp::Error> {
let cx = &mut self.cx.clone();
let sessions = self.sessions.borrow();
let session = sessions
.get(&notification.session_id)
.context("Failed to get session")?;
session.thread.update(cx, |thread, cx| {
thread.handle_session_update(notification.update, cx)
})??;
Ok(())
Err(err) => Err(err),
}
}

View File

@@ -0,0 +1,524 @@
// Translates old acp agents into the new schema
use action_log::ActionLog;
use agent_client_protocol as acp;
use agentic_coding_protocol::{self as acp_old, AgentRequest as _};
use anyhow::{Context as _, Result, anyhow};
use futures::channel::oneshot;
use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity};
use project::Project;
use std::{any::Any, cell::RefCell, path::Path, rc::Rc};
use ui::App;
use util::ResultExt as _;
use crate::AgentServerCommand;
use acp_thread::{AcpThread, AgentConnection, AuthRequired};
#[derive(Clone)]
struct OldAcpClientDelegate {
thread: Rc<RefCell<WeakEntity<AcpThread>>>,
cx: AsyncApp,
next_tool_call_id: Rc<RefCell<u64>>,
// sent_buffer_versions: HashMap<Entity<Buffer>, HashMap<u64, BufferSnapshot>>,
}
impl OldAcpClientDelegate {
fn new(thread: Rc<RefCell<WeakEntity<AcpThread>>>, cx: AsyncApp) -> Self {
Self {
thread,
cx,
next_tool_call_id: Rc::new(RefCell::new(0)),
}
}
}
impl acp_old::Client for OldAcpClientDelegate {
async fn stream_assistant_message_chunk(
&self,
params: acp_old::StreamAssistantMessageChunkParams,
) -> Result<(), acp_old::Error> {
let cx = &mut self.cx.clone();
cx.update(|cx| {
self.thread
.borrow()
.update(cx, |thread, cx| match params.chunk {
acp_old::AssistantMessageChunk::Text { text } => {
thread.push_assistant_content_block(text.into(), false, cx)
}
acp_old::AssistantMessageChunk::Thought { thought } => {
thread.push_assistant_content_block(thought.into(), true, cx)
}
})
.log_err();
})?;
Ok(())
}
async fn request_tool_call_confirmation(
&self,
request: acp_old::RequestToolCallConfirmationParams,
) -> Result<acp_old::RequestToolCallConfirmationResponse, acp_old::Error> {
let cx = &mut self.cx.clone();
let old_acp_id = *self.next_tool_call_id.borrow() + 1;
self.next_tool_call_id.replace(old_acp_id);
let tool_call = into_new_tool_call(
acp::ToolCallId(old_acp_id.to_string().into()),
request.tool_call,
);
let mut options = match request.confirmation {
acp_old::ToolCallConfirmation::Edit { .. } => vec![(
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
acp::PermissionOptionKind::AllowAlways,
"Always Allow Edits".to_string(),
)],
acp_old::ToolCallConfirmation::Execute { root_command, .. } => vec![(
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
acp::PermissionOptionKind::AllowAlways,
format!("Always Allow {}", root_command),
)],
acp_old::ToolCallConfirmation::Mcp {
server_name,
tool_name,
..
} => vec![
(
acp_old::ToolCallConfirmationOutcome::AlwaysAllowMcpServer,
acp::PermissionOptionKind::AllowAlways,
format!("Always Allow {}", server_name),
),
(
acp_old::ToolCallConfirmationOutcome::AlwaysAllowTool,
acp::PermissionOptionKind::AllowAlways,
format!("Always Allow {}", tool_name),
),
],
acp_old::ToolCallConfirmation::Fetch { .. } => vec![(
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
acp::PermissionOptionKind::AllowAlways,
"Always Allow".to_string(),
)],
acp_old::ToolCallConfirmation::Other { .. } => vec![(
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
acp::PermissionOptionKind::AllowAlways,
"Always Allow".to_string(),
)],
};
options.extend([
(
acp_old::ToolCallConfirmationOutcome::Allow,
acp::PermissionOptionKind::AllowOnce,
"Allow".to_string(),
),
(
acp_old::ToolCallConfirmationOutcome::Reject,
acp::PermissionOptionKind::RejectOnce,
"Reject".to_string(),
),
]);
let mut outcomes = Vec::with_capacity(options.len());
let mut acp_options = Vec::with_capacity(options.len());
for (index, (outcome, kind, label)) in options.into_iter().enumerate() {
outcomes.push(outcome);
acp_options.push(acp::PermissionOption {
id: acp::PermissionOptionId(index.to_string().into()),
name: label,
kind,
})
}
let response = cx
.update(|cx| {
self.thread.borrow().update(cx, |thread, cx| {
thread.request_tool_call_authorization(tool_call.into(), acp_options, cx)
})
})??
.context("Failed to update thread")?
.await;
let outcome = match response {
Ok(option_id) => outcomes[option_id.0.parse::<usize>().unwrap_or(0)],
Err(oneshot::Canceled) => acp_old::ToolCallConfirmationOutcome::Cancel,
};
Ok(acp_old::RequestToolCallConfirmationResponse {
id: acp_old::ToolCallId(old_acp_id),
outcome,
})
}
async fn push_tool_call(
&self,
request: acp_old::PushToolCallParams,
) -> Result<acp_old::PushToolCallResponse, acp_old::Error> {
let cx = &mut self.cx.clone();
let old_acp_id = *self.next_tool_call_id.borrow() + 1;
self.next_tool_call_id.replace(old_acp_id);
cx.update(|cx| {
self.thread.borrow().update(cx, |thread, cx| {
thread.upsert_tool_call(
into_new_tool_call(acp::ToolCallId(old_acp_id.to_string().into()), request),
cx,
)
})
})??
.context("Failed to update thread")?;
Ok(acp_old::PushToolCallResponse {
id: acp_old::ToolCallId(old_acp_id),
})
}
async fn update_tool_call(
&self,
request: acp_old::UpdateToolCallParams,
) -> Result<(), acp_old::Error> {
let cx = &mut self.cx.clone();
cx.update(|cx| {
self.thread.borrow().update(cx, |thread, cx| {
thread.update_tool_call(
acp::ToolCallUpdate {
id: acp::ToolCallId(request.tool_call_id.0.to_string().into()),
fields: acp::ToolCallUpdateFields {
status: Some(into_new_tool_call_status(request.status)),
content: Some(
request
.content
.into_iter()
.map(into_new_tool_call_content)
.collect::<Vec<_>>(),
),
..Default::default()
},
},
cx,
)
})
})?
.context("Failed to update thread")??;
Ok(())
}
async fn update_plan(&self, request: acp_old::UpdatePlanParams) -> Result<(), acp_old::Error> {
let cx = &mut self.cx.clone();
cx.update(|cx| {
self.thread.borrow().update(cx, |thread, cx| {
thread.update_plan(
acp::Plan {
entries: request
.entries
.into_iter()
.map(into_new_plan_entry)
.collect(),
},
cx,
)
})
})?
.context("Failed to update thread")?;
Ok(())
}
async fn read_text_file(
&self,
acp_old::ReadTextFileParams { path, line, limit }: acp_old::ReadTextFileParams,
) -> Result<acp_old::ReadTextFileResponse, acp_old::Error> {
let content = self
.cx
.update(|cx| {
self.thread.borrow().update(cx, |thread, cx| {
thread.read_text_file(path, line, limit, false, cx)
})
})?
.context("Failed to update thread")?
.await?;
Ok(acp_old::ReadTextFileResponse { content })
}
async fn write_text_file(
&self,
acp_old::WriteTextFileParams { path, content }: acp_old::WriteTextFileParams,
) -> Result<(), acp_old::Error> {
self.cx
.update(|cx| {
self.thread
.borrow()
.update(cx, |thread, cx| thread.write_text_file(path, content, cx))
})?
.context("Failed to update thread")?
.await?;
Ok(())
}
}
fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams) -> acp::ToolCall {
acp::ToolCall {
id,
title: request.label,
kind: acp_kind_from_old_icon(request.icon),
status: acp::ToolCallStatus::InProgress,
content: request
.content
.into_iter()
.map(into_new_tool_call_content)
.collect(),
locations: request
.locations
.into_iter()
.map(into_new_tool_call_location)
.collect(),
raw_input: None,
raw_output: None,
}
}
fn acp_kind_from_old_icon(icon: acp_old::Icon) -> acp::ToolKind {
match icon {
acp_old::Icon::FileSearch => acp::ToolKind::Search,
acp_old::Icon::Folder => acp::ToolKind::Search,
acp_old::Icon::Globe => acp::ToolKind::Search,
acp_old::Icon::Hammer => acp::ToolKind::Other,
acp_old::Icon::LightBulb => acp::ToolKind::Think,
acp_old::Icon::Pencil => acp::ToolKind::Edit,
acp_old::Icon::Regex => acp::ToolKind::Search,
acp_old::Icon::Terminal => acp::ToolKind::Execute,
}
}
fn into_new_tool_call_status(status: acp_old::ToolCallStatus) -> acp::ToolCallStatus {
match status {
acp_old::ToolCallStatus::Running => acp::ToolCallStatus::InProgress,
acp_old::ToolCallStatus::Finished => acp::ToolCallStatus::Completed,
acp_old::ToolCallStatus::Error => acp::ToolCallStatus::Failed,
}
}
fn into_new_tool_call_content(content: acp_old::ToolCallContent) -> acp::ToolCallContent {
match content {
acp_old::ToolCallContent::Markdown { markdown } => markdown.into(),
acp_old::ToolCallContent::Diff { diff } => acp::ToolCallContent::Diff {
diff: into_new_diff(diff),
},
}
}
fn into_new_diff(diff: acp_old::Diff) -> acp::Diff {
acp::Diff {
path: diff.path,
old_text: diff.old_text,
new_text: diff.new_text,
}
}
fn into_new_tool_call_location(location: acp_old::ToolCallLocation) -> acp::ToolCallLocation {
acp::ToolCallLocation {
path: location.path,
line: location.line,
}
}
fn into_new_plan_entry(entry: acp_old::PlanEntry) -> acp::PlanEntry {
acp::PlanEntry {
content: entry.content,
priority: into_new_plan_priority(entry.priority),
status: into_new_plan_status(entry.status),
}
}
fn into_new_plan_priority(priority: acp_old::PlanEntryPriority) -> acp::PlanEntryPriority {
match priority {
acp_old::PlanEntryPriority::Low => acp::PlanEntryPriority::Low,
acp_old::PlanEntryPriority::Medium => acp::PlanEntryPriority::Medium,
acp_old::PlanEntryPriority::High => acp::PlanEntryPriority::High,
}
}
fn into_new_plan_status(status: acp_old::PlanEntryStatus) -> acp::PlanEntryStatus {
match status {
acp_old::PlanEntryStatus::Pending => acp::PlanEntryStatus::Pending,
acp_old::PlanEntryStatus::InProgress => acp::PlanEntryStatus::InProgress,
acp_old::PlanEntryStatus::Completed => acp::PlanEntryStatus::Completed,
}
}
pub struct AcpConnection {
pub name: &'static str,
pub connection: acp_old::AgentConnection,
pub _child_status: Task<Result<()>>,
pub current_thread: Rc<RefCell<WeakEntity<AcpThread>>>,
}
impl AcpConnection {
pub fn stdio(
name: &'static str,
command: AgentServerCommand,
root_dir: &Path,
cx: &mut AsyncApp,
) -> Task<Result<Self>> {
let root_dir = root_dir.to_path_buf();
cx.spawn(async move |cx| {
let mut child = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.current_dir(root_dir)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::inherit())
.kill_on_drop(true)
.spawn()?;
let stdin = child.stdin.take().unwrap();
let stdout = child.stdout.take().unwrap();
log::trace!("Spawned (pid: {})", child.id());
let foreground_executor = cx.foreground_executor().clone();
let thread_rc = Rc::new(RefCell::new(WeakEntity::new_invalid()));
let (connection, io_fut) = acp_old::AgentConnection::connect_to_agent(
OldAcpClientDelegate::new(thread_rc.clone(), cx.clone()),
stdin,
stdout,
move |fut| foreground_executor.spawn(fut).detach(),
);
let io_task = cx.background_spawn(async move {
io_fut.await.log_err();
});
let child_status = cx.background_spawn(async move {
let result = match child.status().await {
Err(e) => Err(anyhow!(e)),
Ok(result) if result.success() => Ok(()),
Ok(result) => Err(anyhow!(result)),
};
drop(io_task);
result
});
Ok(Self {
name,
connection,
_child_status: child_status,
current_thread: thread_rc,
})
})
}
}
impl AgentConnection for AcpConnection {
fn new_thread(
self: Rc<Self>,
project: Entity<Project>,
_cwd: &Path,
cx: &mut App,
) -> Task<Result<Entity<AcpThread>>> {
let task = self.connection.request_any(
acp_old::InitializeParams {
protocol_version: acp_old::ProtocolVersion::latest(),
}
.into_any(),
);
let current_thread = self.current_thread.clone();
cx.spawn(async move |cx| {
let result = task.await?;
let result = acp_old::InitializeParams::response_from_any(result)?;
if !result.is_authenticated {
anyhow::bail!(AuthRequired::new())
}
cx.update(|cx| {
let thread = cx.new(|cx| {
let session_id = acp::SessionId("acp-old-no-id".into());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
AcpThread::new(self.name, self.clone(), project, action_log, session_id)
});
current_thread.replace(thread.downgrade());
thread
})
})
}
fn auth_methods(&self) -> &[acp::AuthMethod] {
&[]
}
fn authenticate(&self, _method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
let task = self
.connection
.request_any(acp_old::AuthenticateParams.into_any());
cx.foreground_executor().spawn(async move {
task.await?;
Ok(())
})
}
fn prompt(
&self,
_id: Option<acp_thread::UserMessageId>,
params: acp::PromptRequest,
cx: &mut App,
) -> Task<Result<acp::PromptResponse>> {
let chunks = params
.prompt
.into_iter()
.filter_map(|block| match block {
acp::ContentBlock::Text(text) => {
Some(acp_old::UserMessageChunk::Text { text: text.text })
}
acp::ContentBlock::ResourceLink(link) => Some(acp_old::UserMessageChunk::Path {
path: link.uri.into(),
}),
_ => None,
})
.collect();
let task = self
.connection
.request_any(acp_old::SendUserMessageParams { chunks }.into_any());
cx.foreground_executor().spawn(async move {
task.await?;
anyhow::Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
})
})
}
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
acp::PromptCapabilities {
image: false,
audio: false,
embedded_context: false,
}
}
fn cancel(&self, _session_id: &acp::SessionId, cx: &mut App) {
let task = self
.connection
.request_any(acp_old::CancelSendMessageParams.into_any());
cx.foreground_executor()
.spawn(async move {
task.await?;
anyhow::Ok(())
})
.detach_and_log_err(cx)
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
}

View File

@@ -0,0 +1,367 @@
use action_log::ActionLog;
use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
use anyhow::anyhow;
use collections::HashMap;
use futures::AsyncBufReadExt as _;
use futures::channel::oneshot;
use futures::io::BufReader;
use project::Project;
use serde::Deserialize;
use std::path::Path;
use std::rc::Rc;
use std::{any::Any, cell::RefCell};
use anyhow::{Context as _, Result};
use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity};
use crate::{AgentServerCommand, acp::UnsupportedVersion};
use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError};
pub struct AcpConnection {
server_name: &'static str,
connection: Rc<acp::ClientSideConnection>,
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
auth_methods: Vec<acp::AuthMethod>,
prompt_capabilities: acp::PromptCapabilities,
_io_task: Task<Result<()>>,
}
pub struct AcpSession {
thread: WeakEntity<AcpThread>,
suppress_abort_err: bool,
}
const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1;
impl AcpConnection {
pub async fn stdio(
server_name: &'static str,
command: AgentServerCommand,
root_dir: &Path,
cx: &mut AsyncApp,
) -> Result<Self> {
let mut child = util::command::new_smol_command(&command.path)
.args(command.args.iter().map(|arg| arg.as_str()))
.envs(command.env.iter().flatten())
.current_dir(root_dir)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.kill_on_drop(true)
.spawn()?;
let stdout = child.stdout.take().context("Failed to take stdout")?;
let stdin = child.stdin.take().context("Failed to take stdin")?;
let stderr = child.stderr.take().context("Failed to take stderr")?;
log::trace!("Spawned (pid: {})", child.id());
let sessions = Rc::new(RefCell::new(HashMap::default()));
let client = ClientDelegate {
sessions: sessions.clone(),
cx: cx.clone(),
};
let (connection, io_task) = acp::ClientSideConnection::new(client, stdin, stdout, {
let foreground_executor = cx.foreground_executor().clone();
move |fut| {
foreground_executor.spawn(fut).detach();
}
});
let io_task = cx.background_spawn(io_task);
cx.background_spawn(async move {
let mut stderr = BufReader::new(stderr);
let mut line = String::new();
while let Ok(n) = stderr.read_line(&mut line).await
&& n > 0
{
log::warn!("agent stderr: {}", &line);
line.clear();
}
})
.detach();
cx.spawn({
let sessions = sessions.clone();
async move |cx| {
let status = child.status().await?;
for session in sessions.borrow().values() {
session
.thread
.update(cx, |thread, cx| {
thread.emit_load_error(LoadError::Exited { status }, cx)
})
.ok();
}
anyhow::Ok(())
}
})
.detach();
let response = connection
.initialize(acp::InitializeRequest {
protocol_version: acp::VERSION,
client_capabilities: acp::ClientCapabilities {
fs: acp::FileSystemCapability {
read_text_file: true,
write_text_file: true,
},
},
})
.await?;
if response.protocol_version < MINIMUM_SUPPORTED_VERSION {
return Err(UnsupportedVersion.into());
}
Ok(Self {
auth_methods: response.auth_methods,
connection: connection.into(),
server_name,
sessions,
prompt_capabilities: response.agent_capabilities.prompt_capabilities,
_io_task: io_task,
})
}
}
impl AgentConnection for AcpConnection {
fn new_thread(
self: Rc<Self>,
project: Entity<Project>,
cwd: &Path,
cx: &mut App,
) -> Task<Result<Entity<AcpThread>>> {
let conn = self.connection.clone();
let sessions = self.sessions.clone();
let cwd = cwd.to_path_buf();
cx.spawn(async move |cx| {
let response = conn
.new_session(acp::NewSessionRequest {
mcp_servers: vec![],
cwd,
})
.await
.map_err(|err| {
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
let mut error = AuthRequired::new();
if err.message != acp::ErrorCode::AUTH_REQUIRED.message {
error = error.with_description(err.message);
}
anyhow!(error)
} else {
anyhow!(err)
}
})?;
let session_id = response.session_id;
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
let thread = cx.new(|_cx| {
AcpThread::new(
self.server_name,
self.clone(),
project,
action_log,
session_id.clone(),
)
})?;
let session = AcpSession {
thread: thread.downgrade(),
suppress_abort_err: false,
};
sessions.borrow_mut().insert(session_id, session);
Ok(thread)
})
}
fn auth_methods(&self) -> &[acp::AuthMethod] {
&self.auth_methods
}
fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
let conn = self.connection.clone();
cx.foreground_executor().spawn(async move {
let result = conn
.authenticate(acp::AuthenticateRequest {
method_id: method_id.clone(),
})
.await?;
Ok(result)
})
}
fn prompt(
&self,
_id: Option<acp_thread::UserMessageId>,
params: acp::PromptRequest,
cx: &mut App,
) -> Task<Result<acp::PromptResponse>> {
let conn = self.connection.clone();
let sessions = self.sessions.clone();
let session_id = params.session_id.clone();
cx.foreground_executor().spawn(async move {
let result = conn.prompt(params).await;
let mut suppress_abort_err = false;
if let Some(session) = sessions.borrow_mut().get_mut(&session_id) {
suppress_abort_err = session.suppress_abort_err;
session.suppress_abort_err = false;
}
match result {
Ok(response) => Ok(response),
Err(err) => {
if err.code != ErrorCode::INTERNAL_ERROR.code {
anyhow::bail!(err)
}
let Some(data) = &err.data else {
anyhow::bail!(err)
};
// Temporary workaround until the following PR is generally available:
// https://github.com/google-gemini/gemini-cli/pull/6656
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct ErrorDetails {
details: Box<str>,
}
match serde_json::from_value(data.clone()) {
Ok(ErrorDetails { details }) => {
if suppress_abort_err && details.contains("This operation was aborted")
{
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Cancelled,
})
} else {
Err(anyhow!(details))
}
}
Err(_) => Err(anyhow!(err)),
}
}
}
})
}
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
self.prompt_capabilities
}
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) {
session.suppress_abort_err = true;
}
let conn = self.connection.clone();
let params = acp::CancelNotification {
session_id: session_id.clone(),
};
cx.foreground_executor()
.spawn(async move { conn.cancel(params).await })
.detach();
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
}
struct ClientDelegate {
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
cx: AsyncApp,
}
impl acp::Client for ClientDelegate {
async fn request_permission(
&self,
arguments: acp::RequestPermissionRequest,
) -> Result<acp::RequestPermissionResponse, acp::Error> {
let cx = &mut self.cx.clone();
let rx = self
.sessions
.borrow()
.get(&arguments.session_id)
.context("Failed to get session")?
.thread
.update(cx, |thread, cx| {
thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx)
})?;
let result = rx?.await;
let outcome = match result {
Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option },
Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled,
};
Ok(acp::RequestPermissionResponse { outcome })
}
async fn write_text_file(
&self,
arguments: acp::WriteTextFileRequest,
) -> Result<(), acp::Error> {
let cx = &mut self.cx.clone();
let task = self
.sessions
.borrow()
.get(&arguments.session_id)
.context("Failed to get session")?
.thread
.update(cx, |thread, cx| {
thread.write_text_file(arguments.path, arguments.content, cx)
})?;
task.await?;
Ok(())
}
async fn read_text_file(
&self,
arguments: acp::ReadTextFileRequest,
) -> Result<acp::ReadTextFileResponse, acp::Error> {
let cx = &mut self.cx.clone();
let task = self
.sessions
.borrow()
.get(&arguments.session_id)
.context("Failed to get session")?
.thread
.update(cx, |thread, cx| {
thread.read_text_file(arguments.path, arguments.line, arguments.limit, false, cx)
})?;
let content = task.await?;
Ok(acp::ReadTextFileResponse { content })
}
async fn session_notification(
&self,
notification: acp::SessionNotification,
) -> Result<(), acp::Error> {
let cx = &mut self.cx.clone();
let sessions = self.sessions.borrow();
let session = sessions
.get(&notification.session_id)
.context("Failed to get session")?;
session.thread.update(cx, |thread, cx| {
thread.handle_session_update(notification.update, cx)
})??;
Ok(())
}
}

View File

@@ -1,6 +1,5 @@
mod acp;
mod claude;
mod custom;
mod gemini;
mod settings;
@@ -8,7 +7,6 @@ mod settings;
pub mod e2e_tests;
pub use claude::*;
pub use custom::*;
pub use gemini::*;
pub use settings::*;
@@ -33,10 +31,9 @@ pub fn init(cx: &mut App) {
pub trait AgentServer: Send {
fn logo(&self) -> ui::IconName;
fn name(&self) -> SharedString;
fn empty_state_headline(&self) -> SharedString;
fn empty_state_message(&self) -> SharedString;
fn telemetry_id(&self) -> &'static str;
fn name(&self) -> &'static str;
fn empty_state_headline(&self) -> &'static str;
fn empty_state_message(&self) -> &'static str;
fn connect(
&self,
@@ -46,8 +43,6 @@ pub trait AgentServer: Send {
) -> Task<Result<Rc<dyn AgentConnection>>>;
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
fn install_command(&self) -> Option<&'static str>;
}
impl dyn AgentServer {
@@ -100,7 +95,7 @@ pub struct AgentServerCommand {
}
impl AgentServerCommand {
pub async fn resolve(
pub(crate) async fn resolve(
path_bin_name: &'static str,
extra_args: &[&'static str],
fallback_path: Option<&Path>,

View File

@@ -30,7 +30,7 @@ use futures::{
io::BufReader,
select_biased,
};
use gpui::{App, AppContext, AsyncApp, Entity, SharedString, Task, WeakEntity};
use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};
use serde::{Deserialize, Serialize};
use util::{ResultExt, debug_panic};
@@ -43,30 +43,22 @@ use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError, MentionUri
pub struct ClaudeCode;
impl AgentServer for ClaudeCode {
fn telemetry_id(&self) -> &'static str {
"claude-code"
fn name(&self) -> &'static str {
"Claude Code"
}
fn name(&self) -> SharedString {
"Claude Code".into()
}
fn empty_state_headline(&self) -> SharedString {
fn empty_state_headline(&self) -> &'static str {
self.name()
}
fn empty_state_message(&self) -> SharedString {
"How can I help you today?".into()
fn empty_state_message(&self) -> &'static str {
"How can I help you today?"
}
fn logo(&self) -> ui::IconName {
ui::IconName::AiClaude
}
fn install_command(&self) -> Option<&'static str> {
Some("npm install -g @anthropic-ai/claude-code@latest")
}
fn connect(
&self,
_root_dir: &Path,
@@ -112,7 +104,11 @@ impl AgentConnection for ClaudeAgentConnection {
)
.await
else {
return Err(LoadError::NotInstalled.into());
return Err(LoadError::NotInstalled {
error_message: "Failed to find Claude Code binary".into(),
install_message: "Install Claude Code".into(),
install_command: "npm install -g @anthropic-ai/claude-code@latest".into(),
}.into());
};
let api_key =
@@ -230,8 +226,17 @@ impl AgentConnection for ClaudeAgentConnection {
|| !help.contains("--session-id"))
{
LoadError::Unsupported {
command: command.path.to_string_lossy().to_string().into(),
current_version: version.to_string().into(),
error_message: format!(
"Your installed version of Claude Code ({}, version {}) does not have required features for use with Zed.",
command.path.to_string_lossy(),
version,
)
.into(),
upgrade_message: "Upgrade Claude Code to latest".into(),
upgrade_command: format!(
"{} update",
command.path.to_string_lossy()
),
}
} else {
LoadError::Exited { status }
@@ -244,19 +249,13 @@ impl AgentConnection for ClaudeAgentConnection {
});
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
let thread = cx.new(|cx| {
let thread = cx.new(|_cx| {
AcpThread::new(
"Claude Code",
self.clone(),
project,
action_log,
session_id.clone(),
watch::Receiver::constant(acp::PromptCapabilities {
image: true,
audio: false,
embedded_context: true,
}),
cx,
)
})?;
@@ -320,6 +319,14 @@ impl AgentConnection for ClaudeAgentConnection {
cx.foreground_executor().spawn(async move { end_rx.await? })
}
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
acp::PromptCapabilities {
image: true,
audio: false,
embedded_context: true,
}
}
fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) {
let sessions = self.sessions.borrow();
let Some(session) = sessions.get(session_id) else {

View File

@@ -1,67 +0,0 @@
use crate::{AgentServerCommand, AgentServerSettings};
use acp_thread::AgentConnection;
use anyhow::Result;
use gpui::{App, Entity, SharedString, Task};
use project::Project;
use std::{path::Path, rc::Rc};
use ui::IconName;
/// A generic agent server implementation for custom user-defined agents
pub struct CustomAgentServer {
name: SharedString,
command: AgentServerCommand,
}
impl CustomAgentServer {
pub fn new(name: SharedString, settings: &AgentServerSettings) -> Self {
Self {
name,
command: settings.command.clone(),
}
}
}
impl crate::AgentServer for CustomAgentServer {
fn telemetry_id(&self) -> &'static str {
"custom"
}
fn name(&self) -> SharedString {
self.name.clone()
}
fn logo(&self) -> IconName {
IconName::Terminal
}
fn empty_state_headline(&self) -> SharedString {
"No conversations yet".into()
}
fn empty_state_message(&self) -> SharedString {
format!("Start a conversation with {}", self.name).into()
}
fn connect(
&self,
root_dir: &Path,
_project: &Entity<Project>,
cx: &mut App,
) -> Task<Result<Rc<dyn AgentConnection>>> {
let server_name = self.name();
let command = self.command.clone();
let root_dir = root_dir.to_path_buf();
cx.spawn(async move |mut cx| {
crate::acp::connect(server_name, command, &root_dir, &mut cx).await
})
}
fn install_command(&self) -> Option<&'static str> {
None
}
fn into_any(self: Rc<Self>) -> Rc<dyn std::any::Any> {
self
}
}

View File

@@ -1,15 +1,17 @@
use crate::AgentServer;
use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
use agent_client_protocol as acp;
use futures::{FutureExt, StreamExt, channel::mpsc, select};
use gpui::{AppContext, Entity, TestAppContext};
use indoc::indoc;
use project::{FakeFs, Project};
use std::{
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use crate::AgentServer;
use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
use agent_client_protocol as acp;
use futures::{FutureExt, StreamExt, channel::mpsc, select};
use gpui::{AppContext, Entity, TestAppContext};
use indoc::indoc;
use project::{FakeFs, Project};
use util::path;
pub async fn test_basic<T, F>(server: F, cx: &mut TestAppContext)
@@ -477,7 +479,6 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
gemini: Some(crate::AgentServerSettings {
command: crate::gemini::tests::local_command(),
}),
custom: collections::HashMap::default(),
},
cx,
);

View File

@@ -1,14 +1,14 @@
use std::rc::Rc;
use std::{any::Any, path::Path};
use crate::acp::AcpConnection;
use crate::{AgentServer, AgentServerCommand};
use acp_thread::{AgentConnection, LoadError};
use anyhow::Result;
use gpui::{App, Entity, SharedString, Task};
use gpui::{Entity, Task};
use language_models::provider::google::GoogleLanguageModelProvider;
use project::Project;
use settings::SettingsStore;
use ui::App;
use crate::AllAgentServersSettings;
@@ -18,30 +18,22 @@ pub struct Gemini;
const ACP_ARG: &str = "--experimental-acp";
impl AgentServer for Gemini {
fn telemetry_id(&self) -> &'static str {
"gemini-cli"
fn name(&self) -> &'static str {
"Gemini CLI"
}
fn name(&self) -> SharedString {
"Gemini CLI".into()
fn empty_state_headline(&self) -> &'static str {
"Welcome to Gemini CLI"
}
fn empty_state_headline(&self) -> SharedString {
self.name()
}
fn empty_state_message(&self) -> SharedString {
"Ask questions, edit files, run commands".into()
fn empty_state_message(&self) -> &'static str {
"Ask questions, edit files, run commands"
}
fn logo(&self) -> ui::IconName {
ui::IconName::AiGemini
}
fn install_command(&self) -> Option<&'static str> {
Some("npm install --engine-strict -g @google/gemini-cli@latest")
}
fn connect(
&self,
root_dir: &Path,
@@ -57,73 +49,48 @@ impl AgentServer for Gemini {
})?;
let Some(mut command) =
AgentServerCommand::resolve("gemini", &[ACP_ARG], None, settings, &project, cx)
.await
AgentServerCommand::resolve("gemini", &[ACP_ARG], None, settings, &project, cx).await
else {
return Err(LoadError::NotInstalled.into());
return Err(LoadError::NotInstalled {
error_message: "Failed to find Gemini CLI binary".into(),
install_message: "Install Gemini CLI".into(),
install_command: "npm install -g @google/gemini-cli@preview".into()
}.into());
};
if let Some(api_key) = cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() {
command
.env
.get_or_insert_default()
.insert("GEMINI_API_KEY".to_owned(), api_key.key);
if let Some(api_key)= cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() {
command.env.get_or_insert_default().insert("GEMINI_API_KEY".to_owned(), api_key.key);
}
let result = crate::acp::connect(server_name, command.clone(), &root_dir, cx).await;
match &result {
Ok(connection) => {
if let Some(connection) = connection.clone().downcast::<AcpConnection>()
&& !connection.prompt_capabilities().image
{
let version_output = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.arg("--version")
.kill_on_drop(true)
.output()
.await;
let current_version =
String::from_utf8(version_output?.stdout)?.trim().to_owned();
if !connection.prompt_capabilities().image {
return Err(LoadError::Unsupported {
current_version: current_version.into(),
command: format!(
"{} {}",
command.path.to_string_lossy(),
command.args.join(" ")
)
.into(),
}
.into());
}
}
}
Err(_) => {
let version_fut = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.arg("--version")
.kill_on_drop(true)
.output();
if result.is_err() {
let version_fut = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.arg("--version")
.kill_on_drop(true)
.output();
let help_fut = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.arg("--help")
.kill_on_drop(true)
.output();
let help_fut = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.arg("--help")
.kill_on_drop(true)
.output();
let (version_output, help_output) =
futures::future::join(version_fut, help_fut).await;
let (version_output, help_output) = futures::future::join(version_fut, help_fut).await;
let current_version = String::from_utf8(version_output?.stdout)?;
let supported = String::from_utf8(help_output?.stdout)?.contains(ACP_ARG);
let current_version = String::from_utf8(version_output?.stdout)?;
let supported = String::from_utf8(help_output?.stdout)?.contains(ACP_ARG);
if !supported {
return Err(LoadError::Unsupported {
current_version: current_version.into(),
command: command.path.to_string_lossy().to_string().into(),
}
.into());
}
if !supported {
return Err(LoadError::Unsupported {
error_message: format!(
"Your installed version of Gemini CLI ({}, version {}) doesn't support the Agentic Coding Protocol (ACP).",
command.path.to_string_lossy(),
current_version
).into(),
upgrade_message: "Upgrade Gemini CLI to latest".into(),
upgrade_command: "npm install -g @google/gemini-cli@preview".into(),
}.into())
}
}
result
@@ -135,20 +102,6 @@ impl AgentServer for Gemini {
}
}
impl Gemini {
pub fn binary_name() -> &'static str {
"gemini"
}
pub fn install_command() -> &'static str {
"npm install --engine-strict -g @google/gemini-cli@latest"
}
pub fn upgrade_command() -> &'static str {
"npm install -g @google/gemini-cli@latest"
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;

View File

@@ -1,7 +1,6 @@
use crate::AgentServerCommand;
use anyhow::Result;
use collections::HashMap;
use gpui::{App, SharedString};
use gpui::App;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
@@ -14,13 +13,9 @@ pub fn init(cx: &mut App) {
pub struct AllAgentServersSettings {
pub gemini: Option<AgentServerSettings>,
pub claude: Option<AgentServerSettings>,
/// Custom agent servers configured by the user
#[serde(flatten)]
pub custom: HashMap<SharedString, AgentServerSettings>,
}
#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)]
pub struct AgentServerSettings {
#[serde(flatten)]
pub command: AgentServerCommand,
@@ -34,26 +29,13 @@ impl settings::Settings for AllAgentServersSettings {
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
let mut settings = AllAgentServersSettings::default();
for AllAgentServersSettings {
gemini,
claude,
custom,
} in sources.defaults_and_customizations()
{
for AllAgentServersSettings { gemini, claude } in sources.defaults_and_customizations() {
if gemini.is_some() {
settings.gemini = gemini.clone();
}
if claude.is_some() {
settings.claude = claude.clone();
}
// Merge custom agents
for (name, config) in custom {
// Skip built-in agent names to avoid conflicts
if name != "gemini" && name != "claude" {
settings.custom.insert(name.clone(), config.clone());
}
}
}
Ok(settings)

View File

@@ -67,7 +67,6 @@ ordered-float.workspace = true
parking_lot.workspace = true
paths.workspace = true
picker.workspace = true
postage.workspace = true
project.workspace = true
prompt_store.workspace = true
proto.workspace = true

View File

@@ -247,9 +247,9 @@ impl ContextPickerCompletionProvider {
let abs_path = project.read(cx).absolute_path(&symbol.path, cx)?;
let uri = MentionUri::Symbol {
abs_path,
path: abs_path,
name: symbol.name.clone(),
line_range: symbol.range.start.0.row..=symbol.range.end.0.row,
line_range: symbol.range.start.0.row..symbol.range.end.0.row,
};
let new_text = format!("{} ", uri.as_link());
let new_text_len = new_text.len();
@@ -805,7 +805,7 @@ pub(crate) fn search_threads(
history_store: &Entity<HistoryStore>,
cx: &mut App,
) -> Task<Vec<HistoryEntry>> {
let threads = history_store.read(cx).entries().collect();
let threads = history_store.read(cx).entries(cx);
if query.is_empty() {
return Task::ready(threads);
}

View File

@@ -1,12 +1,12 @@
use std::{cell::Cell, ops::Range, rc::Rc};
use std::ops::Range;
use acp_thread::{AcpThread, AgentThreadEntry};
use agent_client_protocol::{PromptCapabilities, ToolCallId};
use agent_client_protocol::ToolCallId;
use agent2::HistoryStore;
use collections::HashMap;
use editor::{Editor, EditorMode, MinimapVisibility};
use gpui::{
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable, ScrollHandle,
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable,
TextStyleRefinement, WeakEntity, Window,
};
use language::language_settings::SoftWrap;
@@ -27,7 +27,6 @@ pub struct EntryViewState {
prompt_store: Option<Entity<PromptStore>>,
entries: Vec<Entry>,
prevent_slash_commands: bool,
prompt_capabilities: Rc<Cell<PromptCapabilities>>,
}
impl EntryViewState {
@@ -36,7 +35,6 @@ impl EntryViewState {
project: Entity<Project>,
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
prompt_capabilities: Rc<Cell<PromptCapabilities>>,
prevent_slash_commands: bool,
) -> Self {
Self {
@@ -46,7 +44,6 @@ impl EntryViewState {
prompt_store,
entries: Vec::new(),
prevent_slash_commands,
prompt_capabilities,
}
}
@@ -84,7 +81,6 @@ impl EntryViewState {
self.project.clone(),
self.history_store.clone(),
self.prompt_store.clone(),
self.prompt_capabilities.clone(),
"Edit message @ to include context",
self.prevent_slash_commands,
editor::EditorMode::AutoHeight {
@@ -154,22 +150,10 @@ impl EntryViewState {
});
}
}
AgentThreadEntry::AssistantMessage(message) => {
let entry = if let Some(Entry::AssistantMessage(entry)) =
self.entries.get_mut(index)
{
entry
} else {
self.set_entry(
index,
Entry::AssistantMessage(AssistantMessageEntry::default()),
);
let Some(Entry::AssistantMessage(entry)) = self.entries.get_mut(index) else {
unreachable!()
};
entry
};
entry.sync(message);
AgentThreadEntry::AssistantMessage(_) => {
if index == self.entries.len() {
self.entries.push(Entry::empty())
}
}
};
}
@@ -189,7 +173,7 @@ impl EntryViewState {
pub fn settings_changed(&mut self, cx: &mut App) {
for entry in self.entries.iter() {
match entry {
Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}
Entry::UserMessage { .. } => {}
Entry::Content(response_views) => {
for view in response_views.values() {
if let Ok(diff_editor) = view.clone().downcast::<Editor>() {
@@ -220,29 +204,9 @@ pub enum ViewEvent {
MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent),
}
#[derive(Default, Debug)]
pub struct AssistantMessageEntry {
scroll_handles_by_chunk_index: HashMap<usize, ScrollHandle>,
}
impl AssistantMessageEntry {
pub fn scroll_handle_for_chunk(&self, ix: usize) -> Option<ScrollHandle> {
self.scroll_handles_by_chunk_index.get(&ix).cloned()
}
pub fn sync(&mut self, message: &acp_thread::AssistantMessage) {
if let Some(acp_thread::AssistantMessageChunk::Thought { .. }) = message.chunks.last() {
let ix = message.chunks.len() - 1;
let handle = self.scroll_handles_by_chunk_index.entry(ix).or_default();
handle.scroll_to_bottom();
}
}
}
#[derive(Debug)]
pub enum Entry {
UserMessage(Entity<MessageEditor>),
AssistantMessage(AssistantMessageEntry),
Content(HashMap<EntityId, AnyEntity>),
}
@@ -250,7 +214,7 @@ impl Entry {
pub fn message_editor(&self) -> Option<&Entity<MessageEditor>> {
match self {
Self::UserMessage(editor) => Some(editor),
Self::AssistantMessage(_) | Self::Content(_) => None,
Entry::Content(_) => None,
}
}
@@ -271,16 +235,6 @@ impl Entry {
.map(|entity| entity.downcast::<TerminalView>().unwrap())
}
pub fn scroll_handle_for_assistant_message_chunk(
&self,
chunk_ix: usize,
) -> Option<ScrollHandle> {
match self {
Self::AssistantMessage(message) => message.scroll_handle_for_chunk(chunk_ix),
Self::UserMessage(_) | Self::Content(_) => None,
}
}
fn content_map(&self) -> Option<&HashMap<EntityId, AnyEntity>> {
match self {
Self::Content(map) => Some(map),
@@ -296,7 +250,7 @@ impl Entry {
pub fn has_content(&self) -> bool {
match self {
Self::Content(map) => !map.is_empty(),
Self::UserMessage(_) | Self::AssistantMessage(_) => false,
Self::UserMessage(_) => false,
}
}
}
@@ -449,7 +403,6 @@ mod tests {
project.clone(),
history_store,
None,
Default::default(),
false,
)
});

File diff suppressed because it is too large Load Diff

View File

@@ -3,18 +3,18 @@ use crate::{AgentPanel, RemoveSelectedThread};
use agent2::{HistoryEntry, HistoryStore};
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
use editor::{Editor, EditorEvent};
use fuzzy::StringMatchCandidate;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Stateful, Task,
App, Empty, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Stateful, Task,
UniformListScrollHandle, WeakEntity, Window, uniform_list,
};
use std::{fmt::Display, ops::Range};
use text::Bias;
use std::{fmt::Display, ops::Range, sync::Arc};
use time::{OffsetDateTime, UtcOffset};
use ui::{
HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState,
Tooltip, prelude::*,
};
use util::ResultExt;
pub struct AcpThreadHistory {
pub(crate) history_store: Entity<HistoryStore>,
@@ -22,38 +22,38 @@ pub struct AcpThreadHistory {
selected_index: usize,
hovered_index: Option<usize>,
search_editor: Entity<Editor>,
search_query: SharedString,
visible_items: Vec<ListItemType>,
all_entries: Arc<Vec<HistoryEntry>>,
// When the search is empty, we display date separators between history entries
// This vector contains an enum of either a separator or an actual entry
separated_items: Vec<ListItemType>,
// Maps entry indexes to list item indexes
separated_item_indexes: Vec<u32>,
_separated_items_task: Option<Task<()>>,
search_state: SearchState,
scrollbar_visibility: bool,
scrollbar_state: ScrollbarState,
local_timezone: UtcOffset,
_update_task: Task<()>,
_subscriptions: Vec<gpui::Subscription>,
}
enum SearchState {
Empty,
Searching {
query: SharedString,
_task: Task<()>,
},
Searched {
query: SharedString,
matches: Vec<StringMatch>,
},
}
enum ListItemType {
BucketSeparator(TimeBucket),
Entry {
entry: HistoryEntry,
index: usize,
format: EntryTimeFormat,
},
SearchResult {
entry: HistoryEntry,
positions: Vec<usize>,
},
}
impl ListItemType {
fn history_entry(&self) -> Option<&HistoryEntry> {
match self {
ListItemType::Entry { entry, .. } => Some(entry),
ListItemType::SearchResult { entry, .. } => Some(entry),
_ => None,
}
}
}
pub enum ThreadHistoryEvent {
@@ -78,15 +78,12 @@ impl AcpThreadHistory {
cx.subscribe(&search_editor, |this, search_editor, event, cx| {
if let EditorEvent::BufferEdited = event {
let query = search_editor.read(cx).text(cx);
if this.search_query != query {
this.search_query = query.into();
this.update_visible_items(false, cx);
}
this.search(query.into(), cx);
}
});
let history_store_subscription = cx.observe(&history_store, |this, _, cx| {
this.update_visible_items(true, cx);
this.update_all_entries(cx);
});
let scroll_handle = UniformListScrollHandle::default();
@@ -97,7 +94,10 @@ impl AcpThreadHistory {
scroll_handle,
selected_index: 0,
hovered_index: None,
visible_items: Default::default(),
search_state: SearchState::Empty,
all_entries: Default::default(),
separated_items: Default::default(),
separated_item_indexes: Default::default(),
search_editor,
scrollbar_visibility: true,
scrollbar_state,
@@ -105,61 +105,29 @@ impl AcpThreadHistory {
chrono::Local::now().offset().local_minus_utc(),
)
.unwrap(),
search_query: SharedString::default(),
_subscriptions: vec![search_editor_subscription, history_store_subscription],
_update_task: Task::ready(()),
_separated_items_task: None,
};
this.update_visible_items(false, cx);
this.update_all_entries(cx);
this
}
fn update_visible_items(&mut self, preserve_selected_item: bool, cx: &mut Context<Self>) {
let entries = self
fn update_all_entries(&mut self, cx: &mut Context<Self>) {
let new_entries: Arc<Vec<HistoryEntry>> = self
.history_store
.update(cx, |store, _| store.entries().collect());
let new_list_items = if self.search_query.is_empty() {
self.add_list_separators(entries, cx)
} else {
self.filter_search_results(entries, cx)
};
let selected_history_entry = if preserve_selected_item {
self.selected_history_entry().cloned()
} else {
None
};
.update(cx, |store, cx| store.entries(cx))
.into();
self._update_task = cx.spawn(async move |this, cx| {
let new_visible_items = new_list_items.await;
this.update(cx, |this, cx| {
let new_selected_index = if let Some(history_entry) = selected_history_entry {
let history_entry_id = history_entry.id();
new_visible_items
.iter()
.position(|visible_entry| {
visible_entry
.history_entry()
.is_some_and(|entry| entry.id() == history_entry_id)
})
.unwrap_or(0)
} else {
0
};
self._separated_items_task.take();
this.visible_items = new_visible_items;
this.set_selected_index(new_selected_index, Bias::Right, cx);
cx.notify();
})
.ok();
});
}
let mut items = Vec::with_capacity(new_entries.len() + 1);
let mut indexes = Vec::with_capacity(new_entries.len() + 1);
fn add_list_separators(&self, entries: Vec<HistoryEntry>, cx: &App) -> Task<Vec<ListItemType>> {
cx.background_spawn(async move {
let mut items = Vec::with_capacity(entries.len() + 1);
let bg_task = cx.background_spawn(async move {
let mut bucket = None;
let today = Local::now().naive_local().date();
for entry in entries.into_iter() {
for (index, entry) in new_entries.iter().enumerate() {
let entry_date = entry
.updated_at()
.with_timezone(&Local)
@@ -172,33 +140,75 @@ impl AcpThreadHistory {
items.push(ListItemType::BucketSeparator(entry_bucket));
}
indexes.push(items.len() as u32);
items.push(ListItemType::Entry {
entry,
index,
format: entry_bucket.into(),
});
}
items
})
(new_entries, items, indexes)
});
let task = cx.spawn(async move |this, cx| {
let (new_entries, items, indexes) = bg_task.await;
this.update(cx, |this, cx| {
let previously_selected_entry =
this.all_entries.get(this.selected_index).map(|e| e.id());
this.all_entries = new_entries;
this.separated_items = items;
this.separated_item_indexes = indexes;
match &this.search_state {
SearchState::Empty => {
if this.selected_index >= this.all_entries.len() {
this.set_selected_entry_index(
this.all_entries.len().saturating_sub(1),
cx,
);
} else if let Some(prev_id) = previously_selected_entry
&& let Some(new_ix) = this
.all_entries
.iter()
.position(|probe| probe.id() == prev_id)
{
this.set_selected_entry_index(new_ix, cx);
}
}
SearchState::Searching { query, .. } | SearchState::Searched { query, .. } => {
this.search(query.clone(), cx);
}
}
cx.notify();
})
.log_err();
});
self._separated_items_task = Some(task);
}
fn filter_search_results(
&self,
entries: Vec<HistoryEntry>,
cx: &App,
) -> Task<Vec<ListItemType>> {
let query = self.search_query.clone();
cx.background_spawn({
fn search(&mut self, query: SharedString, cx: &mut Context<Self>) {
if query.is_empty() {
self.search_state = SearchState::Empty;
cx.notify();
return;
}
let all_entries = self.all_entries.clone();
let fuzzy_search_task = cx.background_spawn({
let query = query.clone();
let executor = cx.background_executor().clone();
async move {
let mut candidates = Vec::with_capacity(entries.len());
let mut candidates = Vec::with_capacity(all_entries.len());
for (idx, entry) in entries.iter().enumerate() {
for (idx, entry) in all_entries.iter().enumerate() {
candidates.push(StringMatchCandidate::new(idx, entry.title()));
}
const MAX_MATCHES: usize = 100;
let matches = fuzzy::match_strings(
fuzzy::match_strings(
&candidates,
&query,
false,
@@ -207,61 +217,74 @@ impl AcpThreadHistory {
&Default::default(),
executor,
)
.await;
matches
.into_iter()
.map(|search_match| ListItemType::SearchResult {
entry: entries[search_match.candidate_id].clone(),
positions: search_match.positions,
})
.collect()
.await
}
})
});
let task = cx.spawn({
let query = query.clone();
async move |this, cx| {
let matches = fuzzy_search_task.await;
this.update(cx, |this, cx| {
let SearchState::Searching {
query: current_query,
_task,
} = &this.search_state
else {
return;
};
if &query == current_query {
this.search_state = SearchState::Searched {
query: query.clone(),
matches,
};
this.set_selected_entry_index(0, cx);
cx.notify();
};
})
.log_err();
}
});
self.search_state = SearchState::Searching { query, _task: task };
cx.notify();
}
fn matched_count(&self) -> usize {
match &self.search_state {
SearchState::Empty => self.all_entries.len(),
SearchState::Searching { .. } => 0,
SearchState::Searched { matches, .. } => matches.len(),
}
}
fn list_item_count(&self) -> usize {
match &self.search_state {
SearchState::Empty => self.separated_items.len(),
SearchState::Searching { .. } => 0,
SearchState::Searched { matches, .. } => matches.len(),
}
}
fn search_produced_no_matches(&self) -> bool {
self.visible_items.is_empty() && !self.search_query.is_empty()
}
fn selected_history_entry(&self) -> Option<&HistoryEntry> {
self.get_history_entry(self.selected_index)
}
fn get_history_entry(&self, visible_items_ix: usize) -> Option<&HistoryEntry> {
self.visible_items.get(visible_items_ix)?.history_entry()
}
fn set_selected_index(&mut self, mut index: usize, bias: Bias, cx: &mut Context<Self>) {
if self.visible_items.len() == 0 {
self.selected_index = 0;
return;
match &self.search_state {
SearchState::Empty => false,
SearchState::Searching { .. } => false,
SearchState::Searched { matches, .. } => matches.is_empty(),
}
while matches!(
self.visible_items.get(index),
None | Some(ListItemType::BucketSeparator(..))
) {
index = match bias {
Bias::Left => {
if index == 0 {
self.visible_items.len() - 1
} else {
index - 1
}
}
Bias::Right => {
if index >= self.visible_items.len() - 1 {
0
} else {
index + 1
}
}
};
}
fn get_match(&self, ix: usize) -> Option<&HistoryEntry> {
match &self.search_state {
SearchState::Empty => self.all_entries.get(ix),
SearchState::Searching { .. } => None,
SearchState::Searched { matches, .. } => matches
.get(ix)
.and_then(|m| self.all_entries.get(m.candidate_id)),
}
self.selected_index = index;
self.scroll_handle
.scroll_to_item(index, ScrollStrategy::Top);
cx.notify()
}
pub fn select_previous(
@@ -270,10 +293,13 @@ impl AcpThreadHistory {
_window: &mut Window,
cx: &mut Context<Self>,
) {
if self.selected_index == 0 {
self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx);
} else {
self.set_selected_index(self.selected_index - 1, Bias::Left, cx);
let count = self.matched_count();
if count > 0 {
if self.selected_index == 0 {
self.set_selected_entry_index(count - 1, cx);
} else {
self.set_selected_entry_index(self.selected_index - 1, cx);
}
}
}
@@ -283,10 +309,13 @@ impl AcpThreadHistory {
_window: &mut Window,
cx: &mut Context<Self>,
) {
if self.selected_index == self.visible_items.len() - 1 {
self.set_selected_index(0, Bias::Right, cx);
} else {
self.set_selected_index(self.selected_index + 1, Bias::Right, cx);
let count = self.matched_count();
if count > 0 {
if self.selected_index == count - 1 {
self.set_selected_entry_index(0, cx);
} else {
self.set_selected_entry_index(self.selected_index + 1, cx);
}
}
}
@@ -296,47 +325,35 @@ impl AcpThreadHistory {
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.set_selected_index(0, Bias::Right, cx);
let count = self.matched_count();
if count > 0 {
self.set_selected_entry_index(0, cx);
}
}
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx);
let count = self.matched_count();
if count > 0 {
self.set_selected_entry_index(count - 1, cx);
}
}
fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
self.confirm_entry(self.selected_index, cx);
}
fn set_selected_entry_index(&mut self, entry_index: usize, cx: &mut Context<Self>) {
self.selected_index = entry_index;
fn confirm_entry(&mut self, ix: usize, cx: &mut Context<Self>) {
let Some(entry) = self.get_history_entry(ix) else {
return;
};
cx.emit(ThreadHistoryEvent::Open(entry.clone()));
}
fn remove_selected_thread(
&mut self,
_: &RemoveSelectedThread,
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.remove_thread(self.selected_index, cx)
}
fn remove_thread(&mut self, visible_item_ix: usize, cx: &mut Context<Self>) {
let Some(entry) = self.get_history_entry(visible_item_ix) else {
return;
let scroll_ix = match self.search_state {
SearchState::Empty | SearchState::Searching { .. } => self
.separated_item_indexes
.get(entry_index)
.map(|ix| *ix as usize)
.unwrap_or(entry_index + 1),
SearchState::Searched { .. } => entry_index,
};
let task = match entry {
HistoryEntry::AcpThread(thread) => self
.history_store
.update(cx, |this, cx| this.delete_thread(thread.id.clone(), cx)),
HistoryEntry::TextThread(context) => self.history_store.update(cx, |this, cx| {
this.delete_text_thread(context.path.clone(), cx)
}),
};
task.detach_and_log_err(cx);
self.scroll_handle
.scroll_to_item(scroll_ix, ScrollStrategy::Top);
cx.notify();
}
fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
@@ -376,33 +393,91 @@ impl AcpThreadHistory {
)
}
fn render_list_items(
fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
self.confirm_entry(self.selected_index, cx);
}
fn confirm_entry(&mut self, ix: usize, cx: &mut Context<Self>) {
let Some(entry) = self.get_match(ix) else {
return;
};
cx.emit(ThreadHistoryEvent::Open(entry.clone()));
}
fn remove_selected_thread(
&mut self,
_: &RemoveSelectedThread,
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.remove_thread(self.selected_index, cx)
}
fn remove_thread(&mut self, ix: usize, cx: &mut Context<Self>) {
let Some(entry) = self.get_match(ix) else {
return;
};
let task = match entry {
HistoryEntry::AcpThread(thread) => self
.history_store
.update(cx, |this, cx| this.delete_thread(thread.id.clone(), cx)),
HistoryEntry::TextThread(context) => self.history_store.update(cx, |this, cx| {
this.delete_text_thread(context.path.clone(), cx)
}),
};
task.detach_and_log_err(cx);
}
fn list_items(
&mut self,
range: Range<usize>,
_window: &mut Window,
cx: &mut Context<Self>,
) -> Vec<AnyElement> {
self.visible_items
.get(range.clone())
.into_iter()
.flatten()
.enumerate()
.map(|(ix, item)| self.render_list_item(item, range.start + ix, cx))
.collect()
match &self.search_state {
SearchState::Empty => self
.separated_items
.get(range)
.iter()
.flat_map(|items| {
items
.iter()
.map(|item| self.render_list_item(item, vec![], cx))
})
.collect(),
SearchState::Searched { matches, .. } => matches[range]
.iter()
.filter_map(|m| {
let entry = self.all_entries.get(m.candidate_id)?;
Some(self.render_history_entry(
entry,
EntryTimeFormat::DateAndTime,
m.candidate_id,
m.positions.clone(),
cx,
))
})
.collect(),
SearchState::Searching { .. } => {
vec![]
}
}
}
fn render_list_item(&self, item: &ListItemType, ix: usize, cx: &Context<Self>) -> AnyElement {
fn render_list_item(
&self,
item: &ListItemType,
highlight_positions: Vec<usize>,
cx: &Context<Self>,
) -> AnyElement {
match item {
ListItemType::Entry { entry, format } => self
.render_history_entry(entry, *format, ix, Vec::default(), cx)
.into_any(),
ListItemType::SearchResult { entry, positions } => self.render_history_entry(
entry,
EntryTimeFormat::DateAndTime,
ix,
positions.clone(),
cx,
),
ListItemType::Entry { index, format } => match self.all_entries.get(*index) {
Some(entry) => self
.render_history_entry(entry, *format, *index, highlight_positions, cx)
.into_any(),
None => Empty.into_any_element(),
},
ListItemType::BucketSeparator(bucket) => div()
.px(DynamicSpacing::Base06.rems(cx))
.pt_2()
@@ -420,12 +495,12 @@ impl AcpThreadHistory {
&self,
entry: &HistoryEntry,
format: EntryTimeFormat,
ix: usize,
list_entry_ix: usize,
highlight_positions: Vec<usize>,
cx: &Context<Self>,
) -> AnyElement {
let selected = ix == self.selected_index;
let hovered = Some(ix) == self.hovered_index;
let selected = list_entry_ix == self.selected_index;
let hovered = Some(list_entry_ix) == self.hovered_index;
let timestamp = entry.updated_at().timestamp();
let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
@@ -433,7 +508,7 @@ impl AcpThreadHistory {
.w_full()
.pb_1()
.child(
ListItem::new(ix)
ListItem::new(list_entry_ix)
.rounded()
.toggle_state(selected)
.spacing(ListItemSpacing::Sparse)
@@ -455,14 +530,14 @@ impl AcpThreadHistory {
)
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
if *is_hovered {
this.hovered_index = Some(ix);
} else if this.hovered_index == Some(ix) {
this.hovered_index = Some(list_entry_ix);
} else if this.hovered_index == Some(list_entry_ix) {
this.hovered_index = None;
}
cx.notify();
}))
.end_slot::<IconButton>(if hovered {
.end_slot::<IconButton>(if hovered || selected {
Some(
IconButton::new("delete", IconName::Trash)
.shape(IconButtonShape::Square)
@@ -471,14 +546,16 @@ impl AcpThreadHistory {
.tooltip(move |window, cx| {
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
})
.on_click(
cx.listener(move |this, _, _, cx| this.remove_thread(ix, cx)),
),
.on_click(cx.listener(move |this, _, _, cx| {
this.remove_thread(list_entry_ix, cx)
})),
)
} else {
None
})
.on_click(cx.listener(move |this, _, _, cx| this.confirm_entry(ix, cx))),
.on_click(
cx.listener(move |this, _, _, cx| this.confirm_entry(list_entry_ix, cx)),
),
)
.into_any_element()
}
@@ -501,7 +578,7 @@ impl Render for AcpThreadHistory {
.on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::remove_selected_thread))
.when(!self.history_store.read(cx).is_empty(cx), |parent| {
.when(!self.all_entries.is_empty(), |parent| {
parent.child(
h_flex()
.h(px(41.)) // Match the toolbar perfectly
@@ -527,7 +604,7 @@ impl Render for AcpThreadHistory {
.overflow_hidden()
.flex_grow();
if self.history_store.read(cx).is_empty(cx) {
if self.all_entries.is_empty() {
view.justify_center()
.child(
h_flex().w_full().justify_center().child(
@@ -546,9 +623,9 @@ impl Render for AcpThreadHistory {
.child(
uniform_list(
"thread-history",
self.visible_items.len(),
self.list_item_count(),
cx.processor(|this, range: Range<usize>, window, cx| {
this.render_list_items(range, window, cx)
this.list_items(range, window, cx)
}),
)
.p_1()

File diff suppressed because it is too large Load Diff

View File

@@ -1595,6 +1595,11 @@ impl ActiveThread {
return;
};
if model.provider.must_accept_terms(cx) {
cx.notify();
return;
}
let edited_text = state.editor.read(cx).text(cx);
let creases = state.editor.update(cx, extract_message_creases);

View File

@@ -3,23 +3,19 @@ mod configure_context_server_modal;
mod manage_profiles_modal;
mod tool_picker;
use std::{ops::Range, sync::Arc, time::Duration};
use std::{sync::Arc, time::Duration};
use agent_servers::{AgentServerCommand, AgentServerSettings, AllAgentServersSettings, Gemini};
use agent_settings::AgentSettings;
use anyhow::Result;
use assistant_tool::{ToolSource, ToolWorkingSet};
use cloud_llm_client::Plan;
use collections::HashMap;
use context_server::ContextServerId;
use editor::{Editor, SelectionEffects, scroll::Autoscroll};
use extension::ExtensionManifest;
use extension_host::ExtensionStore;
use fs::Fs;
use gpui::{
Action, Animation, AnimationExt as _, AnyView, App, AsyncWindowContext, Corner, Entity,
EventEmitter, FocusHandle, Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation,
WeakEntity, percentage,
Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle,
Focusable, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage,
};
use language::LanguageRegistry;
use language_model::{
@@ -27,24 +23,23 @@ use language_model::{
};
use notifications::status_toast::{StatusToast, ToastIcon};
use project::{
Project,
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
project_settings::{ContextServerSettings, ProjectSettings},
};
use settings::{Settings, SettingsStore, update_settings_file};
use settings::{Settings, update_settings_file};
use ui::{
Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*,
};
use util::ResultExt as _;
use workspace::{Workspace, create_and_open_local_file};
use workspace::Workspace;
use zed_actions::ExtensionCategoryFilter;
pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
pub(crate) use manage_profiles_modal::ManageProfilesModal;
use crate::{
AddContextServer, ExternalAgent, NewExternalAgentThread,
AddContextServer,
agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider},
};
@@ -52,7 +47,6 @@ pub struct AgentConfiguration {
fs: Arc<dyn Fs>,
language_registry: Arc<LanguageRegistry>,
workspace: WeakEntity<Workspace>,
project: WeakEntity<Project>,
focus_handle: FocusHandle,
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
context_server_store: Entity<ContextServerStore>,
@@ -62,8 +56,6 @@ pub struct AgentConfiguration {
_registry_subscription: Subscription,
scroll_handle: ScrollHandle,
scrollbar_state: ScrollbarState,
gemini_is_installed: bool,
_check_for_gemini: Task<()>,
}
impl AgentConfiguration {
@@ -73,7 +65,6 @@ impl AgentConfiguration {
tools: Entity<ToolWorkingSet>,
language_registry: Arc<LanguageRegistry>,
workspace: WeakEntity<Workspace>,
project: WeakEntity<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -98,34 +89,33 @@ impl AgentConfiguration {
cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify())
.detach();
cx.observe_global_in::<SettingsStore>(window, |this, _, cx| {
this.check_for_gemini(cx);
cx.notify();
})
.detach();
let scroll_handle = ScrollHandle::new();
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
let mut expanded_provider_configurations = HashMap::default();
if LanguageModelRegistry::read_global(cx)
.provider(&ZED_CLOUD_PROVIDER_ID)
.is_some_and(|cloud_provider| cloud_provider.must_accept_terms(cx))
{
expanded_provider_configurations.insert(ZED_CLOUD_PROVIDER_ID, true);
}
let mut this = Self {
fs,
language_registry,
workspace,
project,
focus_handle,
configuration_views_by_provider: HashMap::default(),
context_server_store,
expanded_context_server_tools: HashMap::default(),
expanded_provider_configurations: HashMap::default(),
expanded_provider_configurations,
tools,
_registry_subscription: registry_subscription,
scroll_handle,
scrollbar_state,
gemini_is_installed: false,
_check_for_gemini: Task::ready(()),
};
this.build_provider_configuration_views(window, cx);
this.check_for_gemini(cx);
this
}
@@ -155,34 +145,6 @@ impl AgentConfiguration {
self.configuration_views_by_provider
.insert(provider.id(), configuration_view);
}
fn check_for_gemini(&mut self, cx: &mut Context<Self>) {
let project = self.project.clone();
let settings = AllAgentServersSettings::get_global(cx).clone();
self._check_for_gemini = cx.spawn({
async move |this, cx| {
let Some(project) = project.upgrade() else {
return;
};
let gemini_is_installed = AgentServerCommand::resolve(
Gemini::binary_name(),
&[],
// TODO expose fallback path from the Gemini/CC types so we don't have to hardcode it again here
None,
settings.gemini,
&project,
cx,
)
.await
.is_some();
this.update(cx, |this, cx| {
this.gemini_is_installed = gemini_is_installed;
cx.notify();
})
.ok();
}
});
}
}
impl Focusable for AgentConfiguration {
@@ -230,7 +192,7 @@ impl AgentConfiguration {
let is_signed_in = self
.workspace
.read_with(cx, |workspace, _| {
workspace.client().status().borrow().is_connected()
!workspace.client().status().borrow().is_signed_out()
})
.unwrap_or(false);
@@ -257,6 +219,7 @@ impl AgentConfiguration {
.child(
h_flex()
.id(provider_id_string.clone())
.cursor_pointer()
.px_2()
.py_0p5()
.w_full()
@@ -276,7 +239,10 @@ impl AgentConfiguration {
h_flex()
.w_full()
.gap_1()
.child(Label::new(provider_name.clone()))
.child(
Label::new(provider_name.clone())
.size(LabelSize::Large),
)
.map(|this| {
if is_zed_provider && is_signed_in {
this.child(
@@ -321,7 +287,7 @@ impl AgentConfiguration {
"Start New Thread",
)
.icon_position(IconPosition::Start)
.icon(IconName::Thread)
.icon(IconName::Plus)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.label_size(LabelSize::Small)
@@ -420,7 +386,7 @@ impl AgentConfiguration {
),
)
.child(
Label::new("Add at least one provider to use AI-powered features with Zed's native agent.")
Label::new("Add at least one provider to use AI-powered features.")
.color(Color::Muted),
),
),
@@ -561,14 +527,6 @@ impl AgentConfiguration {
}
}
fn card_item_bg_color(&self, cx: &mut Context<Self>) -> Hsla {
cx.theme().colors().background.opacity(0.25)
}
fn card_item_border_color(&self, cx: &mut Context<Self>) -> Hsla {
cx.theme().colors().border.opacity(0.6)
}
fn render_context_servers_section(
&mut self,
window: &mut Window,
@@ -586,12 +544,7 @@ impl AgentConfiguration {
v_flex()
.gap_0p5()
.child(Headline::new("Model Context Protocol (MCP) Servers"))
.child(
Label::new(
"All context servers connected through the Model Context Protocol.",
)
.color(Color::Muted),
),
.child(Label::new("Connect to context servers through the Model Context Protocol, either using Zed extensions or directly.").color(Color::Muted)),
)
.children(
context_server_ids.into_iter().map(|context_server_id| {
@@ -601,7 +554,7 @@ impl AgentConfiguration {
.child(
h_flex()
.justify_between()
.gap_1p5()
.gap_2()
.child(
h_flex().w_full().child(
Button::new("add-context-server", "Add Custom Server")
@@ -692,6 +645,8 @@ impl AgentConfiguration {
.map_or([].as_slice(), |tools| tools.as_slice());
let tool_count = tools.len();
let border_color = cx.theme().colors().border.opacity(0.6);
let (source_icon, source_tooltip) = if is_from_extension {
(
IconName::ZedMcpExtension,
@@ -834,8 +789,8 @@ impl AgentConfiguration {
.id(item_id.clone())
.border_1()
.rounded_md()
.border_color(self.card_item_border_color(cx))
.bg(self.card_item_bg_color(cx))
.border_color(border_color)
.bg(cx.theme().colors().background.opacity(0.2))
.overflow_hidden()
.child(
h_flex()
@@ -843,11 +798,7 @@ impl AgentConfiguration {
.justify_between()
.when(
error.is_some() || are_tools_expanded && tool_count >= 1,
|element| {
element
.border_b_1()
.border_color(self.card_item_border_color(cx))
},
|element| element.border_b_1().border_color(border_color),
)
.child(
h_flex()
@@ -1029,195 +980,6 @@ impl AgentConfiguration {
))
})
}
fn render_agent_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let settings = AllAgentServersSettings::get_global(cx).clone();
let user_defined_agents = settings
.custom
.iter()
.map(|(name, settings)| {
self.render_agent_server(
IconName::Ai,
name.clone(),
ExternalAgent::Custom {
name: name.clone(),
settings: settings.clone(),
},
None,
cx,
)
.into_any_element()
})
.collect::<Vec<_>>();
v_flex()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.pr(DynamicSpacing::Base20.rems(cx))
.gap_2()
.child(
v_flex()
.gap_0p5()
.child(
h_flex()
.w_full()
.gap_2()
.justify_between()
.child(Headline::new("External Agents"))
.child(
Button::new("add-agent", "Add Agent")
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.label_size(LabelSize::Small)
.on_click(
move |_, window, cx| {
if let Some(workspace) = window.root().flatten() {
let workspace = workspace.downgrade();
window
.spawn(cx, async |cx| {
open_new_agent_servers_entry_in_settings_editor(
workspace,
cx,
).await
})
.detach_and_log_err(cx);
}
}
),
)
)
.child(
Label::new(
"Bring the agent of your choice to Zed via our new Agent Client Protocol.",
)
.color(Color::Muted),
),
)
.child(self.render_agent_server(
IconName::AiGemini,
"Gemini CLI",
ExternalAgent::Gemini,
(!self.gemini_is_installed).then_some(Gemini::install_command().into()),
cx,
))
// TODO add CC
.children(user_defined_agents),
)
}
fn render_agent_server(
&self,
icon: IconName,
name: impl Into<SharedString>,
agent: ExternalAgent,
install_command: Option<SharedString>,
cx: &mut Context<Self>,
) -> impl IntoElement {
let name = name.into();
h_flex()
.p_1()
.pl_2()
.gap_1p5()
.justify_between()
.border_1()
.rounded_md()
.border_color(self.card_item_border_color(cx))
.bg(self.card_item_bg_color(cx))
.overflow_hidden()
.child(
h_flex()
.gap_1p5()
.child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
.child(Label::new(name.clone())),
)
.map(|this| {
if let Some(install_command) = install_command {
this.child(
Button::new(
SharedString::from(format!("install_external_agent-{name}")),
"Install Agent",
)
.label_size(LabelSize::Small)
.icon(IconName::Plus)
.icon_position(IconPosition::Start)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.tooltip(Tooltip::text(install_command.clone()))
.on_click(cx.listener(
move |this, _, window, cx| {
let Some(project) = this.project.upgrade() else {
return;
};
let Some(workspace) = this.workspace.upgrade() else {
return;
};
let cwd = project.read(cx).first_project_directory(cx);
let shell =
project.read(cx).terminal_settings(&cwd, cx).shell.clone();
let spawn_in_terminal = task::SpawnInTerminal {
id: task::TaskId(install_command.to_string()),
full_label: install_command.to_string(),
label: install_command.to_string(),
command: Some(install_command.to_string()),
args: Vec::new(),
command_label: install_command.to_string(),
cwd,
env: Default::default(),
use_new_terminal: true,
allow_concurrent_runs: true,
reveal: Default::default(),
reveal_target: Default::default(),
hide: Default::default(),
shell,
show_summary: true,
show_command: true,
show_rerun: false,
};
let task = workspace.update(cx, |workspace, cx| {
workspace.spawn_in_terminal(spawn_in_terminal, window, cx)
});
cx.spawn(async move |this, cx| {
task.await;
this.update(cx, |this, cx| {
this.check_for_gemini(cx);
})
.ok();
})
.detach();
},
)),
)
} else {
this.child(
h_flex().gap_1().child(
Button::new(
SharedString::from(format!("start_acp_thread-{name}")),
"Start New Thread",
)
.label_size(LabelSize::Small)
.icon(IconName::Thread)
.icon_position(IconPosition::Start)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.on_click(move |_, window, cx| {
window.dispatch_action(
NewExternalAgentThread {
agent: Some(agent.clone()),
}
.boxed_clone(),
cx,
);
}),
),
)
}
})
}
}
impl Render for AgentConfiguration {
@@ -1237,7 +999,6 @@ impl Render for AgentConfiguration {
.size_full()
.overflow_y_scroll()
.child(self.render_general_settings_section(cx))
.child(self.render_agent_servers_section(cx))
.child(self.render_context_servers_section(window, cx))
.child(self.render_provider_configuration_section(cx)),
)
@@ -1356,109 +1117,3 @@ fn show_unable_to_uninstall_extension_with_context_server(
workspace.toggle_status_toast(status_toast, cx);
}
async fn open_new_agent_servers_entry_in_settings_editor(
workspace: WeakEntity<Workspace>,
cx: &mut AsyncWindowContext,
) -> Result<()> {
let settings_editor = workspace
.update_in(cx, |_, window, cx| {
create_and_open_local_file(paths::settings_file(), window, cx, || {
settings::initial_user_settings_content().as_ref().into()
})
})?
.await?
.downcast::<Editor>()
.unwrap();
settings_editor
.downgrade()
.update_in(cx, |item, window, cx| {
let text = item.buffer().read(cx).snapshot(cx).text();
let settings = cx.global::<SettingsStore>();
let mut unique_server_name = None;
let edits = settings.edits_for_update::<AllAgentServersSettings>(&text, |file| {
let server_name: Option<SharedString> = (0..u8::MAX)
.map(|i| {
if i == 0 {
"your_agent".into()
} else {
format!("your_agent_{}", i).into()
}
})
.find(|name| !file.custom.contains_key(name));
if let Some(server_name) = server_name {
unique_server_name = Some(server_name.clone());
file.custom.insert(
server_name,
AgentServerSettings {
command: AgentServerCommand {
path: "path_to_executable".into(),
args: vec![],
env: Some(HashMap::default()),
},
},
);
}
});
if edits.is_empty() {
return;
}
let ranges = edits
.iter()
.map(|(range, _)| range.clone())
.collect::<Vec<_>>();
item.edit(edits, cx);
if let Some((unique_server_name, buffer)) =
unique_server_name.zip(item.buffer().read(cx).as_singleton())
{
let snapshot = buffer.read(cx).snapshot();
if let Some(range) =
find_text_in_buffer(&unique_server_name, ranges[0].start, &snapshot)
{
item.change_selections(
SelectionEffects::scroll(Autoscroll::newest()),
window,
cx,
|selections| {
selections.select_ranges(vec![range]);
},
);
}
}
})
}
fn find_text_in_buffer(
text: &str,
start: usize,
snapshot: &language::BufferSnapshot,
) -> Option<Range<usize>> {
let chars = text.chars().collect::<Vec<char>>();
let mut offset = start;
let mut char_offset = 0;
for c in snapshot.chars_at(start) {
if char_offset >= chars.len() {
break;
}
offset += 1;
if c == chars[char_offset] {
char_offset += 1;
} else {
char_offset = 0;
}
}
if char_offset == chars.len() {
Some(offset.saturating_sub(chars.len())..offset)
} else {
None
}
}

View File

@@ -1529,7 +1529,6 @@ impl AgentDiff {
| AcpThreadEvent::TokenUsageUpdated
| AcpThreadEvent::EntriesRemoved(_)
| AcpThreadEvent::ToolAuthorizationRequired
| AcpThreadEvent::PromptCapabilitiesUpdated
| AcpThreadEvent::Retry(_) => {}
}
}

View File

@@ -5,16 +5,12 @@ use std::sync::Arc;
use std::time::Duration;
use acp_thread::AcpThread;
use agent_servers::AgentServerSettings;
use agent2::{DbThreadMetadata, HistoryEntry};
use db::kvp::{Dismissable, KEY_VALUE_STORE};
use serde::{Deserialize, Serialize};
use zed_actions::OpenBrowser;
use zed_actions::agent::ReauthenticateAgent;
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
use crate::agent_diff::AgentDiffThread;
use crate::ui::AcpOnboardingModal;
use crate::{
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
@@ -29,6 +25,7 @@ use crate::{
slash_command::SlashCommandCompletionProvider,
text_thread_editor::{
AgentPanelDelegate, TextThreadEditor, humanize_token_count, make_lsp_adapter_delegate,
render_remaining_tokens,
},
thread_history::{HistoryEntryElement, ThreadHistory},
ui::{AgentOnboardingModal, EndTrialUpsell},
@@ -57,7 +54,9 @@ use gpui::{
Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
};
use language::LanguageRegistry;
use language_model::{ConfigurationError, ConfiguredModel, LanguageModelRegistry};
use language_model::{
ConfigurationError, ConfiguredModel, LanguageModelProviderTosView, LanguageModelRegistry,
};
use project::{DisableAiSettings, Project, ProjectPath, Worktree};
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
use rules_library::{RulesLibrary, open_rules_library};
@@ -77,10 +76,7 @@ use workspace::{
};
use zed_actions::{
DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
agent::{
OpenAcpOnboardingModal, OpenOnboardingModal, OpenSettings, ResetOnboarding,
ToggleModelSelector,
},
agent::{OpenOnboardingModal, OpenSettings, ResetOnboarding, ToggleModelSelector},
assistant::{OpenRulesLibrary, ToggleFocus},
};
@@ -134,7 +130,7 @@ pub fn init(cx: &mut App) {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
panel.update(cx, |panel, cx| {
panel.external_thread(action.agent.clone(), None, None, window, cx)
panel.external_thread(action.agent, None, None, window, cx)
});
}
})
@@ -204,9 +200,6 @@ pub fn init(cx: &mut App) {
.register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
AgentOnboardingModal::toggle(workspace, window, cx)
})
.register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| {
AcpOnboardingModal::toggle(workspace, window, cx)
})
.register_action(|_workspace, _: &ResetOnboarding, window, cx| {
window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
window.refresh();
@@ -248,8 +241,7 @@ enum WhichFontSize {
None,
}
// TODO unify this with ExternalAgent
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AgentType {
#[default]
Zed,
@@ -257,29 +249,23 @@ pub enum AgentType {
Gemini,
ClaudeCode,
NativeAgent,
Custom {
name: SharedString,
settings: AgentServerSettings,
},
}
impl AgentType {
fn label(&self) -> SharedString {
fn label(self) -> impl Into<SharedString> {
match self {
Self::Zed | Self::TextThread => "Zed Agent".into(),
Self::NativeAgent => "Agent 2".into(),
Self::Gemini => "Gemini CLI".into(),
Self::ClaudeCode => "Claude Code".into(),
Self::Custom { name, .. } => name.into(),
Self::Zed | Self::TextThread => "Zed Agent",
Self::NativeAgent => "Agent 2",
Self::Gemini => "Gemini CLI",
Self::ClaudeCode => "Claude Code",
}
}
fn icon(&self) -> Option<IconName> {
fn icon(self) -> Option<IconName> {
match self {
Self::Zed | Self::NativeAgent | Self::TextThread => None,
Self::Gemini => Some(IconName::AiGemini),
Self::ClaudeCode => Some(IconName::AiClaude),
Self::Custom { .. } => Some(IconName::Terminal),
}
}
}
@@ -533,7 +519,7 @@ pub struct AgentPanel {
impl AgentPanel {
fn serialize(&mut self, cx: &mut Context<Self>) {
let width = self.width;
let selected_agent = self.selected_agent.clone();
let selected_agent = self.selected_agent;
self.pending_serialization = Some(cx.background_spawn(async move {
KEY_VALUE_STORE
.write_kvp(
@@ -597,6 +583,17 @@ impl AgentPanel {
None
};
// Wait for the Gemini/Native feature flag to be available.
let client = workspace.read_with(cx, |workspace, _| workspace.client().clone())?;
if !client.status().borrow().is_signed_out() {
cx.update(|_, cx| {
cx.wait_for_flag_or_timeout::<feature_flags::GeminiAndNativeFeatureFlag>(
Duration::from_secs(2),
)
})?
.await;
}
let panel = workspace.update_in(cx, |workspace, window, cx| {
let panel = cx.new(|cx| {
Self::new(
@@ -612,15 +609,11 @@ impl AgentPanel {
panel.update(cx, |panel, cx| {
panel.width = serialized_panel.width.map(|w| w.round());
if let Some(selected_agent) = serialized_panel.selected_agent {
panel.selected_agent = selected_agent.clone();
panel.selected_agent = selected_agent;
panel.new_agent_thread(selected_agent, window, cx);
}
cx.notify();
});
} else {
panel.update(cx, |panel, cx| {
panel.new_agent_thread(AgentType::NativeAgent, window, cx);
});
}
panel
})?;
@@ -912,7 +905,7 @@ impl AgentPanel {
fn active_thread_view(&self) -> Option<&Entity<AcpThreadView>> {
match &self.active_view {
ActiveView::ExternalAgentThread { thread_view, .. } => Some(thread_view),
ActiveView::ExternalAgentThread { thread_view } => Some(thread_view),
ActiveView::Thread { .. }
| ActiveView::TextThread { .. }
| ActiveView::History
@@ -1026,8 +1019,6 @@ impl AgentPanel {
}
fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
telemetry::event!("Agent Thread Started", agent = "zed-text");
let context = self
.context_store
.update(cx, |context_store, cx| context_store.create(cx));
@@ -1088,17 +1079,14 @@ impl AgentPanel {
cx.spawn_in(window, async move |this, cx| {
let ext_agent = match agent_choice {
Some(agent) => {
cx.background_spawn({
let agent = agent.clone();
async move {
if let Some(serialized) =
serde_json::to_string(&LastUsedExternalAgent { agent }).log_err()
{
KEY_VALUE_STORE
.write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized)
.await
.log_err();
}
cx.background_spawn(async move {
if let Some(serialized) =
serde_json::to_string(&LastUsedExternalAgent { agent }).log_err()
{
KEY_VALUE_STORE
.write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized)
.await
.log_err();
}
})
.detach();
@@ -1120,15 +1108,11 @@ impl AgentPanel {
}
};
telemetry::event!("Agent Thread Started", agent = ext_agent.name());
let server = ext_agent.server(fs, history);
this.update_in(cx, |this, window, cx| {
match ext_agent {
crate::ExternalAgent::Gemini
| crate::ExternalAgent::NativeAgent
| crate::ExternalAgent::Custom { .. } => {
crate::ExternalAgent::Gemini | crate::ExternalAgent::NativeAgent => {
if !cx.has_flag::<GeminiAndNativeFeatureFlag>() {
return;
}
@@ -1479,7 +1463,6 @@ impl AgentPanel {
tools,
self.language_registry.clone(),
self.workspace.clone(),
self.project.downgrade(),
window,
cx,
)
@@ -1851,8 +1834,21 @@ impl AgentPanel {
menu
}
pub fn set_selected_agent(
&mut self,
agent: AgentType,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.selected_agent != agent {
self.selected_agent = agent;
self.serialize(cx);
}
self.new_agent_thread(agent, window, cx);
}
pub fn selected_agent(&self) -> AgentType {
self.selected_agent.clone()
self.selected_agent
}
pub fn new_agent_thread(
@@ -1861,11 +1857,6 @@ impl AgentPanel {
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.selected_agent != agent {
self.selected_agent = agent.clone();
self.serialize(cx);
}
match agent {
AgentType::Zed => {
window.dispatch_action(
@@ -1896,13 +1887,6 @@ impl AgentPanel {
window,
cx,
),
AgentType::Custom { name, settings } => self.external_thread(
Some(crate::ExternalAgent::Custom { name, settings }),
None,
None,
window,
cx,
),
}
}
@@ -2057,11 +2041,9 @@ impl AgentPanel {
match state {
ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT)
.truncate()
.color(Color::Muted)
.into_any_element(),
ThreadSummary::Generating => Label::new(LOADING_SUMMARY_PLACEHOLDER)
.truncate()
.color(Color::Muted)
.into_any_element(),
ThreadSummary::Ready(_) => div()
.w_full()
@@ -2093,33 +2075,9 @@ impl AgentPanel {
}
}
ActiveView::ExternalAgentThread { thread_view } => {
if let Some(title_editor) = thread_view.read(cx).title_editor() {
div()
.w_full()
.on_action({
let thread_view = thread_view.downgrade();
move |_: &menu::Confirm, window, cx| {
if let Some(thread_view) = thread_view.upgrade() {
thread_view.focus_handle(cx).focus(window);
}
}
})
.on_action({
let thread_view = thread_view.downgrade();
move |_: &editor::actions::Cancel, window, cx| {
if let Some(thread_view) = thread_view.upgrade() {
thread_view.focus_handle(cx).focus(window);
}
}
})
.child(title_editor)
.into_any_element()
} else {
Label::new(thread_view.read(cx).title())
.color(Color::Muted)
.truncate()
.into_any_element()
}
Label::new(thread_view.read(cx).title(cx))
.truncate()
.into_any_element()
}
ActiveView::TextThread {
title_editor,
@@ -2130,7 +2088,6 @@ impl AgentPanel {
match summary {
ContextSummary::Pending => Label::new(ContextSummary::DEFAULT)
.color(Color::Muted)
.truncate()
.into_any_element(),
ContextSummary::Content(summary) => {
@@ -2142,7 +2099,6 @@ impl AgentPanel {
} else {
Label::new(LOADING_SUMMARY_PLACEHOLDER)
.truncate()
.color(Color::Muted)
.into_any_element()
}
}
@@ -2203,8 +2159,6 @@ impl AgentPanel {
"Enable Full Screen"
};
let selected_agent = self.selected_agent.clone();
PopoverMenu::new("agent-options-menu")
.trigger_with_tooltip(
IconButton::new("agent-options-menu", IconName::Ellipsis)
@@ -2284,11 +2238,6 @@ impl AgentPanel {
.action("Settings", Box::new(OpenSettings))
.separator()
.action(full_screen_label, Box::new(ToggleZoom));
if selected_agent == AgentType::Gemini {
menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent))
}
menu
}))
}
@@ -2323,8 +2272,6 @@ impl AgentPanel {
.menu({
let menu = self.assistant_navigation_menu.clone();
move |window, cx| {
telemetry::event!("View Thread History Clicked");
if let Some(menu) = menu.as_ref() {
menu.update(cx, |_, cx| {
cx.defer_in(window, |menu, window, cx| {
@@ -2503,8 +2450,6 @@ impl AgentPanel {
let workspace = self.workspace.clone();
move |window, cx| {
telemetry::event!("New Thread Clicked");
let active_thread = active_thread.clone();
Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
menu = menu
@@ -2546,7 +2491,7 @@ impl AgentPanel {
workspace.panel::<AgentPanel>(cx)
{
panel.update(cx, |panel, cx| {
panel.new_agent_thread(
panel.set_selected_agent(
AgentType::NativeAgent,
window,
cx,
@@ -2572,7 +2517,7 @@ impl AgentPanel {
workspace.panel::<AgentPanel>(cx)
{
panel.update(cx, |panel, cx| {
panel.new_agent_thread(
panel.set_selected_agent(
AgentType::TextThread,
window,
cx,
@@ -2600,7 +2545,7 @@ impl AgentPanel {
workspace.panel::<AgentPanel>(cx)
{
panel.update(cx, |panel, cx| {
panel.new_agent_thread(
panel.set_selected_agent(
AgentType::Gemini,
window,
cx,
@@ -2627,7 +2572,7 @@ impl AgentPanel {
workspace.panel::<AgentPanel>(cx)
{
panel.update(cx, |panel, cx| {
panel.new_agent_thread(
panel.set_selected_agent(
AgentType::ClaudeCode,
window,
cx,
@@ -2639,64 +2584,13 @@ impl AgentPanel {
}
}),
)
})
.when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |mut menu| {
// Add custom agents from settings
let settings =
agent_servers::AllAgentServersSettings::get_global(cx);
for (agent_name, agent_settings) in &settings.custom {
menu = menu.item(
ContextMenuEntry::new(format!("New {} Thread", agent_name))
.icon(IconName::Terminal)
.icon_color(Color::Muted)
.handler({
let workspace = workspace.clone();
let agent_name = agent_name.clone();
let agent_settings = agent_settings.clone();
move |window, cx| {
if let Some(workspace) = workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
if let Some(panel) =
workspace.panel::<AgentPanel>(cx)
{
panel.update(cx, |panel, cx| {
panel.new_agent_thread(
AgentType::Custom {
name: agent_name
.clone(),
settings:
agent_settings
.clone(),
},
window,
cx,
);
});
}
});
}
}
}),
);
}
menu
})
.when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |menu| {
menu.separator().link(
"Add Other Agents",
OpenBrowser {
url: zed_urls::external_agents_docs(cx),
}
.boxed_clone(),
)
});
menu
}))
}
});
let selected_agent_label = self.selected_agent.label();
let selected_agent_label = self.selected_agent.label().into();
let selected_agent = div()
.id("selected_agent_icon")
.when_some(self.selected_agent.icon(), |this, icon| {
@@ -2874,8 +2768,12 @@ impl AgentPanel {
Some(token_count)
}
ActiveView::TextThread { context_editor, .. } => {
let element = render_remaining_tokens(context_editor, cx)?;
Some(element.into_any_element())
}
ActiveView::ExternalAgentThread { .. }
| ActiveView::TextThread { .. }
| ActiveView::History
| ActiveView::Configuration => None,
}
@@ -3277,6 +3175,17 @@ impl AgentPanel {
ConfigurationError::ModelNotFound
| ConfigurationError::ProviderNotAuthenticated(_)
| ConfigurationError::NoProvider => callout.into_any_element(),
ConfigurationError::ProviderPendingTermsAcceptance(provider) => {
Banner::new()
.severity(Severity::Warning)
.child(h_flex().w_full().children(
provider.render_accept_terms(
LanguageModelProviderTosView::ThreadEmptyState,
cx,
),
))
.into_any_element()
}
}
}
@@ -3766,11 +3675,6 @@ impl Render for AgentPanel {
}
}))
.on_action(cx.listener(Self::toggle_burn_mode))
.on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| {
if let Some(thread_view) = this.active_thread_view() {
thread_view.update(cx, |thread_view, cx| thread_view.reauthenticate(window, cx))
}
}))
.child(self.render_toolbar(window, cx))
.children(self.render_onboarding(window, cx))
.map(|parent| match &self.active_view {

View File

@@ -28,14 +28,13 @@ use std::rc::Rc;
use std::sync::Arc;
use agent::{Thread, ThreadId};
use agent_servers::AgentServerSettings;
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection};
use assistant_slash_command::SlashCommandRegistry;
use client::Client;
use command_palette_hooks::CommandPaletteFilter;
use feature_flags::FeatureFlagAppExt as _;
use fs::Fs;
use gpui::{Action, App, Entity, SharedString, actions};
use gpui::{Action, App, Entity, actions};
use language::LanguageRegistry;
use language_model::{
ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
@@ -160,43 +159,25 @@ pub struct NewNativeAgentThreadFromSummary {
from_session_id: agent_client_protocol::SessionId,
}
// TODO unify this with AgentType
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[derive(Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
enum ExternalAgent {
#[default]
Gemini,
ClaudeCode,
NativeAgent,
Custom {
name: SharedString,
settings: AgentServerSettings,
},
}
impl ExternalAgent {
fn name(&self) -> &'static str {
match self {
Self::NativeAgent => "zed",
Self::Gemini => "gemini-cli",
Self::ClaudeCode => "claude-code",
Self::Custom { .. } => "custom",
}
}
pub fn server(
&self,
fs: Arc<dyn fs::Fs>,
history: Entity<agent2::HistoryStore>,
) -> Rc<dyn agent_servers::AgentServer> {
match self {
Self::Gemini => Rc::new(agent_servers::Gemini),
Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
Self::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)),
Self::Custom { name, settings } => Rc::new(agent_servers::CustomAgentServer::new(
name.clone(),
settings,
)),
ExternalAgent::Gemini => Rc::new(agent_servers::Gemini),
ExternalAgent::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
ExternalAgent::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)),
}
}
}

View File

@@ -378,13 +378,18 @@ impl MessageEditor {
}
fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(ConfiguredModel { model, .. }) = self
let Some(ConfiguredModel { model, provider }) = self
.thread
.update(cx, |thread, cx| thread.get_or_init_configured_model(cx))
else {
return;
};
if provider.must_accept_terms(cx) {
cx.notify();
return;
}
let (user_message, user_message_creases) = self.editor.update(cx, |editor, cx| {
let creases = extract_message_creases(editor, cx);
let text = editor.text(cx);

View File

@@ -190,6 +190,7 @@ pub struct TextThreadEditor {
invoked_slash_command_creases: HashMap<InvokedSlashCommandId, CreaseId>,
_subscriptions: Vec<Subscription>,
last_error: Option<AssistError>,
show_accept_terms: bool,
pub(crate) slash_menu_handle:
PopoverMenuHandle<Picker<slash_command_picker::SlashCommandDelegate>>,
// dragged_file_worktrees is used to keep references to worktrees that were added
@@ -288,6 +289,7 @@ impl TextThreadEditor {
invoked_slash_command_creases: HashMap::default(),
_subscriptions,
last_error: None,
show_accept_terms: false,
slash_menu_handle: Default::default(),
dragged_file_worktrees: Vec::new(),
language_model_selector: cx.new(|cx| {
@@ -361,12 +363,24 @@ impl TextThreadEditor {
if self.sending_disabled(cx) {
return;
}
telemetry::event!("Agent Message Sent", agent = "zed-text");
self.send_to_model(window, cx);
}
fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let provider = LanguageModelRegistry::read_global(cx)
.default_model()
.map(|default| default.provider);
if provider
.as_ref()
.is_some_and(|provider| provider.must_accept_terms(cx))
{
self.show_accept_terms = true;
cx.notify();
return;
}
self.last_error = None;
if let Some(user_message) = self.context.update(cx, |context, cx| context.assist(cx)) {
let new_selection = {
let cursor = user_message
@@ -1857,53 +1871,6 @@ impl TextThreadEditor {
.update(cx, |context, cx| context.summarize(true, cx));
}
fn render_remaining_tokens(&self, cx: &App) -> Option<impl IntoElement + use<>> {
let (token_count_color, token_count, max_token_count, tooltip) =
match token_state(&self.context, cx)? {
TokenState::NoTokensLeft {
max_token_count,
token_count,
} => (
Color::Error,
token_count,
max_token_count,
Some("Token Limit Reached"),
),
TokenState::HasMoreTokens {
max_token_count,
token_count,
over_warn_threshold,
} => {
let (color, tooltip) = if over_warn_threshold {
(Color::Warning, Some("Token Limit is Close to Exhaustion"))
} else {
(Color::Muted, None)
};
(color, token_count, max_token_count, tooltip)
}
};
Some(
h_flex()
.id("token-count")
.gap_0p5()
.child(
Label::new(humanize_token_count(token_count))
.size(LabelSize::Small)
.color(token_count_color),
)
.child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
.child(
Label::new(humanize_token_count(max_token_count))
.size(LabelSize::Small)
.color(Color::Muted),
)
.when_some(tooltip, |element, tooltip| {
element.tooltip(Tooltip::text(tooltip))
}),
)
}
fn render_send_button(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle(cx);
@@ -1963,6 +1930,7 @@ impl TextThreadEditor {
ConfigurationError::NoProvider
| ConfigurationError::ModelNotFound
| ConfigurationError::ProviderNotAuthenticated(_) => true,
ConfigurationError::ProviderPendingTermsAcceptance(_) => self.show_accept_terms,
}
}
@@ -2467,14 +2435,9 @@ impl Render for TextThreadEditor {
)
.child(
h_flex()
.gap_2p5()
.children(self.render_remaining_tokens(cx))
.child(
h_flex()
.gap_1()
.child(self.render_language_model_selector(window, cx))
.child(self.render_send_button(window, cx)),
),
.gap_1()
.child(self.render_language_model_selector(window, cx))
.child(self.render_send_button(window, cx)),
),
)
}
@@ -2762,6 +2725,58 @@ impl FollowableItem for TextThreadEditor {
}
}
pub fn render_remaining_tokens(
context_editor: &Entity<TextThreadEditor>,
cx: &App,
) -> Option<impl IntoElement + use<>> {
let context = &context_editor.read(cx).context;
let (token_count_color, token_count, max_token_count, tooltip) = match token_state(context, cx)?
{
TokenState::NoTokensLeft {
max_token_count,
token_count,
} => (
Color::Error,
token_count,
max_token_count,
Some("Token Limit Reached"),
),
TokenState::HasMoreTokens {
max_token_count,
token_count,
over_warn_threshold,
} => {
let (color, tooltip) = if over_warn_threshold {
(Color::Warning, Some("Token Limit is Close to Exhaustion"))
} else {
(Color::Muted, None)
};
(color, token_count, max_token_count, tooltip)
}
};
Some(
h_flex()
.id("token-count")
.gap_0p5()
.child(
Label::new(humanize_token_count(token_count))
.size(LabelSize::Small)
.color(token_count_color),
)
.child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
.child(
Label::new(humanize_token_count(max_token_count))
.size(LabelSize::Small)
.color(Color::Muted),
)
.when_some(tooltip, |element, tooltip| {
element.tooltip(Tooltip::text(tooltip))
}),
)
}
enum PendingSlashCommand {}
fn invoked_slash_command_fold_placeholder(

View File

@@ -1,16 +1,12 @@
mod acp_onboarding_modal;
mod agent_notification;
mod burn_mode_tooltip;
mod context_pill;
mod end_trial_upsell;
mod onboarding_modal;
pub mod preview;
mod unavailable_editing_tooltip;
pub use acp_onboarding_modal::*;
pub use agent_notification::*;
pub use burn_mode_tooltip::*;
pub use context_pill::*;
pub use end_trial_upsell::*;
pub use onboarding_modal::*;
pub use unavailable_editing_tooltip::*;

View File

@@ -1,254 +0,0 @@
use client::zed_urls;
use gpui::{
ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render,
linear_color_stop, linear_gradient,
};
use ui::{TintColor, Vector, VectorName, prelude::*};
use workspace::{ModalView, Workspace};
use crate::agent_panel::{AgentPanel, AgentType};
macro_rules! acp_onboarding_event {
($name:expr) => {
telemetry::event!($name, source = "ACP Onboarding");
};
($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {
telemetry::event!($name, source = "ACP Onboarding", $($key $(= $value)?),+);
};
}
pub struct AcpOnboardingModal {
focus_handle: FocusHandle,
workspace: Entity<Workspace>,
}
impl AcpOnboardingModal {
pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
let workspace_entity = cx.entity();
workspace.toggle_modal(window, cx, |_window, cx| Self {
workspace: workspace_entity,
focus_handle: cx.focus_handle(),
});
}
fn open_panel(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
self.workspace.update(cx, |workspace, cx| {
workspace.focus_panel::<AgentPanel>(window, cx);
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
panel.update(cx, |panel, cx| {
panel.new_agent_thread(AgentType::Gemini, window, cx);
});
}
});
cx.emit(DismissEvent);
acp_onboarding_event!("Open Panel Clicked");
}
fn view_docs(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
cx.open_url(&zed_urls::external_agents_docs(cx));
cx.notify();
acp_onboarding_event!("Documentation Link Clicked");
}
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
}
impl EventEmitter<DismissEvent> for AcpOnboardingModal {}
impl Focusable for AcpOnboardingModal {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl ModalView for AcpOnboardingModal {}
impl Render for AcpOnboardingModal {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let illustration_element = |label: bool, opacity: f32| {
h_flex()
.px_1()
.py_0p5()
.gap_1()
.rounded_sm()
.bg(cx.theme().colors().element_active.opacity(0.05))
.border_1()
.border_color(cx.theme().colors().border)
.border_dashed()
.child(
Icon::new(IconName::Stop)
.size(IconSize::Small)
.color(Color::Custom(cx.theme().colors().text_muted.opacity(0.15))),
)
.map(|this| {
if label {
this.child(
Label::new("Your Agent Here")
.size(LabelSize::Small)
.color(Color::Muted),
)
} else {
this.child(
div().w_16().h_1().rounded_full().bg(cx
.theme()
.colors()
.element_active
.opacity(0.6)),
)
}
})
.opacity(opacity)
};
let illustration = h_flex()
.relative()
.h(rems_from_px(126.))
.bg(cx.theme().colors().editor_background)
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.justify_center()
.gap_8()
.rounded_t_md()
.overflow_hidden()
.child(
div().absolute().inset_0().w(px(515.)).h(px(126.)).child(
Vector::new(VectorName::AcpGrid, rems_from_px(515.), rems_from_px(126.))
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.02))),
),
)
.child(div().absolute().inset_0().size_full().bg(linear_gradient(
0.,
linear_color_stop(
cx.theme().colors().elevated_surface_background.opacity(0.1),
0.9,
),
linear_color_stop(
cx.theme().colors().elevated_surface_background.opacity(0.),
0.,
),
)))
.child(
div()
.absolute()
.inset_0()
.size_full()
.bg(gpui::black().opacity(0.15)),
)
.child(
h_flex()
.gap_4()
.child(
Vector::new(VectorName::AcpLogo, rems_from_px(106.), rems_from_px(40.))
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
)
.child(
Vector::new(
VectorName::AcpLogoSerif,
rems_from_px(111.),
rems_from_px(41.),
)
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
),
)
.child(
v_flex()
.gap_1p5()
.child(illustration_element(false, 0.15))
.child(illustration_element(true, 0.3))
.child(
h_flex()
.pl_1()
.pr_2()
.py_0p5()
.gap_1()
.rounded_sm()
.bg(cx.theme().colors().element_active.opacity(0.2))
.border_1()
.border_color(cx.theme().colors().border)
.child(
Icon::new(IconName::AiGemini)
.size(IconSize::Small)
.color(Color::Muted),
)
.child(Label::new("New Gemini CLI Thread").size(LabelSize::Small)),
)
.child(illustration_element(true, 0.3))
.child(illustration_element(false, 0.15)),
);
let heading = v_flex()
.w_full()
.gap_1()
.child(
Label::new("Now Available")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(Headline::new("Bring Your Own Agent to Zed").size(HeadlineSize::Large));
let copy = "Bring the agent of your choice to Zed via our new Agent Client Protocol (ACP), starting with Google's Gemini CLI integration.";
let open_panel_button = Button::new("open-panel", "Start with Gemini CLI")
.icon_size(IconSize::Indicator)
.style(ButtonStyle::Tinted(TintColor::Accent))
.full_width()
.on_click(cx.listener(Self::open_panel));
let docs_button = Button::new("add-other-agents", "Add Other Agents")
.icon(IconName::ArrowUpRight)
.icon_size(IconSize::Indicator)
.icon_color(Color::Muted)
.full_width()
.on_click(cx.listener(Self::view_docs));
let close_button = h_flex().absolute().top_2().right_2().child(
IconButton::new("cancel", IconName::Close).on_click(cx.listener(
|_, _: &ClickEvent, _window, cx| {
acp_onboarding_event!("Canceled", trigger = "X click");
cx.emit(DismissEvent);
},
)),
);
v_flex()
.id("acp-onboarding")
.key_context("AcpOnboardingModal")
.relative()
.w(rems(34.))
.h_full()
.elevation_3(cx)
.track_focus(&self.focus_handle(cx))
.overflow_hidden()
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
acp_onboarding_event!("Canceled", trigger = "Action");
cx.emit(DismissEvent);
}))
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
this.focus_handle.focus(window);
}))
.child(illustration)
.child(
v_flex()
.p_4()
.gap_2()
.child(heading)
.child(Label::new(copy).color(Color::Muted))
.child(
v_flex()
.w_full()
.mt_2()
.gap_1()
.child(open_panel_button)
.child(docs_button),
),
)
.child(close_button)
}
}

View File

@@ -86,18 +86,23 @@ impl RenderOnce for UsageCallout {
(IconName::Warning, Severity::Warning)
};
Callout::new()
.icon(icon)
.severity(severity)
.icon(icon)
.title(title)
.description(message)
.actions_slot(
Button::new("upgrade", button_text)
.label_size(LabelSize::Small)
.on_click(move |_, _, cx| {
cx.open_url(&url);
}),
div()
.border_t_1()
.border_color(cx.theme().colors().border)
.child(
Callout::new()
.icon(icon)
.severity(severity)
.icon(icon)
.title(title)
.description(message)
.actions_slot(
Button::new("upgrade", button_text)
.label_size(LabelSize::Small)
.on_click(move |_, _, cx| {
cx.open_url(&url);
}),
),
)
.into_any_element()
}

View File

@@ -1,29 +0,0 @@
use gpui::{Context, IntoElement, Render, Window};
use ui::{prelude::*, tooltip_container};
pub struct UnavailableEditingTooltip {
agent_name: SharedString,
}
impl UnavailableEditingTooltip {
pub fn new(agent_name: SharedString) -> Self {
Self { agent_name }
}
}
impl Render for UnavailableEditingTooltip {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
tooltip_container(window, cx, |this, _, _| {
this.child(Label::new("Unavailable Editing")).child(
div().max_w_64().child(
Label::new(format!(
"Editing previous messages is not available for {} yet.",
self.agent_name
))
.size(LabelSize::Small)
.color(Color::Muted),
),
)
})
}
}

View File

@@ -19,7 +19,7 @@ use std::sync::Arc;
use client::{Client, UserStore, zed_urls};
use gpui::{AnyElement, Entity, IntoElement, ParentElement};
use ui::{Divider, RegisterComponent, Tooltip, prelude::*};
use ui::{Divider, RegisterComponent, TintColor, Tooltip, prelude::*};
#[derive(PartialEq)]
pub enum SignInStatus {
@@ -43,10 +43,12 @@ impl From<client::Status> for SignInStatus {
#[derive(RegisterComponent, IntoElement)]
pub struct ZedAiOnboarding {
pub sign_in_status: SignInStatus,
pub has_accepted_terms_of_service: bool,
pub plan: Option<Plan>,
pub account_too_young: bool,
pub continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
pub accept_terms_of_service: Arc<dyn Fn(&mut Window, &mut App)>,
pub dismiss_onboarding: Option<Arc<dyn Fn(&mut Window, &mut App)>>,
}
@@ -62,9 +64,17 @@ impl ZedAiOnboarding {
Self {
sign_in_status: status.into(),
has_accepted_terms_of_service: store.has_accepted_terms_of_service(),
plan: store.plan(),
account_too_young: store.account_too_young(),
continue_with_zed_ai,
accept_terms_of_service: Arc::new({
let store = user_store.clone();
move |_window, cx| {
let task = store.update(cx, |store, cx| store.accept_terms_of_service(cx));
task.detach_and_log_err(cx);
}
}),
sign_in: Arc::new(move |_window, cx| {
cx.spawn({
let client = client.clone();
@@ -84,6 +94,42 @@ impl ZedAiOnboarding {
self
}
fn render_accept_terms_of_service(&self) -> AnyElement {
v_flex()
.gap_1()
.w_full()
.child(Headline::new("Accept Terms of Service"))
.child(
Label::new("We dont sell your data, track you across the web, or compromise your privacy.")
.color(Color::Muted)
.mb_2(),
)
.child(
Button::new("terms_of_service", "Review Terms of Service")
.full_width()
.style(ButtonStyle::Outlined)
.icon(IconName::ArrowUpRight)
.icon_color(Color::Muted)
.icon_size(IconSize::Small)
.on_click(move |_, _window, cx| {
telemetry::event!("Review Terms of Service Clicked");
cx.open_url(&zed_urls::terms_of_service(cx))
}),
)
.child(
Button::new("accept_terms", "Accept")
.full_width()
.style(ButtonStyle::Tinted(TintColor::Accent))
.on_click({
let callback = self.accept_terms_of_service.clone();
move |_, window, cx| {
telemetry::event!("Terms of Service Accepted");
(callback)(window, cx)}
}),
)
.into_any_element()
}
fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement {
let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
let plan_definitions = PlanDefinitions;
@@ -313,10 +359,14 @@ impl ZedAiOnboarding {
impl RenderOnce for ZedAiOnboarding {
fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
if matches!(self.sign_in_status, SignInStatus::SignedIn) {
match self.plan {
None | Some(Plan::ZedFree) => self.render_free_plan_state(cx),
Some(Plan::ZedProTrial) => self.render_trial_state(cx),
Some(Plan::ZedPro) => self.render_pro_plan_state(cx),
if self.has_accepted_terms_of_service {
match self.plan {
None | Some(Plan::ZedFree) => self.render_free_plan_state(cx),
Some(Plan::ZedProTrial) => self.render_trial_state(cx),
Some(Plan::ZedPro) => self.render_pro_plan_state(cx),
}
} else {
self.render_accept_terms_of_service()
}
} else {
self.render_sign_in_disclaimer(cx)
@@ -340,15 +390,18 @@ impl Component for ZedAiOnboarding {
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn onboarding(
sign_in_status: SignInStatus,
has_accepted_terms_of_service: bool,
plan: Option<Plan>,
account_too_young: bool,
) -> AnyElement {
ZedAiOnboarding {
sign_in_status,
has_accepted_terms_of_service,
plan,
account_too_young,
continue_with_zed_ai: Arc::new(|_, _| {}),
sign_in: Arc::new(|_, _| {}),
accept_terms_of_service: Arc::new(|_, _| {}),
dismiss_onboarding: None,
}
.into_any_element()
@@ -362,23 +415,27 @@ impl Component for ZedAiOnboarding {
.children(vec![
single_example(
"Not Signed-in",
onboarding(SignInStatus::SignedOut, None, false),
onboarding(SignInStatus::SignedOut, false, None, false),
),
single_example(
"Not Accepted ToS",
onboarding(SignInStatus::SignedIn, false, None, false),
),
single_example(
"Young Account",
onboarding(SignInStatus::SignedIn, None, true),
onboarding(SignInStatus::SignedIn, true, None, true),
),
single_example(
"Free Plan",
onboarding(SignInStatus::SignedIn, Some(Plan::ZedFree), false),
onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedFree), false),
),
single_example(
"Pro Trial",
onboarding(SignInStatus::SignedIn, Some(Plan::ZedProTrial), false),
onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedProTrial), false),
),
single_example(
"Pro Plan",
onboarding(SignInStatus::SignedIn, Some(Plan::ZedPro), false),
onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedPro), false),
),
])
.into_any_element(),

View File

@@ -118,7 +118,7 @@ impl Tool for FetchTool {
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
true
false
}
fn may_perform_edits(&self) -> bool {

View File

@@ -435,8 +435,8 @@ mod test {
assert_eq!(
matches,
&[
PathBuf::from(path!("root/apple/banana/carrot")),
PathBuf::from(path!("root/apple/bandana/carbonara"))
PathBuf::from("root/apple/banana/carrot"),
PathBuf::from("root/apple/bandana/carbonara")
]
);
@@ -447,8 +447,8 @@ mod test {
assert_eq!(
matches,
&[
PathBuf::from(path!("root/apple/banana/carrot")),
PathBuf::from(path!("root/apple/bandana/carbonara"))
PathBuf::from("root/apple/banana/carrot"),
PathBuf::from("root/apple/bandana/carbonara")
]
);
}

View File

@@ -68,7 +68,7 @@ impl Tool for ReadFileTool {
}
fn icon(&self) -> IconName {
IconName::ToolSearch
IconName::ToolRead
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {

View File

@@ -15,9 +15,10 @@ doctest = false
[dependencies]
anyhow.workspace = true
collections.workspace = true
derive_more.workspace = true
gpui.workspace = true
parking_lot.workspace = true
settings.workspace = true
schemars.workspace = true
serde.workspace = true
rodio = { workspace = true, features = [ "wav", "playback", "tracing" ] }
util.workspace = true
workspace-hack.workspace = true

View File

@@ -1,54 +0,0 @@
use std::{io::Cursor, sync::Arc};
use anyhow::{Context as _, Result};
use collections::HashMap;
use gpui::{App, AssetSource, Global};
use rodio::{Decoder, Source, source::Buffered};
type Sound = Buffered<Decoder<Cursor<Vec<u8>>>>;
pub struct SoundRegistry {
cache: Arc<parking_lot::Mutex<HashMap<String, Sound>>>,
assets: Box<dyn AssetSource>,
}
struct GlobalSoundRegistry(Arc<SoundRegistry>);
impl Global for GlobalSoundRegistry {}
impl SoundRegistry {
pub fn new(source: impl AssetSource) -> Arc<Self> {
Arc::new(Self {
cache: Default::default(),
assets: Box::new(source),
})
}
pub fn global(cx: &App) -> Arc<Self> {
cx.global::<GlobalSoundRegistry>().0.clone()
}
pub(crate) fn set_global(source: impl AssetSource, cx: &mut App) {
cx.set_global(GlobalSoundRegistry(SoundRegistry::new(source)));
}
pub fn get(&self, name: &str) -> Result<impl Source<Item = f32> + use<>> {
if let Some(wav) = self.cache.lock().get(name) {
return Ok(wav.clone());
}
let path = format!("sounds/{}.wav", name);
let bytes = self
.assets
.load(&path)?
.map(anyhow::Ok)
.with_context(|| format!("No asset available for path {path}"))??
.into_owned();
let cursor = Cursor::new(bytes);
let source = Decoder::new(cursor)?.buffered();
self.cache.lock().insert(name.to_string(), source.clone());
Ok(source)
}
}

View File

@@ -1,16 +1,19 @@
use assets::SoundRegistry;
use derive_more::{Deref, DerefMut};
use gpui::{App, AssetSource, BorrowAppContext, Global};
use rodio::{OutputStream, OutputStreamBuilder};
use anyhow::{Context as _, Result, anyhow};
use collections::HashMap;
use gpui::{App, BorrowAppContext, Global};
use rodio::{Decoder, OutputStream, OutputStreamBuilder, Source, source::Buffered};
use settings::Settings;
use std::io::Cursor;
use util::ResultExt;
mod assets;
mod audio_settings;
pub use audio_settings::AudioSettings;
pub fn init(source: impl AssetSource, cx: &mut App) {
SoundRegistry::set_global(source, cx);
cx.set_global(GlobalAudio(Audio::new()));
pub fn init(cx: &mut App) {
AudioSettings::register(cx);
}
#[derive(Copy, Clone, Eq, Hash, PartialEq)]
pub enum Sound {
Joined,
Leave,
@@ -38,18 +41,12 @@ impl Sound {
#[derive(Default)]
pub struct Audio {
output_handle: Option<OutputStream>,
source_cache: HashMap<Sound, Buffered<Decoder<Cursor<Vec<u8>>>>>,
}
#[derive(Deref, DerefMut)]
struct GlobalAudio(Audio);
impl Global for GlobalAudio {}
impl Global for Audio {}
impl Audio {
pub fn new() -> Self {
Self::default()
}
fn ensure_output_exists(&mut self) -> Option<&OutputStream> {
if self.output_handle.is_none() {
self.output_handle = OutputStreamBuilder::open_default_stream().log_err();
@@ -58,26 +55,51 @@ impl Audio {
self.output_handle.as_ref()
}
pub fn play_sound(sound: Sound, cx: &mut App) {
if !cx.has_global::<GlobalAudio>() {
return;
}
pub fn play_source(
source: impl rodio::Source + Send + 'static,
cx: &mut App,
) -> anyhow::Result<()> {
cx.update_default_global(|this: &mut Self, _cx| {
let output_handle = this
.ensure_output_exists()
.ok_or_else(|| anyhow!("Could not open audio output"))?;
output_handle.mixer().add(source);
Ok(())
})
}
cx.update_global::<GlobalAudio, _>(|this, cx| {
pub fn play_sound(sound: Sound, cx: &mut App) {
cx.update_default_global(|this: &mut Self, cx| {
let source = this.sound_source(sound, cx).log_err()?;
let output_handle = this.ensure_output_exists()?;
let source = SoundRegistry::global(cx).get(sound.file()).log_err()?;
output_handle.mixer().add(source);
Some(())
});
}
pub fn end_call(cx: &mut App) {
if !cx.has_global::<GlobalAudio>() {
return;
}
cx.update_global::<GlobalAudio, _>(|this, _| {
cx.update_default_global(|this: &mut Self, _cx| {
this.output_handle.take();
});
}
fn sound_source(&mut self, sound: Sound, cx: &App) -> Result<impl Source + use<>> {
if let Some(wav) = self.source_cache.get(&sound) {
return Ok(wav.clone());
}
let path = format!("sounds/{}.wav", sound.file());
let bytes = cx
.asset_source()
.load(&path)?
.map(anyhow::Ok)
.with_context(|| format!("No asset available for path {path}"))??
.into_owned();
let cursor = Cursor::new(bytes);
let source = Decoder::new(cursor)?.buffered();
self.source_cache.insert(sound, source.clone());
Ok(source)
}
}

View File

@@ -0,0 +1,33 @@
use anyhow::Result;
use gpui::App;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
#[derive(Deserialize, Debug)]
pub struct AudioSettings {
/// Opt into the new audio system.
#[serde(rename = "experimental.rodio_audio", default)]
pub rodio_audio: bool, // default is false
}
/// Configuration of audio in Zed.
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
#[serde(default)]
pub struct AudioSettingsContent {
/// Whether to use the experimental audio system
#[serde(rename = "experimental.rodio_audio", default)]
pub rodio_audio: bool,
}
impl Settings for AudioSettings {
const KEY: Option<&'static str> = Some("audio");
type FileContent = AudioSettingsContent;
fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut App) -> Result<Self> {
sources.json_merge()
}
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}

View File

@@ -154,23 +154,7 @@ impl BufferDiffSnapshot {
BufferDiffSnapshot {
inner: BufferDiffInner {
base_text: language::Buffer::build_empty_snapshot(cx),
hunks: SumTree::new(buffer),
pending_hunks: SumTree::new(buffer),
base_text_exists: false,
},
secondary_diff: None,
}
}
fn unchanged(
buffer: &text::BufferSnapshot,
base_text: language::BufferSnapshot,
) -> BufferDiffSnapshot {
debug_assert_eq!(buffer.text(), base_text.text());
BufferDiffSnapshot {
inner: BufferDiffInner {
base_text,
hunks: SumTree::new(buffer),
hunks: SumTree::new(),
pending_hunks: SumTree::new(buffer),
base_text_exists: false,
},
@@ -229,10 +213,7 @@ impl BufferDiffSnapshot {
cx: &App,
) -> impl Future<Output = Self> + use<> {
let base_text_exists = base_text.is_some();
let base_text_pair = base_text.map(|text| {
debug_assert_eq!(&*text, &base_text_snapshot.text());
(text, base_text_snapshot.as_rope().clone())
});
let base_text_pair = base_text.map(|text| (text, base_text_snapshot.as_rope().clone()));
cx.background_executor()
.spawn_labeled(*CALCULATE_DIFF_TASK, async move {
Self {
@@ -892,18 +873,6 @@ impl BufferDiff {
}
}
pub fn new_unchanged(
buffer: &text::BufferSnapshot,
base_text: language::BufferSnapshot,
) -> Self {
debug_assert_eq!(buffer.text(), base_text.text());
BufferDiff {
buffer_id: buffer.remote_id(),
inner: BufferDiffSnapshot::unchanged(buffer, base_text).inner,
secondary_diff: None,
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn new_with_base_text(
base_text: &str,

View File

@@ -1290,21 +1290,19 @@ impl Client {
"http" => Http,
_ => Err(anyhow!("invalid rpc url: {}", rpc_url))?,
};
let rpc_host = rpc_url
.host_str()
.zip(rpc_url.port_or_known_default())
.context("missing host in rpc url")?;
let stream = gpui_tokio::Tokio::spawn_result(cx, {
let rpc_url = rpc_url.clone();
async move {
let rpc_host = rpc_url
.host_str()
.zip(rpc_url.port_or_known_default())
.context("missing host in rpc url")?;
Ok(match proxy {
Some(proxy) => connect_proxy_stream(&proxy, rpc_host).await?,
None => Box::new(TcpStream::connect(rpc_host).await?),
})
let stream = {
let handle = cx.update(|cx| gpui_tokio::Tokio::handle(cx)).ok().unwrap();
let _guard = handle.enter();
match proxy {
Some(proxy) => connect_proxy_stream(&proxy, rpc_host).await?,
None => Box::new(TcpStream::connect(rpc_host).await?),
}
})?
.await?;
};
log::info!("connected to rpc endpoint {}", rpc_url);

View File

@@ -1,5 +1,5 @@
use super::{Client, Status, TypedEnvelope, proto};
use anyhow::{Context as _, Result};
use anyhow::{Context as _, Result, anyhow};
use chrono::{DateTime, Utc};
use cloud_api_client::websocket_protocol::MessageToClient;
use cloud_api_client::{GetAuthenticatedUserResponse, PlanInfo};
@@ -116,6 +116,7 @@ pub struct UserStore {
edit_prediction_usage: Option<EditPredictionUsage>,
plan_info: Option<PlanInfo>,
current_user: watch::Receiver<Option<Arc<User>>>,
accepted_tos_at: Option<Option<cloud_api_client::Timestamp>>,
contacts: Vec<Arc<Contact>>,
incoming_contact_requests: Vec<Arc<User>>,
outgoing_contact_requests: Vec<Arc<User>>,
@@ -193,6 +194,7 @@ impl UserStore {
plan_info: None,
model_request_usage: None,
edit_prediction_usage: None,
accepted_tos_at: None,
contacts: Default::default(),
incoming_contact_requests: Default::default(),
participant_indices: Default::default(),
@@ -269,6 +271,7 @@ impl UserStore {
Status::SignedOut => {
current_user_tx.send(None).await.ok();
this.update(cx, |this, cx| {
this.accepted_tos_at = None;
cx.emit(Event::PrivateUserInfoUpdated);
cx.notify();
this.clear_contacts()
@@ -788,6 +791,19 @@ impl UserStore {
.set_authenticated_user_info(Some(response.user.metrics_id.clone()), staff);
}
let accepted_tos_at = {
#[cfg(debug_assertions)]
if std::env::var("ZED_IGNORE_ACCEPTED_TOS").is_ok() {
None
} else {
response.user.accepted_tos_at
}
#[cfg(not(debug_assertions))]
response.user.accepted_tos_at
};
self.accepted_tos_at = Some(accepted_tos_at);
self.model_request_usage = Some(ModelRequestUsage(RequestUsage {
limit: response.plan.usage.model_requests.limit,
amount: response.plan.usage.model_requests.used as i32,
@@ -830,6 +846,32 @@ impl UserStore {
self.current_user.clone()
}
pub fn has_accepted_terms_of_service(&self) -> bool {
self.accepted_tos_at
.is_some_and(|accepted_tos_at| accepted_tos_at.is_some())
}
pub fn accept_terms_of_service(&self, cx: &Context<Self>) -> Task<Result<()>> {
if self.current_user().is_none() {
return Task::ready(Err(anyhow!("no current user")));
};
let client = self.client.clone();
cx.spawn(async move |this, cx| -> anyhow::Result<()> {
let client = client.upgrade().context("client not found")?;
let response = client
.cloud_client()
.accept_terms_of_service()
.await
.context("error accepting tos")?;
this.update(cx, |this, cx| {
this.accepted_tos_at = Some(response.user.accepted_tos_at);
cx.emit(Event::PrivateUserInfoUpdated);
})?;
Ok(())
})
}
fn load_users(
&self,
request: impl RequestMessage<Response = UsersResponse>,

View File

@@ -43,11 +43,3 @@ pub fn ai_privacy_and_security(cx: &App) -> String {
server_url = server_url(cx)
)
}
/// Returns the URL to Zed AI's external agents documentation.
pub fn external_agents_docs(cx: &App) -> String {
format!(
"{server_url}/docs/ai/external-agents",
server_url = server_url(cx)
)
}

View File

@@ -102,7 +102,13 @@ impl CloudApiClient {
let credentials = credentials.as_ref().context("no credentials provided")?;
let authorization_header = format!("{} {}", credentials.user_id, credentials.access_token);
Ok(Tokio::spawn_result(cx, async move {
Ok(cx.spawn(async move |cx| {
let handle = cx
.update(|cx| Tokio::handle(cx))
.ok()
.context("failed to get Tokio handle")?;
let _guard = handle.enter();
let ws = WebSocket::connect(connect_url)
.with_request(
request::Builder::new()
@@ -115,6 +121,34 @@ impl CloudApiClient {
}))
}
pub async fn accept_terms_of_service(&self) -> Result<AcceptTermsOfServiceResponse> {
let request = self.build_request(
Request::builder().method(Method::POST).uri(
self.http_client
.build_zed_cloud_url("/client/terms_of_service/accept", &[])?
.as_ref(),
),
AsyncBody::default(),
)?;
let mut response = self.http_client.send(request).await?;
if !response.status().is_success() {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
anyhow::bail!(
"Failed to accept terms of service.\nStatus: {:?}\nBody: {body}",
response.status()
)
}
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
Ok(serde_json::from_str(&body)?)
}
pub async fn create_llm_token(
&self,
system_id: Option<String>,

View File

@@ -0,0 +1,4 @@
alter table billing_subscriptions
add column orb_subscription_status text,
add column orb_current_billing_period_start_date timestamp without time zone,
add column orb_current_billing_period_end_date timestamp without time zone;

View File

@@ -400,6 +400,8 @@ impl Server {
.add_request_handler(forward_mutating_project_request::<proto::SaveBuffer>)
.add_request_handler(forward_mutating_project_request::<proto::BlameBuffer>)
.add_request_handler(multi_lsp_query)
.add_request_handler(lsp_query)
.add_message_handler(broadcast_project_message_from_host::<proto::LspQueryResponse>)
.add_request_handler(forward_mutating_project_request::<proto::RestartLanguageServers>)
.add_request_handler(forward_mutating_project_request::<proto::StopLanguageServers>)
.add_request_handler(forward_mutating_project_request::<proto::LinkedEditingRange>)
@@ -910,7 +912,9 @@ impl Server {
user_id=field::Empty,
login=field::Empty,
impersonator=field::Empty,
// todo(lsp) remove after Zed Stable hits v0.204.x
multi_lsp_query_request=field::Empty,
lsp_query_request=field::Empty,
release_channel=field::Empty,
{ TOTAL_DURATION_MS }=field::Empty,
{ PROCESSING_DURATION_MS }=field::Empty,
@@ -2356,6 +2360,7 @@ where
Ok(())
}
// todo(lsp) remove after Zed Stable hits v0.204.x
async fn multi_lsp_query(
request: MultiLspQuery,
response: Response<MultiLspQuery>,
@@ -2366,6 +2371,21 @@ async fn multi_lsp_query(
forward_mutating_project_request(request, response, session).await
}
async fn lsp_query(
request: proto::LspQuery,
response: Response<proto::LspQuery>,
session: MessageContext,
) -> Result<()> {
let (name, should_write) = request.query_name_and_write_permissions();
tracing::Span::current().record("lsp_query_request", name);
tracing::info!("lsp_query message received");
if should_write {
forward_mutating_project_request(request, response, session).await
} else {
forward_read_only_project_request(request, response, session).await
}
}
/// Notify other participants that a new buffer has been created
async fn create_buffer_for_peer(
request: proto::CreateBufferForPeer,

View File

@@ -15,13 +15,14 @@ use editor::{
},
};
use fs::Fs;
use futures::{StreamExt, lock::Mutex};
use futures::{SinkExt, StreamExt, channel::mpsc, lock::Mutex};
use gpui::{App, Rgba, TestAppContext, UpdateGlobal, VisualContext, VisualTestContext};
use indoc::indoc;
use language::{
FakeLspAdapter,
language_settings::{AllLanguageSettings, InlayHintSettings},
};
use lsp::LSP_REQUEST_TIMEOUT;
use project::{
ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT,
lsp_store::lsp_ext_command::{ExpandedMacro, LspExtExpandMacro},
@@ -1017,6 +1018,211 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
})
}
#[gpui::test]
async fn test_slow_lsp_server(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
let mut server = TestServer::start(cx_a.executor()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
cx_b.update(editor::init);
let command_name = "test_command";
let capabilities = lsp::ServerCapabilities {
code_lens_provider: Some(lsp::CodeLensOptions {
resolve_provider: None,
}),
execute_command_provider: Some(lsp::ExecuteCommandOptions {
commands: vec![command_name.to_string()],
..lsp::ExecuteCommandOptions::default()
}),
..lsp::ServerCapabilities::default()
};
client_a.language_registry().add(rust_lang());
let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
"Rust",
FakeLspAdapter {
capabilities: capabilities.clone(),
..FakeLspAdapter::default()
},
);
client_b.language_registry().add(rust_lang());
client_b.language_registry().register_fake_lsp_adapter(
"Rust",
FakeLspAdapter {
capabilities,
..FakeLspAdapter::default()
},
);
client_a
.fs()
.insert_tree(
path!("/dir"),
json!({
"one.rs": "const ONE: usize = 1;"
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.join_remote_project(project_id, cx_b).await;
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
let editor_b = workspace_b
.update_in(cx_b, |workspace, window, cx| {
workspace.open_path((worktree_id, "one.rs"), None, true, window, cx)
})
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
let (lsp_store_b, buffer_b) = editor_b.update(cx_b, |editor, cx| {
let lsp_store = editor.project().unwrap().read(cx).lsp_store();
let buffer = editor.buffer().read(cx).as_singleton().unwrap();
(lsp_store, buffer)
});
let fake_language_server = fake_language_servers.next().await.unwrap();
cx_a.run_until_parked();
cx_b.run_until_parked();
let long_request_time = LSP_REQUEST_TIMEOUT / 2;
let (request_started_tx, mut request_started_rx) = mpsc::unbounded();
let requests_started = Arc::new(AtomicUsize::new(0));
let requests_completed = Arc::new(AtomicUsize::new(0));
let _lens_requests = fake_language_server
.set_request_handler::<lsp::request::CodeLensRequest, _, _>({
let request_started_tx = request_started_tx.clone();
let requests_started = requests_started.clone();
let requests_completed = requests_completed.clone();
move |params, cx| {
let mut request_started_tx = request_started_tx.clone();
let requests_started = requests_started.clone();
let requests_completed = requests_completed.clone();
async move {
assert_eq!(
params.text_document.uri.as_str(),
uri!("file:///dir/one.rs")
);
requests_started.fetch_add(1, atomic::Ordering::Release);
request_started_tx.send(()).await.unwrap();
cx.background_executor().timer(long_request_time).await;
let i = requests_completed.fetch_add(1, atomic::Ordering::Release) + 1;
Ok(Some(vec![lsp::CodeLens {
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 9)),
command: Some(lsp::Command {
title: format!("LSP Command {i}"),
command: command_name.to_string(),
arguments: None,
}),
data: None,
}]))
}
}
});
// Move cursor to a location, this should trigger the code lens call.
editor_b.update_in(cx_b, |editor, window, cx| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([7..7])
});
});
let () = request_started_rx.next().await.unwrap();
assert_eq!(
requests_started.load(atomic::Ordering::Acquire),
1,
"Selection change should have initiated the first request"
);
assert_eq!(
requests_completed.load(atomic::Ordering::Acquire),
0,
"Slow requests should be running still"
);
let _first_task = lsp_store_b.update(cx_b, |lsp_store, cx| {
lsp_store
.forget_code_lens_task(buffer_b.read(cx).remote_id())
.expect("Should have the fetch task started")
});
editor_b.update_in(cx_b, |editor, window, cx| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([1..1])
});
});
let () = request_started_rx.next().await.unwrap();
assert_eq!(
requests_started.load(atomic::Ordering::Acquire),
2,
"Selection change should have initiated the second request"
);
assert_eq!(
requests_completed.load(atomic::Ordering::Acquire),
0,
"Slow requests should be running still"
);
let _second_task = lsp_store_b.update(cx_b, |lsp_store, cx| {
lsp_store
.forget_code_lens_task(buffer_b.read(cx).remote_id())
.expect("Should have the fetch task started for the 2nd time")
});
editor_b.update_in(cx_b, |editor, window, cx| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([2..2])
});
});
let () = request_started_rx.next().await.unwrap();
assert_eq!(
requests_started.load(atomic::Ordering::Acquire),
3,
"Selection change should have initiated the third request"
);
assert_eq!(
requests_completed.load(atomic::Ordering::Acquire),
0,
"Slow requests should be running still"
);
_first_task.await.unwrap();
_second_task.await.unwrap();
cx_b.run_until_parked();
assert_eq!(
requests_started.load(atomic::Ordering::Acquire),
3,
"No selection changes should trigger no more code lens requests"
);
assert_eq!(
requests_completed.load(atomic::Ordering::Acquire),
3,
"After enough time, all 3 LSP requests should have been served by the language server"
);
let resulting_lens_actions = editor_b
.update(cx_b, |editor, cx| {
let lsp_store = editor.project().unwrap().read(cx).lsp_store();
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.code_lens_actions(&buffer_b, cx)
})
})
.await
.unwrap()
.unwrap();
assert_eq!(
resulting_lens_actions.len(),
1,
"Should have fetched one code lens action, but got: {resulting_lens_actions:?}"
);
assert_eq!(
resulting_lens_actions.first().unwrap().lsp_action.title(),
"LSP Command 3",
"Only the final code lens action should be in the data"
)
}
#[gpui::test(iterations = 10)]
async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
let mut server = TestServer::start(cx_a.executor()).await;

View File

@@ -4850,6 +4850,7 @@ async fn test_definition(
let definitions_1 = project_b
.update(cx_b, |p, cx| p.definitions(&buffer_b, 23, cx))
.await
.unwrap()
.unwrap();
cx_b.read(|cx| {
assert_eq!(
@@ -4885,6 +4886,7 @@ async fn test_definition(
let definitions_2 = project_b
.update(cx_b, |p, cx| p.definitions(&buffer_b, 33, cx))
.await
.unwrap()
.unwrap();
cx_b.read(|cx| {
assert_eq!(definitions_2.len(), 1);
@@ -4922,6 +4924,7 @@ async fn test_definition(
let type_definitions = project_b
.update(cx_b, |p, cx| p.type_definitions(&buffer_b, 7, cx))
.await
.unwrap()
.unwrap();
cx_b.read(|cx| {
assert_eq!(
@@ -5060,7 +5063,7 @@ async fn test_references(
])))
.unwrap();
let references = references.await.unwrap();
let references = references.await.unwrap().unwrap();
executor.run_until_parked();
project_b.read_with(cx_b, |project, cx| {
// User is informed that a request is no longer pending.
@@ -5104,7 +5107,7 @@ async fn test_references(
lsp_response_tx
.unbounded_send(Err(anyhow!("can't find references")))
.unwrap();
assert_eq!(references.await.unwrap(), []);
assert_eq!(references.await.unwrap().unwrap(), []);
// User is informed that the request is no longer pending.
executor.run_until_parked();
@@ -5505,7 +5508,8 @@ async fn test_lsp_hover(
// Request hover information as the guest.
let mut hovers = project_b
.update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
.await;
.await
.unwrap();
assert_eq!(
hovers.len(),
2,
@@ -5764,7 +5768,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
definitions = project_b.update(cx_b, |p, cx| p.definitions(&buffer_b1, 23, cx));
}
let definitions = definitions.await.unwrap();
let definitions = definitions.await.unwrap().unwrap();
assert_eq!(
definitions.len(),
1,

View File

@@ -1,6 +1,6 @@
use anyhow::Context as _;
use collections::HashMap;
use futures::{FutureExt, Stream, StreamExt as _, future::BoxFuture, lock::Mutex};
use futures::{Stream, StreamExt as _, lock::Mutex};
use gpui::BackgroundExecutor;
use std::{pin::Pin, sync::Arc};
@@ -14,12 +14,9 @@ pub fn create_fake_transport(
executor: BackgroundExecutor,
) -> FakeTransport {
let name = name.into();
FakeTransport::new(executor).on_request::<crate::types::requests::Initialize, _>(
move |_params| {
let name = name.clone();
async move { create_initialize_response(name.clone()) }
},
)
FakeTransport::new(executor).on_request::<crate::types::requests::Initialize>(move |_params| {
create_initialize_response(name.clone())
})
}
fn create_initialize_response(server_name: String) -> InitializeResponse {
@@ -35,10 +32,8 @@ fn create_initialize_response(server_name: String) -> InitializeResponse {
}
pub struct FakeTransport {
request_handlers: HashMap<
&'static str,
Arc<dyn Send + Sync + Fn(serde_json::Value) -> BoxFuture<'static, serde_json::Value>>,
>,
request_handlers:
HashMap<&'static str, Arc<dyn Fn(serde_json::Value) -> serde_json::Value + Send + Sync>>,
tx: futures::channel::mpsc::UnboundedSender<String>,
rx: Arc<Mutex<futures::channel::mpsc::UnboundedReceiver<String>>>,
executor: BackgroundExecutor,
@@ -55,25 +50,18 @@ impl FakeTransport {
}
}
pub fn on_request<T, Fut>(
pub fn on_request<T: crate::types::Request>(
mut self,
handler: impl 'static + Send + Sync + Fn(T::Params) -> Fut,
) -> Self
where
T: crate::types::Request,
Fut: 'static + Send + Future<Output = T::Response>,
{
handler: impl Fn(T::Params) -> T::Response + Send + Sync + 'static,
) -> Self {
self.request_handlers.insert(
T::METHOD,
Arc::new(move |value| {
let params = value
.get("params")
.cloned()
.unwrap_or(serde_json::Value::Null);
let params = value.get("params").expect("Missing parameters").clone();
let params: T::Params =
serde_json::from_value(params).expect("Invalid parameters received");
let response = handler(params);
async move { serde_json::to_value(response.await).unwrap() }.boxed()
serde_json::to_value(response).unwrap()
}),
);
self
@@ -89,7 +77,7 @@ impl Transport for FakeTransport {
if let Some(method) = msg.get("method") {
let method = method.as_str().expect("Invalid method received");
if let Some(handler) = self.request_handlers.get(method) {
let payload = handler(msg).await;
let payload = handler(msg);
let response = serde_json::json!({
"jsonrpc": "2.0",
"id": id,

View File

@@ -99,6 +99,7 @@ fn handle_preprocessing() -> Result<()> {
let mut errors = HashSet::<PreprocessorError>::new();
handle_frontmatter(&mut book, &mut errors);
template_big_table_of_actions(&mut book);
template_and_validate_keybindings(&mut book, &mut errors);
template_and_validate_actions(&mut book, &mut errors);
@@ -147,6 +148,18 @@ fn handle_frontmatter(book: &mut Book, errors: &mut HashSet<PreprocessorError>)
});
}
fn template_big_table_of_actions(book: &mut Book) {
for_each_chapter_mut(book, |chapter| {
let needle = "{#ACTIONS_TABLE#}";
if let Some(start) = chapter.content.rfind(needle) {
chapter.content.replace_range(
start..start + needle.len(),
&generate_big_table_of_actions(),
);
}
});
}
fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<PreprocessorError>) {
let regex = Regex::new(r"\{#kb (.*?)\}").unwrap();
@@ -277,6 +290,7 @@ struct ActionDef {
name: &'static str,
human_name: String,
deprecated_aliases: &'static [&'static str],
docs: Option<&'static str>,
}
fn dump_all_gpui_actions() -> Vec<ActionDef> {
@@ -285,6 +299,7 @@ fn dump_all_gpui_actions() -> Vec<ActionDef> {
name: action.name,
human_name: command_palette::humanize_action_name(action.name),
deprecated_aliases: action.deprecated_aliases,
docs: action.documentation,
})
.collect::<Vec<ActionDef>>();
@@ -418,3 +433,54 @@ fn title_regex() -> &'static Regex {
static TITLE_REGEX: OnceLock<Regex> = OnceLock::new();
TITLE_REGEX.get_or_init(|| Regex::new(r"<title>\s*(.*?)\s*</title>").unwrap())
}
fn generate_big_table_of_actions() -> String {
let actions = &*ALL_ACTIONS;
let mut output = String::new();
let mut actions_sorted = actions.iter().collect::<Vec<_>>();
actions_sorted.sort_by_key(|a| a.name);
// Start the definition list with custom styling for better spacing
output.push_str("<dl style=\"line-height: 1.8;\">\n");
for action in actions_sorted.into_iter() {
// Add the humanized action name as the term with margin
output.push_str(
"<dt style=\"margin-top: 1.5em; margin-bottom: 0.5em; font-weight: bold;\"><code>",
);
output.push_str(&action.human_name);
output.push_str("</code></dt>\n");
// Add the definition with keymap name and description
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 {
output.push_str(
&description
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;"),
);
output.push_str("<br>\n");
}
output.push_str("Keymap Name: <code>");
output.push_str(action.name);
output.push_str("</code><br>\n");
if !action.deprecated_aliases.is_empty() {
output.push_str("Deprecated Aliases:");
for alias in action.deprecated_aliases.iter() {
output.push_str("<code>");
output.push_str(alias);
output.push_str("</code>, ");
}
}
output.push_str("\n</dd>\n");
}
// Close the definition list
output.push_str("</dl>\n");
output
}

View File

@@ -89,6 +89,9 @@ pub trait EditPredictionProvider: 'static + Sized {
debounce: bool,
cx: &mut Context<Self>,
);
fn needs_terms_acceptance(&self, _cx: &App) -> bool {
false
}
fn cycle(
&mut self,
buffer: Entity<Buffer>,
@@ -121,6 +124,7 @@ pub trait EditPredictionProviderHandle {
fn data_collection_state(&self, cx: &App) -> DataCollectionState;
fn usage(&self, cx: &App) -> Option<EditPredictionUsage>;
fn toggle_data_collection(&self, cx: &mut App);
fn needs_terms_acceptance(&self, cx: &App) -> bool;
fn is_refreshing(&self, cx: &App) -> bool;
fn refresh(
&self,
@@ -192,6 +196,10 @@ where
self.read(cx).is_enabled(buffer, cursor_position, cx)
}
fn needs_terms_acceptance(&self, cx: &App) -> bool {
self.read(cx).needs_terms_acceptance(cx)
}
fn is_refreshing(&self, cx: &App) -> bool {
self.read(cx).is_refreshing()
}

View File

@@ -242,9 +242,13 @@ impl Render for EditPredictionButton {
IconName::ZedPredictDisabled
};
if zeta::should_show_upsell_modal() {
if zeta::should_show_upsell_modal(&self.user_store, cx) {
let tooltip_meta = if self.user_store.read(cx).current_user().is_some() {
"Choose a Plan"
if self.user_store.read(cx).has_accepted_terms_of_service() {
"Choose a Plan"
} else {
"Accept the Terms of Service"
}
} else {
"Sign In"
};

View File

@@ -253,6 +253,7 @@ pub type RenderDiffHunkControlsFn = Arc<
enum ReportEditorEvent {
Saved { auto_saved: bool },
EditorOpened,
ZetaTosClicked,
Closed,
}
@@ -261,6 +262,7 @@ impl ReportEditorEvent {
match self {
Self::Saved { .. } => "Editor Saved",
Self::EditorOpened => "Editor Opened",
Self::ZetaTosClicked => "Edit Prediction Provider ToS Clicked",
Self::Closed => "Editor Closed",
}
}
@@ -1898,6 +1900,60 @@ impl Editor {
editor.update_lsp_data(false, Some(*buffer_id), window, cx);
}
}
project::Event::EntryRenamed(transaction) => {
let Some(workspace) = editor.workspace() else {
return;
};
let Some(active_editor) = workspace.read(cx).active_item_as::<Self>(cx)
else {
return;
};
if active_editor.entity_id() == cx.entity_id() {
let edited_buffers_already_open = {
let other_editors: Vec<Entity<Editor>> = workspace
.read(cx)
.panes()
.iter()
.flat_map(|pane| pane.read(cx).items_of_type::<Editor>())
.filter(|editor| editor.entity_id() != cx.entity_id())
.collect();
transaction.0.keys().all(|buffer| {
other_editors.iter().any(|editor| {
let multi_buffer = editor.read(cx).buffer();
multi_buffer.read(cx).is_singleton()
&& multi_buffer.read(cx).as_singleton().map_or(
false,
|singleton| {
singleton.entity_id() == buffer.entity_id()
},
)
})
})
};
if !edited_buffers_already_open {
let workspace = workspace.downgrade();
let transaction = transaction.clone();
cx.defer_in(window, move |_, window, cx| {
cx.spawn_in(window, async move |editor, cx| {
Self::open_project_transaction(
&editor,
workspace,
transaction,
"Rename".to_string(),
cx,
)
.await
.ok()
})
.detach();
});
}
}
}
_ => {}
},
));
@@ -6280,7 +6336,7 @@ impl Editor {
}
pub async fn open_project_transaction(
this: &WeakEntity<Editor>,
editor: &WeakEntity<Editor>,
workspace: WeakEntity<Workspace>,
transaction: ProjectTransaction,
title: String,
@@ -6298,7 +6354,7 @@ impl Editor {
if let Some((buffer, transaction)) = entries.first() {
if entries.len() == 1 {
let excerpt = this.update(cx, |editor, cx| {
let excerpt = editor.update(cx, |editor, cx| {
editor
.buffer()
.read(cx)
@@ -9113,6 +9169,45 @@ impl Editor {
let provider = self.edit_prediction_provider.as_ref()?;
let provider_icon = Self::get_prediction_provider_icon_name(&self.edit_prediction_provider);
if provider.provider.needs_terms_acceptance(cx) {
return Some(
h_flex()
.min_w(min_width)
.flex_1()
.px_2()
.py_1()
.gap_3()
.elevation_2(cx)
.hover(|style| style.bg(cx.theme().colors().element_hover))
.id("accept-terms")
.cursor_pointer()
.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
.on_click(cx.listener(|this, _event, window, cx| {
cx.stop_propagation();
this.report_editor_event(ReportEditorEvent::ZetaTosClicked, None, cx);
window.dispatch_action(
zed_actions::OpenZedPredictOnboarding.boxed_clone(),
cx,
);
}))
.child(
h_flex()
.flex_1()
.gap_2()
.child(Icon::new(provider_icon))
.child(Label::new("Accept Terms of Service"))
.child(div().w_full())
.child(
Icon::new(IconName::ArrowUpRight)
.color(Color::Muted)
.size(IconSize::Small),
)
.into_any_element(),
)
.into_any(),
);
}
let is_refreshing = provider.provider.is_refreshing(cx);
fn pending_completion_container(icon: IconName) -> Div {
@@ -15659,7 +15754,9 @@ impl Editor {
};
cx.spawn_in(window, async move |editor, cx| {
let definitions = definitions.await?;
let Some(definitions) = definitions.await? else {
return Ok(Navigated::No);
};
let navigated = editor
.update_in(cx, |editor, window, cx| {
editor.navigate_to_hover_links(
@@ -16001,7 +16098,9 @@ impl Editor {
}
});
let locations = references.await?;
let Some(locations) = references.await? else {
return anyhow::Ok(Navigated::No);
};
if locations.is_empty() {
return anyhow::Ok(Navigated::No);
}
@@ -21783,7 +21882,7 @@ pub trait SemanticsProvider {
buffer: &Entity<Buffer>,
position: text::Anchor,
cx: &mut App,
) -> Option<Task<Vec<project::Hover>>>;
) -> Option<Task<Option<Vec<project::Hover>>>>;
fn inline_values(
&self,
@@ -21822,7 +21921,7 @@ pub trait SemanticsProvider {
position: text::Anchor,
kind: GotoDefinitionKind,
cx: &mut App,
) -> Option<Task<Result<Vec<LocationLink>>>>;
) -> Option<Task<Result<Option<Vec<LocationLink>>>>>;
fn range_for_rename(
&self,
@@ -21935,7 +22034,13 @@ impl CodeActionProvider for Entity<Project> {
Ok(code_lens_actions
.context("code lens fetch")?
.into_iter()
.chain(code_actions.context("code action fetch")?)
.flatten()
.chain(
code_actions
.context("code action fetch")?
.into_iter()
.flatten(),
)
.collect())
})
})
@@ -22230,7 +22335,7 @@ impl SemanticsProvider for Entity<Project> {
buffer: &Entity<Buffer>,
position: text::Anchor,
cx: &mut App,
) -> Option<Task<Vec<project::Hover>>> {
) -> Option<Task<Option<Vec<project::Hover>>>> {
Some(self.update(cx, |project, cx| project.hover(buffer, position, cx)))
}
@@ -22251,7 +22356,7 @@ impl SemanticsProvider for Entity<Project> {
position: text::Anchor,
kind: GotoDefinitionKind,
cx: &mut App,
) -> Option<Task<Result<Vec<LocationLink>>>> {
) -> Option<Task<Result<Option<Vec<LocationLink>>>>> {
Some(self.update(cx, |project, cx| match kind {
GotoDefinitionKind::Symbol => project.definitions(buffer, position, cx),
GotoDefinitionKind::Declaration => project.declarations(buffer, position, cx),

View File

@@ -1,6 +1,7 @@
use crate::{Editor, RangeToAnchorExt};
use gpui::{Context, Window};
use gpui::{Context, HighlightStyle, Window};
use language::CursorShape;
use theme::ActiveTheme;
enum MatchingBracketHighlight {}
@@ -9,7 +10,7 @@ pub fn refresh_matching_bracket_highlights(
window: &mut Window,
cx: &mut Context<Editor>,
) {
editor.clear_background_highlights::<MatchingBracketHighlight>(cx);
editor.clear_highlights::<MatchingBracketHighlight>(cx);
let newest_selection = editor.selections.newest::<usize>(cx);
// Don't highlight brackets if the selection isn't empty
@@ -35,12 +36,19 @@ pub fn refresh_matching_bracket_highlights(
.buffer_snapshot
.innermost_enclosing_bracket_ranges(head..tail, None)
{
editor.highlight_background::<MatchingBracketHighlight>(
&[
editor.highlight_text::<MatchingBracketHighlight>(
vec![
opening_range.to_anchors(&snapshot.buffer_snapshot),
closing_range.to_anchors(&snapshot.buffer_snapshot),
],
|theme| theme.colors().editor_document_highlight_bracket_background,
HighlightStyle {
background_color: Some(
cx.theme()
.colors()
.editor_document_highlight_bracket_background,
),
..Default::default()
},
cx,
)
}
@@ -104,7 +112,7 @@ mod tests {
another_test(1, 2, 3);
}
"#});
cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
cx.assert_editor_text_highlights::<MatchingBracketHighlight>(indoc! {r#"
pub fn test«(»"Test argument"«)» {
another_test(1, 2, 3);
}
@@ -115,7 +123,7 @@ mod tests {
another_test(1, ˇ2, 3);
}
"#});
cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
cx.assert_editor_text_highlights::<MatchingBracketHighlight>(indoc! {r#"
pub fn test("Test argument") {
another_test«(»1, 2, 3«)»;
}
@@ -126,7 +134,7 @@ mod tests {
anotherˇ_test(1, 2, 3);
}
"#});
cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
cx.assert_editor_text_highlights::<MatchingBracketHighlight>(indoc! {r#"
pub fn test("Test argument") «{»
another_test(1, 2, 3);
«}»
@@ -138,7 +146,7 @@ mod tests {
another_test(1, 2, 3);
}
"#});
cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
cx.assert_editor_text_highlights::<MatchingBracketHighlight>(indoc! {r#"
pub fn test("Test argument") {
another_test(1, 2, 3);
}
@@ -150,8 +158,8 @@ mod tests {
another_test(1, 2, 3);
}
"#});
cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
pub fn test("Test argument") {
cx.assert_editor_text_highlights::<MatchingBracketHighlight>(indoc! {r#"
pub fn test«("Test argument") {
another_test(1, 2, 3);
}
"#});

View File

@@ -562,7 +562,7 @@ pub fn show_link_definition(
provider.definitions(&buffer, buffer_position, preferred_kind, cx)
})?;
if let Some(task) = task {
task.await.ok().map(|definition_result| {
task.await.ok().flatten().map(|definition_result| {
(
definition_result.iter().find_map(|link| {
link.origin.as_ref().and_then(|origin| {

View File

@@ -428,7 +428,7 @@ fn show_hover(
};
let hovers_response = if let Some(hover_request) = hover_request {
hover_request.await
hover_request.await.unwrap_or_default()
} else {
Vec::new()
};

View File

@@ -431,7 +431,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
buffer: &Entity<Buffer>,
position: text::Anchor,
cx: &mut App,
) -> Option<Task<Vec<project::Hover>>> {
) -> Option<Task<Option<Vec<project::Hover>>>> {
let buffer = self.to_base(buffer, &[position], cx)?;
self.0.hover(&buffer, position, cx)
}
@@ -490,7 +490,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
position: text::Anchor,
kind: crate::GotoDefinitionKind,
cx: &mut App,
) -> Option<Task<anyhow::Result<Vec<project::LocationLink>>>> {
) -> Option<Task<anyhow::Result<Option<Vec<project::LocationLink>>>>> {
let buffer = self.to_base(buffer, &[position], cx)?;
self.0.definitions(&buffer, position, kind, cx)
}

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