Compare commits
99 Commits
code-actio
...
test-drive
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
447eb8e1c9 | ||
|
|
e434117018 | ||
|
|
36271b79b3 | ||
|
|
41644a53cc | ||
|
|
08a9c4af09 | ||
|
|
3187f28405 | ||
|
|
101f3b100f | ||
|
|
39c8b7bf5f | ||
|
|
08b41252f6 | ||
|
|
152bbca238 | ||
|
|
107d8ca483 | ||
|
|
4278d894d2 | ||
|
|
a91948aeb4 | ||
|
|
2178b36cbc | ||
|
|
0fb0059b5f | ||
|
|
fbf7caf93e | ||
|
|
d48152d958 | ||
|
|
bafc086d27 | ||
|
|
f737c4d01e | ||
|
|
8f308d835a | ||
|
|
703a68eedf | ||
|
|
cc2fcb2f42 | ||
|
|
f0ef3110d3 | ||
|
|
0454e7a22e | ||
|
|
d88b06a5dc | ||
|
|
98ceffe026 | ||
|
|
bab28560ef | ||
|
|
8102a16747 | ||
|
|
9875521d4e | ||
|
|
8c55063417 | ||
|
|
7abe2c9c31 | ||
|
|
73a767fc45 | ||
|
|
327fee4d22 | ||
|
|
b1d5918fdc | ||
|
|
6f685b9f8e | ||
|
|
65a7076ba8 | ||
|
|
3fe15ee915 | ||
|
|
bae3ef01c6 | ||
|
|
810b39ce34 | ||
|
|
a93aa598d6 | ||
|
|
28aba94369 | ||
|
|
e147ef006c | ||
|
|
e18d787dbc | ||
|
|
d02c8bcc02 | ||
|
|
e27f6a984f | ||
|
|
cce661b64b | ||
|
|
3932a6c51e | ||
|
|
1e0ae35f69 | ||
|
|
4405ed04d0 | ||
|
|
c585dbd8ff | ||
|
|
c7fc95e732 | ||
|
|
f97546b6ef | ||
|
|
7badd6053d | ||
|
|
502a0f6535 | ||
|
|
eea6cfb383 | ||
|
|
0dc0701967 | ||
|
|
62b8ef980b | ||
|
|
269f6403dd | ||
|
|
3538acec7c | ||
|
|
87512d0814 | ||
|
|
6254efe39d | ||
|
|
72218f4a61 | ||
|
|
5f7189e5af | ||
|
|
f6d13645ce | ||
|
|
6ffd3f034f | ||
|
|
6e0732a9d7 | ||
|
|
f8d097acd6 | ||
|
|
7cf4926130 | ||
|
|
676cc109a3 | ||
|
|
ba7f886c62 | ||
|
|
c2cd4fd7a1 | ||
|
|
4095011af5 | ||
|
|
80a2f71d8e | ||
|
|
d93141bded | ||
|
|
b402007de6 | ||
|
|
be63d51eb7 | ||
|
|
8660101b83 | ||
|
|
1aa1b2bede | ||
|
|
58d8b91131 | ||
|
|
ba588161d9 | ||
|
|
7e928dd615 | ||
|
|
fade49a11a | ||
|
|
e4f692ac75 | ||
|
|
c21bca07e2 | ||
|
|
acc4a5ccb3 | ||
|
|
7a95c14625 | ||
|
|
6dd622d6c3 | ||
|
|
e7afbbd725 | ||
|
|
133932ed74 | ||
|
|
3ca63584b9 | ||
|
|
2a878ee6d0 | ||
|
|
8117940aca | ||
|
|
002235d0da | ||
|
|
f07695c4cd | ||
|
|
bdd0cbb717 | ||
|
|
022a110f8e | ||
|
|
b0200c4368 | ||
|
|
ae47829fa8 | ||
|
|
5ebb18c47e |
1
.clinerules
Symbolic link
1
.clinerules
Symbolic link
@@ -0,0 +1 @@
|
||||
.rules
|
||||
1
.cursorrules
Symbolic link
1
.cursorrules
Symbolic link
@@ -0,0 +1 @@
|
||||
.rules
|
||||
2
.github/actions/run_tests/action.yml
vendored
2
.github/actions/run_tests/action.yml
vendored
@@ -10,7 +10,7 @@ runs:
|
||||
cargo install cargo-nextest --locked
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
|
||||
2
.github/actions/run_tests_windows/action.yml
vendored
2
.github/actions/run_tests_windows/action.yml
vendored
@@ -16,7 +16,7 @@ runs:
|
||||
run: cargo install cargo-nextest --locked
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -519,7 +519,7 @@ jobs:
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
|
||||
2
.github/workflows/danger.yml
vendored
2
.github/workflows/danger.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "pnpm"
|
||||
|
||||
2
.github/workflows/issue_response.yml
vendored
2
.github/workflows/issue_response.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "pnpm"
|
||||
|
||||
2
.github/workflows/randomized_tests.yml
vendored
2
.github/workflows/randomized_tests.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
- buildjet-16vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
|
||||
2
.github/workflows/release_nightly.yml
vendored
2
.github/workflows/release_nightly.yml
vendored
@@ -71,7 +71,7 @@ jobs:
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -18,7 +18,6 @@
|
||||
.venv
|
||||
.vscode
|
||||
.wrangler
|
||||
/.direnv
|
||||
/assets/*licenses.*
|
||||
/crates/collab/seed.json
|
||||
/crates/theme/schemas/theme.json
|
||||
|
||||
110
.rules
Normal file
110
.rules
Normal file
@@ -0,0 +1,110 @@
|
||||
# Rust coding guidelines
|
||||
|
||||
* Prioritize code correctness and clarity. Speed and efficiency are secondary priorities unless otherwise specified.
|
||||
* Do not write organizational or comments that summarize the code. Comments should only be written in order to explain "why" the code is written in some way in the case there is a reason that is tricky / non-obvious.
|
||||
* Prefer implementing functionality in existing files unless it is a new logical component. Avoid creating many small files.
|
||||
* Avoid using functions that panic like `unwrap()`, instead use mechanisms like `?` to propagate errors.
|
||||
* Be careful with operations like indexing which may panic if the indexes are out of bounds.
|
||||
|
||||
# GPUI
|
||||
|
||||
GPUI is a UI framework which also provides primitives for state and concurrency management.
|
||||
|
||||
## Context
|
||||
|
||||
Context types allow interaction with global state, windows, entities, and system services. They are typically passed to functions as the argument named `cx`. When a function takes callbacks they come after the `cx` parameter.
|
||||
|
||||
* `App` is the root context type, providing access to global state and read and update of entities.
|
||||
* `Context<T>` is provided when updating an `Entity<T>`. This context dereferences into `App`, so functions which take `&App` can also take `&Context<T>`.
|
||||
* `AsyncApp` and `AsyncWindowContext` are provided by `cx.spawn` and `cx.spawn_in`. These can be held across await points.
|
||||
|
||||
## `Window`
|
||||
|
||||
`Window` provides access to the state of an application window. It is passed to functions as an argument named `window` and comes before `cx` when present. It is used for managing focus, dispatching actions, directly drawing, getting user input state, etc.
|
||||
|
||||
## Entities
|
||||
|
||||
An `Entity<T>` is a handle to state of type `T`. With `thing: Entity<T>`:
|
||||
|
||||
* `thing.entity_id()` returns `EntityId`
|
||||
* `thing.downgrade()` returns `WeakEntity<T>`
|
||||
* `thing.read(cx: &App)` returns `&T`.
|
||||
* `thing.read_with(cx, |thing: &T, cx: &App| ...)` returns the closure's return value.
|
||||
* `thing.update(cx, |thing: &mut T, cx: &mut Context<T>| ...)` allows the closure to mutate the state, and provides a `Context<T>` for interacting with the entity. It returns the closure's return value.
|
||||
* `thing.update_in(cx, |thing: &mut T, window: &mut Window, cx: &mut Context<T>| ...)` takes a `AsyncWindowContext` or `VisualTestContext`. It's the same as `update` while also providing the `Window`.
|
||||
|
||||
Within the closures, the inner `cx` provided to the closure must be used instead of the outer `cx` to avoid issues with multiple borrows.
|
||||
|
||||
Trying to update an entity while it's already being updated must be avoided as this will cause a panic.
|
||||
|
||||
When `read_with`, `update`, or `update_in` are used with an async context, the closure's return value is wrapped in an `anyhow::Result`.
|
||||
|
||||
`WeakEntity<T>` is a weak handle. It has `read_with`, `update`, and `update_in` methods that work the same, but always return an `anyhow::Result` so that they can fail if the entity no longer exists. This can be useful to avoid memory leaks - if entities have mutually recursive handles to eachother they will never be dropped.
|
||||
|
||||
## Concurrency
|
||||
|
||||
All use of entities and UI rendering occurs on a single foreground thread.
|
||||
|
||||
`cx.spawn(async move |cx| ...)` runs an async closure on the foreground thread. Within the closure, `cx` is an async context like `AsyncApp` or `AsyncWindowContext`.
|
||||
|
||||
When the outer cx is a `Context<T>`, the use of `spawn` instead looks like `cx.spawn(async move |handle, cx| ...)`, where `handle: WeakEntity<T>`.
|
||||
|
||||
To do work on other threads, `cx.background_spawn(async move { ... })` is used. Often this background task is awaited on by a foreground task which uses the results to update state.
|
||||
|
||||
Both `cx.spawn` and `cx.background_spawn` return a `Task<R>`, which is a future that can be awaited upon. If this task is dropped, then its work is cancelled. To prevent this one of the following must be done:
|
||||
|
||||
* Awaiting the task in some other async context.
|
||||
* Detaching the task via `task.detach()` or `task.detach_and_log_err(cx)`, allowing it to run indefinitely.
|
||||
* Storing the task in a field, if the work should be halted when the struct is dropped.
|
||||
|
||||
A task which doesn't do anything but provide a value can be created with `Task::ready(value)`.
|
||||
|
||||
## Elements
|
||||
|
||||
The `Render` trait is used to render some state into an element tree that is laid out using flexbox layout. An `Entity<T>` where `T` implements `Render` is sometimes called a "view".
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
struct TextWithBorder(SharedString);
|
||||
|
||||
impl Render for TextWithBorder {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().border_1().child(self.0.clone())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Since `impl IntoElement for SharedString` exists, it can be used as an argument to `child`. `SharedString` is used to avoid copying strings, and is either an `&'static str` or `Arc<str>`.
|
||||
|
||||
UI components that are constructed just to be turned into elements can instead implement the `RenderOnce` trait, which is similar to `Render`, but its `render` method takes ownership of `self`. Types that implement this trait can use `#[derive(IntoElement)]` to use them directly as children.
|
||||
|
||||
The style methods on elements are similar to those used by Tailwind CSS.
|
||||
|
||||
If some attributes or children of an element tree are conditional, `.when(condition, |this| ...)` can be used to run the closure only when `condition` is true. Similarly, `.when_some(option, |this, value| ...)` runs the closure when the `Option` has a value.
|
||||
|
||||
## Input events
|
||||
|
||||
Input event handlers can be registered on an element via methods like `.on_click(|event, window, cx: &mut App| ...)`.
|
||||
|
||||
Often event handlers will want to update the entity that's in the current `Context<T>`. The `cx.listener` method provides this - its use looks like `.on_click(cx.listener(|this: &mut T, event, window, cx: &mut Context<T>| ...)`.
|
||||
|
||||
## Actions
|
||||
|
||||
Actions are dispatched via user keyboard interaction or in code via `window.dispatch_action(SomeAction.boxed_clone(), cx)` or `focus_handle.dispatch_action(&SomeAction, window, cx)`.
|
||||
|
||||
Actions which have no data inside are created and registered with the `actions!(some_namespace, [SomeAction, AnotherAction])` macro call.
|
||||
|
||||
Actions that do have data must implement `Clone, Default, PartialEq, Deserialize, JsonSchema` and can be registered with an `impl_actions!(some_namespace, [SomeActionWithData])` macro call.
|
||||
|
||||
Action handlers can be registered on an element via the event handler `.on_action(|action, window, cx| ...)`. Like other event handlers, this is often used with `cx.listener`.
|
||||
|
||||
## Notify
|
||||
|
||||
When a view's state has changed in a way that may affect its rendering, it should call `cx.notify()`. This will cause the view to be rerendered. It will also cause any observe callbacks registered for the entity with `cx.observe` to be called.
|
||||
|
||||
## Entity events
|
||||
|
||||
While updating an entity (`cx: Context<T>`), it can emit an event using `cx.emit(event)`. Entities register which events they can emit by declaring `impl EventEmittor<EventType> for EntityType {}`.
|
||||
|
||||
Other entities can then register a callback to handle these events by doing `cx.subscribe(other_entity, |this, other_entity, event, cx| ...)`. This will return a `Subscription` which deregisters the callback when dropped. Typically `cx.subscribe` happens when creating a new entity and the subscriptions are stored in a `_subscriptions: Vec<Subscription>` field.
|
||||
1
.windsurfrules
Symbolic link
1
.windsurfrules
Symbolic link
@@ -0,0 +1 @@
|
||||
.rules
|
||||
@@ -45,5 +45,6 @@
|
||||
"hard_tabs": false,
|
||||
"formatter": "auto",
|
||||
"remove_trailing_whitespace_on_save": true,
|
||||
"ensure_final_newline_on_save": true
|
||||
"ensure_final_newline_on_save": true,
|
||||
"file_scan_exclusions": ["crates/eval/worktrees/", "crates/eval/repos/"]
|
||||
}
|
||||
|
||||
163
Cargo.lock
generated
163
Cargo.lock
generated
@@ -125,6 +125,37 @@ dependencies = [
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agent2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assistant_tool",
|
||||
"assistant_tools",
|
||||
"chrono",
|
||||
"client",
|
||||
"collections",
|
||||
"ctor",
|
||||
"env_logger 0.11.8",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"gpui_tokio",
|
||||
"language_model",
|
||||
"language_models",
|
||||
"parking_lot",
|
||||
"project",
|
||||
"reqwest_client",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"thiserror 2.0.12",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -703,21 +734,27 @@ dependencies = [
|
||||
"assistant_tool",
|
||||
"chrono",
|
||||
"collections",
|
||||
"component",
|
||||
"feature_flags",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"html_to_markdown",
|
||||
"http_client",
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"language_model",
|
||||
"linkme",
|
||||
"open",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"tree-sitter-rust",
|
||||
"ui",
|
||||
"unindent",
|
||||
"util",
|
||||
@@ -1337,12 +1374,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-bedrockruntime"
|
||||
version = "1.80.0"
|
||||
version = "1.82.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39ee8ef191b908d013659ca2c0670215f0c920c781998e1dc55904d6bdb73b51"
|
||||
checksum = "8cb95f77abd4321348dd2f52a25e1de199732f54d2a35860ad20f5df21c66b44"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
"aws-sigv4",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-eventstream",
|
||||
"aws-smithy-http",
|
||||
@@ -1354,6 +1392,7 @@ dependencies = [
|
||||
"bytes 1.10.1",
|
||||
"fastrand 2.3.0",
|
||||
"http 0.2.12",
|
||||
"hyper 0.14.32",
|
||||
"once_cell",
|
||||
"regex-lite",
|
||||
"tracing",
|
||||
@@ -2717,9 +2756,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.35"
|
||||
version = "4.5.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944"
|
||||
checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -2727,9 +2766,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.35"
|
||||
version = "4.5.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9"
|
||||
checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -4919,36 +4958,6 @@ dependencies = [
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "evals"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"dap",
|
||||
"env_logger 0.11.8",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"gpui",
|
||||
"http_client",
|
||||
"language",
|
||||
"languages",
|
||||
"node_runtime",
|
||||
"open_ai",
|
||||
"project",
|
||||
"reqwest_client",
|
||||
"semantic_index",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "2.5.3"
|
||||
@@ -6192,6 +6201,7 @@ dependencies = [
|
||||
"windows 0.61.1",
|
||||
"windows-core 0.61.0",
|
||||
"windows-numerics",
|
||||
"windows-registry 0.5.1",
|
||||
"workspace-hack",
|
||||
"x11-clipboard",
|
||||
"x11rb",
|
||||
@@ -7134,10 +7144,12 @@ dependencies = [
|
||||
name = "inline_completion"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gpui",
|
||||
"language",
|
||||
"project",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7168,6 +7180,7 @@ dependencies = [
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
"zed_llm_client",
|
||||
"zeta",
|
||||
]
|
||||
|
||||
@@ -7669,7 +7682,6 @@ dependencies = [
|
||||
"http_client",
|
||||
"icons",
|
||||
"image",
|
||||
"log",
|
||||
"open_ai",
|
||||
"parking_lot",
|
||||
"proto",
|
||||
@@ -7682,6 +7694,7 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7907,9 +7920,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.171"
|
||||
version = "0.2.172"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
|
||||
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
|
||||
|
||||
[[package]]
|
||||
name = "libdbus-sys"
|
||||
@@ -7961,9 +7974,9 @@ checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
|
||||
|
||||
[[package]]
|
||||
name = "libmimalloc-sys"
|
||||
version = "0.1.41"
|
||||
version = "0.1.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b20daca3a4ac14dbdc753c5e90fc7b490a48a9131daed3c9a9ced7b2defd37b"
|
||||
checksum = "ec9d6fac27761dabcd4ee73571cdb06b7022dc99089acbe5435691edffaac0f4"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -8634,9 +8647,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mimalloc"
|
||||
version = "0.1.45"
|
||||
version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03cb1f88093fe50061ca1195d336ffec131347c7b833db31f9ab62a2d1b7925f"
|
||||
checksum = "995942f432bbb4822a7e9c3faa87a695185b0d09273ba85f097b54f4e458f2af"
|
||||
dependencies = [
|
||||
"libmimalloc-sys",
|
||||
]
|
||||
@@ -10806,9 +10819,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.94"
|
||||
version = "1.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -11948,7 +11961,7 @@ dependencies = [
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"windows-registry",
|
||||
"windows-registry 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -12646,9 +12659,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sea-orm"
|
||||
version = "1.1.8"
|
||||
version = "1.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "013d6c9e421b9c44c6eb6ebbee283b2a2c90eab2682088c4a2449706a42d117f"
|
||||
checksum = "21e61af841881c137d4bc8e0d8411cee9168548b404f9e4788e8af7e8f94bd4e"
|
||||
dependencies = [
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
@@ -12675,9 +12688,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sea-orm-macros"
|
||||
version = "1.1.8"
|
||||
version = "1.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "31feeff3ad5e999c64b2b8fd30933b2871911567c4d62dcf70b3effd970c7891"
|
||||
checksum = "d6b86e3e77b548e6c6c1f612a1ca024d557dffdb81b838bf482ad3222140c77b"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
@@ -13330,6 +13343,7 @@ dependencies = [
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"parking_lot",
|
||||
"paths",
|
||||
"schemars",
|
||||
@@ -13458,9 +13472,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx"
|
||||
version = "0.8.3"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f"
|
||||
checksum = "f3c3a85280daca669cfd3bcb68a337882a8bc57ec882f72c5d13a430613a738e"
|
||||
dependencies = [
|
||||
"sqlx-core",
|
||||
"sqlx-macros",
|
||||
@@ -13471,10 +13485,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-core"
|
||||
version = "0.8.3"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0"
|
||||
checksum = "f743f2a3cea30a58cd479013f75550e879009e3a02f616f18ca699335aa248c3"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bigdecimal",
|
||||
"bytes 1.10.1",
|
||||
"chrono",
|
||||
@@ -13495,7 +13510,6 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"rust_decimal",
|
||||
"rustls 0.23.26",
|
||||
"rustls-pemfile 2.2.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
@@ -13512,9 +13526,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-macros"
|
||||
version = "0.8.3"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3112e2ad78643fef903618d78cf0aec1cb3134b019730edb039b69eaf531f310"
|
||||
checksum = "7f4200e0fde19834956d4252347c12a083bdcb237d7a1a1446bffd8768417dce"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -13525,9 +13539,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-macros-core"
|
||||
version = "0.8.3"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e9f90acc5ab146a99bf5061a7eb4976b573f560bc898ef3bf8435448dd5e7ad"
|
||||
checksum = "882ceaa29cade31beca7129b6beeb05737f44f82dbe2a9806ecea5a7093d00b7"
|
||||
dependencies = [
|
||||
"dotenvy",
|
||||
"either",
|
||||
@@ -13551,9 +13565,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-mysql"
|
||||
version = "0.8.3"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233"
|
||||
checksum = "0afdd3aa7a629683c2d750c2df343025545087081ab5942593a5288855b1b7a7"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64 0.22.1",
|
||||
@@ -13598,9 +13612,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-postgres"
|
||||
version = "0.8.3"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613"
|
||||
checksum = "a0bedbe1bbb5e2615ef347a5e9d8cd7680fb63e77d9dafc0f29be15e53f1ebe6"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64 0.22.1",
|
||||
@@ -13641,9 +13655,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-sqlite"
|
||||
version = "0.8.3"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540"
|
||||
checksum = "c26083e9a520e8eb87a06b12347679b142dc2ea29e6e409f805644a7a979a5bc"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"chrono",
|
||||
@@ -13659,6 +13673,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_urlencoded",
|
||||
"sqlx-core",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"tracing",
|
||||
"url",
|
||||
@@ -15854,7 +15869,6 @@ dependencies = [
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"libc",
|
||||
"log",
|
||||
"lsp",
|
||||
"multi_buffer",
|
||||
@@ -17061,6 +17075,17 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad1da3e436dc7653dfdf3da67332e22bff09bb0e28b0239e1624499c7830842e"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-result 0.3.2",
|
||||
"windows-strings 0.4.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.1.2"
|
||||
@@ -18342,6 +18367,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"schemars",
|
||||
"serde",
|
||||
"uuid",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -18388,10 +18414,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_llm_client"
|
||||
version = "0.5.1"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ee4d410dbc030c3e6e3af78fc76296f6bebe20dcb6d7d3fa24bca306fc8c1ce"
|
||||
checksum = "ad17428120f5ca776dc5195e2411a282f5150a26d5536671f8943c622c31274f"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.27.1",
|
||||
|
||||
@@ -3,6 +3,7 @@ resolver = "2"
|
||||
members = [
|
||||
"crates/activity_indicator",
|
||||
"crates/agent",
|
||||
"crates/agent2",
|
||||
"crates/anthropic",
|
||||
"crates/askpass",
|
||||
"crates/assets",
|
||||
@@ -47,7 +48,6 @@ members = [
|
||||
"crates/docs_preprocessor",
|
||||
"crates/editor",
|
||||
"crates/eval",
|
||||
"crates/evals",
|
||||
"crates/extension",
|
||||
"crates/extension_api",
|
||||
"crates/extension_cli",
|
||||
@@ -605,7 +605,7 @@ wasmtime-wasi = "29"
|
||||
which = "6.0.0"
|
||||
wit-component = "0.221"
|
||||
workspace-hack = "0.1.0"
|
||||
zed_llm_client = "0.5.1"
|
||||
zed_llm_client = "0.6.1"
|
||||
zstd = "0.11"
|
||||
metal = "0.29"
|
||||
|
||||
@@ -696,7 +696,6 @@ breadcrumbs = { codegen-units = 1 }
|
||||
collections = { codegen-units = 1 }
|
||||
command_palette = { codegen-units = 1 }
|
||||
command_palette_hooks = { codegen-units = 1 }
|
||||
evals = { codegen-units = 1 }
|
||||
extension_cli = { codegen-units = 1 }
|
||||
feature_flags = { codegen-units = 1 }
|
||||
file_icons = { codegen-units = 1 }
|
||||
|
||||
@@ -49,15 +49,6 @@
|
||||
"down": "menu::SelectNext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Prompt",
|
||||
"bindings": {
|
||||
"left": "menu::SelectPrevious",
|
||||
"right": "menu::SelectNext",
|
||||
"h": "menu::SelectPrevious",
|
||||
"l": "menu::SelectNext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
@@ -139,24 +130,6 @@
|
||||
"ctrl-shift-alt-backspace": "editor::GoToNextChange"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !agent_diff",
|
||||
"bindings": {
|
||||
"ctrl-k ctrl-r": "git::Restore",
|
||||
"ctrl-alt-y": "git::ToggleStaged",
|
||||
"alt-y": "git::StageAndNext",
|
||||
"alt-shift-y": "git::UnstageAndNext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentDiff",
|
||||
"bindings": {
|
||||
"ctrl-y": "agent::Keep",
|
||||
"ctrl-n": "agent::Reject",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
@@ -205,6 +178,31 @@
|
||||
"ctrl-c": "markdown::Copy"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && jupyter && !ContextEditor",
|
||||
"bindings": {
|
||||
"ctrl-shift-enter": "repl::Run",
|
||||
"ctrl-alt-enter": "repl::RunInPlace"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !agent_diff",
|
||||
"bindings": {
|
||||
"ctrl-k ctrl-r": "git::Restore",
|
||||
"ctrl-alt-y": "git::ToggleStaged",
|
||||
"alt-y": "git::StageAndNext",
|
||||
"alt-shift-y": "git::UnstageAndNext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentDiff",
|
||||
"bindings": {
|
||||
"ctrl-y": "agent::Keep",
|
||||
"ctrl-n": "agent::Reject",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AssistantPanel",
|
||||
"bindings": {
|
||||
@@ -220,6 +218,93 @@
|
||||
"ctrl-n": "assistant::NewChat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ContextEditor > Editor",
|
||||
"bindings": {
|
||||
"ctrl-enter": "assistant::Assist",
|
||||
"ctrl-shift-enter": "assistant::Edit",
|
||||
"ctrl-s": "workspace::Save",
|
||||
"save": "workspace::Save",
|
||||
"ctrl->": "assistant::QuoteSelection",
|
||||
"ctrl-<": "assistant::InsertIntoEditor",
|
||||
"ctrl-alt-/": "assistant::ToggleModelSelector",
|
||||
"shift-enter": "assistant::Split",
|
||||
"ctrl-r": "assistant::CycleMessageRole",
|
||||
"enter": "assistant::ConfirmCommand",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel",
|
||||
"bindings": {
|
||||
"ctrl-n": "agent::NewThread",
|
||||
"ctrl-alt-n": "agent::NewTextThread",
|
||||
"ctrl-shift-h": "agent::OpenHistory",
|
||||
"ctrl-alt-c": "agent::OpenConfiguration",
|
||||
"ctrl-alt-p": "assistant::OpenPromptLibrary",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"ctrl-alt-/": "assistant::ToggleModelSelector",
|
||||
"ctrl-shift-a": "agent::ToggleContextPicker",
|
||||
"shift-escape": "agent::ExpandMessageEditor",
|
||||
"ctrl-e": "agent::ChatMode",
|
||||
"ctrl-alt-e": "agent::RemoveAllContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel > Markdown",
|
||||
"bindings": {
|
||||
"copy": "markdown::CopyAsMarkdown",
|
||||
"ctrl-c": "markdown::CopyAsMarkdown"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel && prompt_editor",
|
||||
"bindings": {
|
||||
"cmd-n": "agent::NewTextThread",
|
||||
"cmd-alt-t": "agent::NewThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor > Editor",
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "EditMessageEditor > Editor",
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentFeedbackMessageEditor > Editor",
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ContextStrip",
|
||||
"bindings": {
|
||||
"up": "agent::FocusUp",
|
||||
"right": "agent::FocusRight",
|
||||
"left": "agent::FocusLeft",
|
||||
"down": "agent::FocusDown",
|
||||
"backspace": "agent::RemoveFocusedContext",
|
||||
"enter": "agent::AcceptSuggestedContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ThreadHistory",
|
||||
"bindings": {
|
||||
"backspace": "agent::RemoveSelectedThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "PromptLibrary",
|
||||
"bindings": {
|
||||
@@ -602,100 +687,6 @@
|
||||
"ctrl-:": "editor::ToggleInlayHints"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && jupyter && !ContextEditor",
|
||||
"bindings": {
|
||||
"ctrl-shift-enter": "repl::Run",
|
||||
"ctrl-alt-enter": "repl::RunInPlace"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ContextEditor > Editor",
|
||||
"bindings": {
|
||||
"ctrl-enter": "assistant::Assist",
|
||||
"ctrl-shift-enter": "assistant::Edit",
|
||||
"ctrl-s": "workspace::Save",
|
||||
"save": "workspace::Save",
|
||||
"ctrl->": "assistant::QuoteSelection",
|
||||
"ctrl-<": "assistant::InsertIntoEditor",
|
||||
"ctrl-alt-/": "assistant::ToggleModelSelector",
|
||||
"shift-enter": "assistant::Split",
|
||||
"ctrl-r": "assistant::CycleMessageRole",
|
||||
"enter": "assistant::ConfirmCommand",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel",
|
||||
"bindings": {
|
||||
"ctrl-n": "agent::NewThread",
|
||||
"ctrl-alt-n": "agent::NewTextThread",
|
||||
"ctrl-shift-h": "agent::OpenHistory",
|
||||
"ctrl-alt-c": "agent::OpenConfiguration",
|
||||
"ctrl-alt-p": "assistant::OpenPromptLibrary",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"ctrl-alt-/": "assistant::ToggleModelSelector",
|
||||
"ctrl-shift-a": "agent::ToggleContextPicker",
|
||||
"shift-escape": "agent::ExpandMessageEditor",
|
||||
"ctrl-e": "agent::ChatMode",
|
||||
"ctrl-alt-e": "agent::RemoveAllContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel > Markdown",
|
||||
"bindings": {
|
||||
"copy": "markdown::CopyAsMarkdown",
|
||||
"ctrl-c": "markdown::CopyAsMarkdown"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel && prompt_editor",
|
||||
"bindings": {
|
||||
"cmd-n": "agent::NewTextThread",
|
||||
"cmd-alt-t": "agent::NewThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor > Editor",
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "EditMessageEditor > Editor",
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentFeedbackMessageEditor > Editor",
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ContextStrip",
|
||||
"bindings": {
|
||||
"up": "agent::FocusUp",
|
||||
"right": "agent::FocusRight",
|
||||
"left": "agent::FocusLeft",
|
||||
"down": "agent::FocusDown",
|
||||
"backspace": "agent::RemoveFocusedContext",
|
||||
"enter": "agent::AcceptSuggestedContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ThreadHistory",
|
||||
"bindings": {
|
||||
"backspace": "agent::RemoveSelectedThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "PromptEditor",
|
||||
"bindings": {
|
||||
@@ -704,6 +695,15 @@
|
||||
"ctrl-alt-e": "agent::RemoveAllContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Prompt",
|
||||
"bindings": {
|
||||
"left": "menu::SelectPrevious",
|
||||
"right": "menu::SelectNext",
|
||||
"h": "menu::SelectPrevious",
|
||||
"l": "menu::SelectNext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchBar && !in_replace",
|
||||
"bindings": {
|
||||
@@ -920,6 +920,7 @@
|
||||
"ctrl-enter": "assistant::InlineAssist",
|
||||
"alt-b": ["terminal::SendText", "\u001bb"],
|
||||
"alt-f": ["terminal::SendText", "\u001bf"],
|
||||
"alt-.": ["terminal::SendText", "\u001b."],
|
||||
// Overrides for conflicting keybindings
|
||||
"ctrl-b": ["terminal::SendKeystroke", "ctrl-b"],
|
||||
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
|
||||
|
||||
@@ -1005,6 +1005,7 @@
|
||||
"alt-right": ["terminal::SendText", "\u001bf"],
|
||||
"alt-b": ["terminal::SendText", "\u001bb"],
|
||||
"alt-f": ["terminal::SendText", "\u001bf"],
|
||||
"alt-.": ["terminal::SendText", "\u001b."],
|
||||
// There are conflicting bindings for these keys in the global context.
|
||||
// these bindings override them, remove at your own risk:
|
||||
"up": ["terminal::SendKeystroke", "up"],
|
||||
|
||||
@@ -190,7 +190,8 @@
|
||||
"ctrl-w g shift-d": "editor::GoToTypeDefinitionSplit",
|
||||
"ctrl-w space": "editor::OpenExcerptsSplit",
|
||||
"ctrl-w g space": "editor::OpenExcerptsSplit",
|
||||
"ctrl-6": "pane::AlternateFile"
|
||||
"ctrl-6": "pane::AlternateFile",
|
||||
"ctrl-^": "pane::AlternateFile"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -786,8 +787,7 @@
|
||||
"{": "project_panel::SelectPrevDirectory",
|
||||
"shift-g": "menu::SelectLast",
|
||||
"g g": "menu::SelectFirst",
|
||||
"-": "project_panel::SelectParent",
|
||||
"ctrl-6": "pane::AlternateFile"
|
||||
"-": "project_panel::SelectParent"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,162 +1,71 @@
|
||||
You are an AI assistant integrated into a code editor. You have the programming ability of an expert programmer who takes pride in writing high-quality code and is driven to the point of obsession about solving problems effectively. Your goal is to do one of the following two things:
|
||||
You are a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.
|
||||
|
||||
1. Help users answer questions and perform tasks related to their codebase.
|
||||
2. Answer general-purpose questions unrelated to their particular codebase.
|
||||
## Communication
|
||||
|
||||
It will be up to you to decide which of these you are doing based on what the user has told you. When unclear, ask clarifying questions to understand the user's intent before proceeding.
|
||||
1. Be conversational but professional.
|
||||
2. Refer to the USER in the second person and yourself in the first person.
|
||||
3. Format your responses in markdown. Use backticks to format file, directory, function, and class names.
|
||||
4. NEVER lie or make things up.
|
||||
5. Refrain from apologizing all the time when results are unexpected. Instead, just try your best to proceed or explain the circumstances to the user without apologizing.
|
||||
|
||||
You should only perform actions that modify the user's system if explicitly requested by the user:
|
||||
- If the user asks a question about how to accomplish a task, provide guidance or information, and use read-only tools (e.g., search) to assist. You may suggest potential actions, but do not directly modify the user's system without explicit instruction.
|
||||
- If the user clearly requests that you perform an action, carry out the action directly without explaining why you are doing so.
|
||||
## Tool Use
|
||||
|
||||
When answering questions, it's okay to give incomplete examples containing comments about what would go there in a real version. When being asked to directly perform tasks on the code base, you must ALWAYS make fully working code. You may never "simplify" the code by omitting or deleting functionality you know the user has requested, and you must NEVER write comments like "in a full version, this would..." - instead, you must actually implement the real version. Don't be lazy!
|
||||
1. Make sure to adhere to the tools schema.
|
||||
2. Provide every required argument.
|
||||
3. DO NOT use tools to access items that are already available in the context section.
|
||||
4. Use only the tools that are currently available.
|
||||
5. DO NOT use a tool that is not available just because it appears in the conversation. This means the user turned it off.
|
||||
|
||||
Note that project files are automatically backed up. The user can always get them back later if anything goes wrong, so there's
|
||||
no need to create backup files (e.g. `.bak` files) because these files will just take up unnecessary space on the user's disk.
|
||||
## Searching and Reading
|
||||
|
||||
When attempting to resolve issues around failing tests, never simply remove the failing tests. Unless the user explicitly asks you to remove tests, ALWAYS attempt to fix the code causing the tests to fail.
|
||||
If you are unsure how to fulfill the user's request, gather more information with tool calls and/or clarifying questions.
|
||||
|
||||
Ignore "TODO"-type comments unless they're relevant to the user's explicit request or the user specifically asks you to address them. It is, however, okay to include them in codebase summaries.
|
||||
|
||||
<style>
|
||||
Editing code:
|
||||
- Make sure to take previous edits into account.
|
||||
- The edits you perform might lead to errors or warnings. At the end of your changes, check whether you introduced any problems, and fix them before providing a summary of the changes you made.
|
||||
- You may only attempt to fix these up to 3 times. If you have tried 3 times to fix them, and there are still problems remaining, you must not continue trying to fix them, and must instead tell the user that there are problems remaining - and ask if the user would like you to attempt to solve them further.
|
||||
- Do not fix errors unrelated to your changes unless the user explicitly asks you to do so.
|
||||
- Prefer to move files over recreating them. The move can be followed by minor edits if required.
|
||||
- If you seem to be stuck, never go back and "simplify the implementation" by deleting the parts of the implementation you're stuck on and replacing them with comments. If you ever feel the urge to do this, instead immediately stop whatever you're doing (even if the code is in a broken state), report that you are stuck, explain what you're stuck on, and ask the user how to proceed.
|
||||
|
||||
Tool use:
|
||||
- Make sure to adhere to the tools schema.
|
||||
- Provide every required argument.
|
||||
- DO NOT use tools to access items that are already available in the context section.
|
||||
- Use only the tools that are currently available.
|
||||
- DO NOT use a tool that is not available just because it appears in the conversation. This means the user turned it off.
|
||||
|
||||
Responding:
|
||||
- Be concise and direct in your responses.
|
||||
- Never apologize or thank the user.
|
||||
- Don't comment that you have just realized or understood something.
|
||||
- When you are going to make a tool call, tersely explain your reasoning for choosing to use that tool, with no flourishes or commentary beyond that information.
|
||||
For example, rather than saying "You're absolutely right! Thank you for providing that context. Now I understand that we're missing a dependency, and I need to add it:" say "I'll add that missing dependency:" instead.
|
||||
- Also, don't restate what a tool call is about to do (or just did).
|
||||
For example, don't say "Now I'm going to check diagnostics to see if there are any warnings or errors," followed by running a tool which checks diagnostics and reports warnings or errors; instead, just request the tool call without saying anything.
|
||||
- All tool results are provided to you automatically, so DO NOT thank the user when this happens.
|
||||
|
||||
Whenever you mention a code block, you MUST use ONLY the following format:
|
||||
|
||||
```language path/to/Something.blah#L123-456
|
||||
(code goes here)
|
||||
```
|
||||
|
||||
The `#L123-456` means the line number range 123 through 456, and the path/to/Something.blah
|
||||
is a path in the project. (If there is no valid path in the project, then you can use
|
||||
/dev/null/path.extension for its path.) This is the ONLY valid way to format code blocks, because the Markdown parser
|
||||
does not understand the more common ```language syntax, or bare ``` blocks. It only
|
||||
understands this path-based syntax, and if the path is missing, then it will error and you will have to do it over again.
|
||||
|
||||
Just to be really clear about this, if you ever find yourself writing three backticks followed by a language name, STOP!
|
||||
You have made a mistake. You can only ever put paths after triple backticks!
|
||||
|
||||
<example>
|
||||
Based on all the information I've gathered, here's a summary of how this system works:
|
||||
1. The README file is loaded into the system.
|
||||
2. The system finds the first two headers, including everything in between. In this case, that would be:
|
||||
|
||||
```path/to/README.md#L8-12
|
||||
# First Header
|
||||
|
||||
This is the info under the first header.
|
||||
|
||||
## Sub-header
|
||||
```
|
||||
|
||||
3. Then the system finds the last header in the README:
|
||||
|
||||
```path/to/README.md#L27-29
|
||||
## Last Header
|
||||
|
||||
This is the last header in the README.
|
||||
```
|
||||
|
||||
4. Finally, it passes this information on to the next process.
|
||||
</example>
|
||||
|
||||
<example>
|
||||
In Markdown, hash marks signify headings. For example:
|
||||
|
||||
```/dev/null/example.md#L1-3
|
||||
# Level 1 heading
|
||||
## Level 2 heading
|
||||
### Level 3 heading
|
||||
```
|
||||
</example>
|
||||
|
||||
Here are examples of ways you must never render code blocks:
|
||||
|
||||
<bad_example_do_not_do_this>
|
||||
In Markdown, hash marks signify headings. For example:
|
||||
|
||||
```
|
||||
# Level 1 heading
|
||||
## Level 2 heading
|
||||
### Level 3 heading
|
||||
```
|
||||
</bad_example_do_not_do_this>
|
||||
|
||||
This example is unacceptable because it does not include the path.
|
||||
|
||||
<bad_example_do_not_do_this>
|
||||
In Markdown, hash marks signify headings. For example:
|
||||
|
||||
```markdown
|
||||
# Level 1 heading
|
||||
## Level 2 heading
|
||||
### Level 3 heading
|
||||
```
|
||||
</bad_example_do_not_do_this>
|
||||
|
||||
This example is unacceptable because it has the language instead of the path.
|
||||
|
||||
<bad_example_do_not_do_this>
|
||||
In Markdown, hash marks signify headings. For example:
|
||||
|
||||
# Level 1 heading
|
||||
## Level 2 heading
|
||||
### Level 3 heading
|
||||
</bad_example_do_not_do_this>
|
||||
|
||||
This example is unacceptable because it uses indentation to mark the code block
|
||||
instead of backticks with a path.
|
||||
|
||||
<bad_example_do_not_do_this>
|
||||
In Markdown, hash marks signify headings. For example:
|
||||
|
||||
```markdown
|
||||
/dev/null/example.md#L1-3
|
||||
# Level 1 heading
|
||||
## Level 2 heading
|
||||
### Level 3 heading
|
||||
```
|
||||
</bad_example_do_not_do_this>
|
||||
|
||||
This example is unacceptable because the path is in the wrong place. The path must be directly after the opening backticks.
|
||||
</style>
|
||||
|
||||
The user has opened a project that contains the following root directories/files. Whenever you specify a path in the project, it must be a relative path which begins with one of these root directories/files:
|
||||
{{! TODO: If there are files, we should mention it but otherwise omit that fact }}
|
||||
If appropriate, use tool calls to explore the current project, which contains the following root directories:
|
||||
|
||||
{{#each worktrees}}
|
||||
- `{{root_name}}` (absolute path: `{{abs_path}}`)
|
||||
- `{{root_name}}`
|
||||
{{/each}}
|
||||
{{#if has_rules}}
|
||||
|
||||
There are rules that apply to these root directories:
|
||||
- When providing paths to tools, the path should always begin with a path that starts with a project root directory listed above.
|
||||
- When looking for symbols in the project, prefer the `grep` tool.
|
||||
- As you learn about the structure of the project, use that information to scope `grep` searches to targeted subtrees of the project.
|
||||
- Bias towards not asking the user for help if you can find the answer yourself.
|
||||
|
||||
## Fixing Diagnostics
|
||||
|
||||
1. Make 1-2 attempts at fixing diagnostics, then defer to the user.
|
||||
2. Never simplify code you've written just to solve diagnostics. Complete, mostly correct code is more valuable than perfect code that doesn't solve the problem.
|
||||
|
||||
## Debugging
|
||||
|
||||
When debugging, only make code changes if you are certain that you can solve the problem.
|
||||
Otherwise, follow debugging best practices:
|
||||
1. Address the root cause instead of the symptoms.
|
||||
2. Add descriptive logging statements and error messages to track variable and code state.
|
||||
3. Add test functions and statements to isolate the problem.
|
||||
|
||||
## Calling External APIs
|
||||
|
||||
1. Unless explicitly requested by the user, use the best suited external APIs and packages to solve the task. There is no need to ask the user for permission.
|
||||
2. When selecting which version of an API or package to use, choose one that is compatible with the user's dependency management file. If no such file exists or if the package is not present, use the latest version that is in your training data.
|
||||
3. If an external API requires an API Key, be sure to point this out to the user. Adhere to best security practices (e.g. DO NOT hardcode an API key in a place where it can be exposed)
|
||||
|
||||
## System Information
|
||||
|
||||
Operating System: {{os}}
|
||||
Default Shell: {{shell}}
|
||||
|
||||
{{#if (or has_rules has_default_user_rules)}}
|
||||
## User's Custom Instructions
|
||||
|
||||
The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the tool use guidelines.
|
||||
|
||||
{{#if has_rules}}
|
||||
There are project rules that apply to these root directories:
|
||||
{{#each worktrees}}
|
||||
{{#if rules_file}}
|
||||
|
||||
`{{root_name}}/{{rules_file.path_in_worktree}}`:
|
||||
|
||||
``````
|
||||
{{{rules_file.text}}}
|
||||
``````
|
||||
@@ -164,7 +73,16 @@ There are rules that apply to these root directories:
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
<user_environment>
|
||||
Operating System: {{os}} ({{arch}})
|
||||
Shell: {{shell}}
|
||||
</user_environment>
|
||||
{{#if has_default_user_rules}}
|
||||
The user has specified the following rules that should be applied:
|
||||
{{#each default_user_rules}}
|
||||
|
||||
{{#if title}}
|
||||
Rules title: {{title}}
|
||||
{{/if}}
|
||||
``````
|
||||
{{contents}}}
|
||||
``````
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
@@ -181,8 +181,6 @@
|
||||
"current_line_highlight": "all",
|
||||
// Whether to highlight all occurrences of the selected text in an editor.
|
||||
"selection_highlight": true,
|
||||
// The debounce delay before querying highlights based on the selected text.
|
||||
"selection_highlight_debounce": 50,
|
||||
// The debounce delay before querying highlights from the language
|
||||
// server based on the current cursor location.
|
||||
"lsp_highlight_debounce": 75,
|
||||
@@ -587,7 +585,6 @@
|
||||
//
|
||||
// Default: main
|
||||
"fallback_branch_name": "main",
|
||||
|
||||
"scrollbar": {
|
||||
// When to show the scrollbar in the git panel.
|
||||
//
|
||||
@@ -651,7 +648,7 @@
|
||||
"now": true,
|
||||
"path_search": true,
|
||||
"read_file": true,
|
||||
"regex_search": true,
|
||||
"grep": true,
|
||||
"thinking": true,
|
||||
"web_search": true
|
||||
}
|
||||
@@ -660,25 +657,25 @@
|
||||
"name": "Write",
|
||||
"enable_all_context_servers": true,
|
||||
"tools": {
|
||||
"terminal": true,
|
||||
"batch_tool": true,
|
||||
"code_actions": true,
|
||||
"code_symbols": true,
|
||||
"contents": true,
|
||||
"batch_tool": false,
|
||||
"code_actions": false,
|
||||
"code_symbols": false,
|
||||
"contents": false,
|
||||
"copy_path": false,
|
||||
"create_file": true,
|
||||
"delete_path": false,
|
||||
"diagnostics": true,
|
||||
"find_replace_file": true,
|
||||
"edit_file": true,
|
||||
"fetch": true,
|
||||
"list_directory": false,
|
||||
"list_directory": true,
|
||||
"move_path": false,
|
||||
"now": true,
|
||||
"now": false,
|
||||
"path_search": true,
|
||||
"read_file": true,
|
||||
"regex_search": true,
|
||||
"rename": true,
|
||||
"symbol_info": true,
|
||||
"grep": true,
|
||||
"rename": false,
|
||||
"symbol_info": false,
|
||||
"terminal": true,
|
||||
"thinking": true,
|
||||
"web_search": true
|
||||
}
|
||||
@@ -1562,7 +1559,6 @@
|
||||
// }
|
||||
// ]
|
||||
"ssh_connections": [],
|
||||
|
||||
// Configures context servers for use in the Assistant.
|
||||
"context_servers": {},
|
||||
"debugger": {
|
||||
|
||||
@@ -90,6 +90,7 @@ uuid.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
buffer_diff = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::context::{AssistantContext, ContextId, format_context_as_string};
|
||||
use crate::context_picker::MentionLink;
|
||||
use crate::thread::{
|
||||
LastRestoreCheckpoint, MessageId, MessageSegment, RequestKind, Thread, ThreadError,
|
||||
ThreadEvent, ThreadFeedback,
|
||||
LastRestoreCheckpoint, MessageId, MessageSegment, Thread, ThreadError, ThreadEvent,
|
||||
ThreadFeedback,
|
||||
};
|
||||
use crate::thread_store::{RulesLoadingError, ThreadStore};
|
||||
use crate::tool_use::{PendingToolUseStatus, ToolUse};
|
||||
@@ -23,7 +23,8 @@ use gpui::{
|
||||
};
|
||||
use language::{Buffer, LanguageRegistry};
|
||||
use language_model::{
|
||||
LanguageModelRegistry, LanguageModelRequestMessage, LanguageModelToolUseId, Role, StopReason,
|
||||
LanguageModelRegistry, LanguageModelRequestMessage, LanguageModelToolUseId, RequestUsage, Role,
|
||||
StopReason,
|
||||
};
|
||||
use markdown::parser::{CodeBlockKind, CodeBlockMetadata};
|
||||
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown};
|
||||
@@ -41,6 +42,7 @@ use ui::{
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{OpenOptions, Workspace};
|
||||
use zed_actions::assistant::OpenPromptLibrary;
|
||||
|
||||
use crate::context_store::ContextStore;
|
||||
|
||||
@@ -63,6 +65,7 @@ pub struct ActiveThread {
|
||||
expanded_thinking_segments: HashMap<(MessageId, usize), bool>,
|
||||
expanded_code_blocks: HashMap<(MessageId, usize), bool>,
|
||||
last_error: Option<ThreadError>,
|
||||
last_usage: Option<RequestUsage>,
|
||||
notifications: Vec<WindowHandle<AgentNotification>>,
|
||||
copied_code_block_ids: HashSet<(MessageId, usize)>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
@@ -130,18 +133,23 @@ impl RenderedMessage {
|
||||
}
|
||||
|
||||
fn push_segment(&mut self, segment: &MessageSegment, cx: &mut App) {
|
||||
let rendered_segment = match segment {
|
||||
MessageSegment::Thinking(text) => RenderedMessageSegment::Thinking {
|
||||
content: parse_markdown(text.into(), self.language_registry.clone(), cx),
|
||||
scroll_handle: ScrollHandle::default(),
|
||||
},
|
||||
MessageSegment::Text(text) => RenderedMessageSegment::Text(parse_markdown(
|
||||
text.into(),
|
||||
self.language_registry.clone(),
|
||||
cx,
|
||||
)),
|
||||
match segment {
|
||||
MessageSegment::Thinking { text, .. } => {
|
||||
self.segments.push(RenderedMessageSegment::Thinking {
|
||||
content: parse_markdown(text.into(), self.language_registry.clone(), cx),
|
||||
scroll_handle: ScrollHandle::default(),
|
||||
})
|
||||
}
|
||||
MessageSegment::Text(text) => {
|
||||
self.segments
|
||||
.push(RenderedMessageSegment::Text(parse_markdown(
|
||||
text.into(),
|
||||
self.language_registry.clone(),
|
||||
cx,
|
||||
)))
|
||||
}
|
||||
MessageSegment::RedactedThinking(_) => {}
|
||||
};
|
||||
self.segments.push(rendered_segment);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,12 +508,13 @@ fn render_markdown_code_block(
|
||||
.blend(cx.theme().colors().editor_foreground.opacity(0.01));
|
||||
|
||||
let codeblock_header = h_flex()
|
||||
.group("codeblock_header")
|
||||
.p_1()
|
||||
.py_1()
|
||||
.pl_1p5()
|
||||
.pr_1()
|
||||
.gap_1()
|
||||
.justify_between()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.border_color(cx.theme().colors().border.opacity(0.6))
|
||||
.bg(codeblock_header_bg)
|
||||
.rounded_t_md()
|
||||
.children(label)
|
||||
@@ -513,7 +522,7 @@ fn render_markdown_code_block(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
div().visible_on_hover("codeblock_header").child(
|
||||
div().visible_on_hover("codeblock_container").child(
|
||||
IconButton::new(
|
||||
("copy-markdown-code", ix),
|
||||
if codeblock_was_copied {
|
||||
@@ -595,11 +604,12 @@ fn render_markdown_code_block(
|
||||
);
|
||||
|
||||
v_flex()
|
||||
.group("codeblock_container")
|
||||
.my_2()
|
||||
.overflow_hidden()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.border_color(cx.theme().colors().border.opacity(0.6))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(codeblock_header)
|
||||
.when(
|
||||
@@ -732,6 +742,7 @@ impl ActiveThread {
|
||||
hide_scrollbar_task: None,
|
||||
editing_message: None,
|
||||
last_error: None,
|
||||
last_usage: None,
|
||||
copied_code_block_ids: HashSet::default(),
|
||||
notifications: Vec::new(),
|
||||
_subscriptions: subscriptions,
|
||||
@@ -756,6 +767,10 @@ impl ActiveThread {
|
||||
this
|
||||
}
|
||||
|
||||
pub fn context_store(&self) -> &Entity<ContextStore> {
|
||||
&self.context_store
|
||||
}
|
||||
|
||||
pub fn thread(&self) -> &Entity<Thread> {
|
||||
&self.thread
|
||||
}
|
||||
@@ -786,6 +801,10 @@ impl ActiveThread {
|
||||
self.last_error.take();
|
||||
}
|
||||
|
||||
pub fn last_usage(&self) -> Option<RequestUsage> {
|
||||
self.last_usage
|
||||
}
|
||||
|
||||
/// Returns the editing message id and the estimated token count in the content
|
||||
pub fn editing_message_id(&self) -> Option<(MessageId, usize)> {
|
||||
self.editing_message
|
||||
@@ -870,6 +889,9 @@ impl ActiveThread {
|
||||
ThreadEvent::ShowError(error) => {
|
||||
self.last_error = Some(error.clone());
|
||||
}
|
||||
ThreadEvent::UsageUpdated(usage) => {
|
||||
self.last_usage = Some(*usage);
|
||||
}
|
||||
ThreadEvent::StreamedCompletion
|
||||
| ThreadEvent::SummaryGenerated
|
||||
| ThreadEvent::SummaryChanged => {
|
||||
@@ -1195,6 +1217,8 @@ impl ActiveThread {
|
||||
}
|
||||
|
||||
let request = language_model::LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: language_model::Role::User,
|
||||
content: vec![content.into()],
|
||||
@@ -1260,7 +1284,8 @@ impl ActiveThread {
|
||||
}
|
||||
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.send_to_model(model.model, RequestKind::Chat, cx)
|
||||
thread.advance_prompt_id();
|
||||
thread.send_to_model(model.model, cx)
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
@@ -1491,16 +1516,23 @@ impl ActiveThread {
|
||||
let editor_bg_color = colors.editor_background;
|
||||
let bg_user_message_header = editor_bg_color.blend(active_color.opacity(0.25));
|
||||
|
||||
let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileCode)
|
||||
let open_as_markdown = IconButton::new(("open-as-markdown", ix), IconName::FileCode)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Ignored)
|
||||
.tooltip(Tooltip::text("Open Thread as Markdown"))
|
||||
.on_click(|_event, window, cx| {
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(Box::new(OpenActiveThreadAsMarkdown), cx)
|
||||
});
|
||||
|
||||
let feedback_container = h_flex().py_2().px_4().gap_1().justify_between();
|
||||
// For all items that should be aligned with the Assistant's response.
|
||||
const RESPONSE_PADDING_X: Pixels = px(18.);
|
||||
|
||||
let feedback_container = h_flex()
|
||||
.py_2()
|
||||
.px(RESPONSE_PADDING_X)
|
||||
.gap_1()
|
||||
.justify_between();
|
||||
let feedback_items = match self.thread.read(cx).message_feedback(message_id) {
|
||||
Some(feedback) => feedback_container
|
||||
.child(
|
||||
@@ -1701,9 +1733,9 @@ impl ActiveThread {
|
||||
this.pt_4()
|
||||
}
|
||||
})
|
||||
.pb_4()
|
||||
.pl_2()
|
||||
.pr_2p5()
|
||||
.pb_4()
|
||||
.child(
|
||||
v_flex()
|
||||
.bg(colors.editor_background)
|
||||
@@ -1810,9 +1842,8 @@ impl ActiveThread {
|
||||
),
|
||||
Role::Assistant => v_flex()
|
||||
.id(("message-container", ix))
|
||||
.ml_2p5()
|
||||
.pl_2()
|
||||
.pr_4()
|
||||
.px(RESPONSE_PADDING_X)
|
||||
.gap_2()
|
||||
.children(message_content)
|
||||
.when(has_tool_uses, |parent| {
|
||||
parent.children(
|
||||
@@ -1836,9 +1867,10 @@ impl ActiveThread {
|
||||
message_id > *editing_message_id
|
||||
});
|
||||
|
||||
let panel_background = cx.theme().colors().panel_background;
|
||||
|
||||
v_flex()
|
||||
.w_full()
|
||||
.when(after_editing_message, |parent| parent.opacity(0.2))
|
||||
.when_some(checkpoint, |parent, checkpoint| {
|
||||
let mut is_pending = false;
|
||||
let mut error = None;
|
||||
@@ -2000,6 +2032,18 @@ impl ActiveThread {
|
||||
},
|
||||
)
|
||||
})
|
||||
.when(after_editing_message, |parent| {
|
||||
// Backdrop to dim out the whole thread below the editing user message
|
||||
parent.relative().child(
|
||||
div()
|
||||
.occlude()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.size_full()
|
||||
.bg(panel_background)
|
||||
.opacity(0.8),
|
||||
)
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
|
||||
@@ -2026,6 +2070,15 @@ impl ActiveThread {
|
||||
None
|
||||
};
|
||||
|
||||
let message_role = self
|
||||
.thread
|
||||
.read(cx)
|
||||
.message(message_id)
|
||||
.map(|m| m.role)
|
||||
.unwrap_or(Role::User);
|
||||
|
||||
let is_assistant = message_role == Role::Assistant;
|
||||
|
||||
v_flex()
|
||||
.text_ui(cx)
|
||||
.gap_2()
|
||||
@@ -2046,80 +2099,100 @@ impl ActiveThread {
|
||||
cx,
|
||||
)
|
||||
.into_any_element(),
|
||||
RenderedMessageSegment::Text(markdown) => div()
|
||||
.child(
|
||||
MarkdownElement::new(
|
||||
markdown.clone(),
|
||||
default_markdown_style(window, cx),
|
||||
)
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Custom {
|
||||
render: Arc::new({
|
||||
let workspace = workspace.clone();
|
||||
let active_thread = cx.entity();
|
||||
move |kind, parsed_markdown, range, metadata, window, cx| {
|
||||
render_markdown_code_block(
|
||||
message_id,
|
||||
range.start,
|
||||
kind,
|
||||
parsed_markdown,
|
||||
metadata,
|
||||
active_thread.clone(),
|
||||
workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}),
|
||||
transform: Some(Arc::new({
|
||||
let active_thread = cx.entity();
|
||||
move |el, range, metadata, _, cx| {
|
||||
let is_expanded = active_thread
|
||||
.read(cx)
|
||||
.expanded_code_blocks
|
||||
.get(&(message_id, range.start))
|
||||
.copied()
|
||||
.unwrap_or(false);
|
||||
RenderedMessageSegment::Text(markdown) => {
|
||||
let markdown_element = MarkdownElement::new(
|
||||
markdown.clone(),
|
||||
default_markdown_style(window, cx),
|
||||
);
|
||||
|
||||
if is_expanded
|
||||
|| metadata.line_count
|
||||
<= MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK
|
||||
{
|
||||
return el;
|
||||
let markdown_element = if is_assistant {
|
||||
markdown_element.code_block_renderer(
|
||||
markdown::CodeBlockRenderer::Custom {
|
||||
render: Arc::new({
|
||||
let workspace = workspace.clone();
|
||||
let active_thread = cx.entity();
|
||||
move |kind,
|
||||
parsed_markdown,
|
||||
range,
|
||||
metadata,
|
||||
window,
|
||||
cx| {
|
||||
render_markdown_code_block(
|
||||
message_id,
|
||||
range.start,
|
||||
kind,
|
||||
parsed_markdown,
|
||||
metadata,
|
||||
active_thread.clone(),
|
||||
workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
el.child(
|
||||
div()
|
||||
.absolute()
|
||||
.bottom_0()
|
||||
.left_0()
|
||||
.w_full()
|
||||
.h_1_4()
|
||||
.rounded_b_lg()
|
||||
.bg(gpui::linear_gradient(
|
||||
0.,
|
||||
gpui::linear_color_stop(
|
||||
cx.theme().colors().editor_background,
|
||||
}),
|
||||
transform: Some(Arc::new({
|
||||
let active_thread = cx.entity();
|
||||
move |el, range, metadata, _, cx| {
|
||||
let is_expanded = active_thread
|
||||
.read(cx)
|
||||
.expanded_code_blocks
|
||||
.get(&(message_id, range.start))
|
||||
.copied()
|
||||
.unwrap_or(false);
|
||||
|
||||
if is_expanded
|
||||
|| metadata.line_count
|
||||
<= MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK
|
||||
{
|
||||
return el;
|
||||
}
|
||||
el.child(
|
||||
div()
|
||||
.absolute()
|
||||
.bottom_0()
|
||||
.left_0()
|
||||
.w_full()
|
||||
.h_1_4()
|
||||
.rounded_b_lg()
|
||||
.bg(gpui::linear_gradient(
|
||||
0.,
|
||||
),
|
||||
gpui::linear_color_stop(
|
||||
cx.theme()
|
||||
.colors()
|
||||
.editor_background
|
||||
.opacity(0.),
|
||||
1.,
|
||||
),
|
||||
)),
|
||||
)
|
||||
}
|
||||
})),
|
||||
})
|
||||
.on_url_click({
|
||||
gpui::linear_color_stop(
|
||||
cx.theme()
|
||||
.colors()
|
||||
.editor_background,
|
||||
0.,
|
||||
),
|
||||
gpui::linear_color_stop(
|
||||
cx.theme()
|
||||
.colors()
|
||||
.editor_background
|
||||
.opacity(0.),
|
||||
1.,
|
||||
),
|
||||
)),
|
||||
)
|
||||
}
|
||||
})),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
markdown_element.code_block_renderer(
|
||||
markdown::CodeBlockRenderer::Default {
|
||||
copy_button: false,
|
||||
border: true,
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
div()
|
||||
.child(markdown_element.on_url_click({
|
||||
let workspace = self.workspace.clone();
|
||||
move |text, window, cx| {
|
||||
open_markdown_link(text, workspace.clone(), window, cx);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.into_any_element(),
|
||||
}))
|
||||
.into_any_element()
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -2388,6 +2461,7 @@ impl ActiveThread {
|
||||
.get(&tool_use.id)
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
|
||||
let is_status_finished = matches!(&tool_use.status, ToolUseStatus::Finished(_));
|
||||
|
||||
let fs = self
|
||||
@@ -2448,6 +2522,7 @@ impl ActiveThread {
|
||||
)
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button: false,
|
||||
border: false,
|
||||
})
|
||||
.on_url_click({
|
||||
let workspace = self.workspace.clone();
|
||||
@@ -2477,6 +2552,7 @@ impl ActiveThread {
|
||||
)
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button: false,
|
||||
border: false,
|
||||
})
|
||||
.on_url_click({
|
||||
let workspace = self.workspace.clone();
|
||||
@@ -2579,11 +2655,10 @@ impl ActiveThread {
|
||||
))
|
||||
};
|
||||
|
||||
div().map(|element| {
|
||||
v_flex().gap_1().mb_3().map(|element| {
|
||||
if !edit_tools {
|
||||
element.child(
|
||||
v_flex()
|
||||
.my_2()
|
||||
.child(
|
||||
h_flex()
|
||||
.group("disclosure-header")
|
||||
@@ -2605,7 +2680,7 @@ impl ActiveThread {
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
h_flex().pr_8().text_ui_sm(cx).children(
|
||||
h_flex().pr_8().text_size(rems(0.8125)).children(
|
||||
rendered_tool_use.map(|rendered| MarkdownElement::new(rendered.label, tool_use_markdown_style(window, cx)).on_url_click({let workspace = self.workspace.clone(); move |text, window, cx| {
|
||||
open_markdown_link(text, workspace.clone(), window, cx);
|
||||
}}))
|
||||
@@ -2655,7 +2730,7 @@ impl ActiveThread {
|
||||
)
|
||||
} else {
|
||||
v_flex()
|
||||
.my_2()
|
||||
.mb_2()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
@@ -2882,53 +2957,116 @@ impl ActiveThread {
|
||||
return div().into_any();
|
||||
};
|
||||
|
||||
let default_user_rules_text = if project_context.default_user_rules.is_empty() {
|
||||
None
|
||||
} else if project_context.default_user_rules.len() == 1 {
|
||||
let user_rules = &project_context.default_user_rules[0];
|
||||
|
||||
match user_rules.title.as_ref() {
|
||||
Some(title) => Some(format!("Using \"{title}\" user rule")),
|
||||
None => Some("Using user rule".into()),
|
||||
}
|
||||
} else {
|
||||
Some(format!(
|
||||
"Using {} user rules",
|
||||
project_context.default_user_rules.len()
|
||||
))
|
||||
};
|
||||
|
||||
let first_default_user_rules_id = project_context
|
||||
.default_user_rules
|
||||
.first()
|
||||
.map(|user_rules| user_rules.uuid);
|
||||
|
||||
let rules_files = project_context
|
||||
.worktrees
|
||||
.iter()
|
||||
.filter_map(|worktree| worktree.rules_file.as_ref())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let label_text = match rules_files.as_slice() {
|
||||
&[] => return div().into_any(),
|
||||
&[rules_file] => {
|
||||
format!("Using {:?} file", rules_file.path_in_worktree)
|
||||
}
|
||||
rules_files => {
|
||||
format!("Using {} rules files", rules_files.len())
|
||||
}
|
||||
let rules_file_text = match rules_files.as_slice() {
|
||||
&[] => None,
|
||||
&[rules_file] => Some(format!(
|
||||
"Using project {:?} file",
|
||||
rules_file.path_in_worktree
|
||||
)),
|
||||
rules_files => Some(format!("Using {} project rules files", rules_files.len())),
|
||||
};
|
||||
|
||||
div()
|
||||
if default_user_rules_text.is_none() && rules_file_text.is_none() {
|
||||
return div().into_any();
|
||||
}
|
||||
|
||||
v_flex()
|
||||
.pt_2()
|
||||
.px_2p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
.gap_1()
|
||||
.when_some(
|
||||
default_user_rules_text,
|
||||
|parent, default_user_rules_text| {
|
||||
parent.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.w_full()
|
||||
.child(
|
||||
Icon::new(IconName::File)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Disabled),
|
||||
)
|
||||
.child(
|
||||
Label::new(label_text)
|
||||
Label::new(default_user_rules_text)
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
.truncate()
|
||||
.buffer_font(cx)
|
||||
.ml_1p5()
|
||||
.mr_0p5(),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("open-prompt-library", IconName::ArrowUpRightAlt)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Ignored)
|
||||
// TODO: Figure out a way to pass focus handle here so we can display the `OpenPromptLibrary` keybinding
|
||||
.tooltip(Tooltip::text("View User Rules"))
|
||||
.on_click(move |_event, window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(OpenPromptLibrary {
|
||||
prompt_to_focus: first_default_user_rules_id,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("open-rule", IconName::ArrowUpRightAlt)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Ignored)
|
||||
.on_click(cx.listener(Self::handle_open_rules))
|
||||
.tooltip(Tooltip::text("View Rules")),
|
||||
),
|
||||
},
|
||||
)
|
||||
.when_some(rules_file_text, |parent, rules_file_text| {
|
||||
parent.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.child(
|
||||
Icon::new(IconName::File)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Disabled),
|
||||
)
|
||||
.child(
|
||||
Label::new(rules_file_text)
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx)
|
||||
.ml_1p5()
|
||||
.mr_0p5(),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("open-rule", IconName::ArrowUpRightAlt)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Ignored)
|
||||
.on_click(cx.listener(Self::handle_open_rules))
|
||||
.tooltip(Tooltip::text("View Rules")),
|
||||
),
|
||||
)
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
|
||||
@@ -3145,28 +3283,21 @@ pub(crate) fn open_context(
|
||||
.start
|
||||
.to_point(&snapshot);
|
||||
|
||||
let open_task = workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_path(project_path, None, true, window, cx)
|
||||
});
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
if let Some(active_editor) = open_task
|
||||
.await
|
||||
.log_err()
|
||||
.and_then(|item| item.downcast::<Editor>())
|
||||
{
|
||||
active_editor
|
||||
.downgrade()
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.go_to_singleton_buffer_point(
|
||||
target_position,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
open_editor_at_position(project_path, target_position, &workspace, window, cx)
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
AssistantContext::Excerpt(excerpt_context) => {
|
||||
if let Some(project_path) = excerpt_context
|
||||
.context_buffer
|
||||
.buffer
|
||||
.read(cx)
|
||||
.project_path(cx)
|
||||
{
|
||||
let snapshot = excerpt_context.context_buffer.buffer.read(cx).snapshot();
|
||||
let target_position = excerpt_context.range.start.to_point(&snapshot);
|
||||
|
||||
open_editor_at_position(project_path, target_position, &workspace, window, cx)
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
@@ -3187,3 +3318,29 @@ pub(crate) fn open_context(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn open_editor_at_position(
|
||||
project_path: project::ProjectPath,
|
||||
target_position: Point,
|
||||
workspace: &Entity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<()> {
|
||||
let open_task = workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_path(project_path, None, true, window, cx)
|
||||
});
|
||||
window.spawn(cx, async move |cx| {
|
||||
if let Some(active_editor) = open_task
|
||||
.await
|
||||
.log_err()
|
||||
.and_then(|item| item.downcast::<Editor>())
|
||||
{
|
||||
active_editor
|
||||
.downgrade()
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.go_to_singleton_buffer_point(target_position, window, cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -922,6 +922,7 @@ mod tests {
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
AssistantSettings::register(cx);
|
||||
prompt_store::init(cx);
|
||||
thread_store::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
ThemeSettings::register(cx);
|
||||
@@ -951,7 +952,8 @@ mod tests {
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
|
||||
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ pub use crate::active_thread::ActiveThread;
|
||||
use crate::assistant_configuration::{AddContextServerModal, ManageProfilesModal};
|
||||
pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate};
|
||||
pub use crate::inline_assistant::InlineAssistant;
|
||||
pub use crate::thread::{Message, RequestKind, Thread, ThreadEvent};
|
||||
pub use crate::thread::{Message, Thread, ThreadEvent};
|
||||
pub use crate::thread_store::ThreadStore;
|
||||
pub use agent_diff::{AgentDiff, AgentDiffToolbar};
|
||||
|
||||
|
||||
@@ -132,7 +132,11 @@ impl AssistantConfiguration {
|
||||
.cloned();
|
||||
|
||||
v_flex()
|
||||
.pt_3()
|
||||
.pb_1()
|
||||
.gap_1p5()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border.opacity(0.6))
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
@@ -144,7 +148,7 @@ impl AssistantConfiguration {
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new(provider_name.clone())),
|
||||
.child(Label::new(provider_name.clone()).size(LabelSize::Large)),
|
||||
)
|
||||
.when(provider.is_authenticated(cx), |parent| {
|
||||
parent.child(
|
||||
@@ -169,20 +173,12 @@ impl AssistantConfiguration {
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.p(DynamicSpacing::Base08.rems(cx))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_sm()
|
||||
.map(|parent| match configuration_view {
|
||||
Some(configuration_view) => parent.child(configuration_view),
|
||||
None => parent.child(div().child(Label::new(format!(
|
||||
"No configuration view for {provider_name}",
|
||||
)))),
|
||||
}),
|
||||
)
|
||||
.map(|parent| match configuration_view {
|
||||
Some(configuration_view) => parent.child(configuration_view),
|
||||
None => parent.child(div().child(Label::new(format!(
|
||||
"No configuration view for {provider_name}",
|
||||
)))),
|
||||
})
|
||||
}
|
||||
|
||||
fn render_provider_configuration_section(
|
||||
@@ -199,7 +195,7 @@ impl AssistantConfiguration {
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(Headline::new("LLM Providers").size(HeadlineSize::Small))
|
||||
.child(Headline::new("LLM Providers"))
|
||||
.child(
|
||||
Label::new("Add at least one provider to use AI-powered features.")
|
||||
.color(Color::Muted),
|
||||
@@ -215,21 +211,16 @@ impl AssistantConfiguration {
|
||||
fn render_command_permission(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let always_allow_tool_actions = AssistantSettings::get_global(cx).always_allow_tool_actions;
|
||||
|
||||
const HEADING: &str = "Allow running tools without asking for confirmation";
|
||||
const HEADING: &str = "Allow running editing tools without asking for confirmation";
|
||||
|
||||
v_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.pr(DynamicSpacing::Base20.rems(cx))
|
||||
.gap_2()
|
||||
.flex_1()
|
||||
.child(Headline::new("General Settings").size(HeadlineSize::Small))
|
||||
.child(Headline::new("General Settings"))
|
||||
.child(
|
||||
h_flex()
|
||||
.p_2p5()
|
||||
.rounded_sm()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.gap_4()
|
||||
.justify_between()
|
||||
.flex_wrap()
|
||||
@@ -277,10 +268,7 @@ impl AssistantConfiguration {
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Headline::new("Model Context Protocol (MCP) Servers")
|
||||
.size(HeadlineSize::Small),
|
||||
)
|
||||
.child(Headline::new("Model Context Protocol (MCP) Servers"))
|
||||
.child(Label::new(SUBHEADING).color(Color::Muted)),
|
||||
)
|
||||
.children(context_servers.into_iter().map(|context_server| {
|
||||
@@ -301,9 +289,9 @@ impl AssistantConfiguration {
|
||||
v_flex()
|
||||
.id(SharedString::from(context_server.id()))
|
||||
.border_1()
|
||||
.rounded_sm()
|
||||
.rounded_md()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.bg(cx.theme().colors().background.opacity(0.25))
|
||||
.child(
|
||||
h_flex()
|
||||
.p_1()
|
||||
@@ -386,34 +374,28 @@ impl AssistantConfiguration {
|
||||
return parent;
|
||||
}
|
||||
|
||||
parent.child(v_flex().children(tools.into_iter().enumerate().map(
|
||||
|(ix, tool)| {
|
||||
parent.child(v_flex().py_1p5().px_1().gap_1().children(
|
||||
tools.into_iter().enumerate().map(|(ix, tool)| {
|
||||
h_flex()
|
||||
.id("tool-item")
|
||||
.pl_2()
|
||||
.pr_1()
|
||||
.py_1()
|
||||
.id(("tool-item", ix))
|
||||
.px_1()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.when(ix < tool_count - 1, |element| {
|
||||
element
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
})
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover))
|
||||
.rounded_sm()
|
||||
.child(
|
||||
Label::new(tool.name())
|
||||
.buffer_font(cx)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
IconButton::new(("tool-description", ix), IconName::Info)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Ignored)
|
||||
.tooltip(Tooltip::text(tool.description())),
|
||||
Icon::new(IconName::Info)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Ignored),
|
||||
)
|
||||
},
|
||||
)))
|
||||
.tooltip(Tooltip::text(tool.description()))
|
||||
}),
|
||||
))
|
||||
})
|
||||
}))
|
||||
.child(
|
||||
@@ -422,7 +404,7 @@ impl AssistantConfiguration {
|
||||
.gap_2()
|
||||
.child(
|
||||
h_flex().w_full().child(
|
||||
Button::new("add-context-server", "Add MCPs Directly")
|
||||
Button::new("add-context-server", "Add Custom Server")
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.full_width()
|
||||
|
||||
@@ -2,7 +2,7 @@ use context_server::{ContextServerSettings, ServerCommand, ServerConfig};
|
||||
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity, prelude::*};
|
||||
use serde_json::json;
|
||||
use settings::update_settings_file;
|
||||
use ui::{Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
|
||||
use ui::{KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
|
||||
use ui_input::SingleLineInput;
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
@@ -34,9 +34,9 @@ impl AddContextServerModal {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let name_editor =
|
||||
cx.new(|cx| SingleLineInput::new(window, cx, "Your server name").label("Name"));
|
||||
cx.new(|cx| SingleLineInput::new(window, cx, "my-custom-server").label("Name"));
|
||||
let command_editor = cx.new(|cx| {
|
||||
SingleLineInput::new(window, cx, "Command").label("Command to run the context server")
|
||||
SingleLineInput::new(window, cx, "Command").label("Command to run the MCP server")
|
||||
});
|
||||
|
||||
Self {
|
||||
@@ -46,7 +46,7 @@ impl AddContextServerModal {
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm(&mut self, cx: &mut Context<Self>) {
|
||||
fn confirm(&mut self, _: &menu::Confirm, cx: &mut Context<Self>) {
|
||||
let name = self
|
||||
.name_editor
|
||||
.read(cx)
|
||||
@@ -96,7 +96,7 @@ impl AddContextServerModal {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn cancel(&mut self, cx: &mut Context<Self>) {
|
||||
fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
@@ -112,38 +112,68 @@ impl Focusable for AddContextServerModal {
|
||||
impl EventEmitter<DismissEvent> for AddContextServerModal {}
|
||||
|
||||
impl Render for AddContextServerModal {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let is_name_empty = self.name_editor.read(cx).is_empty(cx);
|
||||
let is_command_empty = self.command_editor.read(cx).is_empty(cx);
|
||||
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
div()
|
||||
.elevation_3(cx)
|
||||
.w(rems(34.))
|
||||
.key_context("AddContextServerModal")
|
||||
.on_action(cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(cx)))
|
||||
.on_action(cx.listener(|this, _: &menu::Confirm, _window, cx| this.confirm(cx)))
|
||||
.on_action(
|
||||
cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(&menu::Cancel, cx)),
|
||||
)
|
||||
.on_action(
|
||||
cx.listener(|this, _: &menu::Confirm, _window, cx| {
|
||||
this.confirm(&menu::Confirm, cx)
|
||||
}),
|
||||
)
|
||||
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
|
||||
this.focus_handle(cx).focus(window);
|
||||
}))
|
||||
.on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
|
||||
.child(
|
||||
Modal::new("add-context-server", None)
|
||||
.header(ModalHeader::new().headline("Add Context Server"))
|
||||
.header(ModalHeader::new().headline("Add MCP Server"))
|
||||
.section(
|
||||
Section::new()
|
||||
.child(self.name_editor.clone())
|
||||
.child(self.command_editor.clone()),
|
||||
Section::new().child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(self.name_editor.clone())
|
||||
.child(self.command_editor.clone()),
|
||||
),
|
||||
)
|
||||
.footer(
|
||||
ModalFooter::new()
|
||||
.start_slot(
|
||||
Button::new("cancel", "Cancel").on_click(
|
||||
cx.listener(|this, _event, _window, cx| this.cancel(cx)),
|
||||
),
|
||||
Button::new("cancel", "Cancel")
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&menu::Cancel,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||
this.cancel(&menu::Cancel, cx)
|
||||
})),
|
||||
)
|
||||
.end_slot(
|
||||
Button::new("add-server", "Add Server")
|
||||
.disabled(is_name_empty || is_command_empty)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&menu::Confirm,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.map(|button| {
|
||||
if is_name_empty {
|
||||
button.tooltip(Tooltip::text("Name is required"))
|
||||
@@ -153,9 +183,9 @@ impl Render for AddContextServerModal {
|
||||
button
|
||||
}
|
||||
})
|
||||
.on_click(
|
||||
cx.listener(|this, _event, _window, cx| this.confirm(cx)),
|
||||
),
|
||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||
this.confirm(&menu::Confirm, cx)
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::ops::Range;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -12,7 +13,7 @@ use assistant_slash_command::SlashCommandWorkingSet;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
|
||||
use client::zed_urls;
|
||||
use editor::{Editor, EditorEvent, MultiBuffer};
|
||||
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, Corner, Entity,
|
||||
@@ -24,7 +25,7 @@ use language_model::{LanguageModelProviderTosView, LanguageModelRegistry};
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
use project::Project;
|
||||
use prompt_library::{PromptLibrary, open_prompt_library};
|
||||
use prompt_store::PromptBuilder;
|
||||
use prompt_store::{PromptBuilder, PromptId};
|
||||
use proto::Plan;
|
||||
use settings::{Settings, update_settings_file};
|
||||
use time::UtcOffset;
|
||||
@@ -44,8 +45,9 @@ use crate::message_editor::{MessageEditor, MessageEditorEvent};
|
||||
use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
|
||||
use crate::thread_history::{PastContext, PastThread, ThreadHistory};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::ui::UsageBanner;
|
||||
use crate::{
|
||||
AgentDiff, ExpandMessageEditor, InlineAssistant, NewTextThread, NewThread,
|
||||
AddContextServer, AgentDiff, ExpandMessageEditor, InlineAssistant, NewTextThread, NewThread,
|
||||
OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ThreadEvent, ToggleContextPicker,
|
||||
};
|
||||
|
||||
@@ -81,7 +83,7 @@ pub fn init(cx: &mut App) {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.deploy_prompt_library(&OpenPromptLibrary, window, cx)
|
||||
panel.deploy_prompt_library(&OpenPromptLibrary::default(), window, cx)
|
||||
});
|
||||
}
|
||||
})
|
||||
@@ -112,7 +114,9 @@ enum ActiveView {
|
||||
change_title_editor: Entity<Editor>,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
},
|
||||
PromptEditor,
|
||||
PromptEditor {
|
||||
context_editor: Entity<ContextEditor>,
|
||||
},
|
||||
History,
|
||||
Configuration,
|
||||
}
|
||||
@@ -184,7 +188,6 @@ pub struct AssistantPanel {
|
||||
message_editor: Entity<MessageEditor>,
|
||||
_active_thread_subscriptions: Vec<Subscription>,
|
||||
context_store: Entity<assistant_context_editor::ContextStore>,
|
||||
context_editor: Option<Entity<ContextEditor>>,
|
||||
configuration: Option<Entity<AssistantConfiguration>>,
|
||||
configuration_subscription: Option<Subscription>,
|
||||
local_timezone: UtcOffset,
|
||||
@@ -210,7 +213,7 @@ impl AssistantPanel {
|
||||
let project = workspace.project().clone();
|
||||
ThreadStore::load(project, tools.clone(), prompt_builder.clone(), cx)
|
||||
})?
|
||||
.await;
|
||||
.await?;
|
||||
|
||||
let slash_commands = Arc::new(SlashCommandWorkingSet::default());
|
||||
let context_store = workspace
|
||||
@@ -316,7 +319,6 @@ impl AssistantPanel {
|
||||
message_editor_subscription,
|
||||
],
|
||||
context_store,
|
||||
context_editor: None,
|
||||
configuration: None,
|
||||
configuration_subscription: None,
|
||||
local_timezone: UtcOffset::from_whole_seconds(
|
||||
@@ -453,8 +455,6 @@ impl AssistantPanel {
|
||||
}
|
||||
|
||||
fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.set_active_view(ActiveView::PromptEditor, window, cx);
|
||||
|
||||
let context = self
|
||||
.context_store
|
||||
.update(cx, |context_store, cx| context_store.create(cx));
|
||||
@@ -462,7 +462,7 @@ impl AssistantPanel {
|
||||
.log_err()
|
||||
.flatten();
|
||||
|
||||
self.context_editor = Some(cx.new(|cx| {
|
||||
let context_editor = cx.new(|cx| {
|
||||
let mut editor = ContextEditor::for_context(
|
||||
context,
|
||||
self.fs.clone(),
|
||||
@@ -474,16 +474,21 @@ impl AssistantPanel {
|
||||
);
|
||||
editor.insert_default_prompt(window, cx);
|
||||
editor
|
||||
}));
|
||||
});
|
||||
|
||||
if let Some(context_editor) = self.context_editor.as_ref() {
|
||||
context_editor.focus_handle(cx).focus(window);
|
||||
}
|
||||
self.set_active_view(
|
||||
ActiveView::PromptEditor {
|
||||
context_editor: context_editor.clone(),
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
context_editor.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
fn deploy_prompt_library(
|
||||
&mut self,
|
||||
_: &OpenPromptLibrary,
|
||||
action: &OpenPromptLibrary,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -497,6 +502,7 @@ impl AssistantPanel {
|
||||
None,
|
||||
))
|
||||
}),
|
||||
action.prompt_to_focus.map(|uuid| PromptId::User { uuid }),
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
@@ -545,8 +551,13 @@ impl AssistantPanel {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
this.set_active_view(ActiveView::PromptEditor, window, cx);
|
||||
this.context_editor = Some(editor);
|
||||
this.set_active_view(
|
||||
ActiveView::PromptEditor {
|
||||
context_editor: editor,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
@@ -777,8 +788,15 @@ impl AssistantPanel {
|
||||
.update(cx, |this, cx| this.delete_thread(thread_id, cx))
|
||||
}
|
||||
|
||||
pub(crate) fn has_active_thread(&self) -> bool {
|
||||
matches!(self.active_view, ActiveView::Thread { .. })
|
||||
}
|
||||
|
||||
pub(crate) fn active_context_editor(&self) -> Option<Entity<ContextEditor>> {
|
||||
self.context_editor.clone()
|
||||
match &self.active_view {
|
||||
ActiveView::PromptEditor { context_editor } => Some(context_editor.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn delete_context(
|
||||
@@ -816,16 +834,10 @@ impl AssistantPanel {
|
||||
|
||||
impl Focusable for AssistantPanel {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
match self.active_view {
|
||||
match &self.active_view {
|
||||
ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
|
||||
ActiveView::History => self.history.focus_handle(cx),
|
||||
ActiveView::PromptEditor => {
|
||||
if let Some(context_editor) = self.context_editor.as_ref() {
|
||||
context_editor.focus_handle(cx)
|
||||
} else {
|
||||
cx.focus_handle()
|
||||
}
|
||||
}
|
||||
ActiveView::PromptEditor { context_editor } => context_editor.focus_handle(cx),
|
||||
ActiveView::Configuration => {
|
||||
if let Some(configuration) = self.configuration.as_ref() {
|
||||
configuration.focus_handle(cx)
|
||||
@@ -949,15 +961,8 @@ impl AssistantPanel {
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
ActiveView::PromptEditor => {
|
||||
let title = self
|
||||
.context_editor
|
||||
.as_ref()
|
||||
.map(|context_editor| {
|
||||
SharedString::from(context_editor.read(cx).title(cx).to_string())
|
||||
})
|
||||
.unwrap_or_else(|| SharedString::from(LOADING_SUMMARY_PLACEHOLDER));
|
||||
|
||||
ActiveView::PromptEditor { context_editor } => {
|
||||
let title = SharedString::from(context_editor.read(cx).title(cx).to_string());
|
||||
Label::new(title).ml_2().truncate().into_any_element()
|
||||
}
|
||||
ActiveView::History => Label::new("History").truncate().into_any_element(),
|
||||
@@ -984,7 +989,7 @@ impl AssistantPanel {
|
||||
|
||||
let show_token_count = match &self.active_view {
|
||||
ActiveView::Thread { .. } => !is_empty,
|
||||
ActiveView::PromptEditor => self.context_editor.is_some(),
|
||||
ActiveView::PromptEditor { .. } => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
@@ -1115,17 +1120,19 @@ impl AssistantPanel {
|
||||
"New Text Thread",
|
||||
NewTextThread.boxed_clone(),
|
||||
)
|
||||
.action("Prompt Library", Box::new(OpenPromptLibrary))
|
||||
.action("Prompt Library", Box::new(OpenPromptLibrary::default()))
|
||||
.action("Settings", Box::new(OpenConfiguration))
|
||||
.separator()
|
||||
.header("MCPs")
|
||||
.action(
|
||||
"Install MCPs",
|
||||
"View Server Extensions",
|
||||
Box::new(zed_actions::Extensions {
|
||||
category_filter: Some(
|
||||
zed_actions::ExtensionCategoryFilter::ContextServers,
|
||||
),
|
||||
}),
|
||||
)
|
||||
.action("Add Custom Server", Box::new(AddContextServer))
|
||||
},
|
||||
))
|
||||
}),
|
||||
@@ -1156,7 +1163,7 @@ impl AssistantPanel {
|
||||
|
||||
let is_waiting_to_update_token_count = message_editor.is_waiting_to_update_token_count();
|
||||
|
||||
match self.active_view {
|
||||
match &self.active_view {
|
||||
ActiveView::Thread { .. } => {
|
||||
if total_token_usage.total == 0 {
|
||||
return None;
|
||||
@@ -1177,8 +1184,8 @@ impl AssistantPanel {
|
||||
parent
|
||||
.child(
|
||||
h_flex()
|
||||
.mr_0p5()
|
||||
.size_2()
|
||||
.mr_1()
|
||||
.size_2p5()
|
||||
.justify_center()
|
||||
.rounded_full()
|
||||
.bg(cx.theme().colors().text.opacity(0.1))
|
||||
@@ -1229,9 +1236,8 @@ impl AssistantPanel {
|
||||
|
||||
Some(token_count)
|
||||
}
|
||||
ActiveView::PromptEditor => {
|
||||
let editor = self.context_editor.as_ref()?;
|
||||
let element = render_remaining_tokens(editor, cx)?;
|
||||
ActiveView::PromptEditor { context_editor } => {
|
||||
let element = render_remaining_tokens(context_editor, cx)?;
|
||||
|
||||
Some(element.into_any_element())
|
||||
}
|
||||
@@ -1539,6 +1545,12 @@ impl AssistantPanel {
|
||||
})
|
||||
}
|
||||
|
||||
fn render_usage_banner(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
let usage = self.thread.read(cx).last_usage()?;
|
||||
|
||||
Some(UsageBanner::new(zed_llm_client::Plan::ZedProTrial, usage.amount).into_any_element())
|
||||
}
|
||||
|
||||
fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
let last_error = self.thread.read(cx).last_error()?;
|
||||
|
||||
@@ -1769,7 +1781,7 @@ impl AssistantPanel {
|
||||
fn key_context(&self) -> KeyContext {
|
||||
let mut key_context = KeyContext::new_with_defaults();
|
||||
key_context.add("AgentPanel");
|
||||
if matches!(self.active_view, ActiveView::PromptEditor) {
|
||||
if matches!(self.active_view, ActiveView::PromptEditor { .. }) {
|
||||
key_context.add("prompt_editor");
|
||||
}
|
||||
key_context
|
||||
@@ -1797,13 +1809,14 @@ impl Render for AssistantPanel {
|
||||
.on_action(cx.listener(Self::open_agent_diff))
|
||||
.on_action(cx.listener(Self::go_back))
|
||||
.child(self.render_toolbar(window, cx))
|
||||
.map(|parent| match self.active_view {
|
||||
.map(|parent| match &self.active_view {
|
||||
ActiveView::Thread { .. } => parent
|
||||
.child(self.render_active_thread_or_empty_state(window, cx))
|
||||
.children(self.render_usage_banner(cx))
|
||||
.child(h_flex().child(self.message_editor.clone()))
|
||||
.children(self.render_last_error(cx)),
|
||||
ActiveView::History => parent.child(self.history.clone()),
|
||||
ActiveView::PromptEditor => parent.children(self.context_editor.clone()),
|
||||
ActiveView::PromptEditor { context_editor } => parent.child(context_editor.clone()),
|
||||
ActiveView::Configuration => parent.children(self.configuration.clone()),
|
||||
})
|
||||
}
|
||||
@@ -1868,7 +1881,7 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Option<Entity<ContextEditor>> {
|
||||
let panel = workspace.panel::<AssistantPanel>(cx)?;
|
||||
panel.update(cx, |panel, _cx| panel.context_editor.clone())
|
||||
panel.read(cx).active_context_editor()
|
||||
}
|
||||
|
||||
fn open_saved_context(
|
||||
@@ -1900,7 +1913,8 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
fn quote_selection(
|
||||
&self,
|
||||
workspace: &mut Workspace,
|
||||
creases: Vec<(String, String)>,
|
||||
selection_ranges: Vec<Range<Anchor>>,
|
||||
buffer: Entity<MultiBuffer>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
@@ -1916,9 +1930,40 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
// Wait to create a new context until the workspace is no longer
|
||||
// being updated.
|
||||
cx.defer_in(window, move |panel, window, cx| {
|
||||
if let Some(context) = panel.active_context_editor() {
|
||||
context.update(cx, |context, cx| context.quote_creases(creases, window, cx));
|
||||
};
|
||||
if panel.has_active_thread() {
|
||||
panel.thread.update(cx, |thread, cx| {
|
||||
thread.context_store().update(cx, |store, cx| {
|
||||
let buffer = buffer.read(cx);
|
||||
let selection_ranges = selection_ranges
|
||||
.into_iter()
|
||||
.flat_map(|range| {
|
||||
let (start_buffer, start) =
|
||||
buffer.text_anchor_for_position(range.start, cx)?;
|
||||
let (end_buffer, end) =
|
||||
buffer.text_anchor_for_position(range.end, cx)?;
|
||||
if start_buffer != end_buffer {
|
||||
return None;
|
||||
}
|
||||
Some((start_buffer, start..end))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for (buffer, range) in selection_ranges {
|
||||
store.add_excerpt(range, buffer, cx).detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
})
|
||||
} else if let Some(context_editor) = panel.active_context_editor() {
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
let selection_ranges = selection_ranges
|
||||
.into_iter()
|
||||
.map(|range| range.to_point(&snapshot))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
context_editor.update(cx, |context_editor, cx| {
|
||||
context_editor.quote_ranges(selection_ranges, snapshot, window, cx)
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -425,6 +425,8 @@ impl CodegenAlternative {
|
||||
request_message.content.push(prompt.into());
|
||||
|
||||
Ok(LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
temperature: None,
|
||||
|
||||
@@ -4,6 +4,7 @@ use gpui::{App, Entity, SharedString};
|
||||
use language::{Buffer, File};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use project::{ProjectPath, Worktree};
|
||||
use rope::Point;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use text::{Anchor, BufferId};
|
||||
use ui::IconName;
|
||||
@@ -23,6 +24,7 @@ pub enum ContextKind {
|
||||
File,
|
||||
Directory,
|
||||
Symbol,
|
||||
Excerpt,
|
||||
FetchedUrl,
|
||||
Thread,
|
||||
}
|
||||
@@ -33,6 +35,7 @@ impl ContextKind {
|
||||
ContextKind::File => IconName::File,
|
||||
ContextKind::Directory => IconName::Folder,
|
||||
ContextKind::Symbol => IconName::Code,
|
||||
ContextKind::Excerpt => IconName::Code,
|
||||
ContextKind::FetchedUrl => IconName::Globe,
|
||||
ContextKind::Thread => IconName::MessageBubbles,
|
||||
}
|
||||
@@ -46,6 +49,7 @@ pub enum AssistantContext {
|
||||
Symbol(SymbolContext),
|
||||
FetchedUrl(FetchedUrlContext),
|
||||
Thread(ThreadContext),
|
||||
Excerpt(ExcerptContext),
|
||||
}
|
||||
|
||||
impl AssistantContext {
|
||||
@@ -56,6 +60,7 @@ impl AssistantContext {
|
||||
Self::Symbol(symbol) => symbol.id,
|
||||
Self::FetchedUrl(url) => url.id,
|
||||
Self::Thread(thread) => thread.id,
|
||||
Self::Excerpt(excerpt) => excerpt.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,6 +160,14 @@ pub struct ContextSymbolId {
|
||||
pub range: Range<Anchor>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExcerptContext {
|
||||
pub id: ContextId,
|
||||
pub range: Range<Anchor>,
|
||||
pub line_range: Range<Point>,
|
||||
pub context_buffer: ContextBuffer,
|
||||
}
|
||||
|
||||
/// Formats a collection of contexts into a string representation
|
||||
pub fn format_context_as_string<'a>(
|
||||
contexts: impl Iterator<Item = &'a AssistantContext>,
|
||||
@@ -163,6 +176,7 @@ pub fn format_context_as_string<'a>(
|
||||
let mut file_context = Vec::new();
|
||||
let mut directory_context = Vec::new();
|
||||
let mut symbol_context = Vec::new();
|
||||
let mut excerpt_context = Vec::new();
|
||||
let mut fetch_context = Vec::new();
|
||||
let mut thread_context = Vec::new();
|
||||
|
||||
@@ -171,6 +185,7 @@ pub fn format_context_as_string<'a>(
|
||||
AssistantContext::File(context) => file_context.push(context),
|
||||
AssistantContext::Directory(context) => directory_context.push(context),
|
||||
AssistantContext::Symbol(context) => symbol_context.push(context),
|
||||
AssistantContext::Excerpt(context) => excerpt_context.push(context),
|
||||
AssistantContext::FetchedUrl(context) => fetch_context.push(context),
|
||||
AssistantContext::Thread(context) => thread_context.push(context),
|
||||
}
|
||||
@@ -179,6 +194,7 @@ pub fn format_context_as_string<'a>(
|
||||
if file_context.is_empty()
|
||||
&& directory_context.is_empty()
|
||||
&& symbol_context.is_empty()
|
||||
&& excerpt_context.is_empty()
|
||||
&& fetch_context.is_empty()
|
||||
&& thread_context.is_empty()
|
||||
{
|
||||
@@ -216,6 +232,15 @@ pub fn format_context_as_string<'a>(
|
||||
result.push_str("</symbols>\n");
|
||||
}
|
||||
|
||||
if !excerpt_context.is_empty() {
|
||||
result.push_str("<excerpts>\n");
|
||||
for context in excerpt_context {
|
||||
result.push_str(&context.context_buffer.text);
|
||||
result.push('\n');
|
||||
}
|
||||
result.push_str("</excerpts>\n");
|
||||
}
|
||||
|
||||
if !fetch_context.is_empty() {
|
||||
result.push_str("<fetched_urls>\n");
|
||||
for context in &fetch_context {
|
||||
|
||||
@@ -9,14 +9,14 @@ use futures::{self, Future, FutureExt, future};
|
||||
use gpui::{App, AppContext as _, Context, Entity, SharedString, Task, WeakEntity};
|
||||
use language::{Buffer, File};
|
||||
use project::{Project, ProjectItem, ProjectPath, Worktree};
|
||||
use rope::Rope;
|
||||
use rope::{Point, Rope};
|
||||
use text::{Anchor, BufferId, OffsetRangeExt};
|
||||
use util::{ResultExt as _, maybe};
|
||||
|
||||
use crate::ThreadStore;
|
||||
use crate::context::{
|
||||
AssistantContext, ContextBuffer, ContextId, ContextSymbol, ContextSymbolId, DirectoryContext,
|
||||
FetchedUrlContext, FileContext, SymbolContext, ThreadContext,
|
||||
ExcerptContext, FetchedUrlContext, FileContext, SymbolContext, ThreadContext,
|
||||
};
|
||||
use crate::context_strip::SuggestedContext;
|
||||
use crate::thread::{Thread, ThreadId};
|
||||
@@ -110,7 +110,7 @@ impl ContextStore {
|
||||
}
|
||||
|
||||
let (buffer_info, text_task) =
|
||||
this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, None, cx))??;
|
||||
this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, cx))??;
|
||||
|
||||
let text = text_task.await;
|
||||
|
||||
@@ -129,7 +129,7 @@ impl ContextStore {
|
||||
) -> Task<Result<()>> {
|
||||
cx.spawn(async move |this, cx| {
|
||||
let (buffer_info, text_task) =
|
||||
this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, None, cx))??;
|
||||
this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, cx))??;
|
||||
|
||||
let text = text_task.await;
|
||||
|
||||
@@ -206,7 +206,7 @@ impl ContextStore {
|
||||
// Skip all binary files and other non-UTF8 files
|
||||
for buffer in buffers.into_iter().flatten() {
|
||||
if let Some((buffer_info, text_task)) =
|
||||
collect_buffer_info_and_text(buffer, None, cx).log_err()
|
||||
collect_buffer_info_and_text(buffer, cx).log_err()
|
||||
{
|
||||
buffer_infos.push(buffer_info);
|
||||
text_tasks.push(text_task);
|
||||
@@ -290,11 +290,14 @@ impl ContextStore {
|
||||
}
|
||||
}
|
||||
|
||||
let (buffer_info, collect_content_task) =
|
||||
match collect_buffer_info_and_text(buffer, Some(symbol_enclosing_range.clone()), cx) {
|
||||
Ok((buffer_info, collect_context_task)) => (buffer_info, collect_context_task),
|
||||
Err(err) => return Task::ready(Err(err)),
|
||||
};
|
||||
let (buffer_info, collect_content_task) = match collect_buffer_info_and_text_for_range(
|
||||
buffer,
|
||||
symbol_enclosing_range.clone(),
|
||||
cx,
|
||||
) {
|
||||
Ok((_, buffer_info, collect_context_task)) => (buffer_info, collect_context_task),
|
||||
Err(err) => return Task::ready(Err(err)),
|
||||
};
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let content = collect_content_task.await;
|
||||
@@ -416,6 +419,49 @@ impl ContextStore {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn add_excerpt(
|
||||
&mut self,
|
||||
range: Range<Anchor>,
|
||||
buffer: Entity<Buffer>,
|
||||
cx: &mut Context<ContextStore>,
|
||||
) -> Task<Result<()>> {
|
||||
cx.spawn(async move |this, cx| {
|
||||
let (line_range, buffer_info, text_task) = this.update(cx, |_, cx| {
|
||||
collect_buffer_info_and_text_for_range(buffer, range.clone(), cx)
|
||||
})??;
|
||||
|
||||
let text = text_task.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.insert_excerpt(
|
||||
make_context_buffer(buffer_info, text),
|
||||
range,
|
||||
line_range,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn insert_excerpt(
|
||||
&mut self,
|
||||
context_buffer: ContextBuffer,
|
||||
range: Range<Anchor>,
|
||||
line_range: Range<Point>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let id = self.next_context_id.post_inc();
|
||||
self.context.push(AssistantContext::Excerpt(ExcerptContext {
|
||||
id,
|
||||
range,
|
||||
line_range,
|
||||
context_buffer,
|
||||
}));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn accept_suggested_context(
|
||||
&mut self,
|
||||
suggested: &SuggestedContext,
|
||||
@@ -465,6 +511,7 @@ impl ContextStore {
|
||||
self.symbol_buffers.remove(&symbol.context_symbol.id);
|
||||
self.symbols.retain(|_, context_id| *context_id != id);
|
||||
}
|
||||
AssistantContext::Excerpt(_) => {}
|
||||
AssistantContext::FetchedUrl(_) => {
|
||||
self.fetched_urls.retain(|_, context_id| *context_id != id);
|
||||
}
|
||||
@@ -592,6 +639,7 @@ impl ContextStore {
|
||||
}
|
||||
AssistantContext::Directory(_)
|
||||
| AssistantContext::Symbol(_)
|
||||
| AssistantContext::Excerpt(_)
|
||||
| AssistantContext::FetchedUrl(_)
|
||||
| AssistantContext::Thread(_) => None,
|
||||
})
|
||||
@@ -643,41 +691,78 @@ fn make_context_symbol(
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_buffer_info_and_text_for_range(
|
||||
buffer: Entity<Buffer>,
|
||||
range: Range<Anchor>,
|
||||
cx: &App,
|
||||
) -> Result<(Range<Point>, BufferInfo, Task<SharedString>)> {
|
||||
let content = buffer
|
||||
.read(cx)
|
||||
.text_for_range(range.clone())
|
||||
.collect::<Rope>();
|
||||
|
||||
let line_range = range.to_point(&buffer.read(cx).snapshot());
|
||||
|
||||
let buffer_info = collect_buffer_info(buffer, cx)?;
|
||||
let full_path = buffer_info.file.full_path(cx);
|
||||
|
||||
let text_task = cx.background_spawn({
|
||||
let line_range = line_range.clone();
|
||||
async move { to_fenced_codeblock(&full_path, content, Some(line_range)) }
|
||||
});
|
||||
|
||||
Ok((line_range, buffer_info, text_task))
|
||||
}
|
||||
|
||||
fn collect_buffer_info_and_text(
|
||||
buffer: Entity<Buffer>,
|
||||
range: Option<Range<Anchor>>,
|
||||
cx: &App,
|
||||
) -> Result<(BufferInfo, Task<SharedString>)> {
|
||||
let content = buffer.read(cx).as_rope().clone();
|
||||
|
||||
let buffer_info = collect_buffer_info(buffer, cx)?;
|
||||
let full_path = buffer_info.file.full_path(cx);
|
||||
|
||||
let text_task =
|
||||
cx.background_spawn(async move { to_fenced_codeblock(&full_path, content, None) });
|
||||
|
||||
Ok((buffer_info, text_task))
|
||||
}
|
||||
|
||||
fn collect_buffer_info(buffer: Entity<Buffer>, cx: &App) -> Result<BufferInfo> {
|
||||
let buffer_ref = buffer.read(cx);
|
||||
let file = buffer_ref.file().context("file context must have a path")?;
|
||||
|
||||
// Important to collect version at the same time as content so that staleness logic is correct.
|
||||
let version = buffer_ref.version();
|
||||
let content = if let Some(range) = range {
|
||||
buffer_ref.text_for_range(range).collect::<Rope>()
|
||||
} else {
|
||||
buffer_ref.as_rope().clone()
|
||||
};
|
||||
|
||||
let buffer_info = BufferInfo {
|
||||
Ok(BufferInfo {
|
||||
buffer,
|
||||
id: buffer_ref.remote_id(),
|
||||
file: file.clone(),
|
||||
version,
|
||||
};
|
||||
|
||||
let full_path = file.full_path(cx);
|
||||
let text_task = cx.background_spawn(async move { to_fenced_codeblock(&full_path, content) });
|
||||
|
||||
Ok((buffer_info, text_task))
|
||||
})
|
||||
}
|
||||
|
||||
fn to_fenced_codeblock(path: &Path, content: Rope) -> SharedString {
|
||||
fn to_fenced_codeblock(
|
||||
path: &Path,
|
||||
content: Rope,
|
||||
line_range: Option<Range<Point>>,
|
||||
) -> SharedString {
|
||||
let line_range_text = line_range.map(|range| {
|
||||
if range.start.row == range.end.row {
|
||||
format!(":{}", range.start.row + 1)
|
||||
} else {
|
||||
format!(":{}-{}", range.start.row + 1, range.end.row + 1)
|
||||
}
|
||||
});
|
||||
|
||||
let path_extension = path.extension().and_then(|ext| ext.to_str());
|
||||
let path_string = path.to_string_lossy();
|
||||
let capacity = 3
|
||||
+ path_extension.map_or(0, |extension| extension.len() + 1)
|
||||
+ path_string.len()
|
||||
+ line_range_text.as_ref().map_or(0, |text| text.len())
|
||||
+ 1
|
||||
+ content.len()
|
||||
+ 5;
|
||||
@@ -691,6 +776,10 @@ fn to_fenced_codeblock(path: &Path, content: Rope) -> SharedString {
|
||||
}
|
||||
buffer.push_str(&path_string);
|
||||
|
||||
if let Some(line_range_text) = line_range_text {
|
||||
buffer.push_str(&line_range_text);
|
||||
}
|
||||
|
||||
buffer.push('\n');
|
||||
for chunk in content.chunks() {
|
||||
buffer.push_str(&chunk);
|
||||
@@ -769,6 +858,14 @@ pub fn refresh_context_store_text(
|
||||
return refresh_symbol_text(context_store, symbol_context, cx);
|
||||
}
|
||||
}
|
||||
AssistantContext::Excerpt(excerpt_context) => {
|
||||
if changed_buffers.is_empty()
|
||||
|| changed_buffers.contains(&excerpt_context.context_buffer.buffer)
|
||||
{
|
||||
let context_store = context_store.clone();
|
||||
return refresh_excerpt_text(context_store, excerpt_context, cx);
|
||||
}
|
||||
}
|
||||
AssistantContext::Thread(thread_context) => {
|
||||
if changed_buffers.is_empty() {
|
||||
let context_store = context_store.clone();
|
||||
@@ -880,6 +977,34 @@ fn refresh_symbol_text(
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_excerpt_text(
|
||||
context_store: Entity<ContextStore>,
|
||||
excerpt_context: &ExcerptContext,
|
||||
cx: &App,
|
||||
) -> Option<Task<()>> {
|
||||
let id = excerpt_context.id;
|
||||
let range = excerpt_context.range.clone();
|
||||
let task = refresh_context_excerpt(&excerpt_context.context_buffer, range.clone(), cx);
|
||||
if let Some(task) = task {
|
||||
Some(cx.spawn(async move |cx| {
|
||||
let (line_range, context_buffer) = task.await;
|
||||
context_store
|
||||
.update(cx, |context_store, _| {
|
||||
let new_excerpt_context = ExcerptContext {
|
||||
id,
|
||||
range,
|
||||
line_range,
|
||||
context_buffer,
|
||||
};
|
||||
context_store.replace_context(AssistantContext::Excerpt(new_excerpt_context));
|
||||
})
|
||||
.ok();
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_thread_text(
|
||||
context_store: Entity<ContextStore>,
|
||||
thread_context: &ThreadContext,
|
||||
@@ -908,13 +1033,29 @@ fn refresh_context_buffer(
|
||||
let buffer = context_buffer.buffer.read(cx);
|
||||
if buffer.version.changed_since(&context_buffer.version) {
|
||||
let (buffer_info, text_task) =
|
||||
collect_buffer_info_and_text(context_buffer.buffer.clone(), None, cx).log_err()?;
|
||||
collect_buffer_info_and_text(context_buffer.buffer.clone(), cx).log_err()?;
|
||||
Some(text_task.map(move |text| make_context_buffer(buffer_info, text)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_context_excerpt(
|
||||
context_buffer: &ContextBuffer,
|
||||
range: Range<Anchor>,
|
||||
cx: &App,
|
||||
) -> Option<impl Future<Output = (Range<Point>, ContextBuffer)> + use<>> {
|
||||
let buffer = context_buffer.buffer.read(cx);
|
||||
if buffer.version.changed_since(&context_buffer.version) {
|
||||
let (line_range, buffer_info, text_task) =
|
||||
collect_buffer_info_and_text_for_range(context_buffer.buffer.clone(), range, cx)
|
||||
.log_err()?;
|
||||
Some(text_task.map(move |text| (line_range, make_context_buffer(buffer_info, text))))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_context_symbol(
|
||||
context_symbol: &ContextSymbol,
|
||||
cx: &App,
|
||||
@@ -922,9 +1063,9 @@ fn refresh_context_symbol(
|
||||
let buffer = context_symbol.buffer.read(cx);
|
||||
let project_path = buffer.project_path(cx)?;
|
||||
if buffer.version.changed_since(&context_symbol.buffer_version) {
|
||||
let (buffer_info, text_task) = collect_buffer_info_and_text(
|
||||
let (_, buffer_info, text_task) = collect_buffer_info_and_text_for_range(
|
||||
context_symbol.buffer.clone(),
|
||||
Some(context_symbol.enclosing_range.clone()),
|
||||
context_symbol.enclosing_range.clone(),
|
||||
cx,
|
||||
)
|
||||
.log_err()?;
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::assistant_model_selector::ModelType;
|
||||
use crate::context::format_context_as_string;
|
||||
use crate::context::{AssistantContext, format_context_as_string};
|
||||
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
|
||||
use buffer_diff::BufferDiff;
|
||||
use collections::HashSet;
|
||||
@@ -34,7 +34,7 @@ use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider};
|
||||
use crate::context_store::{ContextStore, refresh_context_store_text};
|
||||
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
||||
use crate::profile_selector::ProfileSelector;
|
||||
use crate::thread::{RequestKind, Thread, TokenUsageRatio};
|
||||
use crate::thread::{Thread, TokenUsageRatio};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::{
|
||||
AgentDiff, Chat, ChatMode, ExpandMessageEditor, NewThread, OpenAgentDiff, RemoveAllContext,
|
||||
@@ -234,7 +234,7 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
self.set_editor_is_expanded(false, cx);
|
||||
self.send_to_model(RequestKind::Chat, window, cx);
|
||||
self.send_to_model(window, cx);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
@@ -249,12 +249,7 @@ impl MessageEditor {
|
||||
.is_some()
|
||||
}
|
||||
|
||||
fn send_to_model(
|
||||
&mut self,
|
||||
request_kind: RequestKind,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let Some(ConfiguredModel { model, provider }) = model_registry.default_model() else {
|
||||
return;
|
||||
@@ -293,6 +288,21 @@ impl MessageEditor {
|
||||
})
|
||||
.log_err();
|
||||
|
||||
context_store
|
||||
.update(cx, |context_store, cx| {
|
||||
let excerpt_ids = context_store
|
||||
.context()
|
||||
.iter()
|
||||
.filter(|ctx| matches!(ctx, AssistantContext::Excerpt(_)))
|
||||
.map(|ctx| ctx.id())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for id in excerpt_ids {
|
||||
context_store.remove_context(id, cx);
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
|
||||
if let Some(wait_for_summaries) = context_store
|
||||
.update(cx, |context_store, cx| context_store.wait_for_summaries(cx))
|
||||
.log_err()
|
||||
@@ -315,7 +325,8 @@ impl MessageEditor {
|
||||
// Send to model after summaries are done
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send_to_model(model, request_kind, cx);
|
||||
thread.advance_prompt_id();
|
||||
thread.send_to_model(model, cx);
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
@@ -329,7 +340,7 @@ impl MessageEditor {
|
||||
|
||||
if cancelled {
|
||||
self.set_editor_is_expanded(false, cx);
|
||||
self.send_to_model(RequestKind::Chat, window, cx);
|
||||
self.send_to_model(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -998,6 +1009,8 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
let request = language_model::LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: language_model::Role::User,
|
||||
content: vec![content.into()],
|
||||
|
||||
@@ -261,6 +261,8 @@ impl TerminalInlineAssistant {
|
||||
request_message.content.push(prompt.into());
|
||||
|
||||
Ok(LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
messages: vec![request_message],
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
|
||||
use chrono::{DateTime, Utc};
|
||||
@@ -19,7 +19,8 @@ use language_model::{
|
||||
LanguageModelKnownError, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
|
||||
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent,
|
||||
ModelRequestLimitReachedError, PaymentRequiredError, Role, StopReason, TokenUsage,
|
||||
ModelRequestLimitReachedError, PaymentRequiredError, RequestUsage, Role, StopReason,
|
||||
TokenUsage,
|
||||
};
|
||||
use project::Project;
|
||||
use project::git_store::{GitStore, GitStoreCheckpoint, RepositoryState};
|
||||
@@ -39,13 +40,6 @@ use crate::thread_store::{
|
||||
};
|
||||
use crate::tool_use::{PendingToolUse, ToolUse, ToolUseState, USING_TOOL_MARKER};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum RequestKind {
|
||||
Chat,
|
||||
/// Used when summarizing a thread.
|
||||
Summarize,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize, JsonSchema,
|
||||
)]
|
||||
@@ -69,6 +63,24 @@ impl From<&str> for ThreadId {
|
||||
}
|
||||
}
|
||||
|
||||
/// The ID of the user prompt that initiated a request.
|
||||
///
|
||||
/// This equates to the user physically submitting a message to the model (e.g., by pressing the Enter key).
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
|
||||
pub struct PromptId(Arc<str>);
|
||||
|
||||
impl PromptId {
|
||||
pub fn new() -> Self {
|
||||
Self(Uuid::new_v4().to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PromptId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct MessageId(pub(crate) usize);
|
||||
|
||||
@@ -94,12 +106,21 @@ impl Message {
|
||||
self.segments.iter().all(|segment| segment.should_display())
|
||||
}
|
||||
|
||||
pub fn push_thinking(&mut self, text: &str) {
|
||||
if let Some(MessageSegment::Thinking(segment)) = self.segments.last_mut() {
|
||||
pub fn push_thinking(&mut self, text: &str, signature: Option<String>) {
|
||||
if let Some(MessageSegment::Thinking {
|
||||
text: segment,
|
||||
signature: current_signature,
|
||||
}) = self.segments.last_mut()
|
||||
{
|
||||
if let Some(signature) = signature {
|
||||
*current_signature = Some(signature);
|
||||
}
|
||||
segment.push_str(text);
|
||||
} else {
|
||||
self.segments
|
||||
.push(MessageSegment::Thinking(text.to_string()));
|
||||
self.segments.push(MessageSegment::Thinking {
|
||||
text: text.to_string(),
|
||||
signature,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,11 +142,12 @@ impl Message {
|
||||
for segment in &self.segments {
|
||||
match segment {
|
||||
MessageSegment::Text(text) => result.push_str(text),
|
||||
MessageSegment::Thinking(text) => {
|
||||
result.push_str("<think>");
|
||||
MessageSegment::Thinking { text, .. } => {
|
||||
result.push_str("<think>\n");
|
||||
result.push_str(text);
|
||||
result.push_str("</think>");
|
||||
result.push_str("\n</think>");
|
||||
}
|
||||
MessageSegment::RedactedThinking(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,24 +158,22 @@ impl Message {
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum MessageSegment {
|
||||
Text(String),
|
||||
Thinking(String),
|
||||
Thinking {
|
||||
text: String,
|
||||
signature: Option<String>,
|
||||
},
|
||||
RedactedThinking(Vec<u8>),
|
||||
}
|
||||
|
||||
impl MessageSegment {
|
||||
pub fn text_mut(&mut self) -> &mut String {
|
||||
match self {
|
||||
Self::Text(text) => text,
|
||||
Self::Thinking(text) => text,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn should_display(&self) -> bool {
|
||||
// We add USING_TOOL_MARKER when making a request that includes tool uses
|
||||
// without non-whitespace text around them, and this can cause the model
|
||||
// to mimic the pattern, so we consider those segments not displayable.
|
||||
match self {
|
||||
Self::Text(text) => text.is_empty() || text.trim() == USING_TOOL_MARKER,
|
||||
Self::Thinking(text) => text.is_empty() || text.trim() == USING_TOOL_MARKER,
|
||||
Self::Thinking { text, .. } => text.is_empty() || text.trim() == USING_TOOL_MARKER,
|
||||
Self::RedactedThinking(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -273,6 +293,7 @@ pub struct Thread {
|
||||
detailed_summary_state: DetailedSummaryState,
|
||||
messages: Vec<Message>,
|
||||
next_message_id: MessageId,
|
||||
last_prompt_id: PromptId,
|
||||
context: BTreeMap<ContextId, AssistantContext>,
|
||||
context_by_message: HashMap<MessageId, Vec<ContextId>>,
|
||||
project_context: SharedProjectContext,
|
||||
@@ -293,6 +314,9 @@ pub struct Thread {
|
||||
feedback: Option<ThreadFeedback>,
|
||||
message_feedback: HashMap<MessageId, ThreadFeedback>,
|
||||
last_auto_capture_at: Option<Instant>,
|
||||
request_callback: Option<
|
||||
Box<dyn FnMut(&LanguageModelRequest, &[Result<LanguageModelCompletionEvent, String>])>,
|
||||
>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -319,6 +343,7 @@ impl Thread {
|
||||
detailed_summary_state: DetailedSummaryState::NotGenerated,
|
||||
messages: Vec::new(),
|
||||
next_message_id: MessageId(0),
|
||||
last_prompt_id: PromptId::new(),
|
||||
context: BTreeMap::default(),
|
||||
context_by_message: HashMap::default(),
|
||||
project_context: system_prompt,
|
||||
@@ -344,6 +369,7 @@ impl Thread {
|
||||
feedback: None,
|
||||
message_feedback: HashMap::default(),
|
||||
last_auto_capture_at: None,
|
||||
request_callback: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,8 +409,11 @@ impl Thread {
|
||||
.into_iter()
|
||||
.map(|segment| match segment {
|
||||
SerializedMessageSegment::Text { text } => MessageSegment::Text(text),
|
||||
SerializedMessageSegment::Thinking { text } => {
|
||||
MessageSegment::Thinking(text)
|
||||
SerializedMessageSegment::Thinking { text, signature } => {
|
||||
MessageSegment::Thinking { text, signature }
|
||||
}
|
||||
SerializedMessageSegment::RedactedThinking { data } => {
|
||||
MessageSegment::RedactedThinking(data)
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
@@ -392,6 +421,7 @@ impl Thread {
|
||||
})
|
||||
.collect(),
|
||||
next_message_id,
|
||||
last_prompt_id: PromptId::new(),
|
||||
context: BTreeMap::default(),
|
||||
context_by_message: HashMap::default(),
|
||||
project_context,
|
||||
@@ -412,9 +442,18 @@ impl Thread {
|
||||
feedback: None,
|
||||
message_feedback: HashMap::default(),
|
||||
last_auto_capture_at: None,
|
||||
request_callback: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_request_callback(
|
||||
&mut self,
|
||||
callback: impl 'static
|
||||
+ FnMut(&LanguageModelRequest, &[Result<LanguageModelCompletionEvent, String>]),
|
||||
) {
|
||||
self.request_callback = Some(Box::new(callback));
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &ThreadId {
|
||||
&self.id
|
||||
}
|
||||
@@ -431,6 +470,10 @@ impl Thread {
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
pub fn advance_prompt_id(&mut self) {
|
||||
self.last_prompt_id = PromptId::new();
|
||||
}
|
||||
|
||||
pub fn summary(&self) -> Option<SharedString> {
|
||||
self.summary.clone()
|
||||
}
|
||||
@@ -725,6 +768,12 @@ impl Thread {
|
||||
cx,
|
||||
);
|
||||
}
|
||||
AssistantContext::Excerpt(excerpt_context) => {
|
||||
log.buffer_added_as_context(
|
||||
excerpt_context.context_buffer.buffer.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
AssistantContext::FetchedUrl(_) | AssistantContext::Thread(_) => {}
|
||||
}
|
||||
}
|
||||
@@ -817,9 +866,10 @@ impl Thread {
|
||||
for segment in &message.segments {
|
||||
match segment {
|
||||
MessageSegment::Text(content) => text.push_str(content),
|
||||
MessageSegment::Thinking(content) => {
|
||||
MessageSegment::Thinking { text: content, .. } => {
|
||||
text.push_str(&format!("<think>{}</think>", content))
|
||||
}
|
||||
MessageSegment::RedactedThinking(_) => {}
|
||||
}
|
||||
}
|
||||
text.push('\n');
|
||||
@@ -849,8 +899,16 @@ impl Thread {
|
||||
MessageSegment::Text(text) => {
|
||||
SerializedMessageSegment::Text { text: text.clone() }
|
||||
}
|
||||
MessageSegment::Thinking(text) => {
|
||||
SerializedMessageSegment::Thinking { text: text.clone() }
|
||||
MessageSegment::Thinking { text, signature } => {
|
||||
SerializedMessageSegment::Thinking {
|
||||
text: text.clone(),
|
||||
signature: signature.clone(),
|
||||
}
|
||||
}
|
||||
MessageSegment::RedactedThinking(data) => {
|
||||
SerializedMessageSegment::RedactedThinking {
|
||||
data: data.clone(),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
@@ -884,13 +942,8 @@ impl Thread {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn send_to_model(
|
||||
&mut self,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
request_kind: RequestKind,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let mut request = self.to_completion_request(request_kind, cx);
|
||||
pub fn send_to_model(&mut self, model: Arc<dyn LanguageModel>, cx: &mut Context<Self>) {
|
||||
let mut request = self.to_completion_request(cx);
|
||||
if model.supports_tools() {
|
||||
request.tools = {
|
||||
let mut tools = Vec::new();
|
||||
@@ -929,12 +982,10 @@ impl Thread {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn to_completion_request(
|
||||
&self,
|
||||
request_kind: RequestKind,
|
||||
cx: &App,
|
||||
) -> LanguageModelRequest {
|
||||
pub fn to_completion_request(&self, cx: &mut Context<Self>) -> LanguageModelRequest {
|
||||
let mut request = LanguageModelRequest {
|
||||
thread_id: Some(self.id.to_string()),
|
||||
prompt_id: Some(self.last_prompt_id.to_string()),
|
||||
messages: vec![],
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
@@ -942,20 +993,33 @@ impl Thread {
|
||||
};
|
||||
|
||||
if let Some(project_context) = self.project_context.borrow().as_ref() {
|
||||
if let Some(system_prompt) = self
|
||||
match self
|
||||
.prompt_builder
|
||||
.generate_assistant_system_prompt(project_context)
|
||||
.context("failed to generate assistant system prompt")
|
||||
.log_err()
|
||||
{
|
||||
request.messages.push(LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: vec![MessageContent::Text(system_prompt)],
|
||||
cache: true,
|
||||
});
|
||||
Err(err) => {
|
||||
let message = format!("{err:?}").into();
|
||||
log::error!("{message}");
|
||||
cx.emit(ThreadEvent::ShowError(ThreadError::Message {
|
||||
header: "Error generating system prompt".into(),
|
||||
message,
|
||||
}));
|
||||
}
|
||||
Ok(system_prompt) => {
|
||||
request.messages.push(LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: vec![MessageContent::Text(system_prompt)],
|
||||
cache: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::error!("project_context not set.")
|
||||
let message = "Context for system prompt unexpectedly not ready.".into();
|
||||
log::error!("{message}");
|
||||
cx.emit(ThreadEvent::ShowError(ThreadError::Message {
|
||||
header: "Error generating system prompt".into(),
|
||||
message,
|
||||
}));
|
||||
}
|
||||
|
||||
for message in &self.messages {
|
||||
@@ -965,34 +1029,42 @@ impl Thread {
|
||||
cache: false,
|
||||
};
|
||||
|
||||
match request_kind {
|
||||
RequestKind::Chat => {
|
||||
self.tool_use
|
||||
.attach_tool_results(message.id, &mut request_message);
|
||||
}
|
||||
RequestKind::Summarize => {
|
||||
// We don't care about tool use during summarization.
|
||||
if self.tool_use.message_has_tool_results(message.id) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.tool_use
|
||||
.attach_tool_results(message.id, &mut request_message);
|
||||
|
||||
if !message.segments.is_empty() {
|
||||
if !message.context.is_empty() {
|
||||
request_message
|
||||
.content
|
||||
.push(MessageContent::Text(message.to_string()));
|
||||
.push(MessageContent::Text(message.context.to_string()));
|
||||
}
|
||||
|
||||
match request_kind {
|
||||
RequestKind::Chat => {
|
||||
self.tool_use
|
||||
.attach_tool_uses(message.id, &mut request_message);
|
||||
}
|
||||
RequestKind::Summarize => {
|
||||
// We don't care about tool use during summarization.
|
||||
}
|
||||
};
|
||||
for segment in &message.segments {
|
||||
match segment {
|
||||
MessageSegment::Text(text) => {
|
||||
if !text.is_empty() {
|
||||
request_message
|
||||
.content
|
||||
.push(MessageContent::Text(text.into()));
|
||||
}
|
||||
}
|
||||
MessageSegment::Thinking { text, signature } => {
|
||||
if !text.is_empty() {
|
||||
request_message.content.push(MessageContent::Thinking {
|
||||
text: text.into(),
|
||||
signature: signature.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
MessageSegment::RedactedThinking(data) => {
|
||||
request_message
|
||||
.content
|
||||
.push(MessageContent::RedactedThinking(data.clone()));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
self.tool_use
|
||||
.attach_tool_uses(message.id, &mut request_message);
|
||||
|
||||
request.messages.push(request_message);
|
||||
}
|
||||
@@ -1007,6 +1079,54 @@ impl Thread {
|
||||
request
|
||||
}
|
||||
|
||||
fn to_summarize_request(&self, added_user_message: String) -> LanguageModelRequest {
|
||||
let mut request = LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
messages: vec![],
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
temperature: None,
|
||||
};
|
||||
|
||||
for message in &self.messages {
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
role: message.role,
|
||||
content: Vec::new(),
|
||||
cache: false,
|
||||
};
|
||||
|
||||
// Skip tool results during summarization.
|
||||
if self.tool_use.message_has_tool_results(message.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for segment in &message.segments {
|
||||
match segment {
|
||||
MessageSegment::Text(text) => request_message
|
||||
.content
|
||||
.push(MessageContent::Text(text.clone())),
|
||||
MessageSegment::Thinking { .. } => {}
|
||||
MessageSegment::RedactedThinking(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
if request_message.content.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
request.messages.push(request_message);
|
||||
}
|
||||
|
||||
request.messages.push(LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![MessageContent::Text(added_user_message)],
|
||||
cache: false,
|
||||
});
|
||||
|
||||
request
|
||||
}
|
||||
|
||||
fn attached_tracked_files_state(
|
||||
&self,
|
||||
messages: &mut Vec<LanguageModelRequestMessage>,
|
||||
@@ -1036,15 +1156,6 @@ impl Thread {
|
||||
content.push(stale_message.into());
|
||||
}
|
||||
|
||||
if action_log.has_edited_files_since_project_diagnostics_check() {
|
||||
content.push(
|
||||
"\n\nWhen you're done making changes, make sure to check project diagnostics \
|
||||
and fix all errors AND warnings you introduced! \
|
||||
DO NOT mention you're going to do this until you're done."
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
if !content.is_empty() {
|
||||
let context_message = LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
@@ -1063,16 +1174,36 @@ impl Thread {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let pending_completion_id = post_inc(&mut self.completion_count);
|
||||
let mut request_callback_parameters = if self.request_callback.is_some() {
|
||||
Some((request.clone(), Vec::new()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let prompt_id = self.last_prompt_id.clone();
|
||||
let task = cx.spawn(async move |thread, cx| {
|
||||
let stream = model.stream_completion(request, &cx);
|
||||
let stream_completion_future = model.stream_completion_with_usage(request, &cx);
|
||||
let initial_token_usage =
|
||||
thread.read_with(cx, |thread, _cx| thread.cumulative_token_usage);
|
||||
let stream_completion = async {
|
||||
let mut events = stream.await?;
|
||||
let (mut events, usage) = stream_completion_future.await?;
|
||||
|
||||
let mut stop_reason = StopReason::EndTurn;
|
||||
let mut current_token_usage = TokenUsage::default();
|
||||
|
||||
if let Some(usage) = usage {
|
||||
thread
|
||||
.update(cx, |_thread, cx| {
|
||||
cx.emit(ThreadEvent::UsageUpdated(usage));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
while let Some(event) = events.next().await {
|
||||
if let Some((_, response_events)) = request_callback_parameters.as_mut() {
|
||||
response_events
|
||||
.push(event.as_ref().map_err(|error| error.to_string()).cloned());
|
||||
}
|
||||
|
||||
let event = event?;
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
@@ -1116,10 +1247,13 @@ impl Thread {
|
||||
};
|
||||
}
|
||||
}
|
||||
LanguageModelCompletionEvent::Thinking(chunk) => {
|
||||
LanguageModelCompletionEvent::Thinking {
|
||||
text: chunk,
|
||||
signature,
|
||||
} => {
|
||||
if let Some(last_message) = thread.messages.last_mut() {
|
||||
if last_message.role == Role::Assistant {
|
||||
last_message.push_thinking(&chunk);
|
||||
last_message.push_thinking(&chunk, signature);
|
||||
cx.emit(ThreadEvent::StreamedAssistantThinking(
|
||||
last_message.id,
|
||||
chunk,
|
||||
@@ -1132,7 +1266,10 @@ impl Thread {
|
||||
// will result in duplicating the text of the chunk in the rendered Markdown.
|
||||
thread.insert_message(
|
||||
Role::Assistant,
|
||||
vec![MessageSegment::Thinking(chunk.to_string())],
|
||||
vec![MessageSegment::Thinking {
|
||||
text: chunk.to_string(),
|
||||
signature,
|
||||
}],
|
||||
cx,
|
||||
);
|
||||
};
|
||||
@@ -1171,7 +1308,12 @@ impl Thread {
|
||||
.pending_completions
|
||||
.retain(|completion| completion.id != pending_completion_id);
|
||||
|
||||
if thread.summary.is_none() && thread.messages.len() >= 2 {
|
||||
// If there is a response without tool use, summarize the message. Otherwise,
|
||||
// allow two tool uses before summarizing.
|
||||
if thread.summary.is_none()
|
||||
&& thread.messages.len() >= 2
|
||||
&& (!thread.has_pending_tool_uses() || thread.messages.len() >= 6)
|
||||
{
|
||||
thread.summarize(cx);
|
||||
}
|
||||
})?;
|
||||
@@ -1237,6 +1379,14 @@ impl Thread {
|
||||
}
|
||||
cx.emit(ThreadEvent::Stopped(result.map_err(Arc::new)));
|
||||
|
||||
if let Some((request_callback, (request, response_events))) = thread
|
||||
.request_callback
|
||||
.as_mut()
|
||||
.zip(request_callback_parameters.as_ref())
|
||||
{
|
||||
request_callback(request, response_events);
|
||||
}
|
||||
|
||||
thread.auto_capture_telemetry(cx);
|
||||
|
||||
if let Ok(initial_usage) = initial_token_usage {
|
||||
@@ -1245,6 +1395,7 @@ impl Thread {
|
||||
telemetry::event!(
|
||||
"Assistant Thread Completion",
|
||||
thread_id = thread.id().to_string(),
|
||||
prompt_id = prompt_id,
|
||||
model = model.telemetry_id(),
|
||||
model_provider = model.provider_id().to_string(),
|
||||
input_tokens = usage.input_tokens,
|
||||
@@ -1272,23 +1423,24 @@ impl Thread {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut request = self.to_completion_request(RequestKind::Summarize, cx);
|
||||
request.messages.push(LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![
|
||||
"Generate a concise 3-7 word title for this conversation, omitting punctuation. \
|
||||
Go straight to the title, without any preamble and prefix like `Here's a concise suggestion:...` or `Title:`. \
|
||||
If the conversation is about a specific subject, include it in the title. \
|
||||
Be descriptive. DO NOT speak in the first person."
|
||||
.into(),
|
||||
],
|
||||
cache: false,
|
||||
});
|
||||
let added_user_message = "Generate a concise 3-7 word title for this conversation, omitting punctuation. \
|
||||
Go straight to the title, without any preamble and prefix like `Here's a concise suggestion:...` or `Title:`. \
|
||||
If the conversation is about a specific subject, include it in the title. \
|
||||
Be descriptive. DO NOT speak in the first person.";
|
||||
|
||||
let request = self.to_summarize_request(added_user_message.into());
|
||||
|
||||
self.pending_summary = cx.spawn(async move |this, cx| {
|
||||
async move {
|
||||
let stream = model.model.stream_completion_text(request, &cx);
|
||||
let mut messages = stream.await?;
|
||||
let stream = model.model.stream_completion_text_with_usage(request, &cx);
|
||||
let (mut messages, usage) = stream.await?;
|
||||
|
||||
if let Some(usage) = usage {
|
||||
this.update(cx, |_thread, cx| {
|
||||
cx.emit(ThreadEvent::UsageUpdated(usage));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
let mut new_summary = String::new();
|
||||
while let Some(message) = messages.stream.next().await {
|
||||
@@ -1338,21 +1490,14 @@ impl Thread {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut request = self.to_completion_request(RequestKind::Summarize, cx);
|
||||
let added_user_message = "Generate a detailed summary of this conversation. Include:\n\
|
||||
1. A brief overview of what was discussed\n\
|
||||
2. Key facts or information discovered\n\
|
||||
3. Outcomes or conclusions reached\n\
|
||||
4. Any action items or next steps if any\n\
|
||||
Format it in Markdown with headings and bullet points.";
|
||||
|
||||
request.messages.push(LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![
|
||||
"Generate a detailed summary of this conversation. Include:\n\
|
||||
1. A brief overview of what was discussed\n\
|
||||
2. Key facts or information discovered\n\
|
||||
3. Outcomes or conclusions reached\n\
|
||||
4. Any action items or next steps if any\n\
|
||||
Format it in Markdown with headings and bullet points."
|
||||
.into(),
|
||||
],
|
||||
cache: false,
|
||||
});
|
||||
let request = self.to_summarize_request(added_user_message.into());
|
||||
|
||||
let task = cx.spawn(async move |thread, cx| {
|
||||
let stream = model.stream_completion_text(request, &cx);
|
||||
@@ -1400,7 +1545,7 @@ impl Thread {
|
||||
|
||||
pub fn use_pending_tools(&mut self, cx: &mut Context<Self>) -> Vec<PendingToolUse> {
|
||||
self.auto_capture_telemetry(cx);
|
||||
let request = self.to_completion_request(RequestKind::Chat, cx);
|
||||
let request = self.to_completion_request(cx);
|
||||
let messages = Arc::new(request.messages);
|
||||
let pending_tool_uses = self
|
||||
.tool_use
|
||||
@@ -1512,7 +1657,7 @@ impl Thread {
|
||||
if let Some(ConfiguredModel { model, .. }) = model_registry.default_model() {
|
||||
self.attach_tool_results(cx);
|
||||
if !canceled {
|
||||
self.send_to_model(model, RequestKind::Chat, cx);
|
||||
self.send_to_model(model, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1523,17 +1668,12 @@ impl Thread {
|
||||
});
|
||||
}
|
||||
|
||||
/// Insert an empty message to be populated with tool results upon send.
|
||||
pub fn attach_tool_results(&mut self, cx: &mut Context<Self>) {
|
||||
// Insert a user message to contain the tool results.
|
||||
self.insert_user_message(
|
||||
// TODO: Sending up a user message without any content results in the model sending back
|
||||
// responses that also don't have any content. We currently don't handle this case well,
|
||||
// so for now we provide some text to keep the model on track.
|
||||
"Here are the tool results.",
|
||||
Vec::new(),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
// Tool results are assumed to be waiting on the next message id, so they will populate
|
||||
// this empty message before sending to model. Would prefer this to be more straightforward.
|
||||
self.insert_message(Role::User, vec![], cx);
|
||||
self.auto_capture_telemetry(cx);
|
||||
}
|
||||
|
||||
/// Cancels the last pending completion, if there are any pending.
|
||||
@@ -1812,9 +1952,10 @@ impl Thread {
|
||||
for segment in &message.segments {
|
||||
match segment {
|
||||
MessageSegment::Text(text) => writeln!(markdown, "{}\n", text)?,
|
||||
MessageSegment::Thinking(text) => {
|
||||
writeln!(markdown, "<think>{}</think>\n", text)?
|
||||
MessageSegment::Thinking { text, .. } => {
|
||||
writeln!(markdown, "<think>\n{}\n</think>\n", text)?
|
||||
}
|
||||
MessageSegment::RedactedThinking(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2035,6 +2176,7 @@ pub enum ThreadError {
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ThreadEvent {
|
||||
ShowError(ThreadError),
|
||||
UsageUpdated(RequestUsage),
|
||||
StreamedCompletion,
|
||||
StreamedAssistantText(MessageId, String),
|
||||
StreamedAssistantThinking(MessageId, String),
|
||||
@@ -2140,9 +2282,7 @@ fn main() {{
|
||||
assert_eq!(message.context, expected_context);
|
||||
|
||||
// Check message in request
|
||||
let request = thread.read_with(cx, |thread, cx| {
|
||||
thread.to_completion_request(RequestKind::Chat, cx)
|
||||
});
|
||||
let request = thread.update(cx, |thread, cx| thread.to_completion_request(cx));
|
||||
|
||||
assert_eq!(request.messages.len(), 2);
|
||||
let expected_full_message = format!("{}Please explain this code", expected_context);
|
||||
@@ -2232,9 +2372,7 @@ fn main() {{
|
||||
assert!(message3.context.contains("file3.rs"));
|
||||
|
||||
// Check entire request to make sure all contexts are properly included
|
||||
let request = thread.read_with(cx, |thread, cx| {
|
||||
thread.to_completion_request(RequestKind::Chat, cx)
|
||||
});
|
||||
let request = thread.update(cx, |thread, cx| thread.to_completion_request(cx));
|
||||
|
||||
// The request should contain all 3 messages
|
||||
assert_eq!(request.messages.len(), 4);
|
||||
@@ -2284,9 +2422,7 @@ fn main() {{
|
||||
assert_eq!(message.context, "");
|
||||
|
||||
// Check message in request
|
||||
let request = thread.read_with(cx, |thread, cx| {
|
||||
thread.to_completion_request(RequestKind::Chat, cx)
|
||||
});
|
||||
let request = thread.update(cx, |thread, cx| thread.to_completion_request(cx));
|
||||
|
||||
assert_eq!(request.messages.len(), 2);
|
||||
assert_eq!(
|
||||
@@ -2304,9 +2440,7 @@ fn main() {{
|
||||
assert_eq!(message2.context, "");
|
||||
|
||||
// Check that both messages appear in the request
|
||||
let request = thread.read_with(cx, |thread, cx| {
|
||||
thread.to_completion_request(RequestKind::Chat, cx)
|
||||
});
|
||||
let request = thread.update(cx, |thread, cx| thread.to_completion_request(cx));
|
||||
|
||||
assert_eq!(request.messages.len(), 3);
|
||||
assert_eq!(
|
||||
@@ -2346,9 +2480,7 @@ fn main() {{
|
||||
});
|
||||
|
||||
// Create a request and check that it doesn't have a stale buffer warning yet
|
||||
let initial_request = thread.read_with(cx, |thread, cx| {
|
||||
thread.to_completion_request(RequestKind::Chat, cx)
|
||||
});
|
||||
let initial_request = thread.update(cx, |thread, cx| thread.to_completion_request(cx));
|
||||
|
||||
// Make sure we don't have a stale file warning yet
|
||||
let has_stale_warning = initial_request.messages.iter().any(|msg| {
|
||||
@@ -2376,9 +2508,7 @@ fn main() {{
|
||||
});
|
||||
|
||||
// Create a new request and check for the stale buffer warning
|
||||
let new_request = thread.read_with(cx, |thread, cx| {
|
||||
thread.to_completion_request(RequestKind::Chat, cx)
|
||||
});
|
||||
let new_request = thread.update(cx, |thread, cx| thread.to_completion_request(cx));
|
||||
|
||||
// We should have a stale file warning as the last message
|
||||
let last_message = new_request
|
||||
@@ -2405,6 +2535,7 @@ fn main() {{
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
AssistantSettings::register(cx);
|
||||
prompt_store::init(cx);
|
||||
thread_store::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
ThemeSettings::register(cx);
|
||||
@@ -2444,7 +2575,8 @@ fn main() {{
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
|
||||
let context_store = cx.new(|_cx| ContextStore::new(project.downgrade(), None));
|
||||
|
||||
@@ -12,8 +12,9 @@ use collections::HashMap;
|
||||
use context_server::manager::ContextServerManager;
|
||||
use context_server::{ContextServerFactoryRegistry, ContextServerTool};
|
||||
use fs::Fs;
|
||||
use futures::FutureExt as _;
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
use futures::future::{self, BoxFuture, Shared};
|
||||
use futures::{FutureExt as _, StreamExt as _};
|
||||
use gpui::{
|
||||
App, BackgroundExecutor, Context, Entity, EventEmitter, Global, ReadGlobal, SharedString,
|
||||
Subscription, Task, prelude::*,
|
||||
@@ -22,7 +23,10 @@ use heed::Database;
|
||||
use heed::types::SerdeBincode;
|
||||
use language_model::{LanguageModelToolUseId, Role, TokenUsage};
|
||||
use project::{Project, Worktree};
|
||||
use prompt_store::{ProjectContext, PromptBuilder, RulesFileContext, WorktreeContext};
|
||||
use prompt_store::{
|
||||
DefaultUserRulesContext, ProjectContext, PromptBuilder, PromptId, PromptStore,
|
||||
PromptsUpdatedEvent, RulesFileContext, WorktreeContext,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use util::ResultExt as _;
|
||||
@@ -62,6 +66,8 @@ pub struct ThreadStore {
|
||||
context_server_tool_ids: HashMap<Arc<str>, Vec<ToolId>>,
|
||||
threads: Vec<SerializedThreadMetadata>,
|
||||
project_context: SharedProjectContext,
|
||||
reload_system_prompt_tx: mpsc::Sender<()>,
|
||||
_reload_system_prompt_task: Task<()>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -77,12 +83,22 @@ impl ThreadStore {
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
cx: &mut App,
|
||||
) -> Task<Entity<Self>> {
|
||||
let thread_store = cx.new(|cx| Self::new(project, tools, prompt_builder, cx));
|
||||
let reload = thread_store.update(cx, |store, cx| store.reload_system_prompt(cx));
|
||||
cx.foreground_executor().spawn(async move {
|
||||
reload.await;
|
||||
thread_store
|
||||
) -> Task<Result<Entity<Self>>> {
|
||||
let prompt_store = PromptStore::global(cx);
|
||||
cx.spawn(async move |cx| {
|
||||
let prompt_store = prompt_store.await.ok();
|
||||
let (thread_store, ready_rx) = cx.update(|cx| {
|
||||
let mut option_ready_rx = None;
|
||||
let thread_store = cx.new(|cx| {
|
||||
let (thread_store, ready_rx) =
|
||||
Self::new(project, tools, prompt_builder, prompt_store, cx);
|
||||
option_ready_rx = Some(ready_rx);
|
||||
thread_store
|
||||
});
|
||||
(thread_store, option_ready_rx.take().unwrap())
|
||||
})?;
|
||||
ready_rx.await?;
|
||||
Ok(thread_store)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -90,17 +106,53 @@ impl ThreadStore {
|
||||
project: Entity<Project>,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
) -> (Self, oneshot::Receiver<()>) {
|
||||
let context_server_factory_registry = ContextServerFactoryRegistry::default_global(cx);
|
||||
let context_server_manager = cx.new(|cx| {
|
||||
ContextServerManager::new(context_server_factory_registry, project.clone(), cx)
|
||||
});
|
||||
let settings_subscription =
|
||||
|
||||
let mut subscriptions = vec![
|
||||
cx.observe_global::<SettingsStore>(move |this: &mut Self, cx| {
|
||||
this.load_default_profile(cx);
|
||||
});
|
||||
let project_subscription = cx.subscribe(&project, Self::handle_project_event);
|
||||
}),
|
||||
cx.subscribe(&project, Self::handle_project_event),
|
||||
];
|
||||
|
||||
if let Some(prompt_store) = prompt_store.as_ref() {
|
||||
subscriptions.push(cx.subscribe(
|
||||
prompt_store,
|
||||
|this, _prompt_store, PromptsUpdatedEvent, _cx| {
|
||||
this.enqueue_system_prompt_reload();
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
// This channel and task prevent concurrent and redundant loading of the system prompt.
|
||||
let (reload_system_prompt_tx, mut reload_system_prompt_rx) = mpsc::channel(1);
|
||||
let (ready_tx, ready_rx) = oneshot::channel();
|
||||
let mut ready_tx = Some(ready_tx);
|
||||
let reload_system_prompt_task = cx.spawn({
|
||||
async move |thread_store, cx| {
|
||||
loop {
|
||||
let Some(reload_task) = thread_store
|
||||
.update(cx, |thread_store, cx| {
|
||||
thread_store.reload_system_prompt(prompt_store.clone(), cx)
|
||||
})
|
||||
.ok()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
reload_task.await;
|
||||
if let Some(ready_tx) = ready_tx.take() {
|
||||
ready_tx.send(()).ok();
|
||||
}
|
||||
reload_system_prompt_rx.next().await;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let this = Self {
|
||||
project,
|
||||
@@ -110,23 +162,25 @@ impl ThreadStore {
|
||||
context_server_tool_ids: HashMap::default(),
|
||||
threads: Vec::new(),
|
||||
project_context: SharedProjectContext::default(),
|
||||
_subscriptions: vec![settings_subscription, project_subscription],
|
||||
reload_system_prompt_tx,
|
||||
_reload_system_prompt_task: reload_system_prompt_task,
|
||||
_subscriptions: subscriptions,
|
||||
};
|
||||
this.load_default_profile(cx);
|
||||
this.register_context_server_handlers(cx);
|
||||
this.reload(cx).detach_and_log_err(cx);
|
||||
this
|
||||
(this, ready_rx)
|
||||
}
|
||||
|
||||
fn handle_project_event(
|
||||
&mut self,
|
||||
_project: Entity<Project>,
|
||||
event: &project::Event,
|
||||
cx: &mut Context<Self>,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
project::Event::WorktreeAdded(_) | project::Event::WorktreeRemoved(_) => {
|
||||
self.reload_system_prompt(cx).detach();
|
||||
self.enqueue_system_prompt_reload();
|
||||
}
|
||||
project::Event::WorktreeUpdatedEntries(_, items) => {
|
||||
if items.iter().any(|(path, _, _)| {
|
||||
@@ -134,16 +188,25 @@ impl ThreadStore {
|
||||
.iter()
|
||||
.any(|name| path.as_ref() == Path::new(name))
|
||||
}) {
|
||||
self.reload_system_prompt(cx).detach();
|
||||
self.enqueue_system_prompt_reload();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reload_system_prompt(&self, cx: &mut Context<Self>) -> Task<()> {
|
||||
fn enqueue_system_prompt_reload(&mut self) {
|
||||
self.reload_system_prompt_tx.try_send(()).ok();
|
||||
}
|
||||
|
||||
// Note that this should only be called from `reload_system_prompt_task`.
|
||||
fn reload_system_prompt(
|
||||
&self,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<()> {
|
||||
let project = self.project.read(cx);
|
||||
let tasks = project
|
||||
let worktree_tasks = project
|
||||
.visible_worktrees(cx)
|
||||
.map(|worktree| {
|
||||
Self::load_worktree_info_for_system_prompt(
|
||||
@@ -153,10 +216,23 @@ impl ThreadStore {
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let default_user_rules_task = match prompt_store {
|
||||
None => Task::ready(vec![]),
|
||||
Some(prompt_store) => prompt_store.read_with(cx, |prompt_store, cx| {
|
||||
let prompts = prompt_store.default_prompt_metadata();
|
||||
let load_tasks = prompts.into_iter().map(|prompt_metadata| {
|
||||
let contents = prompt_store.load(prompt_metadata.id, cx);
|
||||
async move { (contents.await, prompt_metadata) }
|
||||
});
|
||||
cx.background_spawn(future::join_all(load_tasks))
|
||||
}),
|
||||
};
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let results = futures::future::join_all(tasks).await;
|
||||
let worktrees = results
|
||||
let (worktrees, default_user_rules) =
|
||||
future::join(future::join_all(worktree_tasks), default_user_rules_task).await;
|
||||
|
||||
let worktrees = worktrees
|
||||
.into_iter()
|
||||
.map(|(worktree, rules_error)| {
|
||||
if let Some(rules_error) = rules_error {
|
||||
@@ -165,8 +241,33 @@ impl ThreadStore {
|
||||
worktree
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let default_user_rules = default_user_rules
|
||||
.into_iter()
|
||||
.flat_map(|(contents, prompt_metadata)| match contents {
|
||||
Ok(contents) => Some(DefaultUserRulesContext {
|
||||
uuid: match prompt_metadata.id {
|
||||
PromptId::User { uuid } => uuid,
|
||||
PromptId::EditWorkflow => return None,
|
||||
},
|
||||
title: prompt_metadata.title.map(|title| title.to_string()),
|
||||
contents,
|
||||
}),
|
||||
Err(err) => {
|
||||
this.update(cx, |_, cx| {
|
||||
cx.emit(RulesLoadingError {
|
||||
message: format!("{err:?}").into(),
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
this.update(cx, |this, _cx| {
|
||||
*this.project_context.0.borrow_mut() = Some(ProjectContext::new(worktrees));
|
||||
*this.project_context.0.borrow_mut() =
|
||||
Some(ProjectContext::new(worktrees, default_user_rules));
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
@@ -178,14 +279,12 @@ impl ThreadStore {
|
||||
cx: &App,
|
||||
) -> Task<(WorktreeContext, Option<RulesLoadingError>)> {
|
||||
let root_name = worktree.root_name().into();
|
||||
let abs_path = worktree.abs_path();
|
||||
|
||||
let rules_task = Self::load_worktree_rules_file(fs, worktree, cx);
|
||||
let Some(rules_task) = rules_task else {
|
||||
return Task::ready((
|
||||
WorktreeContext {
|
||||
root_name,
|
||||
abs_path,
|
||||
rules_file: None,
|
||||
},
|
||||
None,
|
||||
@@ -204,7 +303,6 @@ impl ThreadStore {
|
||||
};
|
||||
let worktree_info = WorktreeContext {
|
||||
root_name,
|
||||
abs_path,
|
||||
rules_file,
|
||||
};
|
||||
(worktree_info, rules_file_error)
|
||||
@@ -562,9 +660,18 @@ pub struct SerializedMessage {
|
||||
#[serde(tag = "type")]
|
||||
pub enum SerializedMessageSegment {
|
||||
#[serde(rename = "text")]
|
||||
Text { text: String },
|
||||
Text {
|
||||
text: String,
|
||||
},
|
||||
#[serde(rename = "thinking")]
|
||||
Thinking { text: String },
|
||||
Thinking {
|
||||
text: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
signature: Option<String>,
|
||||
},
|
||||
RedactedThinking {
|
||||
data: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_tool::{Tool, ToolWorkingSet, ToolWorkingSetEvent};
|
||||
use assistant_tool::{Tool, ToolSource, ToolWorkingSet, ToolWorkingSetEvent};
|
||||
use collections::HashMap;
|
||||
use gpui::{App, Context, Entity, IntoElement, Render, Subscription, Window};
|
||||
use language_model::{LanguageModel, LanguageModelToolSchemaFormat};
|
||||
@@ -73,7 +73,12 @@ impl Render for IncompatibleToolsTooltip {
|
||||
.children(
|
||||
self.incompatible_tools
|
||||
.iter()
|
||||
.map(|tool| Label::new(tool.name()).size(LabelSize::Small).buffer_font(cx)),
|
||||
.map(|tool| h_flex().gap_4().child(Label::new(tool.name()).size(LabelSize::Small)).map(|parent|
|
||||
match tool.source() {
|
||||
ToolSource::Native => parent,
|
||||
ToolSource::ContextServer { id } => parent.child(Label::new(id).size(LabelSize::Small).color(Color::Muted)),
|
||||
}
|
||||
)),
|
||||
),
|
||||
)
|
||||
.child(Label::new("What To Do Instead").size(LabelSize::Small))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
mod agent_notification;
|
||||
mod context_pill;
|
||||
mod user_spending;
|
||||
mod usage_banner;
|
||||
|
||||
pub use agent_notification::*;
|
||||
pub use context_pill::*;
|
||||
// pub use user_spending::*;
|
||||
pub use usage_banner::*;
|
||||
|
||||
@@ -299,6 +299,39 @@ impl AddedContext {
|
||||
summarizing: false,
|
||||
},
|
||||
|
||||
AssistantContext::Excerpt(excerpt_context) => {
|
||||
let full_path = excerpt_context.context_buffer.file.full_path(cx);
|
||||
let mut full_path_string = full_path.to_string_lossy().into_owned();
|
||||
let mut name = full_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| full_path_string.clone());
|
||||
|
||||
let line_range_text = format!(
|
||||
" ({}-{})",
|
||||
excerpt_context.line_range.start.row + 1,
|
||||
excerpt_context.line_range.end.row + 1
|
||||
);
|
||||
|
||||
full_path_string.push_str(&line_range_text);
|
||||
name.push_str(&line_range_text);
|
||||
|
||||
let parent = full_path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|n| n.to_string_lossy().into_owned().into());
|
||||
|
||||
AddedContext {
|
||||
id: excerpt_context.id,
|
||||
kind: ContextKind::File, // Use File icon for excerpts
|
||||
name: name.into(),
|
||||
parent,
|
||||
tooltip: Some(full_path_string.into()),
|
||||
icon_path: FileIcons::get_icon(&full_path, cx),
|
||||
summarizing: false,
|
||||
}
|
||||
}
|
||||
|
||||
AssistantContext::FetchedUrl(fetched_url_context) => AddedContext {
|
||||
id: fetched_url_context.id,
|
||||
kind: ContextKind::FetchedUrl,
|
||||
|
||||
202
crates/agent/src/ui/usage_banner.rs
Normal file
202
crates/agent/src/ui/usage_banner.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
use client::zed_urls;
|
||||
use ui::{Banner, ProgressBar, Severity, prelude::*};
|
||||
use zed_llm_client::{Plan, UsageLimit};
|
||||
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct UsageBanner {
|
||||
plan: Plan,
|
||||
requests: i32,
|
||||
}
|
||||
|
||||
impl UsageBanner {
|
||||
pub fn new(plan: Plan, requests: i32) -> Self {
|
||||
Self { plan, requests }
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for UsageBanner {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let request_limit = self.plan.model_requests_limit();
|
||||
|
||||
let used_percentage = match request_limit {
|
||||
UsageLimit::Limited(limit) => Some((self.requests as f32 / limit as f32) * 100.),
|
||||
UsageLimit::Unlimited => None,
|
||||
};
|
||||
|
||||
let (severity, message) = match request_limit {
|
||||
UsageLimit::Limited(limit) => {
|
||||
if self.requests >= limit {
|
||||
let message = match self.plan {
|
||||
Plan::ZedPro => "Monthly request limit reached",
|
||||
Plan::ZedProTrial => "Trial request limit reached",
|
||||
Plan::Free => "Free tier request limit reached",
|
||||
};
|
||||
|
||||
(Severity::Error, message)
|
||||
} else if (self.requests as f32 / limit as f32) >= 0.9 {
|
||||
(Severity::Warning, "Approaching request limit")
|
||||
} else {
|
||||
let message = match self.plan {
|
||||
Plan::ZedPro => "Zed Pro",
|
||||
Plan::ZedProTrial => "Zed Pro (Trial)",
|
||||
Plan::Free => "Zed Free",
|
||||
};
|
||||
|
||||
(Severity::Info, message)
|
||||
}
|
||||
}
|
||||
UsageLimit::Unlimited => {
|
||||
let message = match self.plan {
|
||||
Plan::ZedPro => "Zed Pro",
|
||||
Plan::ZedProTrial => "Zed Pro (Trial)",
|
||||
Plan::Free => "Zed Free",
|
||||
};
|
||||
|
||||
(Severity::Info, message)
|
||||
}
|
||||
};
|
||||
|
||||
let action = match self.plan {
|
||||
Plan::ZedProTrial | Plan::Free => {
|
||||
Button::new("upgrade", "Upgrade").on_click(|_, _window, cx| {
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
})
|
||||
}
|
||||
Plan::ZedPro => Button::new("manage", "Manage").on_click(|_, _window, cx| {
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
}),
|
||||
};
|
||||
|
||||
Banner::new().severity(severity).children(
|
||||
h_flex().flex_1().gap_1().child(Label::new(message)).child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.justify_end()
|
||||
.gap_1p5()
|
||||
.children(used_percentage.map(|percent| {
|
||||
h_flex()
|
||||
.items_center()
|
||||
.w_full()
|
||||
.max_w(px(180.))
|
||||
.child(ProgressBar::new("usage", percent, 100., cx))
|
||||
}))
|
||||
.child(
|
||||
Label::new(match request_limit {
|
||||
UsageLimit::Limited(limit) => {
|
||||
format!("{} / {limit}", self.requests)
|
||||
}
|
||||
UsageLimit::Unlimited => format!("{} / ∞", self.requests),
|
||||
})
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
// Note: This should go in the banner's `action_slot`, but doing that messes with the size of the
|
||||
// progress bar.
|
||||
.child(action),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for UsageBanner {
|
||||
fn sort_name() -> &'static str {
|
||||
"AgentUsageBanner"
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||
let trial_examples = vec![
|
||||
single_example(
|
||||
"Zed Pro Trial - New User",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(Plan::ZedProTrial, 10))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Zed Pro Trial - Approaching Limit",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(Plan::ZedProTrial, 135))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Zed Pro Trial - Request Limit Reached",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(Plan::ZedProTrial, 150))
|
||||
.into_any_element(),
|
||||
),
|
||||
];
|
||||
|
||||
let free_examples = vec![
|
||||
single_example(
|
||||
"Free - Normal Usage",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(Plan::Free, 25))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Free - Approaching Limit",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(Plan::Free, 45))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Free - Request Limit Reached",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(Plan::Free, 50))
|
||||
.into_any_element(),
|
||||
),
|
||||
];
|
||||
|
||||
let zed_pro_examples = vec![
|
||||
single_example(
|
||||
"Zed Pro - Normal Usage",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(Plan::ZedPro, 250))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Zed Pro - Approaching Limit",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(Plan::ZedPro, 450))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Zed Pro - Request Limit Reached",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(Plan::ZedPro, 500))
|
||||
.into_any_element(),
|
||||
),
|
||||
];
|
||||
|
||||
Some(
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.p_4()
|
||||
.children(vec![
|
||||
Label::new("Trial Plan")
|
||||
.size(LabelSize::Large)
|
||||
.into_any_element(),
|
||||
example_group(trial_examples).vertical().into_any_element(),
|
||||
Label::new("Free Plan")
|
||||
.size(LabelSize::Large)
|
||||
.into_any_element(),
|
||||
example_group(free_examples).vertical().into_any_element(),
|
||||
Label::new("Pro Plan")
|
||||
.size(LabelSize::Large)
|
||||
.into_any_element(),
|
||||
example_group(zed_pro_examples)
|
||||
.vertical()
|
||||
.into_any_element(),
|
||||
])
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
use gpui::{Entity, Render};
|
||||
use ui::{ProgressBar, prelude::*};
|
||||
|
||||
#[derive(RegisterComponent)]
|
||||
pub struct UserSpending {
|
||||
free_tier_current: u32,
|
||||
free_tier_cap: u32,
|
||||
over_tier_current: u32,
|
||||
over_tier_cap: u32,
|
||||
free_tier_progress: Entity<ProgressBar>,
|
||||
over_tier_progress: Entity<ProgressBar>,
|
||||
}
|
||||
|
||||
impl UserSpending {
|
||||
pub fn new(
|
||||
free_tier_current: u32,
|
||||
free_tier_cap: u32,
|
||||
over_tier_current: u32,
|
||||
over_tier_cap: u32,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
let free_tier_capped = free_tier_current == free_tier_cap;
|
||||
let free_tier_near_capped =
|
||||
free_tier_current as f32 / 100.0 >= free_tier_cap as f32 / 100.0 * 0.9;
|
||||
let over_tier_capped = over_tier_current == over_tier_cap;
|
||||
let over_tier_near_capped =
|
||||
over_tier_current as f32 / 100.0 >= over_tier_cap as f32 / 100.0 * 0.9;
|
||||
|
||||
let free_tier_progress = cx.new(|cx| {
|
||||
ProgressBar::new(
|
||||
"free_tier",
|
||||
free_tier_current as f32,
|
||||
free_tier_cap as f32,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let over_tier_progress = cx.new(|cx| {
|
||||
ProgressBar::new(
|
||||
"over_tier",
|
||||
over_tier_current as f32,
|
||||
over_tier_cap as f32,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
if free_tier_capped {
|
||||
free_tier_progress.update(cx, |progress_bar, cx| {
|
||||
progress_bar.fg_color(cx.theme().status().error);
|
||||
});
|
||||
} else if free_tier_near_capped {
|
||||
free_tier_progress.update(cx, |progress_bar, cx| {
|
||||
progress_bar.fg_color(cx.theme().status().warning);
|
||||
});
|
||||
}
|
||||
|
||||
if over_tier_capped {
|
||||
over_tier_progress.update(cx, |progress_bar, cx| {
|
||||
progress_bar.fg_color(cx.theme().status().error);
|
||||
});
|
||||
} else if over_tier_near_capped {
|
||||
over_tier_progress.update(cx, |progress_bar, cx| {
|
||||
progress_bar.fg_color(cx.theme().status().warning);
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
free_tier_current,
|
||||
free_tier_cap,
|
||||
over_tier_current,
|
||||
over_tier_cap,
|
||||
free_tier_progress,
|
||||
over_tier_progress,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for UserSpending {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let formatted_free_tier = format!(
|
||||
"${} / ${}",
|
||||
self.free_tier_current as f32 / 100.0,
|
||||
self.free_tier_cap as f32 / 100.0
|
||||
);
|
||||
let formatted_over_tier = format!(
|
||||
"${} / ${}",
|
||||
self.over_tier_current as f32 / 100.0,
|
||||
self.over_tier_cap as f32 / 100.0
|
||||
);
|
||||
|
||||
v_group()
|
||||
.elevation_2(cx)
|
||||
.py_1p5()
|
||||
.px_2p5()
|
||||
.w(px(360.))
|
||||
.child(
|
||||
v_flex()
|
||||
.child(
|
||||
v_flex()
|
||||
.p_1p5()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(Label::new("Free Tier Usage").size(LabelSize::Small))
|
||||
.child(
|
||||
Label::new(formatted_free_tier)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(self.free_tier_progress.clone()),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.p_1p5()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(Label::new("Current Spending").size(LabelSize::Small))
|
||||
.child(
|
||||
Label::new(formatted_over_tier)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(self.over_tier_progress.clone()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for UserSpending {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::None
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
|
||||
let new_user = cx.new(|cx| UserSpending::new(0, 2000, 0, 2000, cx));
|
||||
let free_capped = cx.new(|cx| UserSpending::new(2000, 2000, 0, 2000, cx));
|
||||
let free_near_capped = cx.new(|cx| UserSpending::new(1800, 2000, 0, 2000, cx));
|
||||
let over_near_capped = cx.new(|cx| UserSpending::new(2000, 2000, 1800, 2000, cx));
|
||||
let over_capped = cx.new(|cx| UserSpending::new(1000, 2000, 2000, 2000, cx));
|
||||
|
||||
Some(
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.p_4()
|
||||
.children(vec![example_group(vec![
|
||||
single_example(
|
||||
"New User",
|
||||
div().size_full().child(new_user.clone()).into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Free Tier Capped",
|
||||
div()
|
||||
.size_full()
|
||||
.child(free_capped.clone())
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Free Tier Near Capped",
|
||||
div()
|
||||
.size_full()
|
||||
.child(free_near_capped.clone())
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Over Tier Near Capped",
|
||||
div()
|
||||
.size_full()
|
||||
.child(over_near_capped.clone())
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Over Tier Capped",
|
||||
div()
|
||||
.size_full()
|
||||
.child(over_capped.clone())
|
||||
.into_any_element(),
|
||||
),
|
||||
])])
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
45
crates/agent2/Cargo.toml
Normal file
45
crates/agent2/Cargo.toml
Normal file
@@ -0,0 +1,45 @@
|
||||
[package]
|
||||
name = "agent2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/agent2.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
assistant_tools.workspace = true
|
||||
chrono.workspace = true
|
||||
collections.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language_model.workspace = true
|
||||
language_models.workspace = true
|
||||
parking_lot.workspace = true
|
||||
project.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
thiserror.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
ctor.workspace = true
|
||||
client = { workspace = true, "features" = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
fs = { workspace = true, "features" = ["test-support"] }
|
||||
gpui = { workspace = true, "features" = ["test-support"] }
|
||||
gpui_tokio.workspace = true
|
||||
language_model = { workspace = true, "features" = ["test-support"] }
|
||||
project = { workspace = true, "features" = ["test-support"] }
|
||||
reqwest_client.workspace = true
|
||||
settings = { workspace = true, "features" = ["test-support"] }
|
||||
278
crates/agent2/src/agent2.rs
Normal file
278
crates/agent2/src/agent2.rs
Normal file
@@ -0,0 +1,278 @@
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use anyhow::Result;
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use futures::{channel::mpsc, future};
|
||||
use gpui::{Context, Entity, Task};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelCompletionEvent, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolSchemaFormat,
|
||||
LanguageModelToolUse, MessageContent, Role, StopReason,
|
||||
};
|
||||
use project::Project;
|
||||
use smol::stream::StreamExt;
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
use util::ResultExt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AgentMessage {
|
||||
pub role: Role,
|
||||
pub content: Vec<MessageContent>,
|
||||
}
|
||||
|
||||
impl AgentMessage {
|
||||
fn to_request_message(&self) -> LanguageModelRequestMessage {
|
||||
LanguageModelRequestMessage {
|
||||
role: self.role,
|
||||
content: self.content.clone(),
|
||||
cache: false, // TODO: Figure out caching
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type AgentResponseEvent = LanguageModelCompletionEvent;
|
||||
|
||||
pub struct Agent {
|
||||
messages: Vec<AgentMessage>,
|
||||
/// Holds the task that handles agent interaction until the end of the turn.
|
||||
/// Survives across multiple requests as the model performs tool calls and
|
||||
/// we run tools, report their results.
|
||||
running_turn: Option<Task<()>>,
|
||||
tools: BTreeMap<Arc<str>, Arc<dyn Tool>>,
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
}
|
||||
|
||||
impl Agent {
|
||||
pub fn new(project: Entity<Project>, action_log: Entity<ActionLog>) -> Self {
|
||||
Self {
|
||||
messages: Vec::new(),
|
||||
running_turn: None,
|
||||
tools: BTreeMap::default(),
|
||||
project,
|
||||
action_log,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_tool(&mut self, tool: Arc<dyn Tool>) {
|
||||
let name = Arc::from(tool.name());
|
||||
self.tools.insert(name, tool);
|
||||
}
|
||||
|
||||
pub fn remove_tool(&mut self, name: &str) -> bool {
|
||||
self.tools.remove(name).is_some()
|
||||
}
|
||||
|
||||
/// Sending a message results in the model streaming a response, which could include tool calls.
|
||||
/// After calling tools, the model will stops and waits for any outstanding tool calls to be completed and their results sent.
|
||||
/// The returned channel will report all the occurrences in which the model stops before erroring or ending its turn.
|
||||
pub fn send(
|
||||
&mut self,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
content: impl Into<MessageContent>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> mpsc::UnboundedReceiver<Result<AgentResponseEvent>> {
|
||||
cx.notify();
|
||||
let (events_tx, events_rx) = mpsc::unbounded();
|
||||
self.messages.push(AgentMessage {
|
||||
role: Role::User,
|
||||
content: vec![content.into()],
|
||||
});
|
||||
self.running_turn = Some(cx.spawn(async move |thread, cx| {
|
||||
let turn_result = async {
|
||||
// Perform one request, then keep looping if the model makes tool calls.
|
||||
loop {
|
||||
let request =
|
||||
thread.update(cx, |thread, _cx| thread.build_completion_request())?;
|
||||
|
||||
println!(
|
||||
"request: {}",
|
||||
serde_json::to_string_pretty(&request).unwrap()
|
||||
);
|
||||
|
||||
// Stream events, appending to messages and collecting up tool uses.
|
||||
let mut events = model.stream_completion(request, cx).await?;
|
||||
let mut tool_uses = Vec::new();
|
||||
while let Some(event) = events.next().await {
|
||||
match event {
|
||||
Ok(event) => {
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
tool_uses.extend(thread.handle_response_event(
|
||||
event,
|
||||
events_tx.clone(),
|
||||
cx,
|
||||
));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(error) => {
|
||||
events_tx.unbounded_send(Err(error)).ok();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no tool uses, the turn is done.
|
||||
if tool_uses.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
// If there are tool uses, wait for their results to be
|
||||
// computed, then send them together in a single message on
|
||||
// the next loop iteration.
|
||||
let tool_results = future::join_all(tool_uses).await;
|
||||
thread
|
||||
.update(cx, |thread, _cx| {
|
||||
thread.messages.push(AgentMessage {
|
||||
role: Role::User,
|
||||
content: tool_results.into_iter().map(Into::into).collect(),
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.await;
|
||||
|
||||
if let Err(error) = turn_result {
|
||||
events_tx.unbounded_send(Err(error)).ok();
|
||||
}
|
||||
}));
|
||||
events_rx
|
||||
}
|
||||
|
||||
fn handle_response_event(
|
||||
&mut self,
|
||||
event: LanguageModelCompletionEvent,
|
||||
events_tx: mpsc::UnboundedSender<Result<AgentResponseEvent>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Task<LanguageModelToolResult>> {
|
||||
use LanguageModelCompletionEvent::*;
|
||||
events_tx.unbounded_send(Ok(event.clone())).ok();
|
||||
|
||||
match event {
|
||||
Text(new_text) => self.handle_text_event(new_text, cx),
|
||||
Thinking { text, signature } => {}
|
||||
ToolUse(tool_use) => {
|
||||
return Some(self.handle_tool_use_event(tool_use, cx));
|
||||
}
|
||||
StartMessage { message_id, role } => {
|
||||
self.messages.push(AgentMessage {
|
||||
role,
|
||||
content: Vec::new(),
|
||||
});
|
||||
}
|
||||
UsageUpdate(token_usage) => {}
|
||||
Stop(stop_reason) => self.handle_stop_event(stop_reason),
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn handle_stop_event(&mut self, stop_reason: StopReason) {
|
||||
match stop_reason {
|
||||
StopReason::EndTurn | StopReason::ToolUse => {}
|
||||
StopReason::MaxTokens => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_text_event(&mut self, new_text: String, cx: &mut Context<Self>) {
|
||||
if let Some(last_message) = self.messages.last_mut() {
|
||||
debug_assert!(last_message.role == Role::Assistant);
|
||||
if let Some(MessageContent::Text(text)) = last_message.content.last_mut() {
|
||||
text.push_str(&new_text);
|
||||
} else {
|
||||
last_message.content.push(MessageContent::Text(new_text));
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
} else {
|
||||
todo!("does this happen in practice?");
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_tool_use_event(
|
||||
&mut self,
|
||||
tool_use: LanguageModelToolUse,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<LanguageModelToolResult> {
|
||||
if let Some(last_message) = self.messages.last_mut() {
|
||||
debug_assert!(last_message.role == Role::Assistant);
|
||||
last_message.content.push(tool_use.clone().into());
|
||||
cx.notify();
|
||||
} else {
|
||||
todo!("does this happen in practice?");
|
||||
}
|
||||
|
||||
if let Some(tool) = self.tools.get(&tool_use.name) {
|
||||
let pending_tool_result = tool.clone().run(
|
||||
tool_use.input,
|
||||
&self.build_request_messages(),
|
||||
self.project.clone(),
|
||||
self.action_log.clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.foreground_executor().spawn(async move {
|
||||
match pending_tool_result.output.await {
|
||||
Ok(tool_output) => LanguageModelToolResult {
|
||||
tool_use_id: tool_use.id,
|
||||
tool_name: tool_use.name,
|
||||
is_error: false,
|
||||
content: Arc::from(tool_output),
|
||||
},
|
||||
Err(error) => LanguageModelToolResult {
|
||||
tool_use_id: tool_use.id,
|
||||
tool_name: tool_use.name,
|
||||
is_error: true,
|
||||
content: Arc::from(error.to_string()),
|
||||
},
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Task::ready(LanguageModelToolResult {
|
||||
content: Arc::from(format!("No tool named {} exists", tool_use.name)),
|
||||
tool_use_id: tool_use.id,
|
||||
tool_name: tool_use.name,
|
||||
is_error: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn build_completion_request(&self) -> LanguageModelRequest {
|
||||
LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
messages: self.build_request_messages(),
|
||||
tools: self
|
||||
.tools
|
||||
.values()
|
||||
.filter_map(|tool| {
|
||||
Some(LanguageModelRequestTool {
|
||||
name: tool.name(),
|
||||
description: tool.description(),
|
||||
input_schema: tool
|
||||
.input_schema(LanguageModelToolSchemaFormat::JsonSchema)
|
||||
.log_err()?,
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
stop: Vec::new(),
|
||||
temperature: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_request_messages(&self) -> Vec<LanguageModelRequestMessage> {
|
||||
self.messages
|
||||
.iter()
|
||||
.map(|message| LanguageModelRequestMessage {
|
||||
role: message.role,
|
||||
content: message.content.clone(),
|
||||
cache: false,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
191
crates/agent2/src/tests.rs
Normal file
191
crates/agent2/src/tests.rs
Normal file
@@ -0,0 +1,191 @@
|
||||
use super::*;
|
||||
use assistant_tool::{IconName, Project, ToolResult};
|
||||
use client::{Client, UserStore};
|
||||
use fs::FakeFs;
|
||||
use gpui::{AppContext, TestAppContext};
|
||||
use language_model::LanguageModelRegistry;
|
||||
use reqwest_client::ReqwestClient;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
|
||||
mod tools;
|
||||
use tools::*;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_echo(cx: &mut TestAppContext) {
|
||||
let AgentTest { model, agent, .. } = agent_test(cx).await;
|
||||
|
||||
let events = agent
|
||||
.update(cx, |agent, cx| {
|
||||
agent.send(model.clone(), "Testing: Reply with 'Hello'", cx)
|
||||
})
|
||||
.collect()
|
||||
.await;
|
||||
agent.update(cx, |agent, _cx| {
|
||||
assert_eq!(
|
||||
agent.messages.last().unwrap().content,
|
||||
vec![MessageContent::Text("Hello".to_string())]
|
||||
);
|
||||
});
|
||||
assert_eq!(stop_events(events), vec![StopReason::EndTurn]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_tool_calls(cx: &mut TestAppContext) {
|
||||
let AgentTest { model, agent, .. } = agent_test(cx).await;
|
||||
|
||||
// Test a tool calls that's likely to complete before streaming stops.
|
||||
let events = agent
|
||||
.update(cx, |agent, cx| {
|
||||
agent.add_tool(Arc::new(EchoTool));
|
||||
agent.send(
|
||||
model.clone(),
|
||||
"Now test the echo tool with 'Hello'. Does it work? Say 'Yes' or 'No'.",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
.await;
|
||||
assert_eq!(
|
||||
stop_events(events),
|
||||
vec![StopReason::ToolUse, StopReason::EndTurn]
|
||||
);
|
||||
|
||||
// Test a tool calls that's likely to complete after streaming stops.
|
||||
let events = agent
|
||||
.update(cx, |agent, cx| {
|
||||
agent.remove_tool(&EchoTool.name());
|
||||
agent.add_tool(Arc::new(DelayTool));
|
||||
agent.send(
|
||||
model.clone(),
|
||||
"Now call the delay tool with 200ms. When the timer goes off, then you echo the output of the tool.",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
.await;
|
||||
assert_eq!(
|
||||
stop_events(events),
|
||||
vec![StopReason::ToolUse, StopReason::EndTurn]
|
||||
);
|
||||
agent.update(cx, |agent, _cx| {
|
||||
assert!(agent
|
||||
.messages
|
||||
.last()
|
||||
.unwrap()
|
||||
.content
|
||||
.iter()
|
||||
.any(|content| {
|
||||
if let MessageContent::Text(text) = content {
|
||||
text.contains("Ding")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_concurrent_tool_calls(cx: &mut TestAppContext) {
|
||||
let AgentTest { model, agent, .. } = agent_test(cx).await;
|
||||
|
||||
// Test concurrent tool calls with different delay times
|
||||
let events = agent
|
||||
.update(cx, |agent, cx| {
|
||||
agent.add_tool(Arc::new(DelayTool));
|
||||
agent.send(
|
||||
model.clone(),
|
||||
"Call the delay tool twice in the same message. Once with 100ms. Once with 300ms. When both timers are complete, describe the outputs.",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.map(|event| dbg!(event))
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
let stop_reasons = stop_events(events);
|
||||
assert_eq!(stop_reasons, vec![StopReason::ToolUse, StopReason::EndTurn]);
|
||||
|
||||
agent.update(cx, |agent, _cx| {
|
||||
let last_message = agent.messages.last().unwrap();
|
||||
let text = last_message
|
||||
.content
|
||||
.iter()
|
||||
.filter_map(|content| {
|
||||
if let MessageContent::Text(text) = content {
|
||||
Some(text.as_str())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<String>();
|
||||
|
||||
assert!(text.contains("Ding"));
|
||||
});
|
||||
}
|
||||
|
||||
fn stop_events(result_events: Vec<Result<AgentResponseEvent>>) -> Vec<StopReason> {
|
||||
result_events
|
||||
.into_iter()
|
||||
.filter_map(|event| match event.unwrap() {
|
||||
LanguageModelCompletionEvent::Stop(stop_reason) => Some(stop_reason),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
struct AgentTest {
|
||||
model: Arc<dyn LanguageModel>,
|
||||
agent: Entity<Agent>,
|
||||
}
|
||||
|
||||
async fn agent_test(cx: &mut TestAppContext) -> AgentTest {
|
||||
cx.executor().allow_parking();
|
||||
cx.update(settings::init);
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let project = Project::test(fs.clone(), [], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let agent = cx.new(|_| Agent::new(project.clone(), action_log.clone()));
|
||||
|
||||
let model = cx
|
||||
.update(|cx| {
|
||||
gpui_tokio::init(cx);
|
||||
let http_client = ReqwestClient::user_agent("agent tests").unwrap();
|
||||
cx.set_http_client(Arc::new(http_client));
|
||||
|
||||
client::init_settings(cx);
|
||||
let client = Client::production(cx);
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
language_model::init(client.clone(), cx);
|
||||
language_models::init(user_store.clone(), client.clone(), fs.clone(), cx);
|
||||
|
||||
let models = LanguageModelRegistry::read_global(cx);
|
||||
let model = models
|
||||
.available_models(cx)
|
||||
.find(|model| model.id().0 == "claude-3-7-sonnet-latest")
|
||||
.unwrap();
|
||||
|
||||
let provider = models.provider(&model.provider_id()).unwrap();
|
||||
let authenticated = provider.authenticate(cx);
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
authenticated.await.unwrap();
|
||||
model
|
||||
})
|
||||
})
|
||||
.await;
|
||||
|
||||
AgentTest {
|
||||
model,
|
||||
agent: agent,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
if std::env::var("RUST_LOG").is_ok() {
|
||||
env_logger::init();
|
||||
}
|
||||
}
|
||||
102
crates/agent2/src/tests/tools.rs
Normal file
102
crates/agent2/src/tests/tools.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use super::*;
|
||||
|
||||
#[derive(JsonSchema, Serialize, Deserialize)]
|
||||
pub struct EchoToolInput {
|
||||
text: String,
|
||||
}
|
||||
|
||||
pub struct EchoTool;
|
||||
|
||||
impl Tool for EchoTool {
|
||||
fn name(&self) -> String {
|
||||
"echo".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"A tool that echoes its input".to_string()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Ai
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &gpui::App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn ui_text(&self, _input: &serde_json::Value) -> String {
|
||||
"Echo".to_string()
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
_project: gpui::Entity<Project>,
|
||||
_action_log: gpui::Entity<assistant_tool::ActionLog>,
|
||||
cx: &mut gpui::App,
|
||||
) -> ToolResult {
|
||||
ToolResult {
|
||||
output: cx.foreground_executor().spawn(async move {
|
||||
let input: EchoToolInput = serde_json::from_value(input)?;
|
||||
Ok(input.text)
|
||||
}),
|
||||
card: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
assistant_tools::json_schema_for::<EchoToolInput>(format)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(JsonSchema, Serialize, Deserialize)]
|
||||
pub struct DelayToolInput {
|
||||
ms: u64,
|
||||
}
|
||||
|
||||
pub struct DelayTool;
|
||||
|
||||
impl Tool for DelayTool {
|
||||
fn name(&self) -> String {
|
||||
"delay".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"A tool that waits for a specified delay".to_string()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Cog
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &gpui::App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn ui_text(&self, _input: &serde_json::Value) -> String {
|
||||
"Delay".to_string()
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
_project: gpui::Entity<Project>,
|
||||
_action_log: gpui::Entity<assistant_tool::ActionLog>,
|
||||
cx: &mut gpui::App,
|
||||
) -> ToolResult {
|
||||
ToolResult {
|
||||
output: cx.foreground_executor().spawn(async move {
|
||||
let input: DelayToolInput = serde_json::from_value(input)?;
|
||||
smol::Timer::after(Duration::from_millis(input.ms)).await;
|
||||
Ok("Ding".to_string())
|
||||
}),
|
||||
card: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
assistant_tools::json_schema_for::<DelayToolInput>(format)
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,10 @@ pub enum Model {
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn default_fast() -> Self {
|
||||
Self::Claude3_5Haiku
|
||||
}
|
||||
|
||||
pub fn from_id(id: &str) -> Result<Self> {
|
||||
if id.starts_with("claude-3-5-sonnet") {
|
||||
Ok(Self::Claude3_5Sonnet)
|
||||
@@ -412,6 +416,7 @@ pub async fn stream_completion_with_rate_limit_info(
|
||||
let beta_headers = Model::from_id(&request.base.model)
|
||||
.map(|model| model.beta_headers())
|
||||
.unwrap_or_else(|_err| Model::DEFAULT_BETA_HEADERS.join(","));
|
||||
|
||||
let request_builder = HttpRequest::builder()
|
||||
.method(Method::POST)
|
||||
.uri(uri)
|
||||
@@ -419,6 +424,7 @@ pub async fn stream_completion_with_rate_limit_info(
|
||||
.header("Anthropic-Beta", beta_headers)
|
||||
.header("X-Api-Key", api_key)
|
||||
.header("Content-Type", "application/json");
|
||||
|
||||
let serialized_request =
|
||||
serde_json::to_string(&request).context("failed to serialize request")?;
|
||||
let request = request_builder
|
||||
@@ -507,6 +513,15 @@ pub enum RequestContent {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
cache_control: Option<CacheControl>,
|
||||
},
|
||||
#[serde(rename = "thinking")]
|
||||
Thinking {
|
||||
thinking: String,
|
||||
signature: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
cache_control: Option<CacheControl>,
|
||||
},
|
||||
#[serde(rename = "redacted_thinking")]
|
||||
RedactedThinking { data: String },
|
||||
#[serde(rename = "image")]
|
||||
Image {
|
||||
source: ImageSource,
|
||||
|
||||
@@ -8,7 +8,7 @@ mod terminal_inline_assistant;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_settings::AssistantSettings;
|
||||
use assistant_settings::{AssistantSettings, LanguageModelSelection};
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::Client;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
@@ -161,71 +161,38 @@ fn init_language_model_settings(cx: &mut App) {
|
||||
|
||||
fn update_active_language_model_from_settings(cx: &mut App) {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
// Default model - used as fallback
|
||||
let active_model_provider_name =
|
||||
LanguageModelProviderId::from(settings.default_model.provider.clone());
|
||||
let active_model_id = LanguageModelId::from(settings.default_model.model.clone());
|
||||
|
||||
// Inline assistant model
|
||||
let inline_assistant_model = settings
|
||||
fn to_selected_model(selection: &LanguageModelSelection) -> language_model::SelectedModel {
|
||||
language_model::SelectedModel {
|
||||
provider: LanguageModelProviderId::from(selection.provider.clone()),
|
||||
model: LanguageModelId::from(selection.model.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
let default = to_selected_model(&settings.default_model);
|
||||
let inline_assistant = settings
|
||||
.inline_assistant_model
|
||||
.as_ref()
|
||||
.unwrap_or(&settings.default_model);
|
||||
let inline_assistant_provider_name =
|
||||
LanguageModelProviderId::from(inline_assistant_model.provider.clone());
|
||||
let inline_assistant_model_id = LanguageModelId::from(inline_assistant_model.model.clone());
|
||||
|
||||
// Commit message model
|
||||
let commit_message_model = settings
|
||||
.map(to_selected_model);
|
||||
let commit_message = settings
|
||||
.commit_message_model
|
||||
.as_ref()
|
||||
.unwrap_or(&settings.default_model);
|
||||
let commit_message_provider_name =
|
||||
LanguageModelProviderId::from(commit_message_model.provider.clone());
|
||||
let commit_message_model_id = LanguageModelId::from(commit_message_model.model.clone());
|
||||
|
||||
// Thread summary model
|
||||
let thread_summary_model = settings
|
||||
.map(to_selected_model);
|
||||
let thread_summary = settings
|
||||
.thread_summary_model
|
||||
.as_ref()
|
||||
.unwrap_or(&settings.default_model);
|
||||
let thread_summary_provider_name =
|
||||
LanguageModelProviderId::from(thread_summary_model.provider.clone());
|
||||
let thread_summary_model_id = LanguageModelId::from(thread_summary_model.model.clone());
|
||||
|
||||
.map(to_selected_model);
|
||||
let inline_alternatives = settings
|
||||
.inline_alternatives
|
||||
.iter()
|
||||
.map(|alternative| {
|
||||
(
|
||||
LanguageModelProviderId::from(alternative.provider.clone()),
|
||||
LanguageModelId::from(alternative.model.clone()),
|
||||
)
|
||||
})
|
||||
.map(to_selected_model)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
// Set the default model
|
||||
registry.select_default_model(&active_model_provider_name, &active_model_id, cx);
|
||||
|
||||
// Set the specific models
|
||||
registry.select_inline_assistant_model(
|
||||
&inline_assistant_provider_name,
|
||||
&inline_assistant_model_id,
|
||||
cx,
|
||||
);
|
||||
registry.select_commit_message_model(
|
||||
&commit_message_provider_name,
|
||||
&commit_message_model_id,
|
||||
cx,
|
||||
);
|
||||
registry.select_thread_summary_model(
|
||||
&thread_summary_provider_name,
|
||||
&thread_summary_model_id,
|
||||
cx,
|
||||
);
|
||||
|
||||
// Set the alternatives
|
||||
registry.select_default_model(Some(&default), cx);
|
||||
registry.select_inline_assistant_model(inline_assistant.as_ref(), cx);
|
||||
registry.select_commit_message_model(commit_message.as_ref(), cx);
|
||||
registry.select_thread_summary_model(thread_summary.as_ref(), cx);
|
||||
registry.select_inline_alternative_models(inline_alternatives, cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use assistant_context_editor::{
|
||||
use assistant_settings::{AssistantDockPosition, AssistantSettings};
|
||||
use assistant_slash_command::SlashCommandWorkingSet;
|
||||
use client::{Client, Status, proto};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, App, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable,
|
||||
@@ -27,10 +27,13 @@ use language_model::{
|
||||
};
|
||||
use project::Project;
|
||||
use prompt_library::{PromptLibrary, open_prompt_library};
|
||||
use prompt_store::PromptBuilder;
|
||||
use prompt_store::{PromptBuilder, PromptId};
|
||||
|
||||
use search::{BufferSearchBar, buffer_search::DivRegistrar};
|
||||
use settings::{Settings, update_settings_file};
|
||||
use smol::stream::StreamExt;
|
||||
|
||||
use std::ops::Range;
|
||||
use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
|
||||
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
|
||||
use ui::{ContextMenu, PopoverMenu, Tooltip, prelude::*};
|
||||
@@ -59,7 +62,7 @@ pub fn init(cx: &mut App) {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.deploy_prompt_library(&OpenPromptLibrary, window, cx)
|
||||
panel.deploy_prompt_library(&OpenPromptLibrary::default(), window, cx)
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -269,7 +272,10 @@ impl AssistantPanel {
|
||||
menu.context(focus_handle.clone())
|
||||
.action("New Chat", Box::new(NewChat))
|
||||
.action("History", Box::new(DeployHistory))
|
||||
.action("Prompt Library", Box::new(OpenPromptLibrary))
|
||||
.action(
|
||||
"Prompt Library",
|
||||
Box::new(OpenPromptLibrary::default()),
|
||||
)
|
||||
.action("Configure", Box::new(ShowConfiguration))
|
||||
.action(zoom_label, Box::new(ToggleZoom))
|
||||
}))
|
||||
@@ -1040,7 +1046,7 @@ impl AssistantPanel {
|
||||
|
||||
fn deploy_prompt_library(
|
||||
&mut self,
|
||||
_: &OpenPromptLibrary,
|
||||
action: &OpenPromptLibrary,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -1054,6 +1060,7 @@ impl AssistantPanel {
|
||||
None,
|
||||
))
|
||||
}),
|
||||
action.prompt_to_focus.map(|uuid| PromptId::User { uuid }),
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
@@ -1413,7 +1420,8 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
fn quote_selection(
|
||||
&self,
|
||||
workspace: &mut Workspace,
|
||||
creases: Vec<(String, String)>,
|
||||
selection_ranges: Vec<Range<Anchor>>,
|
||||
buffer: Entity<MultiBuffer>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
@@ -1425,6 +1433,12 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
workspace.toggle_panel_focus::<AssistantPanel>(window, cx);
|
||||
}
|
||||
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
let selection_ranges = selection_ranges
|
||||
.into_iter()
|
||||
.map(|range| range.to_point(&snapshot))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
panel.update(cx, |_, cx| {
|
||||
// Wait to create a new context until the workspace is no longer
|
||||
// being updated.
|
||||
@@ -1433,7 +1447,9 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
.active_context_editor(cx)
|
||||
.or_else(|| panel.new_context(window, cx))
|
||||
{
|
||||
context.update(cx, |context, cx| context.quote_creases(creases, window, cx));
|
||||
context.update(cx, |context, cx| {
|
||||
context.quote_ranges(selection_ranges, snapshot, window, cx)
|
||||
});
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2978,6 +2978,8 @@ impl CodegenAlternative {
|
||||
});
|
||||
|
||||
Ok(LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
messages,
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
|
||||
@@ -292,6 +292,8 @@ impl TerminalInlineAssistant {
|
||||
});
|
||||
|
||||
Ok(LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
messages,
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
|
||||
@@ -2373,7 +2373,7 @@ impl AssistantContext {
|
||||
LanguageModelCompletionEvent::Stop(reason) => {
|
||||
stop_reason = reason;
|
||||
}
|
||||
LanguageModelCompletionEvent::Thinking(chunk) => {
|
||||
LanguageModelCompletionEvent::Thinking { text: chunk, .. } => {
|
||||
if thought_process_stack.is_empty() {
|
||||
let start =
|
||||
buffer.anchor_before(message_old_end_offset);
|
||||
@@ -2555,6 +2555,8 @@ impl AssistantContext {
|
||||
}
|
||||
|
||||
let mut completion_request = LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
messages: Vec::new(),
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
|
||||
@@ -8,8 +8,8 @@ use assistant_slash_commands::{
|
||||
use client::{proto, zed_urls};
|
||||
use collections::{BTreeSet, HashMap, HashSet, hash_map};
|
||||
use editor::{
|
||||
Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, ProposedChangeLocation,
|
||||
ProposedChangesEditor, RowExt, ToOffset as _, ToPoint,
|
||||
Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, MultiBuffer, MultiBufferSnapshot,
|
||||
ProposedChangeLocation, ProposedChangesEditor, RowExt, ToOffset as _, ToPoint,
|
||||
actions::{MoveToEndOfLine, Newline, ShowCompletions},
|
||||
display_map::{
|
||||
BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata,
|
||||
@@ -155,7 +155,8 @@ pub trait AssistantPanelDelegate {
|
||||
fn quote_selection(
|
||||
&self,
|
||||
workspace: &mut Workspace,
|
||||
creases: Vec<(String, String)>,
|
||||
selection_ranges: Vec<Range<Anchor>>,
|
||||
buffer: Entity<MultiBuffer>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
);
|
||||
@@ -1800,23 +1801,45 @@ impl ContextEditor {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(creases) = selections_creases(workspace, cx) else {
|
||||
let Some((selections, buffer)) = maybe!({
|
||||
let editor = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.act_as::<Editor>(cx))?;
|
||||
|
||||
let buffer = editor.read(cx).buffer().clone();
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
let selections = editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.selections
|
||||
.all_adjusted(cx)
|
||||
.into_iter()
|
||||
.filter_map(|s| {
|
||||
(!s.is_empty())
|
||||
.then(|| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
Some((selections, buffer))
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if creases.is_empty() {
|
||||
if selections.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
assistant_panel_delegate.quote_selection(workspace, creases, window, cx);
|
||||
assistant_panel_delegate.quote_selection(workspace, selections, buffer, window, cx);
|
||||
}
|
||||
|
||||
pub fn quote_creases(
|
||||
pub fn quote_ranges(
|
||||
&mut self,
|
||||
creases: Vec<(String, String)>,
|
||||
ranges: Vec<Range<Point>>,
|
||||
snapshot: MultiBufferSnapshot,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let creases = selections_creases(ranges, snapshot, cx);
|
||||
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.insert("\n", window, cx);
|
||||
for (text, crease_title) in creases {
|
||||
|
||||
@@ -54,9 +54,9 @@ impl SlashCommand for DefaultSlashCommand {
|
||||
cx: &mut App,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let store = PromptStore::global(cx);
|
||||
cx.background_spawn(async move {
|
||||
cx.spawn(async move |cx| {
|
||||
let store = store.await?;
|
||||
let prompts = store.default_prompt_metadata();
|
||||
let prompts = store.read_with(cx, |store, _cx| store.default_prompt_metadata())?;
|
||||
|
||||
let mut text = String::new();
|
||||
text.push('\n');
|
||||
|
||||
@@ -5,7 +5,7 @@ use assistant_slash_command::{
|
||||
};
|
||||
use gpui::{Task, WeakEntity};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use prompt_store::PromptStore;
|
||||
use prompt_store::{PromptMetadata, PromptStore};
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
@@ -43,8 +43,11 @@ impl SlashCommand for PromptSlashCommand {
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let store = PromptStore::global(cx);
|
||||
let query = arguments.to_owned().join(" ");
|
||||
cx.background_spawn(async move {
|
||||
let prompts = store.await?.search(query).await;
|
||||
cx.spawn(async move |cx| {
|
||||
let prompts: Vec<PromptMetadata> = store
|
||||
.await?
|
||||
.read_with(cx, |store, cx| store.search(query, cx))?
|
||||
.await;
|
||||
Ok(prompts
|
||||
.into_iter()
|
||||
.filter_map(|prompt| {
|
||||
@@ -77,14 +80,18 @@ impl SlashCommand for PromptSlashCommand {
|
||||
|
||||
let store = PromptStore::global(cx);
|
||||
let title = SharedString::from(title.clone());
|
||||
let prompt = cx.background_spawn({
|
||||
let prompt = cx.spawn({
|
||||
let title = title.clone();
|
||||
async move {
|
||||
async move |cx| {
|
||||
let store = store.await?;
|
||||
let prompt_id = store
|
||||
.id_for_title(&title)
|
||||
.with_context(|| format!("no prompt found with title {:?}", title))?;
|
||||
let body = store.load(prompt_id).await?;
|
||||
let body = store
|
||||
.read_with(cx, |store, cx| {
|
||||
let prompt_id = store
|
||||
.id_for_title(&title)
|
||||
.with_context(|| format!("no prompt found with title {:?}", title))?;
|
||||
anyhow::Ok(store.load(prompt_id, cx))
|
||||
})??
|
||||
.await?;
|
||||
anyhow::Ok(body)
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,10 +3,12 @@ use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent,
|
||||
SlashCommandOutputSection, SlashCommandResult,
|
||||
};
|
||||
use editor::Editor;
|
||||
use editor::{Editor, MultiBufferSnapshot};
|
||||
use futures::StreamExt;
|
||||
use gpui::{App, Context, SharedString, Task, WeakEntity, Window};
|
||||
use gpui::{App, SharedString, Task, WeakEntity, Window};
|
||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
|
||||
use rope::Point;
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use ui::IconName;
|
||||
@@ -69,7 +71,22 @@ impl SlashCommand for SelectionCommand {
|
||||
let mut events = vec![];
|
||||
|
||||
let Some(creases) = workspace
|
||||
.update(cx, selections_creases)
|
||||
.update(cx, |workspace, cx| {
|
||||
let editor = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.act_as::<Editor>(cx))?;
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let selection_ranges = editor
|
||||
.selections
|
||||
.all_adjusted(cx)
|
||||
.iter()
|
||||
.map(|selection| selection.range())
|
||||
.collect::<Vec<_>>();
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
Some(selections_creases(selection_ranges, snapshot, cx))
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|e| {
|
||||
events.push(Err(e));
|
||||
None
|
||||
@@ -102,94 +119,82 @@ impl SlashCommand for SelectionCommand {
|
||||
}
|
||||
|
||||
pub fn selections_creases(
|
||||
workspace: &mut workspace::Workspace,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Option<Vec<(String, String)>> {
|
||||
let editor = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.act_as::<Editor>(cx))?;
|
||||
|
||||
let mut creases = vec![];
|
||||
editor.update(cx, |editor, cx| {
|
||||
let selections = editor.selections.all_adjusted(cx);
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
for selection in selections {
|
||||
let range = editor::ToOffset::to_offset(&selection.start, &buffer)
|
||||
..editor::ToOffset::to_offset(&selection.end, &buffer);
|
||||
let selected_text = buffer.text_for_range(range.clone()).collect::<String>();
|
||||
if selected_text.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let start_language = buffer.language_at(range.start);
|
||||
let end_language = buffer.language_at(range.end);
|
||||
let language_name = if start_language == end_language {
|
||||
start_language.map(|language| language.code_fence_block_name())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let language_name = language_name.as_deref().unwrap_or("");
|
||||
let filename = buffer
|
||||
.file_at(selection.start)
|
||||
.map(|file| file.full_path(cx));
|
||||
let text = if language_name == "markdown" {
|
||||
selected_text
|
||||
.lines()
|
||||
.map(|line| format!("> {}", line))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
} else {
|
||||
let start_symbols = buffer
|
||||
.symbols_containing(selection.start, None)
|
||||
.map(|(_, symbols)| symbols);
|
||||
let end_symbols = buffer
|
||||
.symbols_containing(selection.end, None)
|
||||
.map(|(_, symbols)| symbols);
|
||||
|
||||
let outline_text =
|
||||
if let Some((start_symbols, end_symbols)) = start_symbols.zip(end_symbols) {
|
||||
Some(
|
||||
start_symbols
|
||||
.into_iter()
|
||||
.zip(end_symbols)
|
||||
.take_while(|(a, b)| a == b)
|
||||
.map(|(a, _)| a.text)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" > "),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let line_comment_prefix = start_language
|
||||
.and_then(|l| l.default_scope().line_comment_prefixes().first().cloned());
|
||||
|
||||
let fence = codeblock_fence_for_path(
|
||||
filename.as_deref(),
|
||||
Some(selection.start.row..=selection.end.row),
|
||||
);
|
||||
|
||||
if let Some((line_comment_prefix, outline_text)) =
|
||||
line_comment_prefix.zip(outline_text)
|
||||
{
|
||||
let breadcrumb = format!("{line_comment_prefix}Excerpt from: {outline_text}\n");
|
||||
format!("{fence}{breadcrumb}{selected_text}\n```")
|
||||
} else {
|
||||
format!("{fence}{selected_text}\n```")
|
||||
}
|
||||
};
|
||||
let crease_title = if let Some(path) = filename {
|
||||
let start_line = selection.start.row + 1;
|
||||
let end_line = selection.end.row + 1;
|
||||
if start_line == end_line {
|
||||
format!("{}, Line {}", path.display(), start_line)
|
||||
} else {
|
||||
format!("{}, Lines {} to {}", path.display(), start_line, end_line)
|
||||
}
|
||||
} else {
|
||||
"Quoted selection".to_string()
|
||||
};
|
||||
creases.push((text, crease_title));
|
||||
selection_ranges: Vec<Range<Point>>,
|
||||
snapshot: MultiBufferSnapshot,
|
||||
cx: &App,
|
||||
) -> Vec<(String, String)> {
|
||||
let mut creases = Vec::new();
|
||||
for range in selection_ranges {
|
||||
let selected_text = snapshot.text_for_range(range.clone()).collect::<String>();
|
||||
if selected_text.is_empty() {
|
||||
continue;
|
||||
}
|
||||
});
|
||||
Some(creases)
|
||||
let start_language = snapshot.language_at(range.start);
|
||||
let end_language = snapshot.language_at(range.end);
|
||||
let language_name = if start_language == end_language {
|
||||
start_language.map(|language| language.code_fence_block_name())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let language_name = language_name.as_deref().unwrap_or("");
|
||||
let filename = snapshot.file_at(range.start).map(|file| file.full_path(cx));
|
||||
let text = if language_name == "markdown" {
|
||||
selected_text
|
||||
.lines()
|
||||
.map(|line| format!("> {}", line))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
} else {
|
||||
let start_symbols = snapshot
|
||||
.symbols_containing(range.start, None)
|
||||
.map(|(_, symbols)| symbols);
|
||||
let end_symbols = snapshot
|
||||
.symbols_containing(range.end, None)
|
||||
.map(|(_, symbols)| symbols);
|
||||
|
||||
let outline_text =
|
||||
if let Some((start_symbols, end_symbols)) = start_symbols.zip(end_symbols) {
|
||||
Some(
|
||||
start_symbols
|
||||
.into_iter()
|
||||
.zip(end_symbols)
|
||||
.take_while(|(a, b)| a == b)
|
||||
.map(|(a, _)| a.text)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" > "),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let line_comment_prefix = start_language
|
||||
.and_then(|l| l.default_scope().line_comment_prefixes().first().cloned());
|
||||
|
||||
let fence = codeblock_fence_for_path(
|
||||
filename.as_deref(),
|
||||
Some(range.start.row..=range.end.row),
|
||||
);
|
||||
|
||||
if let Some((line_comment_prefix, outline_text)) = line_comment_prefix.zip(outline_text)
|
||||
{
|
||||
let breadcrumb = format!("{line_comment_prefix}Excerpt from: {outline_text}\n");
|
||||
format!("{fence}{breadcrumb}{selected_text}\n```")
|
||||
} else {
|
||||
format!("{fence}{selected_text}\n```")
|
||||
}
|
||||
};
|
||||
let crease_title = if let Some(path) = filename {
|
||||
let start_line = range.start.row + 1;
|
||||
let end_line = range.end.row + 1;
|
||||
if start_line == end_line {
|
||||
format!("{}, Line {}", path.display(), start_line)
|
||||
} else {
|
||||
format!("{}, Lines {} to {}", path.display(), start_line, end_line)
|
||||
}
|
||||
} else {
|
||||
"Quoted selection".to_string()
|
||||
};
|
||||
creases.push((text, crease_title));
|
||||
}
|
||||
creases
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@ use gpui::Context;
|
||||
use gpui::IntoElement;
|
||||
use gpui::Window;
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use icons::IconName;
|
||||
pub use icons::IconName;
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use language_model::LanguageModelToolSchemaFormat;
|
||||
use project::Project;
|
||||
pub use project::Project;
|
||||
|
||||
pub use crate::action_log::*;
|
||||
pub use crate::tool_registry::*;
|
||||
|
||||
@@ -16,14 +16,18 @@ anyhow.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
chrono.workspace = true
|
||||
collections.workspace = true
|
||||
component.workspace = true
|
||||
feature_flags.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
html_to_markdown.workspace = true
|
||||
http_client.workspace = true
|
||||
indoc.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
linkme.workspace = true
|
||||
open.workspace = true
|
||||
project.workspace = true
|
||||
regex.workspace = true
|
||||
schemars.workspace = true
|
||||
@@ -31,10 +35,9 @@ serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
worktree.workspace = true
|
||||
open = { workspace = true }
|
||||
web_search.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
worktree.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
@@ -43,5 +46,8 @@ gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
rand.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
tree-sitter-rust.workspace = true
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
unindent.workspace = true
|
||||
|
||||
@@ -7,15 +7,15 @@ mod create_directory_tool;
|
||||
mod create_file_tool;
|
||||
mod delete_path_tool;
|
||||
mod diagnostics_tool;
|
||||
mod edit_file_tool;
|
||||
mod fetch_tool;
|
||||
mod find_replace_file_tool;
|
||||
mod grep_tool;
|
||||
mod list_directory_tool;
|
||||
mod move_path_tool;
|
||||
mod now_tool;
|
||||
mod open_tool;
|
||||
mod path_search_tool;
|
||||
mod read_file_tool;
|
||||
mod regex_search_tool;
|
||||
mod rename_tool;
|
||||
mod replace;
|
||||
mod schema;
|
||||
@@ -42,45 +42,46 @@ use crate::create_directory_tool::CreateDirectoryTool;
|
||||
use crate::create_file_tool::CreateFileTool;
|
||||
use crate::delete_path_tool::DeletePathTool;
|
||||
use crate::diagnostics_tool::DiagnosticsTool;
|
||||
use crate::edit_file_tool::EditFileTool;
|
||||
use crate::fetch_tool::FetchTool;
|
||||
use crate::find_replace_file_tool::FindReplaceFileTool;
|
||||
use crate::grep_tool::GrepTool;
|
||||
use crate::list_directory_tool::ListDirectoryTool;
|
||||
use crate::now_tool::NowTool;
|
||||
use crate::open_tool::OpenTool;
|
||||
use crate::path_search_tool::PathSearchTool;
|
||||
use crate::read_file_tool::ReadFileTool;
|
||||
use crate::regex_search_tool::RegexSearchTool;
|
||||
use crate::rename_tool::RenameTool;
|
||||
use crate::symbol_info_tool::SymbolInfoTool;
|
||||
use crate::terminal_tool::TerminalTool;
|
||||
use crate::thinking_tool::ThinkingTool;
|
||||
pub use schema::json_schema_for;
|
||||
|
||||
pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||
assistant_tool::init(cx);
|
||||
|
||||
let registry = ToolRegistry::global(cx);
|
||||
registry.register_tool(TerminalTool);
|
||||
registry.register_tool(BatchTool);
|
||||
registry.register_tool(CodeActionTool);
|
||||
registry.register_tool(CodeSymbolsTool);
|
||||
registry.register_tool(ContentsTool);
|
||||
registry.register_tool(CopyPathTool);
|
||||
registry.register_tool(CreateDirectoryTool);
|
||||
registry.register_tool(CreateFileTool);
|
||||
registry.register_tool(CopyPathTool);
|
||||
registry.register_tool(DeletePathTool);
|
||||
registry.register_tool(DiagnosticsTool);
|
||||
registry.register_tool(FetchTool::new(http_client));
|
||||
registry.register_tool(FindReplaceFileTool);
|
||||
registry.register_tool(ListDirectoryTool);
|
||||
registry.register_tool(EditFileTool);
|
||||
registry.register_tool(SymbolInfoTool);
|
||||
registry.register_tool(CodeActionTool);
|
||||
registry.register_tool(MovePathTool);
|
||||
registry.register_tool(DiagnosticsTool);
|
||||
registry.register_tool(ListDirectoryTool);
|
||||
registry.register_tool(NowTool);
|
||||
registry.register_tool(OpenTool);
|
||||
registry.register_tool(CodeSymbolsTool);
|
||||
registry.register_tool(ContentsTool);
|
||||
registry.register_tool(PathSearchTool);
|
||||
registry.register_tool(ReadFileTool);
|
||||
registry.register_tool(RegexSearchTool);
|
||||
registry.register_tool(GrepTool);
|
||||
registry.register_tool(RenameTool);
|
||||
registry.register_tool(SymbolInfoTool);
|
||||
registry.register_tool(TerminalTool);
|
||||
registry.register_tool(ThinkingTool);
|
||||
registry.register_tool(FetchTool::new(http_client));
|
||||
|
||||
cx.observe_flag::<feature_flags::ZedProWebSearchTool, _>({
|
||||
move |is_enabled, cx| {
|
||||
|
||||
@@ -43,7 +43,7 @@ pub struct BatchToolInput {
|
||||
/// }
|
||||
/// },
|
||||
/// {
|
||||
/// "name": "regex_search",
|
||||
/// "name": "grep",
|
||||
/// "input": {
|
||||
/// "regex": "fn run\\("
|
||||
/// }
|
||||
@@ -91,7 +91,7 @@ pub struct BatchToolInput {
|
||||
/// {
|
||||
/// "invocations": [
|
||||
/// {
|
||||
/// "name": "regex_search",
|
||||
/// "name": "grep",
|
||||
/// "input": {
|
||||
/// "regex": "impl Database"
|
||||
/// }
|
||||
|
||||
@@ -147,7 +147,7 @@ impl Tool for CodeSymbolsTool {
|
||||
};
|
||||
|
||||
cx.spawn(async move |cx| match input.path {
|
||||
Some(path) => file_outline(project, path, action_log, regex, input.offset, cx).await,
|
||||
Some(path) => file_outline(project, path, action_log, regex, cx).await,
|
||||
None => project_symbols(project, regex, input.offset, cx).await,
|
||||
})
|
||||
.into()
|
||||
@@ -159,7 +159,6 @@ pub async fn file_outline(
|
||||
path: String,
|
||||
action_log: Entity<ActionLog>,
|
||||
regex: Option<Regex>,
|
||||
offset: u32,
|
||||
cx: &mut AsyncApp,
|
||||
) -> anyhow::Result<String> {
|
||||
let buffer = {
|
||||
@@ -195,7 +194,8 @@ pub async fn file_outline(
|
||||
.into_iter()
|
||||
.map(|item| item.to_point(&snapshot)),
|
||||
regex,
|
||||
offset,
|
||||
0,
|
||||
usize::MAX,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -294,11 +294,10 @@ async fn project_symbols(
|
||||
async fn render_outline(
|
||||
items: impl IntoIterator<Item = OutlineItem<Point>>,
|
||||
regex: Option<Regex>,
|
||||
offset: u32,
|
||||
offset: usize,
|
||||
results_per_page: usize,
|
||||
) -> Result<String> {
|
||||
const RESULTS_PER_PAGE_USIZE: usize = RESULTS_PER_PAGE as usize;
|
||||
|
||||
let mut items = items.into_iter().skip(offset as usize);
|
||||
let mut items = items.into_iter().skip(offset);
|
||||
|
||||
let entries = items
|
||||
.by_ref()
|
||||
@@ -307,7 +306,7 @@ async fn render_outline(
|
||||
.as_ref()
|
||||
.is_none_or(|regex| regex.is_match(&item.text))
|
||||
})
|
||||
.take(RESULTS_PER_PAGE_USIZE)
|
||||
.take(results_per_page)
|
||||
.collect::<Vec<_>>();
|
||||
let has_more = items.next().is_some();
|
||||
|
||||
@@ -338,7 +337,10 @@ async fn render_outline(
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn render_entries(output: &mut String, items: impl IntoIterator<Item = OutlineItem<Point>>) -> u32 {
|
||||
fn render_entries(
|
||||
output: &mut String,
|
||||
items: impl IntoIterator<Item = OutlineItem<Point>>,
|
||||
) -> usize {
|
||||
let mut entries_rendered = 0;
|
||||
|
||||
for item in items {
|
||||
|
||||
@@ -228,7 +228,7 @@ impl Tool for ContentsTool {
|
||||
} else {
|
||||
// File is too big, so return its outline and a suggestion to
|
||||
// read again with a line number range specified.
|
||||
let outline = file_outline(project, file_path, action_log, None, 0, cx).await?;
|
||||
let outline = file_outline(project, file_path, action_log, None, cx).await?;
|
||||
|
||||
Ok(format!("This file was too big to read all at once. Here is an outline of its symbols:\n\n{outline}\n\nUsing the line numbers in this outline, you can call this tool again while specifying the start and end fields to see the implementations of symbols in the outline."))
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ To get a project-wide diagnostic summary:
|
||||
{}
|
||||
</example>
|
||||
|
||||
IMPORTANT: When you're done making changes, you **MUST** get the **project** diagnostics (input: `{}`) at the end of your edits so you can fix any problems you might have introduced. **DO NOT** tell the user you're done before doing this!
|
||||
|
||||
You may only attempt to fix these up to 3 times. If you have tried 3 times to fix them, and there are still problems remaining, you must not continue trying to fix them, and must instead tell the user that there are problems remaining - and ask if the user would like you to attempt to solve them further.
|
||||
<guidelines>
|
||||
- If you think you can fix a diagnostic, make 1-2 attempts and then give up.
|
||||
- Don't remove code you've generated just because you can't fix an error. The user can help you fix it.
|
||||
</guidelines>
|
||||
|
||||
183
crates/assistant_tools/src/edit_file_tool.rs
Normal file
183
crates/assistant_tools/src/edit_file_tool.rs
Normal file
@@ -0,0 +1,183 @@
|
||||
use crate::{replace::replace_with_flexible_indent, schema::json_schema_for};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{App, AppContext, AsyncApp, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use ui::IconName;
|
||||
|
||||
use crate::replace::replace_exact;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct EditFileToolInput {
|
||||
/// The full path of the file to modify in the project.
|
||||
///
|
||||
/// WARNING: When specifying which file path need changing, you MUST
|
||||
/// start each path with one of the project's root directories.
|
||||
///
|
||||
/// The following examples assume we have two root directories in the project:
|
||||
/// - backend
|
||||
/// - frontend
|
||||
///
|
||||
/// <example>
|
||||
/// `backend/src/main.rs`
|
||||
///
|
||||
/// Notice how the file path starts with root-1. Without that, the path
|
||||
/// would be ambiguous and the call would fail!
|
||||
/// </example>
|
||||
///
|
||||
/// <example>
|
||||
/// `frontend/db.js`
|
||||
/// </example>
|
||||
pub path: PathBuf,
|
||||
|
||||
/// A user-friendly markdown description of what's being replaced. This will be shown in the UI.
|
||||
///
|
||||
/// <example>Fix API endpoint URLs</example>
|
||||
/// <example>Update copyright year in `page_footer`</example>
|
||||
pub display_description: String,
|
||||
|
||||
/// The text to replace.
|
||||
pub old_string: String,
|
||||
|
||||
/// The text to replace it with.
|
||||
pub new_string: String,
|
||||
}
|
||||
|
||||
pub struct EditFileTool;
|
||||
|
||||
impl Tool for EditFileTool {
|
||||
fn name(&self) -> String {
|
||||
"edit_file".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
include_str!("edit_file_tool/description.md").to_string()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Pencil
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
json_schema_for::<EditFileToolInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<EditFileToolInput>(input.clone()) {
|
||||
Ok(input) => input.display_description,
|
||||
Err(_) => "Edit file".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<EditFileToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
|
||||
cx.spawn(async move |cx: &mut AsyncApp| {
|
||||
let project_path = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.find_project_path(&input.path, cx)
|
||||
.context("Path not found in project")
|
||||
})??;
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
|
||||
.await?;
|
||||
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
|
||||
if input.old_string.is_empty() {
|
||||
return Err(anyhow!("`old_string` cannot be empty. Use a different tool if you want to create a file."));
|
||||
}
|
||||
|
||||
if input.old_string == input.new_string {
|
||||
return Err(anyhow!("The `old_string` and `new_string` are identical, so no changes would be made."));
|
||||
}
|
||||
|
||||
let result = cx
|
||||
.background_spawn(async move {
|
||||
// Try to match exactly
|
||||
let diff = replace_exact(&input.old_string, &input.new_string, &snapshot)
|
||||
.await
|
||||
// If that fails, try being flexible about indentation
|
||||
.or_else(|| replace_with_flexible_indent(&input.old_string, &input.new_string, &snapshot))?;
|
||||
|
||||
if diff.edits.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let old_text = snapshot.text();
|
||||
|
||||
Some((old_text, diff))
|
||||
})
|
||||
.await;
|
||||
|
||||
let Some((old_text, diff)) = result else {
|
||||
let err = buffer.read_with(cx, |buffer, _cx| {
|
||||
let file_exists = buffer
|
||||
.file()
|
||||
.map_or(false, |file| file.disk_state().exists());
|
||||
|
||||
if !file_exists {
|
||||
anyhow!("{} does not exist", input.path.display())
|
||||
} else if buffer.is_empty() {
|
||||
anyhow!(
|
||||
"{} is empty, so the provided `old_string` wasn't found.",
|
||||
input.path.display()
|
||||
)
|
||||
} else {
|
||||
anyhow!("Failed to match the provided `old_string`")
|
||||
}
|
||||
})?;
|
||||
|
||||
return Err(err)
|
||||
};
|
||||
|
||||
let snapshot = cx.update(|cx| {
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.buffer_read(buffer.clone(), cx)
|
||||
});
|
||||
let snapshot = buffer.update(cx, |buffer, cx| {
|
||||
buffer.finalize_last_transaction();
|
||||
buffer.apply_diff(diff, cx);
|
||||
buffer.finalize_last_transaction();
|
||||
buffer.snapshot()
|
||||
});
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.buffer_edited(buffer.clone(), cx)
|
||||
});
|
||||
snapshot
|
||||
})?;
|
||||
|
||||
project.update( cx, |project, cx| {
|
||||
project.save_buffer(buffer, cx)
|
||||
})?.await?;
|
||||
|
||||
let diff_str = cx.background_spawn(async move {
|
||||
let new_text = snapshot.text();
|
||||
language::unified_diff(&old_text, &new_text)
|
||||
}).await;
|
||||
|
||||
|
||||
Ok(format!("Edited {}:\n\n```diff\n{}\n```", input.path.display(), diff_str))
|
||||
|
||||
}).into()
|
||||
}
|
||||
}
|
||||
45
crates/assistant_tools/src/edit_file_tool/description.md
Normal file
45
crates/assistant_tools/src/edit_file_tool/description.md
Normal file
@@ -0,0 +1,45 @@
|
||||
This is a tool for editing files. For moving or renaming files, you should generally use the `terminal` tool with the 'mv' command instead. For larger edits, use the `create_file` tool to overwrite files.
|
||||
|
||||
Before using this tool:
|
||||
|
||||
1. Use the `read_file` tool to understand the file's contents and context
|
||||
|
||||
2. Verify the directory path is correct (only applicable when creating new files):
|
||||
- Use the `list_directory` tool to verify the parent directory exists and is the correct location
|
||||
|
||||
To make a file edit, provide the following:
|
||||
1. path: The full path to the file you wish to modify in the project. This path must include the root directory in the project.
|
||||
2. old_string: The text to replace (must be unique within the file, and must match the file contents exactly, including all whitespace and indentation)
|
||||
3. new_string: The edited text, which will replace the old_string in the file.
|
||||
|
||||
The tool will replace ONE occurrence of old_string with new_string in the specified file.
|
||||
|
||||
CRITICAL REQUIREMENTS FOR USING THIS TOOL:
|
||||
|
||||
1. UNIQUENESS: The old_string MUST uniquely identify the specific instance you want to change. This means:
|
||||
- Include AT LEAST 3-5 lines of context BEFORE the change point
|
||||
- Include AT LEAST 3-5 lines of context AFTER the change point
|
||||
- Include all whitespace, indentation, and surrounding code exactly as it appears in the file
|
||||
|
||||
2. SINGLE INSTANCE: This tool can only change ONE instance at a time. If you need to change multiple instances:
|
||||
- Make separate calls to this tool for each instance
|
||||
- Each call must uniquely identify its specific instance using extensive context
|
||||
|
||||
3. VERIFICATION: Before using this tool:
|
||||
- Check how many instances of the target text exist in the file
|
||||
- If multiple instances exist, gather enough context to uniquely identify each one
|
||||
- Plan separate tool calls for each instance
|
||||
|
||||
WARNING: If you do not follow these requirements:
|
||||
- The tool will fail if old_string matches multiple locations
|
||||
- The tool will fail if old_string doesn't match exactly (including whitespace)
|
||||
- You may change the wrong instance if you don't include enough context
|
||||
|
||||
When making edits:
|
||||
- Ensure the edit results in idiomatic, correct code
|
||||
- Do not leave the code in a broken state
|
||||
- Always use fully-qualified project paths (starting with the name of one of the project's root directories)
|
||||
|
||||
If you want to create a new file, use the `create_file` tool instead of this tool. Don't pass an empty `old_string`.
|
||||
|
||||
Remember: when making multiple file edits in a row to the same file, you should prefer to send all edits in a single message with multiple calls to this tool, rather than multiple messages with a single call each.
|
||||
424
crates/assistant_tools/src/grep_tool.rs
Normal file
424
crates/assistant_tools/src/grep_tool.rs
Normal file
@@ -0,0 +1,424 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use futures::StreamExt;
|
||||
use gpui::{App, Entity, Task};
|
||||
use language::OffsetRangeExt;
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::{
|
||||
Project,
|
||||
search::{SearchQuery, SearchResult},
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cmp, fmt::Write, sync::Arc};
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
use util::paths::PathMatcher;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct GrepToolInput {
|
||||
/// A regex pattern to search for in the entire project. Note that the regex
|
||||
/// will be parsed by the Rust `regex` crate.
|
||||
pub regex: String,
|
||||
|
||||
/// A glob pattern for the paths of files to include in the search.
|
||||
/// Supports standard glob patterns like "**/*.rs" or "src/**/*.ts".
|
||||
/// If omitted, all files in the project will be searched.
|
||||
pub include_pattern: Option<String>,
|
||||
|
||||
/// Optional starting position for paginated results (0-based).
|
||||
/// When not provided, starts from the beginning.
|
||||
#[serde(default)]
|
||||
pub offset: u32,
|
||||
|
||||
/// Whether the regex is case-sensitive. Defaults to false (case-insensitive).
|
||||
#[serde(default)]
|
||||
pub case_sensitive: bool,
|
||||
}
|
||||
|
||||
impl GrepToolInput {
|
||||
/// Which page of search results this is.
|
||||
pub fn page(&self) -> u32 {
|
||||
1 + (self.offset / RESULTS_PER_PAGE)
|
||||
}
|
||||
}
|
||||
|
||||
const RESULTS_PER_PAGE: u32 = 20;
|
||||
|
||||
pub struct GrepTool;
|
||||
|
||||
impl Tool for GrepTool {
|
||||
fn name(&self) -> String {
|
||||
"grep".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
include_str!("./grep_tool/description.md").into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Regex
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
json_schema_for::<GrepToolInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<GrepToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
let page = input.page();
|
||||
let regex_str = MarkdownString::inline_code(&input.regex);
|
||||
let case_info = if input.case_sensitive {
|
||||
" (case-sensitive)"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
if page > 1 {
|
||||
format!("Get page {page} of search results for regex {regex_str}{case_info}")
|
||||
} else {
|
||||
format!("Search files for regex {regex_str}{case_info}")
|
||||
}
|
||||
}
|
||||
Err(_) => "Search with regex".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
const CONTEXT_LINES: u32 = 2;
|
||||
|
||||
let input = match serde_json::from_value::<GrepToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(error) => {
|
||||
return Task::ready(Err(anyhow!("Failed to parse input: {}", error))).into();
|
||||
}
|
||||
};
|
||||
|
||||
let include_matcher = match PathMatcher::new(
|
||||
input
|
||||
.include_pattern
|
||||
.as_ref()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>(),
|
||||
) {
|
||||
Ok(matcher) => matcher,
|
||||
Err(error) => {
|
||||
return Task::ready(Err(anyhow!("invalid include glob pattern: {}", error))).into();
|
||||
}
|
||||
};
|
||||
|
||||
let query = match SearchQuery::regex(
|
||||
&input.regex,
|
||||
false,
|
||||
input.case_sensitive,
|
||||
false,
|
||||
false,
|
||||
include_matcher,
|
||||
PathMatcher::default(), // For now, keep it simple and don't enable an exclude pattern.
|
||||
true, // Always match file include pattern against *full project paths* that start with a project root.
|
||||
None,
|
||||
) {
|
||||
Ok(query) => query,
|
||||
Err(error) => return Task::ready(Err(error)).into(),
|
||||
};
|
||||
|
||||
let results = project.update(cx, |project, cx| project.search(query, cx));
|
||||
|
||||
cx.spawn(async move|cx| {
|
||||
futures::pin_mut!(results);
|
||||
|
||||
let mut output = String::new();
|
||||
let mut skips_remaining = input.offset;
|
||||
let mut matches_found = 0;
|
||||
let mut has_more_matches = false;
|
||||
|
||||
while let Some(SearchResult::Buffer { buffer, ranges }) = results.next().await {
|
||||
if ranges.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
buffer.read_with(cx, |buffer, cx| -> Result<(), anyhow::Error> {
|
||||
if let Some(path) = buffer.file().map(|file| file.full_path(cx)) {
|
||||
let mut file_header_written = false;
|
||||
let mut ranges = ranges
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
let mut point_range = range.to_point(buffer);
|
||||
point_range.start.row =
|
||||
point_range.start.row.saturating_sub(CONTEXT_LINES);
|
||||
point_range.start.column = 0;
|
||||
point_range.end.row = cmp::min(
|
||||
buffer.max_point().row,
|
||||
point_range.end.row + CONTEXT_LINES,
|
||||
);
|
||||
point_range.end.column = buffer.line_len(point_range.end.row);
|
||||
point_range
|
||||
})
|
||||
.peekable();
|
||||
|
||||
while let Some(mut range) = ranges.next() {
|
||||
if skips_remaining > 0 {
|
||||
skips_remaining -= 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// We'd already found a full page of matches, and we just found one more.
|
||||
if matches_found >= RESULTS_PER_PAGE {
|
||||
has_more_matches = true;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
while let Some(next_range) = ranges.peek() {
|
||||
if range.end.row >= next_range.start.row {
|
||||
range.end = next_range.end;
|
||||
ranges.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !file_header_written {
|
||||
writeln!(output, "\n## Matches in {}", path.display())?;
|
||||
file_header_written = true;
|
||||
}
|
||||
|
||||
let start_line = range.start.row + 1;
|
||||
let end_line = range.end.row + 1;
|
||||
writeln!(output, "\n### Lines {start_line}-{end_line}\n```")?;
|
||||
output.extend(buffer.text_for_range(range));
|
||||
output.push_str("\n```\n");
|
||||
|
||||
matches_found += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})??;
|
||||
}
|
||||
|
||||
if matches_found == 0 {
|
||||
Ok("No matches found".to_string())
|
||||
} else if has_more_matches {
|
||||
Ok(format!(
|
||||
"Showing matches {}-{} (there were more matches found; use offset: {} to see next page):\n{output}",
|
||||
input.offset + 1,
|
||||
input.offset + matches_found,
|
||||
input.offset + RESULTS_PER_PAGE,
|
||||
))
|
||||
} else {
|
||||
Ok(format!("Found {matches_found} matches:\n{output}"))
|
||||
}
|
||||
}).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use assistant_tool::Tool;
|
||||
use gpui::{AppContext, TestAppContext};
|
||||
use project::{FakeFs, Project};
|
||||
use settings::SettingsStore;
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_grep_tool_with_include_pattern(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
serde_json::json!({
|
||||
"src": {
|
||||
"main.rs": "fn main() {\n println!(\"Hello, world!\");\n}",
|
||||
"utils": {
|
||||
"helper.rs": "fn helper() {\n println!(\"I'm a helper!\");\n}",
|
||||
},
|
||||
},
|
||||
"tests": {
|
||||
"test_main.rs": "fn test_main() {\n assert!(true);\n}",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
// Test with include pattern for Rust files inside the root of the project
|
||||
let input = serde_json::to_value(GrepToolInput {
|
||||
regex: "println".to_string(),
|
||||
include_pattern: Some("root/**/*.rs".to_string()),
|
||||
offset: 0,
|
||||
case_sensitive: false,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let result = run_grep_tool(input, project.clone(), cx).await;
|
||||
assert!(result.contains("main.rs"), "Should find matches in main.rs");
|
||||
assert!(
|
||||
result.contains("helper.rs"),
|
||||
"Should find matches in helper.rs"
|
||||
);
|
||||
assert!(
|
||||
!result.contains("test_main.rs"),
|
||||
"Should not include test_main.rs even though it's a .rs file (because it doesn't have the pattern)"
|
||||
);
|
||||
|
||||
// Test with include pattern for src directory only
|
||||
let input = serde_json::to_value(GrepToolInput {
|
||||
regex: "fn".to_string(),
|
||||
include_pattern: Some("root/**/src/**".to_string()),
|
||||
offset: 0,
|
||||
case_sensitive: false,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let result = run_grep_tool(input, project.clone(), cx).await;
|
||||
assert!(
|
||||
result.contains("main.rs"),
|
||||
"Should find matches in src/main.rs"
|
||||
);
|
||||
assert!(
|
||||
result.contains("helper.rs"),
|
||||
"Should find matches in src/utils/helper.rs"
|
||||
);
|
||||
assert!(
|
||||
!result.contains("test_main.rs"),
|
||||
"Should not include test_main.rs as it's not in src directory"
|
||||
);
|
||||
|
||||
// Test with empty include pattern (should default to all files)
|
||||
let input = serde_json::to_value(GrepToolInput {
|
||||
regex: "fn".to_string(),
|
||||
include_pattern: None,
|
||||
offset: 0,
|
||||
case_sensitive: false,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let result = run_grep_tool(input, project.clone(), cx).await;
|
||||
assert!(result.contains("main.rs"), "Should find matches in main.rs");
|
||||
assert!(
|
||||
result.contains("helper.rs"),
|
||||
"Should find matches in helper.rs"
|
||||
);
|
||||
assert!(
|
||||
result.contains("test_main.rs"),
|
||||
"Should include test_main.rs"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_grep_tool_with_case_sensitivity(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
serde_json::json!({
|
||||
"case_test.txt": "This file has UPPERCASE and lowercase text.\nUPPERCASE patterns should match only with case_sensitive: true",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
// Test case-insensitive search (default)
|
||||
let input = serde_json::to_value(GrepToolInput {
|
||||
regex: "uppercase".to_string(),
|
||||
include_pattern: Some("**/*.txt".to_string()),
|
||||
offset: 0,
|
||||
case_sensitive: false,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let result = run_grep_tool(input, project.clone(), cx).await;
|
||||
assert!(
|
||||
result.contains("UPPERCASE"),
|
||||
"Case-insensitive search should match uppercase"
|
||||
);
|
||||
|
||||
// Test case-sensitive search
|
||||
let input = serde_json::to_value(GrepToolInput {
|
||||
regex: "uppercase".to_string(),
|
||||
include_pattern: Some("**/*.txt".to_string()),
|
||||
offset: 0,
|
||||
case_sensitive: true,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let result = run_grep_tool(input, project.clone(), cx).await;
|
||||
assert!(
|
||||
!result.contains("UPPERCASE"),
|
||||
"Case-sensitive search should not match uppercase"
|
||||
);
|
||||
|
||||
// Test case-sensitive search
|
||||
let input = serde_json::to_value(GrepToolInput {
|
||||
regex: "LOWERCASE".to_string(),
|
||||
include_pattern: Some("**/*.txt".to_string()),
|
||||
offset: 0,
|
||||
case_sensitive: true,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let result = run_grep_tool(input, project.clone(), cx).await;
|
||||
|
||||
assert!(
|
||||
!result.contains("lowercase"),
|
||||
"Case-sensitive search should match lowercase"
|
||||
);
|
||||
|
||||
// Test case-sensitive search for lowercase pattern
|
||||
let input = serde_json::to_value(GrepToolInput {
|
||||
regex: "lowercase".to_string(),
|
||||
include_pattern: Some("**/*.txt".to_string()),
|
||||
offset: 0,
|
||||
case_sensitive: true,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let result = run_grep_tool(input, project.clone(), cx).await;
|
||||
assert!(
|
||||
result.contains("lowercase"),
|
||||
"Case-sensitive search should match lowercase text"
|
||||
);
|
||||
}
|
||||
|
||||
async fn run_grep_tool(
|
||||
input: serde_json::Value,
|
||||
project: Entity<Project>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> String {
|
||||
let tool = Arc::new(GrepTool);
|
||||
let action_log = cx.new(|_cx| ActionLog::new(project.clone()));
|
||||
let task = cx.update(|cx| tool.run(input, &[], project, action_log, cx));
|
||||
|
||||
match task.output.await {
|
||||
Ok(result) => result,
|
||||
Err(e) => panic!("Failed to run grep tool: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
8
crates/assistant_tools/src/grep_tool/description.md
Normal file
8
crates/assistant_tools/src/grep_tool/description.md
Normal file
@@ -0,0 +1,8 @@
|
||||
Searches the contents of files in the project with a regular expression
|
||||
|
||||
- Prefer this tool to path search when searching for symbols in the project, because you won't need to guess what path it's in.
|
||||
- Supports full regex syntax (eg. "log.*Error", "function\\s+\\w+", etc.)
|
||||
- Pass an `include_pattern` if you know how to narrow your search on the files system
|
||||
- Never use this tool to search for paths. Only search file contents with this tool.
|
||||
- Use this tool when you need to find files containing specific patterns
|
||||
- Results are paginated with 20 matches per page. Use the optional 'offset' parameter to request subsequent pages.
|
||||
@@ -12,7 +12,7 @@ use util::markdown::MarkdownString;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ListDirectoryToolInput {
|
||||
/// The relative path of the directory to list.
|
||||
/// The fully-qualified path of the directory to list in the project.
|
||||
///
|
||||
/// This path should never be absolute, and the first component
|
||||
/// of the path should always be a root directory in a project.
|
||||
|
||||
@@ -1 +1 @@
|
||||
Lists files and directories in a given path.
|
||||
Lists files and directories in a given path. Prefer the `grep` or `path_search` tools when searching the codebase.
|
||||
|
||||
@@ -6,14 +6,14 @@ use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat}
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use std::{cmp, fmt::Write as _, path::PathBuf, sync::Arc};
|
||||
use ui::IconName;
|
||||
use util::paths::PathMatcher;
|
||||
use worktree::Snapshot;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct PathSearchToolInput {
|
||||
/// The glob to search all project paths for.
|
||||
/// The glob to match against every path in the project.
|
||||
///
|
||||
/// <example>
|
||||
/// If the project has the following root directories:
|
||||
@@ -76,66 +76,125 @@ impl Tool for PathSearchTool {
|
||||
Ok(input) => (input.offset, input.glob),
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
|
||||
let path_matcher = match PathMatcher::new([
|
||||
// Sometimes models try to search for "". In this case, return all paths in the project.
|
||||
if glob.is_empty() { "*" } else { &glob },
|
||||
]) {
|
||||
Ok(matcher) => matcher,
|
||||
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))).into(),
|
||||
};
|
||||
let snapshots: Vec<Snapshot> = project
|
||||
.read(cx)
|
||||
.worktrees(cx)
|
||||
.map(|worktree| worktree.read(cx).snapshot())
|
||||
.collect();
|
||||
|
||||
let offset = offset as usize;
|
||||
let task = search_paths(&glob, project, cx);
|
||||
cx.background_spawn(async move {
|
||||
let mut matches = Vec::new();
|
||||
|
||||
for worktree in snapshots {
|
||||
let root_name = worktree.root_name();
|
||||
|
||||
// Don't consider ignored entries.
|
||||
for entry in worktree.entries(false, 0) {
|
||||
if path_matcher.is_match(&entry.path) {
|
||||
matches.push(
|
||||
PathBuf::from(root_name)
|
||||
.join(&entry.path)
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
let matches = task.await?;
|
||||
let paginated_matches = &matches[cmp::min(offset, matches.len())
|
||||
..cmp::min(offset + RESULTS_PER_PAGE, matches.len())];
|
||||
|
||||
if matches.is_empty() {
|
||||
Ok(format!("No paths in the project matched the glob {glob:?}"))
|
||||
Ok("No matches found".to_string())
|
||||
} else {
|
||||
// Sort to group entries in the same directory together.
|
||||
matches.sort();
|
||||
|
||||
let total_matches = matches.len();
|
||||
let response = if total_matches > RESULTS_PER_PAGE + offset as usize {
|
||||
let paginated_matches: Vec<_> = matches
|
||||
.into_iter()
|
||||
.skip(offset as usize)
|
||||
.take(RESULTS_PER_PAGE)
|
||||
.collect();
|
||||
|
||||
format!(
|
||||
"Found {} total matches. Showing results {}-{} (provide 'offset' parameter for more results):\n\n{}",
|
||||
total_matches,
|
||||
let mut message = format!("Found {} total matches.", matches.len());
|
||||
if matches.len() > RESULTS_PER_PAGE {
|
||||
write!(
|
||||
&mut message,
|
||||
"\nShowing results {}-{} (provide 'offset' parameter for more results):",
|
||||
offset + 1,
|
||||
offset as usize + paginated_matches.len(),
|
||||
paginated_matches.join("\n")
|
||||
offset + paginated_matches.len()
|
||||
)
|
||||
} else {
|
||||
matches.join("\n")
|
||||
};
|
||||
|
||||
Ok(response)
|
||||
.unwrap();
|
||||
}
|
||||
for mat in matches.into_iter().skip(offset).take(RESULTS_PER_PAGE) {
|
||||
write!(&mut message, "\n{}", mat.display()).unwrap();
|
||||
}
|
||||
Ok(message)
|
||||
}
|
||||
}).into()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Result<Vec<PathBuf>>> {
|
||||
let path_matcher = match PathMatcher::new([
|
||||
// Sometimes models try to search for "". In this case, return all paths in the project.
|
||||
if glob.is_empty() { "*" } else { glob },
|
||||
]) {
|
||||
Ok(matcher) => matcher,
|
||||
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))),
|
||||
};
|
||||
let snapshots: Vec<Snapshot> = project
|
||||
.read(cx)
|
||||
.worktrees(cx)
|
||||
.map(|worktree| worktree.read(cx).snapshot())
|
||||
.collect();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
Ok(snapshots
|
||||
.iter()
|
||||
.flat_map(|snapshot| {
|
||||
let root_name = PathBuf::from(snapshot.root_name());
|
||||
snapshot
|
||||
.entries(false, 0)
|
||||
.map(move |entry| root_name.join(&entry.path))
|
||||
.filter(|path| path_matcher.is_match(&path))
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use project::{FakeFs, Project};
|
||||
use settings::SettingsStore;
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_path_search_tool(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
serde_json::json!({
|
||||
"apple": {
|
||||
"banana": {
|
||||
"carrot": "1",
|
||||
},
|
||||
"bandana": {
|
||||
"carbonara": "2",
|
||||
},
|
||||
"endive": "3"
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
let matches = cx
|
||||
.update(|cx| search_paths("root/**/car*", project.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
matches,
|
||||
&[
|
||||
PathBuf::from("root/apple/banana/carrot"),
|
||||
PathBuf::from("root/apple/bandana/carbonara")
|
||||
]
|
||||
);
|
||||
|
||||
let matches = cx
|
||||
.update(|cx| search_paths("**/car*", project.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
matches,
|
||||
&[
|
||||
PathBuf::from("root/apple/banana/carrot"),
|
||||
PathBuf::from("root/apple/bandana/carbonara")
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
Returns paths in the project which match the given glob.
|
||||
Fast file pattern matching tool that works with any codebase size
|
||||
|
||||
Results are paginated with 50 matches per page. Use the optional 'offset' parameter to request subsequent pages.
|
||||
- Supports glob patterns like "**/*.js" or "src/**/*.ts"
|
||||
- Returns matching file paths sorted alphabetically
|
||||
- Prefer the `grep` tool to this tool when searching for symbols unless you have specific information about paths.
|
||||
- Use this tool when you need to find files by name patterns
|
||||
- Results are paginated with 50 matches per page. Use the optional 'offset' parameter to request subsequent pages.
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{code_symbols_tool::file_outline, schema::json_schema_for};
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{App, Entity, Task};
|
||||
use indoc::formatdoc;
|
||||
use itertools::Itertools;
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
|
||||
@@ -95,11 +95,24 @@ impl Tool for ReadFileTool {
|
||||
};
|
||||
|
||||
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
|
||||
return Task::ready(Err(anyhow!("Path {} not found in project", &input.path,))).into();
|
||||
return Task::ready(Err(anyhow!("Path {} not found in project", &input.path))).into();
|
||||
};
|
||||
let Some(worktree) = project
|
||||
.read(cx)
|
||||
.worktree_for_id(project_path.worktree_id, cx)
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("Worktree not found for project path"))).into();
|
||||
};
|
||||
let exists = worktree.update(cx, |worktree, cx| {
|
||||
worktree.file_exists(&project_path.path, cx)
|
||||
});
|
||||
|
||||
let file_path = input.path.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
if !exists.await? {
|
||||
return Err(anyhow!("{} not found", file_path))
|
||||
}
|
||||
|
||||
let buffer = cx
|
||||
.update(|cx| {
|
||||
project.update(cx, |project, cx| project.open_buffer(project_path, cx))
|
||||
@@ -141,11 +154,231 @@ impl Tool for ReadFileTool {
|
||||
} else {
|
||||
// File is too big, so return an error with the outline
|
||||
// and a suggestion to read again with line numbers.
|
||||
let outline = file_outline(project, file_path, action_log, None, 0, cx).await?;
|
||||
let outline = file_outline(project, file_path, action_log, None, cx).await?;
|
||||
Ok(formatdoc! {"
|
||||
This file was too big to read all at once. Here is an outline of its symbols:
|
||||
|
||||
Ok(format!("This file was too big to read all at once. Here is an outline of its symbols:\n\n{outline}\n\nUsing the line numbers in this outline, you can call this tool again while specifying the start_line and end_line fields to see the implementations of symbols in the outline."))
|
||||
{outline}
|
||||
|
||||
Using the line numbers in this outline, you can call this tool again while specifying
|
||||
the start_line and end_line fields to see the implementations of symbols in the outline."
|
||||
})
|
||||
}
|
||||
}
|
||||
}).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use gpui::{AppContext, TestAppContext};
|
||||
use language::{Language, LanguageConfig, LanguageMatcher};
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_read_nonexistent_file(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/root", json!({})).await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
let input = json!({
|
||||
"path": "root/nonexistent_file.txt"
|
||||
});
|
||||
Arc::new(ReadFileTool)
|
||||
.run(input, &[], project.clone(), action_log, cx)
|
||||
.output
|
||||
})
|
||||
.await;
|
||||
assert_eq!(
|
||||
result.unwrap_err().to_string(),
|
||||
"root/nonexistent_file.txt not found"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_read_small_file(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"small_file.txt": "This is a small file content"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
let input = json!({
|
||||
"path": "root/small_file.txt"
|
||||
});
|
||||
Arc::new(ReadFileTool)
|
||||
.run(input, &[], project.clone(), action_log, cx)
|
||||
.output
|
||||
})
|
||||
.await;
|
||||
assert_eq!(result.unwrap(), "This is a small file content");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_read_large_file(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"large_file.rs": (0..1000).map(|i| format!("struct Test{} {{\n a: u32,\n b: usize,\n}}", i)).collect::<Vec<_>>().join("\n")
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
||||
language_registry.add(Arc::new(rust_lang()));
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
let input = json!({
|
||||
"path": "root/large_file.rs"
|
||||
});
|
||||
Arc::new(ReadFileTool)
|
||||
.run(input, &[], project.clone(), action_log.clone(), cx)
|
||||
.output
|
||||
})
|
||||
.await;
|
||||
let content = result.unwrap();
|
||||
assert_eq!(
|
||||
content.lines().skip(2).take(6).collect::<Vec<_>>(),
|
||||
vec![
|
||||
"struct Test0 [L1-4]",
|
||||
" a [L2]",
|
||||
" b [L3]",
|
||||
"struct Test1 [L5-8]",
|
||||
" a [L6]",
|
||||
" b [L7]",
|
||||
]
|
||||
);
|
||||
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
let input = json!({
|
||||
"path": "root/large_file.rs",
|
||||
"offset": 1
|
||||
});
|
||||
Arc::new(ReadFileTool)
|
||||
.run(input, &[], project.clone(), action_log, cx)
|
||||
.output
|
||||
})
|
||||
.await;
|
||||
let content = result.unwrap();
|
||||
let expected_content = (0..1000)
|
||||
.flat_map(|i| {
|
||||
vec![
|
||||
format!("struct Test{} [L{}-{}]", i, i * 4 + 1, i * 4 + 4),
|
||||
format!(" a [L{}]", i * 4 + 2),
|
||||
format!(" b [L{}]", i * 4 + 3),
|
||||
]
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
pretty_assertions::assert_eq!(
|
||||
content
|
||||
.lines()
|
||||
.skip(2)
|
||||
.take(expected_content.len())
|
||||
.collect::<Vec<_>>(),
|
||||
expected_content
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_read_file_with_line_range(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"multiline.txt": "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
let input = json!({
|
||||
"path": "root/multiline.txt",
|
||||
"start_line": 2,
|
||||
"end_line": 4
|
||||
});
|
||||
Arc::new(ReadFileTool)
|
||||
.run(input, &[], project.clone(), action_log, cx)
|
||||
.output
|
||||
})
|
||||
.await;
|
||||
assert_eq!(result.unwrap(), "Line 2\nLine 3");
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn rust_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::LANGUAGE.into()),
|
||||
)
|
||||
.with_outline_query(
|
||||
r#"
|
||||
(line_comment) @annotation
|
||||
|
||||
(struct_item
|
||||
"struct" @context
|
||||
name: (_) @name) @item
|
||||
(enum_item
|
||||
"enum" @context
|
||||
name: (_) @name) @item
|
||||
(enum_variant
|
||||
name: (_) @name) @item
|
||||
(field_declaration
|
||||
name: (_) @name) @item
|
||||
(impl_item
|
||||
"impl" @context
|
||||
trait: (_)? @name
|
||||
"for"? @context
|
||||
type: (_) @name
|
||||
body: (_ "{" (_)* "}")) @item
|
||||
(function_item
|
||||
"fn" @context
|
||||
name: (_) @name) @item
|
||||
(mod_item
|
||||
"mod" @context
|
||||
name: (_) @name) @item
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
Reads the content of the given file in the project.
|
||||
|
||||
If the file is too big to read all at once, and neither a start line
|
||||
nor an end line was specified, then this returns an outline of the
|
||||
file's symbols (with line numbers) instead of the file's contents,
|
||||
so that it can be called again with line ranges.
|
||||
- Never attempt to read a path that hasn't been previously mentioned.
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use futures::StreamExt;
|
||||
use gpui::{App, Entity, Task};
|
||||
use language::OffsetRangeExt;
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::{
|
||||
Project,
|
||||
search::{SearchQuery, SearchResult},
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cmp, fmt::Write, sync::Arc};
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
use util::paths::PathMatcher;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct RegexSearchToolInput {
|
||||
/// A regex pattern to search for in the entire project. Note that the regex
|
||||
/// will be parsed by the Rust `regex` crate.
|
||||
pub regex: String,
|
||||
|
||||
/// Optional starting position for paginated results (0-based).
|
||||
/// When not provided, starts from the beginning.
|
||||
#[serde(default)]
|
||||
pub offset: u32,
|
||||
|
||||
/// Whether the regex is case-sensitive. Defaults to false (case-insensitive).
|
||||
#[serde(default)]
|
||||
pub case_sensitive: bool,
|
||||
}
|
||||
|
||||
impl RegexSearchToolInput {
|
||||
/// Which page of search results this is.
|
||||
pub fn page(&self) -> u32 {
|
||||
1 + (self.offset / RESULTS_PER_PAGE)
|
||||
}
|
||||
}
|
||||
|
||||
const RESULTS_PER_PAGE: u32 = 20;
|
||||
|
||||
pub struct RegexSearchTool;
|
||||
|
||||
impl Tool for RegexSearchTool {
|
||||
fn name(&self) -> String {
|
||||
"regex_search".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
include_str!("./regex_search_tool/description.md").into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Regex
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
json_schema_for::<RegexSearchToolInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<RegexSearchToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
let page = input.page();
|
||||
let regex_str = MarkdownString::inline_code(&input.regex);
|
||||
let case_info = if input.case_sensitive {
|
||||
" (case-sensitive)"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
if page > 1 {
|
||||
format!("Get page {page} of search results for regex {regex_str}{case_info}")
|
||||
} else {
|
||||
format!("Search files for regex {regex_str}{case_info}")
|
||||
}
|
||||
}
|
||||
Err(_) => "Search with regex".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
const CONTEXT_LINES: u32 = 2;
|
||||
|
||||
let (offset, regex, case_sensitive) =
|
||||
match serde_json::from_value::<RegexSearchToolInput>(input) {
|
||||
Ok(input) => (input.offset, input.regex, input.case_sensitive),
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
|
||||
let query = match SearchQuery::regex(
|
||||
®ex,
|
||||
false,
|
||||
case_sensitive,
|
||||
false,
|
||||
false,
|
||||
PathMatcher::default(),
|
||||
PathMatcher::default(),
|
||||
None,
|
||||
) {
|
||||
Ok(query) => query,
|
||||
Err(error) => return Task::ready(Err(error)).into(),
|
||||
};
|
||||
|
||||
let results = project.update(cx, |project, cx| project.search(query, cx));
|
||||
|
||||
cx.spawn(async move|cx| {
|
||||
futures::pin_mut!(results);
|
||||
|
||||
let mut output = String::new();
|
||||
let mut skips_remaining = offset;
|
||||
let mut matches_found = 0;
|
||||
let mut has_more_matches = false;
|
||||
|
||||
while let Some(SearchResult::Buffer { buffer, ranges }) = results.next().await {
|
||||
if ranges.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
buffer.read_with(cx, |buffer, cx| -> Result<(), anyhow::Error> {
|
||||
if let Some(path) = buffer.file().map(|file| file.full_path(cx)) {
|
||||
let mut file_header_written = false;
|
||||
let mut ranges = ranges
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
let mut point_range = range.to_point(buffer);
|
||||
point_range.start.row =
|
||||
point_range.start.row.saturating_sub(CONTEXT_LINES);
|
||||
point_range.start.column = 0;
|
||||
point_range.end.row = cmp::min(
|
||||
buffer.max_point().row,
|
||||
point_range.end.row + CONTEXT_LINES,
|
||||
);
|
||||
point_range.end.column = buffer.line_len(point_range.end.row);
|
||||
point_range
|
||||
})
|
||||
.peekable();
|
||||
|
||||
while let Some(mut range) = ranges.next() {
|
||||
if skips_remaining > 0 {
|
||||
skips_remaining -= 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// We'd already found a full page of matches, and we just found one more.
|
||||
if matches_found >= RESULTS_PER_PAGE {
|
||||
has_more_matches = true;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
while let Some(next_range) = ranges.peek() {
|
||||
if range.end.row >= next_range.start.row {
|
||||
range.end = next_range.end;
|
||||
ranges.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !file_header_written {
|
||||
writeln!(output, "\n## Matches in {}", path.display())?;
|
||||
file_header_written = true;
|
||||
}
|
||||
|
||||
let start_line = range.start.row + 1;
|
||||
let end_line = range.end.row + 1;
|
||||
writeln!(output, "\n### Lines {start_line}-{end_line}\n```")?;
|
||||
output.extend(buffer.text_for_range(range));
|
||||
output.push_str("\n```\n");
|
||||
|
||||
matches_found += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})??;
|
||||
}
|
||||
|
||||
if matches_found == 0 {
|
||||
Ok("No matches found".to_string())
|
||||
} else if has_more_matches {
|
||||
Ok(format!(
|
||||
"Showing matches {}-{} (there were more matches found; use offset: {} to see next page):\n{output}",
|
||||
offset + 1,
|
||||
offset + matches_found,
|
||||
offset + RESULTS_PER_PAGE,
|
||||
))
|
||||
} else {
|
||||
Ok(format!("Found {matches_found} matches:\n{output}"))
|
||||
}
|
||||
}).into()
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
Searches the entire project for the given regular expression.
|
||||
|
||||
Returns a list of paths that matched the query. For each path, it returns some excerpts of the matched text.
|
||||
|
||||
Results are paginated with 20 matches per page. Use the optional 'offset' parameter to request subsequent pages.
|
||||
|
||||
This tool is not aware of semantics and does not use any information from language servers, so it should only be used when no available semantic tool (e.g. one that uses language servers) could fit a particular use case instead.
|
||||
@@ -14,6 +14,7 @@ pub async fn replace_exact(old: &str, new: &str, snapshot: &BufferSnapshot) -> O
|
||||
true,
|
||||
PathMatcher::new(iter::empty::<&str>()).ok()?,
|
||||
PathMatcher::new(iter::empty::<&str>()).ok()?,
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.log_err()?;
|
||||
|
||||
@@ -14,7 +14,7 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ui::{IconName, Tooltip, prelude::*};
|
||||
use web_search::WebSearchRegistry;
|
||||
use zed_llm_client::WebSearchResponse;
|
||||
use zed_llm_client::{WebSearchCitation, WebSearchResponse};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct WebSearchToolInput {
|
||||
@@ -22,6 +22,7 @@ pub struct WebSearchToolInput {
|
||||
query: String,
|
||||
}
|
||||
|
||||
#[derive(RegisterComponent)]
|
||||
pub struct WebSearchTool;
|
||||
|
||||
impl Tool for WebSearchTool {
|
||||
@@ -211,3 +212,111 @@ impl ToolCard for WebSearchToolCard {
|
||||
v_flex().my_2().gap_1().child(header).children(content)
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for WebSearchTool {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::Agent
|
||||
}
|
||||
|
||||
fn sort_name() -> &'static str {
|
||||
"ToolWebSearch"
|
||||
}
|
||||
|
||||
fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
|
||||
let in_progress_search = cx.new(|cx| WebSearchToolCard {
|
||||
response: None,
|
||||
_task: cx.spawn(async move |_this, cx| {
|
||||
loop {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_secs(60))
|
||||
.await
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
let successful_search = cx.new(|_cx| WebSearchToolCard {
|
||||
response: Some(Ok(example_search_response())),
|
||||
_task: Task::ready(()),
|
||||
});
|
||||
|
||||
let error_search = cx.new(|_cx| WebSearchToolCard {
|
||||
response: Some(Err(anyhow!("Failed to resolve https://google.com"))),
|
||||
_task: Task::ready(()),
|
||||
});
|
||||
|
||||
Some(
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.children(vec![example_group(vec![
|
||||
single_example(
|
||||
"In Progress",
|
||||
div()
|
||||
.size_full()
|
||||
.child(in_progress_search.update(cx, |tool, cx| {
|
||||
tool.render(&ToolUseStatus::Pending, window, cx)
|
||||
.into_any_element()
|
||||
}))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Successful",
|
||||
div()
|
||||
.size_full()
|
||||
.child(successful_search.update(cx, |tool, cx| {
|
||||
tool.render(&ToolUseStatus::Finished("".into()), window, cx)
|
||||
.into_any_element()
|
||||
}))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Error",
|
||||
div()
|
||||
.size_full()
|
||||
.child(error_search.update(cx, |tool, cx| {
|
||||
tool.render(&ToolUseStatus::Error("".into()), window, cx)
|
||||
.into_any_element()
|
||||
}))
|
||||
.into_any_element(),
|
||||
),
|
||||
])])
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn example_search_response() -> WebSearchResponse {
|
||||
WebSearchResponse {
|
||||
summary: r#"Toronto boasts a vibrant culinary scene with a diverse array of..."#
|
||||
.to_string(),
|
||||
citations: vec![
|
||||
WebSearchCitation {
|
||||
title: "Alo".to_string(),
|
||||
url: "https://www.google.com/maps/search/Alo%2C+Toronto%2C+Canada".to_string(),
|
||||
range: Some(147..213),
|
||||
},
|
||||
WebSearchCitation {
|
||||
title: "Edulis".to_string(),
|
||||
url: "https://www.google.com/maps/search/Edulis%2C+Toronto%2C+Canada".to_string(),
|
||||
range: Some(447..519),
|
||||
},
|
||||
WebSearchCitation {
|
||||
title: "Sushi Masaki Saito".to_string(),
|
||||
url: "https://www.google.com/maps/search/Sushi+Masaki+Saito%2C+Toronto%2C+Canada"
|
||||
.to_string(),
|
||||
range: Some(776..872),
|
||||
},
|
||||
WebSearchCitation {
|
||||
title: "Shoushin".to_string(),
|
||||
url: "https://www.google.com/maps/search/Shoushin%2C+Toronto%2C+Canada".to_string(),
|
||||
range: Some(1072..1148),
|
||||
},
|
||||
WebSearchCitation {
|
||||
title: "Restaurant 20 Victoria".to_string(),
|
||||
url:
|
||||
"https://www.google.com/maps/search/Restaurant+20+Victoria%2C+Toronto%2C+Canada"
|
||||
.to_string(),
|
||||
range: Some(1291..1395),
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,10 @@ pub enum Model {
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn default_fast() -> Self {
|
||||
Self::Claude3_5Haiku
|
||||
}
|
||||
|
||||
pub fn from_id(id: &str) -> anyhow::Result<Self> {
|
||||
if id.starts_with("claude-3-5-sonnet-v2") {
|
||||
Ok(Self::Claude3_5SonnetV2)
|
||||
|
||||
@@ -117,6 +117,7 @@ CREATE TABLE "project_repositories" (
|
||||
"is_deleted" BOOL NOT NULL,
|
||||
"current_merge_conflicts" VARCHAR,
|
||||
"branch_summary" VARCHAR,
|
||||
"head_commit_details" VARCHAR,
|
||||
PRIMARY KEY (project_id, id)
|
||||
);
|
||||
|
||||
|
||||
@@ -321,8 +321,8 @@ async fn create_billing_subscription(
|
||||
}
|
||||
Some(ProductCode::ZedProTrial) => {
|
||||
stripe_billing
|
||||
.checkout_with_price(
|
||||
app.config.zed_pro_trial_price_id()?,
|
||||
.checkout_with_zed_pro_trial(
|
||||
app.config.zed_pro_price_id()?,
|
||||
customer_id,
|
||||
&user.github_login,
|
||||
&success_url,
|
||||
@@ -444,12 +444,34 @@ async fn manage_billing_subscription(
|
||||
ManageSubscriptionIntent::ManageSubscription => None,
|
||||
ManageSubscriptionIntent::UpgradeToPro => {
|
||||
let zed_pro_price_id = app.config.zed_pro_price_id()?;
|
||||
let zed_pro_trial_price_id = app.config.zed_pro_trial_price_id()?;
|
||||
let zed_free_price_id = app.config.zed_free_price_id()?;
|
||||
|
||||
let stripe_subscription =
|
||||
Subscription::retrieve(&stripe_client, &subscription_id, &[]).await?;
|
||||
|
||||
let is_on_zed_pro_trial = stripe_subscription.status == SubscriptionStatus::Trialing
|
||||
&& stripe_subscription.items.data.iter().any(|item| {
|
||||
item.price
|
||||
.as_ref()
|
||||
.map_or(false, |price| price.id == zed_pro_price_id)
|
||||
});
|
||||
if is_on_zed_pro_trial {
|
||||
// If the user is already on a Zed Pro trial and wants to upgrade to Pro, we just need to end their trial early.
|
||||
Subscription::update(
|
||||
&stripe_client,
|
||||
&stripe_subscription.id,
|
||||
stripe::UpdateSubscription {
|
||||
trial_end: Some(stripe::Scheduled::now()),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
return Ok(Json(ManageBillingSubscriptionResponse {
|
||||
billing_portal_session_url: None,
|
||||
}));
|
||||
}
|
||||
|
||||
let subscription_item_to_update = stripe_subscription
|
||||
.items
|
||||
.data
|
||||
@@ -457,7 +479,7 @@ async fn manage_billing_subscription(
|
||||
.find_map(|item| {
|
||||
let price = item.price.as_ref()?;
|
||||
|
||||
if price.id == zed_free_price_id || price.id == zed_pro_trial_price_id {
|
||||
if price.id == zed_free_price_id {
|
||||
Some(item.id.clone())
|
||||
} else {
|
||||
None
|
||||
@@ -771,16 +793,17 @@ async fn handle_customer_subscription_event(
|
||||
|
||||
let subscription_kind = maybe!({
|
||||
let zed_pro_price_id = app.config.zed_pro_price_id().ok()?;
|
||||
let zed_pro_trial_price_id = app.config.zed_pro_trial_price_id().ok()?;
|
||||
let zed_free_price_id = app.config.zed_free_price_id().ok()?;
|
||||
|
||||
subscription.items.data.iter().find_map(|item| {
|
||||
let price = item.price.as_ref()?;
|
||||
|
||||
if price.id == zed_pro_price_id {
|
||||
Some(SubscriptionKind::ZedPro)
|
||||
} else if price.id == zed_pro_trial_price_id {
|
||||
Some(SubscriptionKind::ZedProTrial)
|
||||
Some(if subscription.status == SubscriptionStatus::Trialing {
|
||||
SubscriptionKind::ZedProTrial
|
||||
} else {
|
||||
SubscriptionKind::ZedPro
|
||||
})
|
||||
} else if price.id == zed_free_price_id {
|
||||
Some(SubscriptionKind::ZedFree)
|
||||
} else {
|
||||
|
||||
@@ -119,8 +119,15 @@ impl Database {
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(
|
||||
billing_subscription::Column::StripeSubscriptionStatus
|
||||
.eq(StripeSubscriptionStatus::Active),
|
||||
Condition::any()
|
||||
.add(
|
||||
billing_subscription::Column::StripeSubscriptionStatus
|
||||
.eq(StripeSubscriptionStatus::Active),
|
||||
)
|
||||
.add(
|
||||
billing_subscription::Column::StripeSubscriptionStatus
|
||||
.eq(StripeSubscriptionStatus::Trialing),
|
||||
),
|
||||
)
|
||||
.add(billing_subscription::Column::Kind.is_not_null()),
|
||||
)
|
||||
|
||||
@@ -348,9 +348,10 @@ impl Database {
|
||||
.unwrap(),
|
||||
)),
|
||||
|
||||
// Old clients do not use abs path or entry ids.
|
||||
// Old clients do not use abs path, entry ids or head_commit_details.
|
||||
abs_path: ActiveValue::set(String::new()),
|
||||
entry_ids: ActiveValue::set("[]".into()),
|
||||
head_commit_details: ActiveValue::set(None),
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -490,6 +491,12 @@ impl Database {
|
||||
.as_ref()
|
||||
.map(|summary| serde_json::to_string(summary).unwrap()),
|
||||
),
|
||||
head_commit_details: ActiveValue::Set(
|
||||
update
|
||||
.head_commit_details
|
||||
.as_ref()
|
||||
.map(|details| serde_json::to_string(details).unwrap()),
|
||||
),
|
||||
current_merge_conflicts: ActiveValue::Set(Some(
|
||||
serde_json::to_string(&update.current_merge_conflicts).unwrap(),
|
||||
)),
|
||||
@@ -505,6 +512,7 @@ impl Database {
|
||||
project_repository::Column::EntryIds,
|
||||
project_repository::Column::AbsPath,
|
||||
project_repository::Column::CurrentMergeConflicts,
|
||||
project_repository::Column::HeadCommitDetails,
|
||||
])
|
||||
.to_owned(),
|
||||
)
|
||||
@@ -928,6 +936,13 @@ impl Database {
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
|
||||
let head_commit_details = db_repository_entry
|
||||
.head_commit_details
|
||||
.as_ref()
|
||||
.map(|head_commit_details| serde_json::from_str(&head_commit_details))
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
|
||||
let entry_ids = serde_json::from_str(&db_repository_entry.entry_ids)
|
||||
.context("failed to deserialize repository's entry ids")?;
|
||||
|
||||
@@ -954,6 +969,7 @@ impl Database {
|
||||
removed_statuses: Vec::new(),
|
||||
current_merge_conflicts,
|
||||
branch_summary,
|
||||
head_commit_details,
|
||||
scan_id: db_repository_entry.scan_id as u64,
|
||||
is_last_update: true,
|
||||
});
|
||||
|
||||
@@ -755,6 +755,13 @@ impl Database {
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
|
||||
let head_commit_details = db_repository
|
||||
.head_commit_details
|
||||
.as_ref()
|
||||
.map(|head_commit_details| serde_json::from_str(&head_commit_details))
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
|
||||
let entry_ids = serde_json::from_str(&db_repository.entry_ids)
|
||||
.context("failed to deserialize repository's entry ids")?;
|
||||
|
||||
@@ -778,6 +785,7 @@ impl Database {
|
||||
removed_statuses,
|
||||
current_merge_conflicts,
|
||||
branch_summary,
|
||||
head_commit_details,
|
||||
project_id: project_id.to_proto(),
|
||||
id: db_repository.id as u64,
|
||||
abs_path: db_repository.abs_path,
|
||||
|
||||
@@ -18,6 +18,8 @@ pub struct Model {
|
||||
pub current_merge_conflicts: Option<String>,
|
||||
// A JSON object representing the current Branch values
|
||||
pub branch_summary: Option<String>,
|
||||
// A JSON object representing the current Head commit values
|
||||
pub head_commit_details: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
||||
@@ -207,13 +207,6 @@ impl Config {
|
||||
Self::parse_stripe_price_id("Zed Pro", self.stripe_zed_pro_price_id.as_deref())
|
||||
}
|
||||
|
||||
pub fn zed_pro_trial_price_id(&self) -> anyhow::Result<stripe::PriceId> {
|
||||
Self::parse_stripe_price_id(
|
||||
"Zed Pro Trial",
|
||||
self.stripe_zed_pro_trial_price_id.as_deref(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn zed_free_price_id(&self) -> anyhow::Result<stripe::PriceId> {
|
||||
Self::parse_stripe_price_id("Zed Free", self.stripe_zed_pro_price_id.as_deref())
|
||||
}
|
||||
|
||||
@@ -405,6 +405,32 @@ impl StripeBilling {
|
||||
let session = stripe::CheckoutSession::create(&self.client, params).await?;
|
||||
Ok(session.url.context("no checkout session URL")?)
|
||||
}
|
||||
|
||||
pub async fn checkout_with_zed_pro_trial(
|
||||
&self,
|
||||
zed_pro_price_id: PriceId,
|
||||
customer_id: stripe::CustomerId,
|
||||
github_login: &str,
|
||||
success_url: &str,
|
||||
) -> Result<String> {
|
||||
let mut params = stripe::CreateCheckoutSession::new();
|
||||
params.subscription_data = Some(stripe::CreateCheckoutSessionSubscriptionData {
|
||||
trial_period_days: Some(14),
|
||||
..Default::default()
|
||||
});
|
||||
params.mode = Some(stripe::CheckoutSessionMode::Subscription);
|
||||
params.customer = Some(customer_id);
|
||||
params.client_reference_id = Some(github_login);
|
||||
params.line_items = Some(vec![stripe::CreateCheckoutSessionLineItems {
|
||||
price: Some(zed_pro_price_id.to_string()),
|
||||
quantity: Some(1),
|
||||
..Default::default()
|
||||
}]);
|
||||
params.success_url = Some(success_url);
|
||||
|
||||
let session = stripe::CheckoutSession::create(&self.client, params).await?;
|
||||
Ok(session.url.context("no checkout session URL")?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
||||
@@ -694,15 +694,7 @@ async fn test_collaborating_with_code_actions(
|
||||
// Confirming the code action will trigger a resolve request.
|
||||
let confirm_action = editor_b
|
||||
.update_in(cx_b, |editor, window, cx| {
|
||||
Editor::confirm_code_action(
|
||||
editor,
|
||||
&ConfirmCodeAction {
|
||||
item_ix: Some(0),
|
||||
from_mouse_context_menu: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
Editor::confirm_code_action(editor, &ConfirmCodeAction { item_ix: Some(0) }, window, cx)
|
||||
})
|
||||
.unwrap();
|
||||
fake_language_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>(
|
||||
|
||||
@@ -5091,6 +5091,7 @@ async fn test_project_search(
|
||||
false,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
|
||||
@@ -882,6 +882,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
false,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
|
||||
@@ -37,6 +37,7 @@ static MENTIONS_SEARCH: LazyLock<SearchQuery> = LazyLock::new(|| {
|
||||
false,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
|
||||
@@ -201,6 +201,7 @@ pub fn components() -> AllComponents {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum ComponentScope {
|
||||
Agent,
|
||||
Collaboration,
|
||||
DataDisplay,
|
||||
Editor,
|
||||
@@ -220,6 +221,7 @@ pub enum ComponentScope {
|
||||
impl Display for ComponentScope {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ComponentScope::Agent => write!(f, "Agent"),
|
||||
ComponentScope::Collaboration => write!(f, "Collaboration"),
|
||||
ComponentScope::DataDisplay => write!(f, "Data Display"),
|
||||
ComponentScope::Editor => write!(f, "Editor"),
|
||||
|
||||
@@ -41,6 +41,10 @@ pub enum Model {
|
||||
O1,
|
||||
#[serde(alias = "o1-mini", rename = "o3-mini")]
|
||||
O3Mini,
|
||||
#[serde(alias = "o3", rename = "o3")]
|
||||
O3,
|
||||
#[serde(alias = "o4-mini", rename = "o4-mini")]
|
||||
O4Mini,
|
||||
#[serde(alias = "claude-3-5-sonnet", rename = "claude-3.5-sonnet")]
|
||||
Claude3_5Sonnet,
|
||||
#[serde(alias = "claude-3-7-sonnet", rename = "claude-3.7-sonnet")]
|
||||
@@ -57,12 +61,18 @@ pub enum Model {
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn default_fast() -> Self {
|
||||
Self::Claude3_7Sonnet
|
||||
}
|
||||
|
||||
pub fn uses_streaming(&self) -> bool {
|
||||
match self {
|
||||
Self::Gpt4o
|
||||
| Self::Gpt4
|
||||
| Self::Gpt4_1
|
||||
| Self::Gpt3_5Turbo
|
||||
| Self::O3
|
||||
| Self::O4Mini
|
||||
| Self::Claude3_5Sonnet
|
||||
| Self::Claude3_7Sonnet
|
||||
| Self::Claude3_7SonnetThinking => true,
|
||||
@@ -78,6 +88,8 @@ impl Model {
|
||||
"gpt-3.5-turbo" => Ok(Self::Gpt3_5Turbo),
|
||||
"o1" => Ok(Self::O1),
|
||||
"o3-mini" => Ok(Self::O3Mini),
|
||||
"o3" => Ok(Self::O3),
|
||||
"o4-mini" => Ok(Self::O4Mini),
|
||||
"claude-3-5-sonnet" => Ok(Self::Claude3_5Sonnet),
|
||||
"claude-3-7-sonnet" => Ok(Self::Claude3_7Sonnet),
|
||||
"claude-3.7-sonnet-thought" => Ok(Self::Claude3_7SonnetThinking),
|
||||
@@ -95,6 +107,8 @@ impl Model {
|
||||
Self::Gpt4o => "gpt-4o",
|
||||
Self::O3Mini => "o3-mini",
|
||||
Self::O1 => "o1",
|
||||
Self::O3 => "o3",
|
||||
Self::O4Mini => "o4-mini",
|
||||
Self::Claude3_5Sonnet => "claude-3-5-sonnet",
|
||||
Self::Claude3_7Sonnet => "claude-3-7-sonnet",
|
||||
Self::Claude3_7SonnetThinking => "claude-3.7-sonnet-thought",
|
||||
@@ -111,6 +125,8 @@ impl Model {
|
||||
Self::Gpt4o => "GPT-4o",
|
||||
Self::O3Mini => "o3-mini",
|
||||
Self::O1 => "o1",
|
||||
Self::O3 => "o3",
|
||||
Self::O4Mini => "o4-mini",
|
||||
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
|
||||
Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
|
||||
Self::Claude3_7SonnetThinking => "Claude 3.7 Sonnet Thinking",
|
||||
@@ -123,10 +139,12 @@ impl Model {
|
||||
match self {
|
||||
Self::Gpt4o => 64_000,
|
||||
Self::Gpt4 => 32_768,
|
||||
Self::Gpt4_1 => 1_047_576,
|
||||
Self::Gpt4_1 => 128_000,
|
||||
Self::Gpt3_5Turbo => 12_288,
|
||||
Self::O3Mini => 64_000,
|
||||
Self::O1 => 20_000,
|
||||
Self::O3 => 128_000,
|
||||
Self::O4Mini => 128_000,
|
||||
Self::Claude3_5Sonnet => 200_000,
|
||||
Self::Claude3_7Sonnet => 90_000,
|
||||
Self::Claude3_7SonnetThinking => 90_000,
|
||||
|
||||
@@ -447,12 +447,35 @@ impl RunningState {
|
||||
workspace::PaneGroup::with_root(root)
|
||||
} else {
|
||||
pane_close_subscriptions.clear();
|
||||
let module_list = if session
|
||||
.read(cx)
|
||||
.capabilities()
|
||||
.supports_modules_request
|
||||
.unwrap_or(false)
|
||||
{
|
||||
Some(&module_list)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let loaded_source_list = if session
|
||||
.read(cx)
|
||||
.capabilities()
|
||||
.supports_loaded_sources_request
|
||||
.unwrap_or(false)
|
||||
{
|
||||
Some(&loaded_source_list)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let root = Self::default_pane_layout(
|
||||
project,
|
||||
&workspace,
|
||||
&stack_frame_list,
|
||||
&variable_list,
|
||||
&module_list,
|
||||
module_list,
|
||||
loaded_source_list,
|
||||
&console,
|
||||
&breakpoint_list,
|
||||
&mut pane_close_subscriptions,
|
||||
@@ -923,7 +946,8 @@ impl RunningState {
|
||||
workspace: &WeakEntity<Workspace>,
|
||||
stack_frame_list: &Entity<StackFrameList>,
|
||||
variable_list: &Entity<VariableList>,
|
||||
module_list: &Entity<ModuleList>,
|
||||
module_list: Option<&Entity<ModuleList>>,
|
||||
loaded_source_list: Option<&Entity<LoadedSourceList>>,
|
||||
console: &Entity<Console>,
|
||||
breakpoints: &Entity<BreakpointList>,
|
||||
subscriptions: &mut HashMap<EntityId, Subscription>,
|
||||
@@ -963,6 +987,7 @@ impl RunningState {
|
||||
this.activate_item(0, false, false, window, cx);
|
||||
});
|
||||
let center_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
|
||||
|
||||
center_pane.update(cx, |this, cx| {
|
||||
this.add_item(
|
||||
Box::new(SubView::new(
|
||||
@@ -978,22 +1003,43 @@ impl RunningState {
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
this.add_item(
|
||||
Box::new(SubView::new(
|
||||
this.focus_handle(cx),
|
||||
module_list.clone().into(),
|
||||
DebuggerPaneItem::Modules,
|
||||
if let Some(module_list) = module_list {
|
||||
this.add_item(
|
||||
Box::new(SubView::new(
|
||||
module_list.focus_handle(cx),
|
||||
module_list.clone().into(),
|
||||
DebuggerPaneItem::Modules,
|
||||
None,
|
||||
cx,
|
||||
)),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
this.activate_item(0, false, false, window, cx);
|
||||
);
|
||||
this.activate_item(0, false, false, window, cx);
|
||||
}
|
||||
|
||||
if let Some(loaded_source_list) = loaded_source_list {
|
||||
this.add_item(
|
||||
Box::new(SubView::new(
|
||||
loaded_source_list.focus_handle(cx),
|
||||
loaded_source_list.clone().into(),
|
||||
DebuggerPaneItem::LoadedSources,
|
||||
None,
|
||||
cx,
|
||||
)),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
this.activate_item(1, false, false, window, cx);
|
||||
}
|
||||
});
|
||||
|
||||
let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
|
||||
rightmost_pane.update(cx, |this, cx| {
|
||||
let weak_console = console.downgrade();
|
||||
|
||||
@@ -64,6 +64,10 @@ pub enum Model {
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn default_fast() -> Self {
|
||||
Model::Chat
|
||||
}
|
||||
|
||||
pub fn from_id(id: &str) -> Result<Self> {
|
||||
match id {
|
||||
"deepseek-chat" => Ok(Self::Chat),
|
||||
|
||||
@@ -99,9 +99,6 @@ pub struct ComposeCompletion {
|
||||
pub struct ConfirmCodeAction {
|
||||
#[serde(default)]
|
||||
pub item_ix: Option<usize>,
|
||||
#[serde(default)]
|
||||
#[serde(skip)]
|
||||
pub from_mouse_context_menu: bool,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
|
||||
|
||||
@@ -632,6 +632,7 @@ impl CompletionsMenu {
|
||||
MarkdownElement::new(markdown.clone(), hover_markdown_style(window, cx))
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button: false,
|
||||
border: false,
|
||||
})
|
||||
.on_url_click(open_markdown_url),
|
||||
)
|
||||
@@ -774,13 +775,36 @@ pub struct AvailableCodeAction {
|
||||
pub provider: Rc<dyn CodeActionProvider>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
#[derive(Clone)]
|
||||
pub struct CodeActionContents {
|
||||
pub tasks: Option<Rc<ResolvedTasks>>,
|
||||
pub actions: Option<Rc<[AvailableCodeAction]>>,
|
||||
tasks: Option<Rc<ResolvedTasks>>,
|
||||
actions: Option<Rc<[AvailableCodeAction]>>,
|
||||
}
|
||||
|
||||
impl CodeActionContents {
|
||||
pub fn new(
|
||||
mut tasks: Option<ResolvedTasks>,
|
||||
actions: Option<Rc<[AvailableCodeAction]>>,
|
||||
cx: &App,
|
||||
) -> Self {
|
||||
if !cx.has_flag::<Debugger>() {
|
||||
if let Some(tasks) = &mut tasks {
|
||||
tasks
|
||||
.templates
|
||||
.retain(|(_, task)| !matches!(task.task_type(), task::TaskType::Debug(_)));
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
tasks: tasks.map(Rc::new),
|
||||
actions,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tasks(&self) -> Option<&ResolvedTasks> {
|
||||
self.tasks.as_deref()
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
match (&self.tasks, &self.actions) {
|
||||
(Some(tasks), Some(actions)) => actions.len() + tasks.templates.len(),
|
||||
@@ -790,7 +814,7 @@ impl CodeActionContents {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
fn is_empty(&self) -> bool {
|
||||
match (&self.tasks, &self.actions) {
|
||||
(Some(tasks), Some(actions)) => actions.is_empty() && tasks.templates.is_empty(),
|
||||
(Some(tasks), None) => tasks.templates.is_empty(),
|
||||
@@ -799,7 +823,7 @@ impl CodeActionContents {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = CodeActionsItem> + '_ {
|
||||
fn iter(&self) -> impl Iterator<Item = CodeActionsItem> + '_ {
|
||||
self.tasks
|
||||
.iter()
|
||||
.flat_map(|tasks| {
|
||||
@@ -867,14 +891,14 @@ pub enum CodeActionsItem {
|
||||
}
|
||||
|
||||
impl CodeActionsItem {
|
||||
pub fn as_task(&self) -> Option<&ResolvedTask> {
|
||||
fn as_task(&self) -> Option<&ResolvedTask> {
|
||||
let Self::Task(_, task) = self else {
|
||||
return None;
|
||||
};
|
||||
Some(task)
|
||||
}
|
||||
|
||||
pub fn as_code_action(&self) -> Option<&CodeAction> {
|
||||
fn as_code_action(&self) -> Option<&CodeAction> {
|
||||
let Self::CodeAction { action, .. } = self else {
|
||||
return None;
|
||||
};
|
||||
@@ -988,17 +1012,6 @@ impl CodeActionsMenu {
|
||||
.iter()
|
||||
.skip(range.start)
|
||||
.take(range.end - range.start)
|
||||
.filter(|action| {
|
||||
if action
|
||||
.as_task()
|
||||
.map(|task| matches!(task.task_type(), task::TaskType::Debug(_)))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
cx.has_flag::<Debugger>()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.enumerate()
|
||||
.map(|(ix, action)| {
|
||||
let item_ix = range.start + ix;
|
||||
@@ -1014,7 +1027,6 @@ impl CodeActionsMenu {
|
||||
if let Some(task) = editor.confirm_code_action(
|
||||
&ConfirmCodeAction {
|
||||
item_ix: Some(item_ix),
|
||||
from_mouse_context_menu: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
@@ -1040,7 +1052,6 @@ impl CodeActionsMenu {
|
||||
if let Some(task) = editor.confirm_code_action(
|
||||
&ConfirmCodeAction {
|
||||
item_ix: Some(item_ix),
|
||||
from_mouse_context_menu: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
|
||||
@@ -215,6 +215,7 @@ const MAX_SELECTION_HISTORY_LEN: usize = 1024;
|
||||
pub(crate) const CURSORS_VISIBLE_FOR: Duration = Duration::from_millis(2000);
|
||||
#[doc(hidden)]
|
||||
pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250);
|
||||
const SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100);
|
||||
|
||||
pub(crate) const CODE_ACTION_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
pub(crate) const FORMAT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
@@ -811,8 +812,8 @@ pub struct Editor {
|
||||
next_completion_id: CompletionId,
|
||||
available_code_actions: Option<(Location, Rc<[AvailableCodeAction]>)>,
|
||||
code_actions_task: Option<Task<Result<()>>>,
|
||||
force_code_action_task: bool,
|
||||
selection_highlight_task: Option<Task<()>>,
|
||||
quick_selection_highlight_task: Option<(Range<Anchor>, Task<()>)>,
|
||||
debounced_selection_highlight_task: Option<(Range<Anchor>, Task<()>)>,
|
||||
document_highlights_task: Option<Task<()>>,
|
||||
linked_editing_range_task: Option<Task<Option<()>>>,
|
||||
linked_edit_ranges: linked_editing_ranges::LinkedEditingRanges,
|
||||
@@ -1591,8 +1592,8 @@ impl Editor {
|
||||
code_action_providers,
|
||||
available_code_actions: Default::default(),
|
||||
code_actions_task: Default::default(),
|
||||
force_code_action_task: false,
|
||||
selection_highlight_task: Default::default(),
|
||||
quick_selection_highlight_task: Default::default(),
|
||||
debounced_selection_highlight_task: Default::default(),
|
||||
document_highlights_task: Default::default(),
|
||||
linked_editing_range_task: Default::default(),
|
||||
pending_rename: Default::default(),
|
||||
@@ -1789,9 +1790,9 @@ impl Editor {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.mouse_context_menu = Some(MouseContextMenu::new(
|
||||
self,
|
||||
crate::mouse_context_menu::MenuPosition::PinnedToScreen(position),
|
||||
context_menu,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
));
|
||||
@@ -4818,19 +4819,18 @@ impl Editor {
|
||||
let suffix = &old_text[lookbehind.min(old_text.len())..];
|
||||
|
||||
let selections = self.selections.all::<usize>(cx);
|
||||
let mut edits = Vec::new();
|
||||
let mut ranges = Vec::new();
|
||||
let mut linked_edits = HashMap::<_, Vec<_>>::default();
|
||||
|
||||
for selection in &selections {
|
||||
let edit = if selection.id == newest_anchor.id {
|
||||
(replace_range_multibuffer.clone(), new_text.as_str())
|
||||
let range = if selection.id == newest_anchor.id {
|
||||
replace_range_multibuffer.clone()
|
||||
} else {
|
||||
let mut range = selection.range();
|
||||
let mut text = new_text.as_str();
|
||||
|
||||
// if prefix is present, don't duplicate it
|
||||
if snapshot.contains_str_at(range.start.saturating_sub(lookbehind), prefix) {
|
||||
text = &new_text[lookbehind.min(new_text.len())..];
|
||||
range.start = range.start.saturating_sub(lookbehind);
|
||||
|
||||
// if suffix is also present, mimic the newest cursor and replace it
|
||||
if selection.id != newest_anchor.id
|
||||
@@ -4839,10 +4839,10 @@ impl Editor {
|
||||
range.end += lookahead;
|
||||
}
|
||||
}
|
||||
(range, text)
|
||||
range
|
||||
};
|
||||
|
||||
edits.push(edit);
|
||||
ranges.push(range);
|
||||
|
||||
if !self.linked_edit_ranges.is_empty() {
|
||||
let start_anchor = snapshot.anchor_before(selection.head());
|
||||
@@ -4868,19 +4868,14 @@ impl Editor {
|
||||
self.transact(window, cx, |this, window, cx| {
|
||||
if let Some(mut snippet) = snippet {
|
||||
snippet.text = new_text.to_string();
|
||||
let ranges = edits
|
||||
.iter()
|
||||
.map(|(range, _)| range.clone())
|
||||
.collect::<Vec<_>>();
|
||||
this.insert_snippet(&ranges, snippet, window, cx).log_err();
|
||||
} else {
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
let auto_indent = if completion.insert_text_mode == Some(InsertTextMode::AS_IS)
|
||||
{
|
||||
None
|
||||
} else {
|
||||
this.autoindent_mode.clone()
|
||||
let auto_indent = match completion.insert_text_mode {
|
||||
Some(InsertTextMode::AS_IS) => None,
|
||||
_ => this.autoindent_mode.clone(),
|
||||
};
|
||||
let edits = ranges.into_iter().map(|range| (range, new_text.as_str()));
|
||||
buffer.edit(edits, auto_indent, cx);
|
||||
});
|
||||
}
|
||||
@@ -5021,15 +5016,15 @@ impl Editor {
|
||||
None => None,
|
||||
};
|
||||
let resolved_tasks =
|
||||
tasks.zip(task_context).map(|(tasks, task_context)| {
|
||||
Rc::new(ResolvedTasks {
|
||||
tasks
|
||||
.zip(task_context)
|
||||
.map(|(tasks, task_context)| ResolvedTasks {
|
||||
templates: tasks.resolve(&task_context).collect(),
|
||||
position: snapshot.buffer_snapshot.anchor_before(Point::new(
|
||||
multibuffer_point.row,
|
||||
tasks.column,
|
||||
)),
|
||||
})
|
||||
});
|
||||
});
|
||||
let spawn_straight_away = resolved_tasks.as_ref().map_or(false, |tasks| {
|
||||
tasks
|
||||
.templates
|
||||
@@ -5050,20 +5045,18 @@ impl Editor {
|
||||
*editor.context_menu.borrow_mut() =
|
||||
Some(CodeContextMenu::CodeActions(CodeActionsMenu {
|
||||
buffer,
|
||||
actions: CodeActionContents {
|
||||
tasks: resolved_tasks,
|
||||
actions: code_actions,
|
||||
},
|
||||
actions: CodeActionContents::new(
|
||||
resolved_tasks,
|
||||
code_actions,
|
||||
cx,
|
||||
),
|
||||
selected_item: Default::default(),
|
||||
scroll_handle: UniformListScrollHandle::default(),
|
||||
deployed_from_indicator,
|
||||
}));
|
||||
if spawn_straight_away {
|
||||
if let Some(task) = editor.confirm_code_action(
|
||||
&ConfirmCodeAction {
|
||||
item_ix: Some(0),
|
||||
from_mouse_context_menu: false,
|
||||
},
|
||||
&ConfirmCodeAction { item_ix: Some(0) },
|
||||
window,
|
||||
cx,
|
||||
) {
|
||||
@@ -5100,27 +5093,17 @@ impl Editor {
|
||||
) -> Option<Task<Result<()>>> {
|
||||
self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction);
|
||||
|
||||
let (action, buffer) = if action.from_mouse_context_menu {
|
||||
if let Some(menu) = self.mouse_context_menu.take() {
|
||||
let code_action_state = menu.code_action_state?;
|
||||
let index = action.item_ix?;
|
||||
let action = code_action_state.contents.get(index)?;
|
||||
(action, code_action_state.buffer)
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
let actions_menu =
|
||||
if let CodeContextMenu::CodeActions(menu) = self.hide_context_menu(window, cx)? {
|
||||
let action_ix = action.item_ix.unwrap_or(menu.selected_item);
|
||||
let action = menu.actions.get(action_ix)?;
|
||||
let buffer = menu.buffer;
|
||||
(action, buffer)
|
||||
menu
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
let action_ix = action.item_ix.unwrap_or(actions_menu.selected_item);
|
||||
let action = actions_menu.actions.get(action_ix)?;
|
||||
let title = action.label();
|
||||
let buffer = actions_menu.buffer;
|
||||
let workspace = self.workspace()?;
|
||||
|
||||
match action {
|
||||
@@ -5325,14 +5308,11 @@ impl Editor {
|
||||
if start_buffer != end_buffer {
|
||||
return None;
|
||||
}
|
||||
let force_code_action_task = self.force_code_action_task;
|
||||
|
||||
self.code_actions_task = Some(cx.spawn_in(window, async move |this, cx| {
|
||||
if !force_code_action_task {
|
||||
cx.background_executor()
|
||||
.timer(CODE_ACTIONS_DEBOUNCE_TIMEOUT)
|
||||
.await;
|
||||
}
|
||||
cx.background_executor()
|
||||
.timer(CODE_ACTIONS_DEBOUNCE_TIMEOUT)
|
||||
.await;
|
||||
|
||||
let (providers, tasks) = this.update_in(cx, |this, window, cx| {
|
||||
let providers = this.code_action_providers.clone();
|
||||
@@ -5498,111 +5478,169 @@ impl Editor {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn refresh_selected_text_highlights(
|
||||
fn prepare_highlight_query_from_selection(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
) -> Option<(String, Range<Anchor>)> {
|
||||
if matches!(self.mode, EditorMode::SingleLine { .. }) {
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
self.selection_highlight_task.take();
|
||||
if !EditorSettings::get_global(cx).selection_highlight {
|
||||
self.clear_background_highlights::<SelectedTextHighlight>(cx);
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
if self.selections.count() != 1 || self.selections.line_mode {
|
||||
self.clear_background_highlights::<SelectedTextHighlight>(cx);
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
let selection = self.selections.newest::<Point>(cx);
|
||||
if selection.is_empty() || selection.start.row != selection.end.row {
|
||||
self.clear_background_highlights::<SelectedTextHighlight>(cx);
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
let debounce = EditorSettings::get_global(cx).selection_highlight_debounce;
|
||||
self.selection_highlight_task = Some(cx.spawn_in(window, async move |editor, cx| {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(debounce))
|
||||
.await;
|
||||
let Some(Some(matches_task)) = editor
|
||||
.update_in(cx, |editor, _, cx| {
|
||||
if editor.selections.count() != 1 || editor.selections.line_mode {
|
||||
editor.clear_background_highlights::<SelectedTextHighlight>(cx);
|
||||
return None;
|
||||
}
|
||||
let selection = editor.selections.newest::<Point>(cx);
|
||||
if selection.is_empty() || selection.start.row != selection.end.row {
|
||||
editor.clear_background_highlights::<SelectedTextHighlight>(cx);
|
||||
return None;
|
||||
}
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
let query = buffer.text_for_range(selection.range()).collect::<String>();
|
||||
if query.trim().is_empty() {
|
||||
editor.clear_background_highlights::<SelectedTextHighlight>(cx);
|
||||
return None;
|
||||
}
|
||||
Some(cx.background_spawn(async move {
|
||||
let mut ranges = Vec::new();
|
||||
let selection_anchors = selection.range().to_anchors(&buffer);
|
||||
for range in [buffer.anchor_before(0)..buffer.anchor_after(buffer.len())] {
|
||||
for (search_buffer, search_range, excerpt_id) in
|
||||
buffer.range_to_buffer_ranges(range)
|
||||
{
|
||||
ranges.extend(
|
||||
project::search::SearchQuery::text(
|
||||
query.clone(),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
.search(search_buffer, Some(search_range.clone()))
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(
|
||||
|match_range| {
|
||||
let start = search_buffer.anchor_after(
|
||||
search_range.start + match_range.start,
|
||||
);
|
||||
let end = search_buffer.anchor_before(
|
||||
search_range.start + match_range.end,
|
||||
);
|
||||
let range = Anchor::range_in_buffer(
|
||||
excerpt_id,
|
||||
search_buffer.remote_id(),
|
||||
start..end,
|
||||
);
|
||||
(range != selection_anchors).then_some(range)
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
ranges
|
||||
}))
|
||||
})
|
||||
.log_err()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let matches = matches_task.await;
|
||||
let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
|
||||
let selection_anchor_range = selection.range().to_anchors(&multi_buffer_snapshot);
|
||||
let query = multi_buffer_snapshot
|
||||
.text_for_range(selection_anchor_range.clone())
|
||||
.collect::<String>();
|
||||
if query.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some((query, selection_anchor_range))
|
||||
}
|
||||
|
||||
fn update_selection_occurrence_highlights(
|
||||
&mut self,
|
||||
query_text: String,
|
||||
query_range: Range<Anchor>,
|
||||
multi_buffer_range_to_query: Range<Point>,
|
||||
use_debounce: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Task<()> {
|
||||
let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
|
||||
cx.spawn_in(window, async move |editor, cx| {
|
||||
if use_debounce {
|
||||
cx.background_executor()
|
||||
.timer(SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT)
|
||||
.await;
|
||||
}
|
||||
let match_task = cx.background_spawn(async move {
|
||||
let buffer_ranges = multi_buffer_snapshot
|
||||
.range_to_buffer_ranges(multi_buffer_range_to_query)
|
||||
.into_iter()
|
||||
.filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty());
|
||||
let mut match_ranges = Vec::new();
|
||||
for (buffer_snapshot, search_range, excerpt_id) in buffer_ranges {
|
||||
match_ranges.extend(
|
||||
project::search::SearchQuery::text(
|
||||
query_text.clone(),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
.search(&buffer_snapshot, Some(search_range.clone()))
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|match_range| {
|
||||
let match_start = buffer_snapshot
|
||||
.anchor_after(search_range.start + match_range.start);
|
||||
let match_end =
|
||||
buffer_snapshot.anchor_before(search_range.start + match_range.end);
|
||||
let match_anchor_range = Anchor::range_in_buffer(
|
||||
excerpt_id,
|
||||
buffer_snapshot.remote_id(),
|
||||
match_start..match_end,
|
||||
);
|
||||
(match_anchor_range != query_range).then_some(match_anchor_range)
|
||||
}),
|
||||
);
|
||||
}
|
||||
match_ranges
|
||||
});
|
||||
let match_ranges = match_task.await;
|
||||
editor
|
||||
.update_in(cx, |editor, _, cx| {
|
||||
editor.clear_background_highlights::<SelectedTextHighlight>(cx);
|
||||
if !matches.is_empty() {
|
||||
if !match_ranges.is_empty() {
|
||||
editor.highlight_background::<SelectedTextHighlight>(
|
||||
&matches,
|
||||
&match_ranges,
|
||||
|theme| theme.editor_document_highlight_bracket_background,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
}));
|
||||
})
|
||||
}
|
||||
|
||||
fn refresh_selected_text_highlights(&mut self, window: &mut Window, cx: &mut Context<Editor>) {
|
||||
let Some((query_text, query_range)) = self.prepare_highlight_query_from_selection(cx)
|
||||
else {
|
||||
self.clear_background_highlights::<SelectedTextHighlight>(cx);
|
||||
self.quick_selection_highlight_task.take();
|
||||
self.debounced_selection_highlight_task.take();
|
||||
return;
|
||||
};
|
||||
let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
|
||||
if self
|
||||
.quick_selection_highlight_task
|
||||
.as_ref()
|
||||
.map_or(true, |(prev_anchor_range, _)| {
|
||||
prev_anchor_range != &query_range
|
||||
})
|
||||
{
|
||||
let multi_buffer_visible_start = self
|
||||
.scroll_manager
|
||||
.anchor()
|
||||
.anchor
|
||||
.to_point(&multi_buffer_snapshot);
|
||||
let multi_buffer_visible_end = multi_buffer_snapshot.clip_point(
|
||||
multi_buffer_visible_start
|
||||
+ Point::new(self.visible_line_count().unwrap_or(0.).ceil() as u32, 0),
|
||||
Bias::Left,
|
||||
);
|
||||
let multi_buffer_visible_range = multi_buffer_visible_start..multi_buffer_visible_end;
|
||||
self.quick_selection_highlight_task = Some((
|
||||
query_range.clone(),
|
||||
self.update_selection_occurrence_highlights(
|
||||
query_text.clone(),
|
||||
query_range.clone(),
|
||||
multi_buffer_visible_range,
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
));
|
||||
}
|
||||
if self
|
||||
.debounced_selection_highlight_task
|
||||
.as_ref()
|
||||
.map_or(true, |(prev_anchor_range, _)| {
|
||||
prev_anchor_range != &query_range
|
||||
})
|
||||
{
|
||||
let multi_buffer_start = multi_buffer_snapshot
|
||||
.anchor_before(0)
|
||||
.to_point(&multi_buffer_snapshot);
|
||||
let multi_buffer_end = multi_buffer_snapshot
|
||||
.anchor_after(multi_buffer_snapshot.len())
|
||||
.to_point(&multi_buffer_snapshot);
|
||||
let multi_buffer_full_range = multi_buffer_start..multi_buffer_end;
|
||||
self.debounced_selection_highlight_task = Some((
|
||||
query_range.clone(),
|
||||
self.update_selection_occurrence_highlights(
|
||||
query_text,
|
||||
query_range,
|
||||
multi_buffer_full_range,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh_inline_completion(
|
||||
@@ -8920,7 +8958,6 @@ impl Editor {
|
||||
self,
|
||||
source,
|
||||
clicked_point,
|
||||
None,
|
||||
context_menu,
|
||||
window,
|
||||
cx,
|
||||
@@ -10180,11 +10217,19 @@ impl Editor {
|
||||
..Point::new(row.0, buffer.line_len(row)),
|
||||
);
|
||||
for row in start.row + 1..=end.row {
|
||||
let mut line_len = buffer.line_len(MultiBufferRow(row));
|
||||
if row == end.row {
|
||||
line_len = end.column;
|
||||
}
|
||||
if line_len == 0 {
|
||||
trimmed_selections
|
||||
.push(Point::new(row, 0)..Point::new(row, line_len));
|
||||
continue;
|
||||
}
|
||||
let row_indent_size = buffer.indent_size_for_line(MultiBufferRow(row));
|
||||
if row_indent_size.len >= first_indent.len {
|
||||
trimmed_selections.push(
|
||||
Point::new(row, first_indent.len)
|
||||
..Point::new(row, buffer.line_len(MultiBufferRow(row))),
|
||||
Point::new(row, first_indent.len)..Point::new(row, line_len),
|
||||
);
|
||||
} else {
|
||||
trimmed_selections.clear();
|
||||
|
||||
@@ -10,7 +10,6 @@ pub struct EditorSettings {
|
||||
pub cursor_shape: Option<CursorShape>,
|
||||
pub current_line_highlight: CurrentLineHighlight,
|
||||
pub selection_highlight: bool,
|
||||
pub selection_highlight_debounce: u64,
|
||||
pub lsp_highlight_debounce: u64,
|
||||
pub hover_popover_enabled: bool,
|
||||
pub hover_popover_delay: u64,
|
||||
@@ -263,10 +262,6 @@ pub struct EditorSettingsContent {
|
||||
///
|
||||
/// Default: true
|
||||
pub selection_highlight: Option<bool>,
|
||||
/// The debounce delay before querying highlights based on the selected text.
|
||||
///
|
||||
/// Default: 75
|
||||
pub selection_highlight_debounce: Option<u64>,
|
||||
/// The debounce delay before querying highlights from the language
|
||||
/// server based on the current cursor location.
|
||||
///
|
||||
|
||||
@@ -5121,6 +5121,36 @@ if is_entire_line {
|
||||
),
|
||||
"When selecting past the indent, nothing is trimmed"
|
||||
);
|
||||
|
||||
cx.set_state(
|
||||
r#" «for selection in selections.iter() {
|
||||
let mut start = selection.start;
|
||||
|
||||
let mut end = selection.end;
|
||||
let is_entire_line = selection.is_empty();
|
||||
if is_entire_line {
|
||||
start = Point::new(start.row, 0);
|
||||
ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
|
||||
}
|
||||
"#,
|
||||
);
|
||||
cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
|
||||
assert_eq!(
|
||||
cx.read_from_clipboard()
|
||||
.and_then(|item| item.text().as_deref().map(str::to_string)),
|
||||
Some(
|
||||
"for selection in selections.iter() {
|
||||
let mut start = selection.start;
|
||||
|
||||
let mut end = selection.end;
|
||||
let is_entire_line = selection.is_empty();
|
||||
if is_entire_line {
|
||||
start = Point::new(start.row, 0);
|
||||
"
|
||||
.to_string()
|
||||
),
|
||||
"Copying with stripping should ignore empty lines"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -10104,7 +10134,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_completion_replacing_suffix_in_multicursors(cx: &mut TestAppContext) {
|
||||
async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
@@ -10118,6 +10148,8 @@ async fn test_completion_replacing_suffix_in_multicursors(cx: &mut TestAppContex
|
||||
)
|
||||
.await;
|
||||
|
||||
// scenario: surrounding text matches completion text
|
||||
let completion_text = "to_offset";
|
||||
let initial_state = indoc! {"
|
||||
1. buf.to_offˇsuffix
|
||||
2. buf.to_offˇsuf
|
||||
@@ -10148,7 +10180,6 @@ async fn test_completion_replacing_suffix_in_multicursors(cx: &mut TestAppContex
|
||||
|
||||
buf.<to_off|suffix> // newest cursor
|
||||
"};
|
||||
let completion_text = "to_offset";
|
||||
let expected = indoc! {"
|
||||
1. buf.to_offsetˇ
|
||||
2. buf.to_offsetˇsuf
|
||||
@@ -10164,24 +10195,122 @@ async fn test_completion_replacing_suffix_in_multicursors(cx: &mut TestAppContex
|
||||
|
||||
buf.to_offsetˇ // newest cursor
|
||||
"};
|
||||
|
||||
cx.set_state(initial_state);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||
});
|
||||
|
||||
let counter = Arc::new(AtomicUsize::new(0));
|
||||
handle_completion_request_with_insert_and_replace(
|
||||
&mut cx,
|
||||
completion_marked_buffer,
|
||||
vec![completion_text],
|
||||
counter.clone(),
|
||||
Arc::new(AtomicUsize::new(0)),
|
||||
)
|
||||
.await;
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
|
||||
let apply_additional_edits = cx.update_editor(|editor, window, cx| {
|
||||
editor
|
||||
.confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
|
||||
.unwrap()
|
||||
});
|
||||
cx.assert_editor_state(expected);
|
||||
handle_resolve_completion_request(&mut cx, None).await;
|
||||
apply_additional_edits.await.unwrap();
|
||||
|
||||
// scenario: surrounding text matches surroundings of newest cursor, inserting at the end
|
||||
let completion_text = "foo_and_bar";
|
||||
let initial_state = indoc! {"
|
||||
1. ooanbˇ
|
||||
2. zooanbˇ
|
||||
3. ooanbˇz
|
||||
4. zooanbˇz
|
||||
5. ooanˇ
|
||||
6. oanbˇ
|
||||
|
||||
ooanbˇ
|
||||
"};
|
||||
let completion_marked_buffer = indoc! {"
|
||||
1. ooanb
|
||||
2. zooanb
|
||||
3. ooanbz
|
||||
4. zooanbz
|
||||
5. ooan
|
||||
6. oanb
|
||||
|
||||
<ooanb|>
|
||||
"};
|
||||
let expected = indoc! {"
|
||||
1. foo_and_barˇ
|
||||
2. zfoo_and_barˇ
|
||||
3. foo_and_barˇz
|
||||
4. zfoo_and_barˇz
|
||||
5. ooanfoo_and_barˇ
|
||||
6. oanbfoo_and_barˇ
|
||||
|
||||
foo_and_barˇ
|
||||
"};
|
||||
cx.set_state(initial_state);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||
});
|
||||
handle_completion_request_with_insert_and_replace(
|
||||
&mut cx,
|
||||
completion_marked_buffer,
|
||||
vec![completion_text],
|
||||
Arc::new(AtomicUsize::new(0)),
|
||||
)
|
||||
.await;
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
let apply_additional_edits = cx.update_editor(|editor, window, cx| {
|
||||
editor
|
||||
.confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
|
||||
.unwrap()
|
||||
});
|
||||
cx.assert_editor_state(expected);
|
||||
handle_resolve_completion_request(&mut cx, None).await;
|
||||
apply_additional_edits.await.unwrap();
|
||||
|
||||
// scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
|
||||
// (expects the same as if it was inserted at the end)
|
||||
let completion_text = "foo_and_bar";
|
||||
let initial_state = indoc! {"
|
||||
1. ooˇanb
|
||||
2. zooˇanb
|
||||
3. ooˇanbz
|
||||
4. zooˇanbz
|
||||
|
||||
ooˇanb
|
||||
"};
|
||||
let completion_marked_buffer = indoc! {"
|
||||
1. ooanb
|
||||
2. zooanb
|
||||
3. ooanbz
|
||||
4. zooanbz
|
||||
|
||||
<oo|anb>
|
||||
"};
|
||||
let expected = indoc! {"
|
||||
1. foo_and_barˇ
|
||||
2. zfoo_and_barˇ
|
||||
3. foo_and_barˇz
|
||||
4. zfoo_and_barˇz
|
||||
|
||||
foo_and_barˇ
|
||||
"};
|
||||
cx.set_state(initial_state);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||
});
|
||||
handle_completion_request_with_insert_and_replace(
|
||||
&mut cx,
|
||||
completion_marked_buffer,
|
||||
vec![completion_text],
|
||||
Arc::new(AtomicUsize::new(0)),
|
||||
)
|
||||
.await;
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
let apply_additional_edits = cx.update_editor(|editor, window, cx| {
|
||||
editor
|
||||
.confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
|
||||
|
||||
@@ -1007,9 +1007,6 @@ impl EditorElement {
|
||||
} else {
|
||||
editor.hide_hovered_link(cx);
|
||||
hover_at(editor, None, window, cx);
|
||||
if gutter_hovered {
|
||||
cx.stop_propagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2095,8 +2092,7 @@ impl EditorElement {
|
||||
})) = editor.context_menu.borrow().as_ref()
|
||||
{
|
||||
actions
|
||||
.tasks
|
||||
.as_ref()
|
||||
.tasks()
|
||||
.map(|tasks| tasks.position.to_display_point(snapshot).row())
|
||||
.or(*deployed_from_indicator)
|
||||
} else {
|
||||
|
||||
@@ -841,6 +841,7 @@ impl InfoPopover {
|
||||
MarkdownElement::new(markdown, hover_markdown_style(window, cx))
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button: false,
|
||||
border: false,
|
||||
})
|
||||
.on_url_click(open_markdown_url),
|
||||
),
|
||||
@@ -969,6 +970,7 @@ impl DiagnosticPopover {
|
||||
})
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button: false,
|
||||
border: false,
|
||||
})
|
||||
.on_url_click(open_markdown_url),
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user