Compare commits

..

1 Commits

Author SHA1 Message Date
Nate Butler
ac3c7dda36 semi_uniform_list 2025-04-04 12:29:06 -04:00
409 changed files with 15629 additions and 22374 deletions

View File

@@ -1,36 +0,0 @@
name: Bug Report (Agent Panel)
description: Zed Agent Panel Bugs
type: "Bug"
labels: ["agent", "ai"]
title: "Agent Panel: <a short description of the Agent Panel bug>"
body:
- type: textarea
attributes:
label: Summary
description: Describe the bug with a one line summary, and provide detailed reproduction steps
value: |
<!-- Please insert a one line summary of the issue below -->
SUMMARY_SENTENCE_HERE
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
<!-- Please include the LLM provider and model name you are using -->
Steps to trigger the problem:
1.
2.
3.
Actual Behavior:
Expected Behavior:
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"'
placeholder: |
Output of "zed: Copy System Specs Into Clipboard"
validations:
required: true

View File

@@ -1,36 +0,0 @@
name: Bug Report (Edit Predictions)
description: Zed Edit Predictions bugs
type: "Bug"
labels: ["ai", "inline completion", "zeta"]
title: "Edit Predictions: <a short description of the Edit Prediction bug>"
body:
- type: textarea
attributes:
label: Summary
description: Describe the bug with a one line summary, and provide detailed reproduction steps
value: |
<!-- Please insert a one line summary of the issue below -->
SUMMARY_SENTENCE_HERE
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
<!-- Please include the LLM provider and model name you are using -->
Steps to trigger the problem:
1.
2.
3.
Actual Behavior:
Expected Behavior:
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"'
placeholder: |
Output of "zed: Copy System Specs Into Clipboard"
validations:
required: true

View File

@@ -1,35 +0,0 @@
name: Bug Report (Git)
description: Zed Git-Related Bugs
type: "Bug"
labels: ["git"]
title: "Git: <a short description of the Git bug>"
body:
- type: textarea
attributes:
label: Summary
description: Describe the bug with a one line summary, and provide detailed reproduction steps
value: |
<!-- Please insert a one line summary of the issue below -->
SUMMARY_SENTENCE_HERE
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
Steps to trigger the problem:
1.
2.
3.
Actual Behavior:
Expected Behavior:
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"'
placeholder: |
Output of "zed: Copy System Specs Into Clipboard"
validations:
required: true

View File

@@ -1,56 +0,0 @@
name: Bug Report (Other)
description: |
Something else is broken in Zed (exclude crashing).
type: "Bug"
body:
- type: textarea
attributes:
label: Summary
description: Provide a one sentence summary and detailed reproduction steps
value: |
<!-- Begin your issue with a one sentence summary -->
SUMMARY_SENTENCE_HERE
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install.
- Any code must be sufficient to reproduce (include context!)
- Code must as text, not just as a screenshot.
- Issues with insufficient detail may be summarily closed.
-->
Steps to reproduce:
1.
2.
3.
4.
Expected Behavior:
Actual Behavior:
<!-- Before Submitting, did you:
1. Include settings.json, keymap.json, .editorconfig if relevant?
2. Check your Zed.log for relevant errors? (please include!)
3. Click Preview to ensure everything looks right?
4. Hide videos, large images and logs in ``` inside collapsible blocks:
<details><summary>click to expand</summary>
```json
```
</details>
-->
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: |
Open Zed, from the command palette select "zed: Copy System Specs Into Clipboard"
placeholder: |
Output of "zed: Copy System Specs Into Clipboard"
validations:
required: true

57
.github/ISSUE_TEMPLATE/1_bug_report.yml vendored Normal file
View File

@@ -0,0 +1,57 @@
name: Bug Report
description: |
Something is broken in Zed (exclude crashing).
type: "Bug"
body:
- type: textarea
attributes:
label: Summary
description: Describe the bug with a one line summary, and provide detailed reproduction steps
value: |
<!-- Please insert a one line summary of the issue below -->
SUMMARY_SENTENCE_HERE
<!-- Be verbose: Include all steps necessary to reproduce from a clean Zed installation. -->
<!-- Code snippets are better than images, a repository link that reproduces the issue is ideal. -->
Steps to trigger the problem:
1.
2.
3.
4.
Actual Behavior:
Expected Behavior:
<!--
Is there anything additional necessary to reproduce this issue?
- settings.json, keymap.json, .editorconfig etc?
- Does it happen intermittently or only with specific projects / file types?
- Have you found a workaround?
Did you check your Zed.log to see if there is any relevant details there?
- When including large items (videos, screenshots, logs, configs) please wrap with:
<details><summary>See inside for XXXXYYY</summary>
```shell
code
```
</details>
-->
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"'
placeholder: |
Output of "zed: Copy System Specs Into Clipboard"
validations:
required: true

View File

@@ -5,12 +5,10 @@ body:
- type: textarea
attributes:
label: Summary
description: Summarize the issue with detailed reproduction steps
description: Describe the bug with a one line summary, and provide detailed reproduction steps
value: |
<!-- Begin your issue with a one sentence summary -->
SUMMARY_SENTENCE_HERE
<!-- Please insert a one line summary of the issue below -->
### Description
<!-- Include all steps necessary to reproduce from a clean Zed installation. Be verbose -->
Steps to trigger the problem:
1.
@@ -18,6 +16,7 @@ body:
3.
Actual Behavior:
Expected Behavior:
validations:
@@ -41,11 +40,10 @@ body:
value: |
<details><summary>Zed.log</summary>
<!-- Paste your log inside the code block. -->
```log
<!-- Click below this line and paste or drag-and-drop your log-->
```
</details>
```
<!-- Click above this line and paste or drag-and-drop your log--></details>
validations:
required: false

View File

@@ -4,6 +4,9 @@ contact_links:
- name: Feature Request
url: https://github.com/zed-industries/zed/discussions/new/choose
about: To request a feature, open a new Discussion in one of the appropriate Discussion categories
- name: "Zed Discord"
- name: Zed Discussion Forum
url: https://github.com/zed-industries/zed/discussions
about: A community discussion forum
- name: "Zed Discord: #Support Channel"
url: https://zed.dev/community-links
about: Real-time discussion and user support

View File

@@ -114,9 +114,7 @@ jobs:
timeout-minutes: 60
name: Check workspace-hack crate
needs: [job_spec]
if: |
github.repository_owner == 'zed-industries' &&
needs.job_spec.outputs.run_tests == 'true'
if: github.repository_owner == 'zed-industries'
runs-on:
- buildjet-8vcpu-ubuntu-2204
steps:
@@ -133,13 +131,13 @@ jobs:
- name: Check workspace-hack Cargo.toml is up-to-date
run: |
cargo hakari generate --diff || {
echo "To fix, run script/update-workspace-hack or script/update-workspace-hack.ps1";
echo "To fix, run script/update-workspace-hack";
false
}
- name: Check all crates depend on workspace-hack
run: |
cargo hakari manage-deps --dry-run || {
echo "To fix, run script/update-workspace-hack or script/update-workspace-hack.ps1"
echo "To fix, run script/update-workspace-hack"
false
}
@@ -225,7 +223,7 @@ jobs:
- name: Check for new vulnerable dependencies
if: github.event_name == 'pull_request'
uses: actions/dependency-review-action@67d4f4bd7a9b17a0db54d2a7519187c65e339de8 # v4
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4
with:
license-check: false
@@ -465,7 +463,6 @@ jobs:
- job_spec
- style
- migration_checks
# run_tests: If adding required tests, add them here and to script below.
- workspace_hack
- linux_tests
- build_remote_server
@@ -483,14 +480,11 @@ jobs:
# Only check test jobs if they were supposed to run
if [[ "${{ needs.job_spec.outputs.run_tests }}" == "true" ]]; then
[[ "${{ needs.workspace_hack.result }}" != 'success' ]] && { RET_CODE=1; echo "Workspace Hack failed"; }
[[ "${{ needs.macos_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "macOS tests failed"; }
[[ "${{ needs.linux_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "Linux tests failed"; }
[[ "${{ needs.windows_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "Windows tests failed"; }
[[ "${{ needs.windows_clippy.result }}" != 'success' ]] && { RET_CODE=1; echo "Windows clippy failed"; }
[[ "${{ needs.build_remote_server.result }}" != 'success' ]] && { RET_CODE=1; echo "Remote server build failed"; }
# This check is intentionally disabled. See: https://github.com/zed-industries/zed/pull/28431
# [[ "${{ needs.migration_checks.result }}" != 'success' ]] && { RET_CODE=1; echo "Migration Checks failed"; }
fi
if [[ "$RET_CODE" -eq 0 ]]; then
echo "All tests passed successfully!"
@@ -743,7 +737,7 @@ jobs:
echo "/nix/var/nix/profiles/default/bin" >> $GITHUB_PATH
echo "/Users/administrator/.nix-profile/bin" >> $GITHUB_PATH
- uses: cachix/install-nix-action@d1ca217b388ee87b2507a9a93bf01368bde7cec2 # v31
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
if: ${{ matrix.system.install_nix }}
with:
github_access_token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -206,7 +206,7 @@ jobs:
echo "/nix/var/nix/profiles/default/bin" >> $GITHUB_PATH
echo "/Users/administrator/.nix-profile/bin" >> $GITHUB_PATH
- uses: cachix/install-nix-action@d1ca217b388ee87b2507a9a93bf01368bde7cec2 # v31
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
if: ${{ matrix.system.install_nix }}
with:
github_access_token: ${{ secrets.GITHUB_TOKEN }}

586
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,13 +3,12 @@ resolver = "2"
members = [
"crates/activity_indicator",
"crates/agent",
"crates/agent_rules",
"crates/anthropic",
"crates/askpass",
"crates/assets",
"crates/assistant",
"crates/assistant_context_editor",
"crates/agent_eval",
"crates/assistant_eval",
"crates/assistant_settings",
"crates/assistant_slash_command",
"crates/assistant_slash_commands",
@@ -47,7 +46,6 @@ members = [
"crates/diagnostics",
"crates/docs_preprocessor",
"crates/editor",
"crates/eval",
"crates/evals",
"crates/extension",
"crates/extension_api",
@@ -211,14 +209,13 @@ edition = "2024"
activity_indicator = { path = "crates/activity_indicator" }
agent = { path = "crates/agent" }
agent_rules = { path = "crates/agent_rules" }
ai = { path = "crates/ai" }
anthropic = { path = "crates/anthropic" }
askpass = { path = "crates/askpass" }
assets = { path = "crates/assets" }
assistant = { path = "crates/assistant" }
assistant_context_editor = { path = "crates/assistant_context_editor" }
assistant_eval = { path = "crates/agent_eval" }
assistant_eval = { path = "crates/assistant_eval" }
assistant_settings = { path = "crates/assistant_settings" }
assistant_slash_command = { path = "crates/assistant_slash_command" }
assistant_slash_commands = { path = "crates/assistant_slash_commands" }
@@ -399,14 +396,14 @@ async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "8
async-recursion = "1.0.0"
async-tar = "0.5.0"
async-trait = "0.1"
async-tungstenite = "0.29.1"
async-tungstenite = "0.28"
async-watch = "0.3.1"
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
aws-config = { version = "1.6.1", features = ["behavior-version-latest"] }
aws-credential-types = { version = "1.2.2", features = ["hardcoded-credentials"] }
aws-sdk-bedrockruntime = { version = "1.80.0", features = ["behavior-version-latest"] }
aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] }
aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
aws-config = { version = "1.5.16", features = ["behavior-version-latest"] }
aws-credential-types = { version = "1.2.1", features = ["hardcoded-credentials"] }
aws-sdk-bedrockruntime = { version = "1.73.0", features = ["behavior-version-latest"] }
aws-smithy-runtime-api = { version = "1.7.3", features = ["http-1x", "client"] }
aws-smithy-types = { version = "1.2.13", features = ["http-body-1-x"] }
base64 = "0.22"
bitflags = "2.6.0"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
@@ -428,7 +425,7 @@ core-foundation = "0.10.0"
core-foundation-sys = "0.8.6"
ctor = "0.4.0"
dashmap = "6.0"
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "be69a016ba710191b9fdded28c8b042af4b617f7" }
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "bfd4af0" }
derive_more = "0.99.17"
dirs = "4.0"
ec4rs = "1.1"
@@ -456,8 +453,8 @@ indoc = "2"
inventory = "0.3.19"
itertools = "0.14.0"
jsonwebtoken = "9.3"
jupyter-protocol = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
jupyter-websocket-client = { git = "https://github.com/ConradIrwin/runtimed" ,rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
jupyter-protocol = { version = "0.6.0" }
jupyter-websocket-client = { version = "0.9.0" }
libc = "0.2"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0"
@@ -466,22 +463,21 @@ log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
markup5ever_rcdom = "0.3.0"
mlua = { version = "0.10", features = ["lua54", "vendored", "async", "send"] }
nanoid = "0.4"
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
nbformat = { version = "0.10.0" }
nix = "0.29"
objc = "0.2"
open = "5.0.0"
num-format = "0.4.4"
ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1"
pathdiff = "0.2"
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
pet-pixi = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1abe5cec5ebfbe97ca71746a4cfc7fe89bddf8e0" }
pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1abe5cec5ebfbe97ca71746a4cfc7fe89bddf8e0" }
pet-pixi = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1abe5cec5ebfbe97ca71746a4cfc7fe89bddf8e0" }
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1abe5cec5ebfbe97ca71746a4cfc7fe89bddf8e0" }
pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1abe5cec5ebfbe97ca71746a4cfc7fe89bddf8e0" }
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1abe5cec5ebfbe97ca71746a4cfc7fe89bddf8e0" }
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1abe5cec5ebfbe97ca71746a4cfc7fe89bddf8e0" }
postage = { version = "0.5", features = ["futures-traits"] }
pretty_assertions = { version = "1.3.0", features = ["unstable"] }
proc-macro2 = "1.0.93"
@@ -504,7 +500,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f
"stream",
] }
rsa = "0.9.6"
runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [
runtimelib = { version = "0.25.0", default-features = false, features = [
"async-dispatcher-runtime",
] }
rustc-demangle = "0.1.23"
@@ -512,7 +508,6 @@ rust-embed = { version = "8.4", features = ["include-exclude"] }
rustc-hash = "2.1.0"
rustls = { version = "0.23.22" }
rustls-platform-verifier = "0.5.0"
scap = { git = "https://github.com/zed-industries/scap", rev = "08f0a01417505cc0990b9931a37e5120db92e0d0", default-features = false }
schemars = { version = "0.8", features = ["impl_json_schema", "indexmap2"] }
semver = "1.0"
serde = { version = "1.0", features = ["derive", "rc"] }
@@ -552,7 +547,7 @@ time = { version = "0.3", features = [
tiny_http = "0.8"
toml = "0.8"
tokio = { version = "1" }
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"] }
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"]}
tower-http = "0.4.4"
tree-sitter = { version = "0.25.3", features = ["wasm"] }
tree-sitter-bash = "0.23"
@@ -664,6 +659,7 @@ features = [
# TODO livekit https://github.com/RustAudio/cpal/pull/891
[patch.crates-io]
cpal = { git = "https://github.com/zed-industries/cpal", rev = "fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50" }
real-async-tls = { git = "https://github.com/zed-industries/async-tls", rev = "1e759a4b5e370f87dc15e40756ac4f8815b61d9d", package = "async-tls" }
notify = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }

View File

@@ -1 +0,0 @@
<svg width="16" height="16" fill="none" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g style="fill:#000;fill-opacity:1" fill="#180c25"><path d="m-116.1-101.4-28.9-28.9a6.7 6.7 0 0 1-1.8-4.7v-41.2c0-2.4-2.4-4.8-4.8-4.8h-9.6a5.2 5.2 0 0 0-4.8 4.8v48c0 2.5 1 5 2.7 6.8l33.6 33.6a9.6 9.6 0 0 0 6.8 2.8h4.8c2.7 0 4.8-2.2 4.8-4.8v-4.8c0-2.5-1-5-2.8-6.8zM-79.6-176.2c0-2.4-2.4-4.8-4.8-4.8h-9.7a5.2 5.2 0 0 0-4.7 4.8v41.2c0 1.8-.8 3.5-2 4.7l-9.6 9.7a9.5 9.5 0 0 0-2.8 6.8v4.8c0 2.6 2.1 4.7 4.8 4.7h4.8c2.4 0 4.9-.9 6.7-2.8l14.4-14.3a9.6 9.6 0 0 0 2.8-6.8v-48z" style="fill:#000;fill-opacity:1;stroke-width:.255894" transform="translate(21.6 22.7) scale(.11067)"/></g></svg>

Before

Width:  |  Height:  |  Size: 677 B

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.1331 11.3776C10.2754 10.6665 10.1331 9.78593 11.1998 8.53327C11.82 7.80489 12.2664 6.96894 12.2664 6.04456C12.2664 4.91305 11.8169 3.82788 11.0168 3.02778C10.2167 2.22769 9.13152 1.7782 8.00001 1.7782C6.8685 1.7782 5.78334 2.22769 4.98324 3.02778C4.18314 3.82788 3.73364 4.91305 3.73364 6.04456C3.73364 6.75562 3.87586 7.6089 4.80024 8.53327C5.86683 9.80679 5.72462 10.6665 5.86683 11.3776M10.1331 11.3776V12.8821C10.1331 13.622 9.53341 14.2218 8.79353 14.2218H7.2065C6.46662 14.2218 5.86683 13.622 5.86683 12.8821V11.3776M10.1331 11.3776H5.86683" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 751 B

View File

@@ -1,4 +0,0 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.09666 3.02263C3.0567 3.00312 3.01178 2.9961 2.96778 3.0025C2.92377 3.00889 2.88271 3.02839 2.84995 3.05847C2.8172 3.08854 2.79426 3.12778 2.78413 3.17108C2.77401 3.21439 2.77716 3.25973 2.79319 3.30121L4.05638 6.69C4.13088 6.89005 4.13088 7.11022 4.05638 7.31027L2.79363 10.6991C2.77769 10.7405 2.77457 10.7858 2.78469 10.829C2.79481 10.8722 2.8177 10.9114 2.85038 10.9414C2.88306 10.9715 2.92402 10.991 2.96794 10.9975C3.01186 11.0039 3.05671 10.997 3.09666 10.9776L11.0943 7.20097C11.1324 7.18297 11.1645 7.15455 11.187 7.11899C11.2096 7.08344 11.2215 7.04222 11.2215 7.00014C11.2215 6.95805 11.2096 6.91683 11.187 6.88128C11.1645 6.84573 11.1324 6.8173 11.0943 6.79931L3.09666 3.02263Z" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.11255 7.00014H11.2216" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1014 B

View File

@@ -1,3 +0,0 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 9.8V4.2C4 4.08954 4.08954 4 4.2 4H9.8C9.91046 4 10 4.08954 10 4.2V9.8C10 9.91046 9.91046 10 9.8 10H4.2C4.08954 10 4 9.91046 4 9.8Z" fill="#C56757" stroke="#C56757" stroke-width="1.25" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 325 B

View File

@@ -150,7 +150,7 @@
"context": "AgentDiff",
"bindings": {
"ctrl-y": "agent::Keep",
"ctrl-n": "agent::Reject"
"ctrl-k ctrl-r": "agent::Reject"
}
},
{
@@ -532,7 +532,6 @@
"context": "Editor && showing_completions",
"bindings": {
"enter": "editor::ConfirmCompletion",
"shift-enter": "editor::ConfirmCompletionReplace",
"tab": "editor::ComposeCompletion"
}
},
@@ -625,6 +624,7 @@
"context": "AgentPanel",
"bindings": {
"ctrl-n": "agent::NewThread",
"new": "agent::NewThread",
"ctrl-alt-n": "agent::NewPromptEditor",
"ctrl-shift-h": "agent::OpenHistory",
"ctrl-alt-c": "agent::OpenConfiguration",
@@ -635,15 +635,9 @@
"ctrl-alt-e": "agent::RemoveAllContext"
}
},
{
"context": "AgentPanel > Markdown",
"bindings": {
"copy": "markdown::CopyAsMarkdown",
"ctrl-c": "markdown::CopyAsMarkdown"
}
},
{
"context": "AgentPanel && prompt_editor",
"use_key_equivalents": true,
"bindings": {
"cmd-n": "agent::NewPromptEditor",
"cmd-alt-t": "agent::NewThread"
@@ -659,6 +653,7 @@
},
{
"context": "EditMessageEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel",
"enter": "menu::Confirm",
@@ -667,6 +662,7 @@
},
{
"context": "AgentFeedbackMessageEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel",
"enter": "menu::Confirm",
@@ -798,6 +794,7 @@
},
{
"context": "GitPanel",
"use_key_equivalents": true,
"bindings": {
"ctrl-g ctrl-g": "git::Fetch",
"ctrl-g up": "git::Push",

View File

@@ -242,7 +242,7 @@
"use_key_equivalents": true,
"bindings": {
"cmd-y": "agent::Keep",
"cmd-n": "agent::Reject"
"cmd-alt-z": "agent::Reject"
}
},
{
@@ -291,13 +291,6 @@
"cmd-alt-e": "agent::RemoveAllContext"
}
},
{
"context": "AgentPanel > Markdown",
"use_key_equivalents": true,
"bindings": {
"cmd-c": "markdown::CopyAsMarkdown"
}
},
{
"context": "AgentPanel && prompt_editor",
"use_key_equivalents": true,
@@ -345,28 +338,10 @@
"enter": "agent::AcceptSuggestedContext"
}
},
{
"context": "AgentConfiguration",
"bindings": {
"ctrl--": "pane::GoBack"
}
},
{
"context": "ThreadHistory",
"bindings": {
"ctrl--": "pane::GoBack"
}
},
{
"context": "ThreadHistory",
"bindings": {
"ctrl--": "pane::GoBack"
}
},
{
"context": "ThreadHistory > Editor",
"bindings": {
"shift-backspace": "agent::RemoveSelectedThread"
"backspace": "agent::RemoveSelectedThread"
}
},
{
@@ -525,7 +500,7 @@
"cmd-k cmd-9": ["editor::FoldAtLevel", 9],
"cmd-k cmd-0": "editor::FoldAll",
"cmd-k cmd-j": "editor::UnfoldAll",
// Using `ctrl-space` / `ctrl-shift-space` in Zed requires disabling the macOS global shortcut.
// Using `ctrl-space` in Zed requires disabling the macOS global shortcut.
// System Preferences->Keyboard->Keyboard Shortcuts->Input Sources->Select the previous input source (uncheck)
"ctrl-space": "editor::ShowCompletions",
"ctrl-shift-space": "editor::ShowWordCompletions",
@@ -681,7 +656,6 @@
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCompletion",
"shift-enter": "editor::ConfirmCompletionReplace",
"tab": "editor::ComposeCompletion"
}
},
@@ -1004,8 +978,6 @@
"cmd-home": "terminal::ScrollToTop",
"shift-end": "terminal::ScrollToBottom",
"cmd-end": "terminal::ScrollToBottom",
// Using `ctrl-shift-space` in Zed requires disabling the macOS global shortcut.
// System Preferences->Keyboard->Keyboard Shortcuts->Input Sources->Select the previous input source (uncheck)
"ctrl-shift-space": "terminal::ToggleViMode",
"ctrl-k up": "pane::SplitUp",
"ctrl-k down": "pane::SplitDown",

View File

@@ -58,8 +58,7 @@
"ctrl-shift-home": "editor::SelectToBeginning",
"ctrl-shift-end": "editor::SelectToEnd",
"ctrl-f8": "editor::ToggleBreakpoint",
"ctrl-shift-f8": "editor::EditLogBreakpoint",
"ctrl-shift-u": "editor::ToggleCase"
"ctrl-shift-f8": "editor::EditLogBreakpoint"
}
},
{

View File

@@ -58,12 +58,6 @@
"ctrl-r": "outline::Toggle"
}
},
{
"context": "Editor && !agent_diff",
"bindings": {
"ctrl-k ctrl-z": "git::Restore"
}
},
{
"context": "Pane",
"bindings": {

View File

@@ -55,8 +55,7 @@
"cmd-shift-home": "editor::SelectToBeginning",
"cmd-shift-end": "editor::SelectToEnd",
"ctrl-f8": "editor::ToggleBreakpoint",
"ctrl-shift-f8": "editor::EditLogBreakpoint",
"cmd-shift-u": "editor::ToggleCase"
"ctrl-shift-f8": "editor::EditLogBreakpoint"
}
},
{

View File

@@ -60,12 +60,6 @@
"cmd-r": "outline::Toggle"
}
},
{
"context": "Editor && !agent_diff",
"bindings": {
"cmd-k cmd-z": "git::Restore"
}
},
{
"context": "Pane",
"bindings": {

View File

@@ -44,12 +44,6 @@
"[ /": "vim::PreviousComment",
"] *": "vim::NextComment",
"] /": "vim::NextComment",
"[ -": "vim::PreviousLesserIndent",
"[ +": "vim::PreviousGreaterIndent",
"[ =": "vim::PreviousSameIndent",
"] -": "vim::NextLesserIndent",
"] +": "vim::NextGreaterIndent",
"] =": "vim::NextSameIndent",
// Word motions
"w": "vim::NextWordStart",
"e": "vim::NextWordEnd",
@@ -341,101 +335,22 @@
}
},
{
"context": "vim_mode == helix_normal && !menu",
"context": "vim_mode == helix_normal",
"bindings": {
"escape": "editor::Cancel",
"ctrl-[": "editor::Cancel",
":": "command_palette::Toggle",
"shift-d": "vim::DeleteToEndOfLine",
"shift-j": "vim::JoinLines",
"y": "editor::Copy",
"shift-y": "vim::YankLine",
"i": "vim::InsertBefore",
"shift-i": "vim::InsertFirstNonWhitespace",
"a": "vim::InsertAfter",
"shift-a": "vim::InsertEndOfLine",
"o": "vim::InsertLineBelow",
"shift-o": "vim::InsertLineAbove",
"~": "vim::ChangeCase",
"ctrl-a": "vim::Increment",
"ctrl-x": "vim::Decrement",
"p": "vim::Paste",
"shift-p": ["vim::Paste", { "before": true }],
"u": "vim::Undo",
"ctrl-r": "vim::Redo",
"r": "vim::PushReplace",
"s": "vim::Substitute",
"shift-s": "vim::SubstituteLine",
">": "vim::Indent",
"<": "vim::Outdent",
"=": "vim::AutoIndent",
"g u": "vim::PushLowercase",
"g shift-u": "vim::PushUppercase",
"g ~": "vim::PushOppositeCase",
"\"": "vim::PushRegister",
"g q": "vim::PushRewrap",
"g w": "vim::PushRewrap",
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-pageup": "pane::ActivatePreviousItem",
"insert": "vim::InsertBefore",
// tree-sitter related commands
"[ x": "editor::SelectLargerSyntaxNode",
"] x": "editor::SelectSmallerSyntaxNode",
"] d": "editor::GoToDiagnostic",
"[ d": "editor::GoToPreviousDiagnostic",
"] c": "editor::GoToHunk",
"[ c": "editor::GoToPreviousHunk",
// Goto mode
"g n": "pane::ActivateNextItem",
"g p": "pane::ActivatePreviousItem",
// "tab": "pane::ActivateNextItem",
// "shift-tab": "pane::ActivatePrevItem",
"shift-h": "pane::ActivatePreviousItem",
"shift-l": "pane::ActivateNextItem",
"g l": "vim::EndOfLine",
"g h": "vim::StartOfLine",
"g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s"
"g e": "vim::EndOfDocument",
"g y": "editor::GoToTypeDefinition",
"g r": "editor::FindAllReferences", // zed specific
"g t": "vim::WindowTop",
"g c": "vim::WindowMiddle",
"g b": "vim::WindowBottom",
"x": "editor::SelectLine",
"shift-x": "editor::SelectLine",
// Window mode
"space w h": "workspace::ActivatePaneLeft",
"space w l": "workspace::ActivatePaneRight",
"space w k": "workspace::ActivatePaneUp",
"space w j": "workspace::ActivatePaneDown",
"space w q": "pane::CloseActiveItem",
"space w s": "pane::SplitRight",
"space w r": "pane::SplitRight",
"space w v": "pane::SplitDown",
"space w d": "pane::SplitDown",
// Space mode
"space f": "file_finder::Toggle",
"space k": "editor::Hover",
"space s": "outline::Toggle",
"space shift-s": "project_symbols::Toggle",
"space d": "editor::GoToDiagnostic",
"space r": "editor::Rename",
"space a": "editor::ToggleCodeActions",
"space h": "editor::SelectAllMatches",
"space c": "editor::ToggleComments",
"space y": "editor::Copy",
"space p": "editor::Paste",
// Match mode
"m m": "vim::Matching",
"m i w": ["workspace::SendKeystrokes", "v i w"],
"shift-u": "editor::Redo",
"ctrl-c": "editor::ToggleComments",
"d": "vim::HelixDelete",
"c": "vim::Substitute",
"shift-c": "editor::AddSelectionBelow"
"w": "vim::NextWordStart",
"e": "vim::NextWordEnd",
"b": "vim::PreviousWordStart",
"h": "vim::Left",
"j": "vim::Down",
"k": "vim::Up",
"l": "vim::Right"
}
},
{
"context": "vim_mode == insert && !(showing_code_actions || showing_completions)",
"bindings": {

View File

@@ -6,26 +6,15 @@ You are an AI assistant integrated into a code editor. You have the programming
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.
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 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 users 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.
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!
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.
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.
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.
@@ -44,106 +33,6 @@ Responding:
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:
{{#each worktrees}}
@@ -155,7 +44,7 @@ There are rules that apply to these root directories:
{{#each worktrees}}
{{#if rules_file}}
`{{root_name}}/{{rules_file.path_in_worktree}}`:
`{{root_name}}/{{rules_file.rel_path}}`:
``````
{{{rules_file.text}}}
@@ -163,8 +52,3 @@ There are rules that apply to these root directories:
{{/if}}
{{/each}}
{{/if}}
<user_environment>
Operating System: {{os}} ({{arch}})
Shell: {{shell}}
</user_environment>

View File

@@ -0,0 +1,8 @@
A software developer is asking a question about their project. The source files in their project have been indexed into a database of semantic text embeddings.
Your task is to generate a list of 4 diverse search queries that can be run on this embedding database, in order to retrieve a list of code snippets
that are relevant to the developer's question. Redundant search queries will be heavily penalized, so only include another query if it's sufficiently
distinct from previous ones.
Here is the question that's been asked, together with context that the developer has added manually:
{{{context_buffer}}}

View File

@@ -644,7 +644,7 @@
"tools": {
"diagnostics": true,
"fetch": true,
"list_directory": false,
"list_directory": true,
"now": true,
"path_search": true,
"read_file": true,
@@ -656,22 +656,22 @@
"name": "Write",
"enable_all_context_servers": true,
"tools": {
"terminal": true,
"code_actions": true,
"bash": true,
"batch_tool": true,
"code_symbols": true,
"copy_path": false,
"copy_path": true,
"create_file": true,
"delete_path": false,
"delete_path": true,
"diagnostics": true,
"find_replace_file": true,
"edit_files": false,
"fetch": true,
"list_directory": false,
"move_path": false,
"list_directory": true,
"move_path": true,
"now": true,
"path_search": true,
"read_file": true,
"regex_search": true,
"rename": true,
"symbol_info": true,
"thinking": true
}
@@ -1137,8 +1137,7 @@
"code_actions_on_format": {},
// Settings related to running tasks.
"tasks": {
"variables": {},
"enabled": true
"variables": {}
},
// An object whose keys are language names, and whose values
// are arrays of filenames or extensions of files that should
@@ -1458,8 +1457,6 @@
"lsp": {
// Specify the LSP name as a key here.
// "rust-analyzer": {
// // A special flag for rust-analyzer integration, to use server-provided tasks
// enable_lsp_tasks": true,
// // These initialization options are merged into Zed's defaults
// "initialization_options": {
// "check": {

View File

@@ -87,9 +87,9 @@
"terminal.ansi.blue": "#83a598ff",
"terminal.ansi.bright_blue": "#414f4aff",
"terminal.ansi.dim_blue": "#c0d2cbff",
"terminal.ansi.magenta": "#d3869bff",
"terminal.ansi.bright_magenta": "#8e5868ff",
"terminal.ansi.dim_magenta": "#ff9ebbff",
"terminal.ansi.magenta": "#a89984ff",
"terminal.ansi.bright_magenta": "#514a41ff",
"terminal.ansi.dim_magenta": "#d2cabfff",
"terminal.ansi.cyan": "#8ec07cff",
"terminal.ansi.bright_cyan": "#45603eff",
"terminal.ansi.dim_cyan": "#c7dfbdff",
@@ -472,9 +472,9 @@
"terminal.ansi.blue": "#83a598ff",
"terminal.ansi.bright_blue": "#414f4aff",
"terminal.ansi.dim_blue": "#c0d2cbff",
"terminal.ansi.magenta": "#d3869bff",
"terminal.ansi.bright_magenta": "#8e5868ff",
"terminal.ansi.dim_magenta": "#ff9ebbff",
"terminal.ansi.magenta": "#a89984ff",
"terminal.ansi.bright_magenta": "#514a41ff",
"terminal.ansi.dim_magenta": "#d2cabfff",
"terminal.ansi.cyan": "#8ec07cff",
"terminal.ansi.bright_cyan": "#45603eff",
"terminal.ansi.dim_cyan": "#c7dfbdff",
@@ -857,9 +857,9 @@
"terminal.ansi.blue": "#83a598ff",
"terminal.ansi.bright_blue": "#414f4aff",
"terminal.ansi.dim_blue": "#c0d2cbff",
"terminal.ansi.magenta": "#d3869bff",
"terminal.ansi.bright_magenta": "#8e5868ff",
"terminal.ansi.dim_magenta": "#ff9ebbff",
"terminal.ansi.magenta": "#a89984ff",
"terminal.ansi.bright_magenta": "#514a41ff",
"terminal.ansi.dim_magenta": "#d2cabfff",
"terminal.ansi.cyan": "#8ec07cff",
"terminal.ansi.bright_cyan": "#45603eff",
"terminal.ansi.dim_cyan": "#c7dfbdff",
@@ -1242,9 +1242,9 @@
"terminal.ansi.blue": "#0b6678ff",
"terminal.ansi.bright_blue": "#8fb0baff",
"terminal.ansi.dim_blue": "#14333bff",
"terminal.ansi.magenta": "#8f3e71ff",
"terminal.ansi.bright_magenta": "#c76da0ff",
"terminal.ansi.dim_magenta": "#5c2848ff",
"terminal.ansi.magenta": "#7c6f64ff",
"terminal.ansi.bright_magenta": "#bcb5afff",
"terminal.ansi.dim_magenta": "#3e3833ff",
"terminal.ansi.cyan": "#437b59ff",
"terminal.ansi.bright_cyan": "#9fbca8ff",
"terminal.ansi.dim_cyan": "#253e2eff",
@@ -1627,9 +1627,9 @@
"terminal.ansi.blue": "#0b6678ff",
"terminal.ansi.bright_blue": "#8fb0baff",
"terminal.ansi.dim_blue": "#14333bff",
"terminal.ansi.magenta": "#8f3e71ff",
"terminal.ansi.bright_magenta": "#c76da0ff",
"terminal.ansi.dim_magenta": "#5c2848ff",
"terminal.ansi.magenta": "#7c6f64ff",
"terminal.ansi.bright_magenta": "#bcb5afff",
"terminal.ansi.dim_magenta": "#3e3833ff",
"terminal.ansi.cyan": "#437b59ff",
"terminal.ansi.bright_cyan": "#9fbca8ff",
"terminal.ansi.dim_cyan": "#253e2eff",
@@ -2012,9 +2012,9 @@
"terminal.ansi.blue": "#0b6678ff",
"terminal.ansi.bright_blue": "#8fb0baff",
"terminal.ansi.dim_blue": "#14333bff",
"terminal.ansi.magenta": "#8f3e71ff",
"terminal.ansi.bright_magenta": "#c76da0ff",
"terminal.ansi.dim_magenta": "#5c2848ff",
"terminal.ansi.magenta": "#7c6f64ff",
"terminal.ansi.bright_magenta": "#bcb5afff",
"terminal.ansi.dim_magenta": "#3e3833ff",
"terminal.ansi.cyan": "#437b59ff",
"terminal.ansi.bright_cyan": "#9fbca8ff",
"terminal.ansi.dim_cyan": "#253e2eff",

View File

@@ -11,22 +11,13 @@ use language::{BinaryStatus, LanguageRegistry, LanguageServerId};
use project::{
EnvironmentErrorMessage, LanguageServerProgress, LspStoreEvent, Project,
ProjectEnvironmentEvent,
git_store::{GitStoreEvent, Repository},
};
use smallvec::SmallVec;
use std::{
cmp::Reverse,
fmt::Write,
path::Path,
sync::Arc,
time::{Duration, Instant},
};
use std::{cmp::Reverse, fmt::Write, path::Path, sync::Arc, time::Duration};
use ui::{ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
use util::truncate_and_trailoff;
use workspace::{StatusItemView, Workspace, item::ItemHandle};
const GIT_OPERATION_DELAY: Duration = Duration::from_millis(0);
actions!(activity_indicator, [ShowErrorMessage]);
pub enum Event {
@@ -114,15 +105,6 @@ impl ActivityIndicator {
)
.detach();
cx.subscribe(
&project.read(cx).git_store().clone(),
|_, _, event: &GitStoreEvent, cx| match event {
project::git_store::GitStoreEvent::JobsUpdated => cx.notify(),
_ => {}
},
)
.detach();
if let Some(auto_updater) = auto_updater.as_ref() {
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
}
@@ -303,34 +285,6 @@ impl ActivityIndicator {
});
}
let current_job = self
.project
.read(cx)
.active_repository(cx)
.map(|r| r.read(cx))
.and_then(Repository::current_job);
// Show any long-running git command
if let Some(job_info) = current_job {
if Instant::now() - job_info.start >= GIT_OPERATION_DELAY {
return Some(Content {
icon: Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(delta)))
},
)
.into_any_element(),
),
message: job_info.message.into(),
on_click: None,
});
}
}
// Show any language server installation info.
let mut downloading = SmallVec::<[_; 3]>::new();
let mut checking_for_update = SmallVec::<[_; 3]>::new();

View File

@@ -19,7 +19,6 @@ test-support = [
]
[dependencies]
agent_rules.workspace = true
anyhow.workspace = true
assistant_context_editor.workspace = true
assistant_settings.workspace = true
@@ -85,6 +84,7 @@ ui.workspace = true
ui_input.workspace = true
util.workspace = true
uuid.workspace = true
vim_mode_setting.workspace = true
workspace.workspace = true
zed_actions.workspace = true
workspace-hack.workspace = true

File diff suppressed because it is too large Load Diff

View File

@@ -45,28 +45,23 @@ impl AgentDiff {
window: &mut Window,
cx: &mut App,
) -> Result<Entity<Self>> {
workspace.update(cx, |workspace, cx| {
Self::deploy_in_workspace(thread, workspace, window, cx)
})
}
pub fn deploy_in_workspace(
thread: Entity<Thread>,
workspace: &mut Workspace,
window: &mut Window,
cx: &mut Context<Workspace>,
) -> Entity<Self> {
let existing_diff = workspace
.items_of_type::<AgentDiff>(cx)
.find(|diff| diff.read(cx).thread == thread);
let existing_diff = workspace.update(cx, |workspace, cx| {
workspace
.items_of_type::<AgentDiff>(cx)
.find(|diff| diff.read(cx).thread == thread)
})?;
if let Some(existing_diff) = existing_diff {
workspace.activate_item(&existing_diff, true, true, window, cx);
existing_diff
workspace.update(cx, |workspace, cx| {
workspace.activate_item(&existing_diff, true, true, window, cx);
})?;
Ok(existing_diff)
} else {
let agent_diff =
cx.new(|cx| AgentDiff::new(thread.clone(), workspace.weak_handle(), window, cx));
workspace.add_item_to_center(Box::new(agent_diff.clone()), window, cx);
agent_diff
cx.new(|cx| AgentDiff::new(thread.clone(), workspace.clone(), window, cx));
workspace.update(cx, |workspace, cx| {
workspace.add_item_to_center(Box::new(agent_diff.clone()), window, cx);
})?;
Ok(agent_diff)
}
}
@@ -243,7 +238,7 @@ impl AgentDiff {
fn handle_thread_event(&mut self, event: &ThreadEvent, cx: &mut Context<Self>) {
match event {
ThreadEvent::SummaryGenerated => self.update_title(cx),
ThreadEvent::SummaryChanged => self.update_title(cx),
_ => {}
}
}
@@ -792,11 +787,15 @@ impl editor::Addon for AgentDiffAddon {
pub struct AgentDiffToolbar {
agent_diff: Option<WeakEntity<AgentDiff>>,
_workspace: WeakEntity<Workspace>,
}
impl AgentDiffToolbar {
pub fn new() -> Self {
Self { agent_diff: None }
pub fn new(workspace: &Workspace, _: &mut Context<Self>) -> Self {
Self {
agent_diff: None,
_workspace: workspace.weak_handle(),
}
}
fn agent_diff(&self, _: &App) -> Option<Entity<AgentDiff>> {

View File

@@ -51,6 +51,7 @@ actions!(
ToggleProfileSelector,
RemoveAllContext,
OpenHistory,
OpenConfiguration,
AddContextServer,
RemoveSelectedThread,
Chat,

View File

@@ -409,7 +409,6 @@ impl Render for AssistantConfiguration {
v_flex()
.id("assistant-configuration")
.key_context("AgentConfiguration")
.track_focus(&self.focus_handle(cx))
.bg(cx.theme().colors().panel_background)
.size_full()

View File

@@ -12,12 +12,12 @@ use assistant_slash_command::SlashCommandWorkingSet;
use assistant_tool::ToolWorkingSet;
use client::zed_urls;
use editor::{Editor, EditorEvent, MultiBuffer};
use editor::{Editor, MultiBuffer};
use fs::Fs;
use gpui::{
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, Corner, Entity,
EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext, Pixels, Subscription, Task,
UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
UpdateGlobal, WeakEntity, action_with_deprecated_aliases, prelude::*, pulsating_between,
};
use language::LanguageRegistry;
use language_model::{LanguageModelProviderTosView, LanguageModelRegistry};
@@ -33,8 +33,7 @@ use ui::{
use util::ResultExt as _;
use workspace::Workspace;
use workspace::dock::{DockPosition, Panel, PanelEvent};
use zed_actions::agent::OpenConfiguration;
use zed_actions::assistant::{OpenPromptLibrary, ToggleFocus};
use zed_actions::assistant::ToggleFocus;
use crate::active_thread::ActiveThread;
use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
@@ -45,9 +44,15 @@ use crate::thread_history::{PastContext, PastThread, ThreadHistory};
use crate::thread_store::ThreadStore;
use crate::{
AgentDiff, InlineAssistant, NewPromptEditor, NewThread, OpenActiveThreadAsMarkdown,
OpenAgentDiff, OpenHistory, ThreadEvent, ToggleContextPicker,
OpenAgentDiff, OpenConfiguration, OpenHistory, ToggleContextPicker,
};
action_with_deprecated_aliases!(
assistant,
OpenPromptLibrary,
["assistant::DeployPromptLibrary"]
);
pub fn init(cx: &mut App) {
cx.observe_new(
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
@@ -87,8 +92,9 @@ pub fn init(cx: &mut App) {
.register_action(|workspace, _: &OpenAgentDiff, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
let thread = panel.read(cx).thread.read(cx).thread().clone();
AgentDiff::deploy_in_workspace(thread, workspace, window, cx);
panel.update(cx, |panel, cx| {
panel.open_agent_diff(&OpenAgentDiff, window, cx);
});
}
});
},
@@ -97,72 +103,12 @@ pub fn init(cx: &mut App) {
}
enum ActiveView {
Thread {
change_title_editor: Entity<Editor>,
_subscriptions: Vec<gpui::Subscription>,
},
Thread,
PromptEditor,
History,
Configuration,
}
impl ActiveView {
pub fn thread(thread: Entity<Thread>, window: &mut Window, cx: &mut App) -> Self {
let summary = thread.read(cx).summary_or_default();
let editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
editor.set_text(summary, window, cx);
editor
});
let subscriptions = vec![
window.subscribe(&editor, cx, {
{
let thread = thread.clone();
move |editor, event, window, cx| match event {
EditorEvent::BufferEdited => {
let new_summary = editor.read(cx).text(cx);
thread.update(cx, |thread, cx| {
thread.set_summary(new_summary, cx);
})
}
EditorEvent::Blurred => {
if editor.read(cx).text(cx).is_empty() {
let summary = thread.read(cx).summary_or_default();
editor.update(cx, |editor, cx| {
editor.set_text(summary, window, cx);
});
}
}
_ => {}
}
}
}),
window.subscribe(&thread, cx, {
let editor = editor.clone();
move |thread, event, window, cx| match event {
ThreadEvent::SummaryGenerated => {
let summary = thread.read(cx).summary_or_default();
editor.update(cx, |editor, cx| {
editor.set_text(summary, window, cx);
})
}
_ => {}
}
}),
];
Self::Thread {
change_title_editor: editor,
_subscriptions: subscriptions,
}
}
}
pub struct AssistantPanel {
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
@@ -170,7 +116,6 @@ pub struct AssistantPanel {
language_registry: Arc<LanguageRegistry>,
thread_store: Entity<ThreadStore>,
thread: Entity<ActiveThread>,
_thread_subscription: Subscription,
message_editor: Entity<MessageEditor>,
context_store: Entity<assistant_context_editor::ContextStore>,
context_editor: Option<Entity<ContextEditor>>,
@@ -178,7 +123,6 @@ pub struct AssistantPanel {
configuration_subscription: Option<Subscription>,
local_timezone: UtcOffset,
active_view: ActiveView,
previous_view: Option<ActiveView>,
history_store: Entity<HistoryStore>,
history: Entity<ThreadHistory>,
assistant_dropdown_menu_handle: PopoverMenuHandle<ContextMenu>,
@@ -227,14 +171,14 @@ impl AssistantPanel {
) -> Self {
let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
let fs = workspace.app_state().fs.clone();
let project = workspace.project();
let project = workspace.project().clone();
let language_registry = project.read(cx).languages().clone();
let workspace = workspace.weak_handle();
let weak_self = cx.entity().downgrade();
let message_editor_context_store = cx.new(|_cx| {
crate::context_store::ContextStore::new(
project.downgrade(),
workspace.clone(),
Some(thread_store.downgrade()),
)
});
@@ -254,15 +198,6 @@ impl AssistantPanel {
let history_store =
cx.new(|cx| HistoryStore::new(thread_store.clone(), context_store.clone(), cx));
cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
let active_view = ActiveView::thread(thread.clone(), window, cx);
let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
if let ThreadEvent::MessageAdded(_) = &event {
// needed to leave empty state
cx.notify();
}
});
let thread = cx.new(|cx| {
ActiveThread::new(
thread.clone(),
@@ -276,14 +211,13 @@ impl AssistantPanel {
});
Self {
active_view,
active_view: ActiveView::Thread,
workspace,
project: project.clone(),
fs: fs.clone(),
language_registry,
thread_store: thread_store.clone(),
thread,
_thread_subscription: thread_subscription,
message_editor,
context_store,
context_editor: None,
@@ -293,7 +227,6 @@ impl AssistantPanel {
chrono::Local::now().offset().local_minus_utc(),
)
.unwrap(),
previous_view: None,
history_store: history_store.clone(),
history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
assistant_dropdown_menu_handle: PopoverMenuHandle::default(),
@@ -339,12 +272,11 @@ impl AssistantPanel {
.thread_store
.update(cx, |this, cx| this.create_thread(cx));
let thread_view = ActiveView::thread(thread.clone(), window, cx);
self.set_active_view(thread_view, window, cx);
self.active_view = ActiveView::Thread;
let message_editor_context_store = cx.new(|_cx| {
crate::context_store::ContextStore::new(
self.project.downgrade(),
self.workspace.clone(),
Some(self.thread_store.downgrade()),
)
});
@@ -380,14 +312,6 @@ impl AssistantPanel {
cx,
)
});
self._thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
if let ThreadEvent::MessageAdded(_) = &event {
// needed to leave empty state
cx.notify();
}
});
self.message_editor = cx.new(|cx| {
MessageEditor::new(
self.fs.clone(),
@@ -403,7 +327,7 @@ impl AssistantPanel {
}
fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.set_active_view(ActiveView::PromptEditor, window, cx);
self.active_view = ActiveView::PromptEditor;
let context = self
.context_store
@@ -453,16 +377,11 @@ impl AssistantPanel {
}
fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if matches!(self.active_view, ActiveView::History) {
if let Some(previous_view) = self.previous_view.take() {
self.set_active_view(previous_view, window, cx);
}
} else {
self.thread_store
.update(cx, |thread_store, cx| thread_store.reload(cx))
.detach_and_log_err(cx);
self.set_active_view(ActiveView::History, window, cx);
}
self.thread_store
.update(cx, |thread_store, cx| thread_store.reload(cx))
.detach_and_log_err(cx);
self.active_view = ActiveView::History;
self.history.focus_handle(cx).focus(window);
cx.notify();
}
@@ -495,7 +414,7 @@ impl AssistantPanel {
cx,
)
});
this.set_active_view(ActiveView::PromptEditor, window, cx);
this.active_view = ActiveView::PromptEditor;
this.context_editor = Some(editor);
anyhow::Ok(())
@@ -517,11 +436,10 @@ impl AssistantPanel {
cx.spawn_in(window, async move |this, cx| {
let thread = open_thread_task.await?;
this.update_in(cx, |this, window, cx| {
let thread_view = ActiveView::thread(thread.clone(), window, cx);
this.set_active_view(thread_view, window, cx);
this.active_view = ActiveView::Thread;
let message_editor_context_store = cx.new(|_cx| {
crate::context_store::ContextStore::new(
this.project.downgrade(),
this.workspace.clone(),
Some(this.thread_store.downgrade()),
)
});
@@ -552,17 +470,6 @@ impl AssistantPanel {
})
}
pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
match self.active_view {
ActiveView::Configuration | ActiveView::History => {
self.active_view =
ActiveView::thread(self.thread.read(cx).thread().clone(), window, cx);
cx.notify();
}
_ => {}
}
}
pub fn open_agent_diff(
&mut self,
_: &OpenAgentDiff,
@@ -570,11 +477,7 @@ impl AssistantPanel {
cx: &mut Context<Self>,
) {
let thread = self.thread.read(cx).thread().clone();
self.workspace
.update(cx, |workspace, cx| {
AgentDiff::deploy_in_workspace(thread, workspace, window, cx)
})
.log_err();
AgentDiff::deploy(thread, self.workspace.clone(), window, cx).log_err();
}
pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -582,7 +485,7 @@ impl AssistantPanel {
let tools = self.thread_store.read(cx).tools();
let fs = self.fs.clone();
self.set_active_view(ActiveView::Configuration, window, cx);
self.active_view = ActiveView::Configuration;
self.configuration =
Some(cx.new(|cx| {
AssistantConfiguration::new(fs, context_server_manager, tools, window, cx)
@@ -689,56 +592,27 @@ impl AssistantPanel {
self.thread.read(cx).thread().clone()
}
pub(crate) fn delete_thread(
&mut self,
thread_id: &ThreadId,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
pub(crate) fn delete_thread(&mut self, thread_id: &ThreadId, cx: &mut Context<Self>) {
self.thread_store
.update(cx, |this, cx| this.delete_thread(thread_id, cx))
.detach_and_log_err(cx);
}
pub(crate) fn active_context_editor(&self) -> Option<Entity<ContextEditor>> {
self.context_editor.clone()
}
pub(crate) fn delete_context(
&mut self,
path: PathBuf,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
pub(crate) fn delete_context(&mut self, path: PathBuf, cx: &mut Context<Self>) {
self.context_store
.update(cx, |this, cx| this.delete_local_context(path, cx))
}
fn set_active_view(
&mut self,
new_view: ActiveView,
window: &mut Window,
cx: &mut Context<Self>,
) {
let current_is_history = matches!(self.active_view, ActiveView::History);
let new_is_history = matches!(new_view, ActiveView::History);
if current_is_history && !new_is_history {
self.active_view = new_view;
} else if !current_is_history && new_is_history {
self.previous_view = Some(std::mem::replace(&mut self.active_view, new_view));
} else {
if !new_is_history {
self.previous_view = None;
}
self.active_view = new_view;
}
self.focus_handle(cx).focus(window);
.detach_and_log_err(cx);
}
}
impl Focusable for AssistantPanel {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match self.active_view {
ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
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() {
@@ -839,64 +713,7 @@ impl Panel for AssistantPanel {
}
impl AssistantPanel {
fn render_title_view(&self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
let content = match &self.active_view {
ActiveView::Thread {
change_title_editor,
..
} => {
let active_thread = self.thread.read(cx);
let is_empty = active_thread.is_empty();
let summary = active_thread.summary(cx);
if is_empty {
Label::new(Thread::DEFAULT_SUMMARY.clone())
.truncate()
.ml_2()
.into_any_element()
} else if summary.is_none() {
Label::new(LOADING_SUMMARY_PLACEHOLDER)
.ml_2()
.truncate()
.into_any_element()
} else {
div()
.ml_2()
.w_full()
.child(change_title_editor.clone())
.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));
Label::new(title).ml_2().truncate().into_any_element()
}
ActiveView::History => Label::new("History").truncate().into_any_element(),
ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
};
h_flex()
.key_context("TitleEditor")
.id("TitleEditor")
.flex_grow()
.w_full()
.max_w_full()
.overflow_x_scroll()
.child(content)
.into_any()
}
fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render_toolbar(&self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let active_thread = self.thread.read(cx);
let thread = active_thread.thread().read(cx);
let token_usage = thread.total_token_usage(cx);
@@ -906,62 +723,57 @@ impl AssistantPanel {
let is_empty = active_thread.is_empty();
let focus_handle = self.focus_handle(cx);
let is_history = matches!(self.active_view, ActiveView::History);
let show_token_count = match &self.active_view {
ActiveView::Thread { .. } => !is_empty,
ActiveView::PromptEditor => self.context_editor.is_some(),
_ => false,
let title = match self.active_view {
ActiveView::Thread => {
if is_empty {
active_thread.summary_or_default(cx)
} else {
active_thread
.summary(cx)
.unwrap_or_else(|| SharedString::from("Loading Summary…"))
}
}
ActiveView::PromptEditor => 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…")),
ActiveView::History => "History".into(),
ActiveView::Configuration => "Settings".into(),
};
let go_back_button = match &self.active_view {
ActiveView::History | ActiveView::Configuration => Some(
div().pl_1().child(
IconButton::new("go-back", IconName::ArrowLeft)
.icon_size(IconSize::Small)
.on_click(cx.listener(|this, _, window, cx| {
this.go_back(&workspace::GoBack, window, cx);
}))
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Go Back",
&workspace::GoBack,
&focus_handle,
window,
cx,
)
}
}),
),
),
_ => None,
let show_token_count = match self.active_view {
ActiveView::Thread => !is_empty,
ActiveView::PromptEditor => self.context_editor.is_some(),
_ => false,
};
h_flex()
.id("assistant-toolbar")
.h(Tab::container_height(cx))
.max_w_full()
.flex_none()
.justify_between()
.gap_2()
.gap(DynamicSpacing::Base08.rems(cx))
.bg(cx.theme().colors().tab_bar_background)
.border_b_1()
.border_color(cx.theme().colors().border)
.child(
h_flex()
.w_full()
.gap_1()
.children(go_back_button)
.child(self.render_title_view(window, cx)),
div()
.id("title")
.overflow_x_scroll()
.px(DynamicSpacing::Base08.rems(cx))
.child(Label::new(title).truncate()),
)
.child(
h_flex()
.h_full()
.pl_2()
.gap_2()
.bg(cx.theme().colors().tab_bar_background)
.when(show_token_count, |parent| match self.active_view {
ActiveView::Thread { .. } => {
ActiveView::Thread => {
if token_usage.total == 0 {
return parent;
}
@@ -974,7 +786,6 @@ impl AssistantPanel {
parent.child(
h_flex()
.flex_shrink_0()
.gap_0p5()
.child(
Label::new(assistant_context_editor::humanize_token_count(
@@ -1026,10 +837,10 @@ impl AssistantPanel {
.child(
h_flex()
.h_full()
.gap(DynamicSpacing::Base02.rems(cx))
.px(DynamicSpacing::Base08.rems(cx))
.border_l_1()
.border_color(cx.theme().colors().border)
.gap(DynamicSpacing::Base02.rems(cx))
.child(
IconButton::new("new", IconName::Plus)
.icon_size(IconSize::Small)
@@ -1050,27 +861,6 @@ impl AssistantPanel {
);
}),
)
.child(
IconButton::new("open-history", IconName::HistoryRerun)
.icon_size(IconSize::Small)
.toggle_state(is_history)
.selected_icon_color(Color::Accent)
.tooltip({
let focus_handle = self.focus_handle(cx);
move |window, cx| {
Tooltip::for_action_in(
"History",
&OpenHistory,
&focus_handle,
window,
cx,
)
}
})
.on_click(move |_event, window, cx| {
window.dispatch_action(OpenHistory.boxed_clone(), cx);
}),
)
.child(
PopoverMenu::new("assistant-menu")
.trigger_with_tooltip(
@@ -1087,7 +877,13 @@ impl AssistantPanel {
cx,
|menu, _window, _cx| {
menu.action(
"New Text Thread",
"New Thread",
Box::new(NewThread {
from_thread_id: None,
}),
)
.action(
"New Prompt Editor",
NewPromptEditor.boxed_clone(),
)
.when(!is_empty, |menu| {
@@ -1099,6 +895,7 @@ impl AssistantPanel {
)
})
.separator()
.action("History", OpenHistory.boxed_clone())
.action("Settings", OpenConfiguration.boxed_clone())
},
))
@@ -1595,10 +1392,9 @@ impl Render for AssistantPanel {
.on_action(cx.listener(Self::open_active_thread_as_markdown))
.on_action(cx.listener(Self::deploy_prompt_library))
.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 {
ActiveView::Thread { .. } => parent
ActiveView::Thread => parent
.child(self.render_active_thread_or_empty_state(window, cx))
.child(h_flex().child(self.message_editor.clone()))
.children(self.render_last_error(cx)),
@@ -1628,21 +1424,7 @@ impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist {
cx: &mut Context<PromptLibrary>,
) {
InlineAssistant::update_global(cx, |assistant, cx| {
let Some(project) = self
.workspace
.upgrade()
.map(|workspace| workspace.read(cx).project().downgrade())
else {
return;
};
assistant.assist(
&prompt_editor,
self.workspace.clone(),
project,
None,
window,
cx,
)
assistant.assist(&prompt_editor, self.workspace.clone(), None, window, cx)
})
}

View File

@@ -28,7 +28,7 @@ use std::{
time::Instant,
};
use streaming_diff::{CharOperation, LineDiff, LineOperation, StreamingDiff};
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
pub struct BufferCodegen {
alternatives: Vec<Entity<CodegenAlternative>>,
@@ -601,7 +601,7 @@ impl CodegenAlternative {
let error_message = result.as_ref().err().map(|error| error.to_string());
report_assistant_event(
AssistantEventData {
AssistantEvent {
conversation_id: None,
message_id,
kind: AssistantKind::Inline,

View File

@@ -1,9 +1,9 @@
use std::{ops::Range, path::Path, sync::Arc};
use std::{ops::Range, sync::Arc};
use gpui::{App, Entity, SharedString};
use language::{Buffer, File};
use language_model::LanguageModelRequestMessage;
use project::{ProjectPath, Worktree};
use project::ProjectPath;
use serde::{Deserialize, Serialize};
use text::{Anchor, BufferId};
use ui::IconName;
@@ -69,21 +69,10 @@ pub struct FileContext {
#[derive(Debug, Clone)]
pub struct DirectoryContext {
pub id: ContextId,
pub worktree: Entity<Worktree>,
pub path: Arc<Path>,
/// Buffers of the files within the directory.
pub project_path: ProjectPath,
pub context_buffers: Vec<ContextBuffer>,
}
impl DirectoryContext {
pub fn project_path(&self, cx: &App) -> ProjectPath {
ProjectPath {
worktree_id: self.worktree.read(cx).id(),
path: self.path.clone(),
}
}
}
#[derive(Debug, Clone)]
pub struct SymbolContext {
pub id: ContextId,
@@ -97,11 +86,12 @@ pub struct FetchedUrlContext {
pub text: SharedString,
}
// TODO: Model<Thread> holds onto the thread even if the thread is deleted. Can either handle this
// explicitly or have a WeakModel<Thread> and remove during snapshot.
#[derive(Debug, Clone)]
pub struct ThreadContext {
pub id: ContextId,
// TODO: Entity<Thread> holds onto the thread even if the thread is deleted. Should probably be
// a WeakEntity and handle removal from the UI when it has dropped.
pub thread: Entity<Thread>,
pub text: SharedString,
}
@@ -115,11 +105,12 @@ impl ThreadContext {
}
}
// TODO: Model<Buffer> holds onto the buffer even if the file is deleted and closed. Should remove
// the context from the message editor in this case.
#[derive(Clone)]
pub struct ContextBuffer {
pub id: BufferId,
// TODO: Entity<Buffer> holds onto the thread even if the thread is deleted. Should probably be
// a WeakEntity and handle removal from the UI when it has dropped.
pub buffer: Entity<Buffer>,
pub file: Arc<dyn File>,
pub version: clock::Global,

View File

@@ -13,8 +13,7 @@ use editor::display_map::{Crease, FoldId};
use editor::{Anchor, AnchorRangeExt as _, Editor, ExcerptId, FoldPlaceholder, ToOffset};
use file_context_picker::render_file_context_entry;
use gpui::{
App, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task,
WeakEntity,
App, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity,
};
use multi_buffer::MultiBufferRow;
use project::{Entry, ProjectPath};
@@ -106,7 +105,6 @@ pub(super) struct ContextPicker {
context_store: WeakEntity<ContextStore>,
thread_store: Option<WeakEntity<ThreadStore>>,
confirm_behavior: ConfirmBehavior,
_subscriptions: Vec<Subscription>,
}
impl ContextPicker {
@@ -118,22 +116,6 @@ impl ContextPicker {
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let subscriptions = context_store
.upgrade()
.map(|context_store| {
cx.observe(&context_store, |this, _, cx| this.notify_current_picker(cx))
})
.into_iter()
.chain(
thread_store
.as_ref()
.and_then(|thread_store| thread_store.upgrade())
.map(|thread_store| {
cx.observe(&thread_store, |this, _, cx| this.notify_current_picker(cx))
}),
)
.collect::<Vec<Subscription>>();
ContextPicker {
mode: ContextPickerState::Default(ContextMenu::build(
window,
@@ -144,7 +126,6 @@ impl ContextPicker {
context_store,
thread_store,
confirm_behavior,
_subscriptions: subscriptions,
}
}
@@ -289,14 +270,12 @@ impl ContextPicker {
path_prefix,
} => {
let context_store = self.context_store.clone();
let worktree_id = project_path.worktree_id;
let path = project_path.path.clone();
ContextMenuItem::custom_entry(
move |_window, cx| {
render_file_context_entry(
ElementId::NamedInteger("ctx-recent".into(), ix),
worktree_id,
&path,
&path_prefix,
false,
@@ -391,16 +370,6 @@ impl ContextPicker {
recent_context_picker_entries(context_store, self.thread_store.clone(), workspace, cx)
}
fn notify_current_picker(&mut self, cx: &mut Context<Self>) {
match &self.mode {
ContextPickerState::Default(entity) => entity.update(cx, |_, cx| cx.notify()),
ContextPickerState::File(entity) => entity.update(cx, |_, cx| cx.notify()),
ContextPickerState::Symbol(entity) => entity.update(cx, |_, cx| cx.notify()),
ContextPickerState::Fetch(entity) => entity.update(cx, |_, cx| cx.notify()),
ContextPickerState::Thread(entity) => entity.update(cx, |_, cx| cx.notify()),
}
}
}
impl EventEmitter<DismissEvent> for ContextPicker {}
@@ -468,7 +437,7 @@ fn recent_context_picker_entries(
recent.extend(
workspace
.recent_navigation_history_iter(cx)
.filter(|(path, _)| !current_files.contains(path))
.filter(|(path, _)| !current_files.contains(&path.path.to_path_buf()))
.take(4)
.filter_map(|(project_path, _)| {
project

View File

@@ -18,133 +18,16 @@ use text::{Anchor, ToPoint};
use ui::prelude::*;
use workspace::Workspace;
use crate::context_picker::file_context_picker::search_files;
use crate::context_picker::symbol_context_picker::search_symbols;
use crate::context::AssistantContext;
use crate::context_store::ContextStore;
use crate::thread_store::ThreadStore;
use super::fetch_context_picker::fetch_url_content;
use super::file_context_picker::FileMatch;
use super::symbol_context_picker::SymbolMatch;
use super::thread_context_picker::{ThreadContextEntry, ThreadMatch, search_threads};
use super::thread_context_picker::ThreadContextEntry;
use super::{
ContextPickerMode, MentionLink, RecentEntry, recent_context_picker_entries,
supported_context_picker_modes,
ContextPickerMode, MentionLink, recent_context_picker_entries, supported_context_picker_modes,
};
pub(crate) enum Match {
Symbol(SymbolMatch),
File(FileMatch),
Thread(ThreadMatch),
Fetch(SharedString),
Mode(ContextPickerMode),
}
fn search(
mode: Option<ContextPickerMode>,
query: String,
cancellation_flag: Arc<AtomicBool>,
recent_entries: Vec<RecentEntry>,
thread_store: Option<WeakEntity<ThreadStore>>,
workspace: Entity<Workspace>,
cx: &mut App,
) -> Task<Vec<Match>> {
match mode {
Some(ContextPickerMode::File) => {
let search_files_task =
search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
cx.background_spawn(async move {
search_files_task
.await
.into_iter()
.map(Match::File)
.collect()
})
}
Some(ContextPickerMode::Symbol) => {
let search_symbols_task =
search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx);
cx.background_spawn(async move {
search_symbols_task
.await
.into_iter()
.map(Match::Symbol)
.collect()
})
}
Some(ContextPickerMode::Thread) => {
if let Some(thread_store) = thread_store.as_ref().and_then(|t| t.upgrade()) {
let search_threads_task =
search_threads(query.clone(), cancellation_flag.clone(), thread_store, cx);
cx.background_spawn(async move {
search_threads_task
.await
.into_iter()
.map(Match::Thread)
.collect()
})
} else {
Task::ready(Vec::new())
}
}
Some(ContextPickerMode::Fetch) => {
if !query.is_empty() {
Task::ready(vec![Match::Fetch(query.into())])
} else {
Task::ready(Vec::new())
}
}
None => {
if query.is_empty() {
let mut matches = recent_entries
.into_iter()
.map(|entry| match entry {
super::RecentEntry::File {
project_path,
path_prefix,
} => Match::File(FileMatch {
mat: fuzzy::PathMatch {
score: 1.,
positions: Vec::new(),
worktree_id: project_path.worktree_id.to_usize(),
path: project_path.path,
path_prefix,
is_dir: false,
distance_to_relative_ancestor: 0,
},
is_recent: true,
}),
super::RecentEntry::Thread(thread_context_entry) => {
Match::Thread(ThreadMatch {
thread: thread_context_entry,
is_recent: true,
})
}
})
.collect::<Vec<_>>();
matches.extend(
supported_context_picker_modes(&thread_store)
.into_iter()
.map(Match::Mode),
);
Task::ready(matches)
} else {
let search_files_task =
search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
cx.background_spawn(async move {
search_files_task
.await
.into_iter()
.map(Match::File)
.collect()
})
}
}
}
}
pub struct ContextPickerCompletionProvider {
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
@@ -167,20 +50,96 @@ impl ContextPickerCompletionProvider {
}
}
fn completion_for_mode(source_range: Range<Anchor>, mode: ContextPickerMode) -> Completion {
Completion {
replace_range: source_range.clone(),
new_text: format!("@{} ", mode.mention_prefix()),
label: CodeLabel::plain(mode.label().to_string(), None),
icon_path: Some(mode.icon().path().into()),
documentation: None,
source: project::CompletionSource::Custom,
insert_text_mode: None,
// This ensures that when a user accepts this completion, the
// completion menu will still be shown after "@category " is
// inserted
confirm: Some(Arc::new(|_, _, _| true)),
fn default_completions(
excerpt_id: ExcerptId,
source_range: Range<Anchor>,
context_store: Entity<ContextStore>,
thread_store: Option<WeakEntity<ThreadStore>>,
editor: Entity<Editor>,
workspace: Entity<Workspace>,
cx: &App,
) -> Vec<Completion> {
let mut completions = Vec::new();
completions.extend(
recent_context_picker_entries(
context_store.clone(),
thread_store.clone(),
workspace.clone(),
cx,
)
.iter()
.filter_map(|entry| match entry {
super::RecentEntry::File {
project_path,
path_prefix,
} => Some(Self::completion_for_path(
project_path.clone(),
path_prefix,
true,
false,
excerpt_id,
source_range.clone(),
editor.clone(),
context_store.clone(),
cx,
)),
super::RecentEntry::Thread(thread_context_entry) => {
let thread_store = thread_store
.as_ref()
.and_then(|thread_store| thread_store.upgrade())?;
Some(Self::completion_for_thread(
thread_context_entry.clone(),
excerpt_id,
source_range.clone(),
true,
editor.clone(),
context_store.clone(),
thread_store,
))
}
}),
);
completions.extend(
supported_context_picker_modes(&thread_store)
.iter()
.map(|mode| {
Completion {
old_range: source_range.clone(),
new_text: format!("@{} ", mode.mention_prefix()),
label: CodeLabel::plain(mode.label().to_string(), None),
icon_path: Some(mode.icon().path().into()),
documentation: None,
source: project::CompletionSource::Custom,
// This ensures that when a user accepts this completion, the
// completion menu will still be shown after "@category " is
// inserted
confirm: Some(Arc::new(|_, _, _| true)),
}
}),
);
completions
}
fn build_code_label_for_full_path(
file_name: &str,
directory: Option<&str>,
cx: &App,
) -> CodeLabel {
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
let mut label = CodeLabel::default();
label.push_str(&file_name, None);
label.push_str(" ", None);
if let Some(directory) = directory {
label.push_str(&directory, comment_id);
}
label.filter_range = 0..label.text().len();
label
}
fn completion_for_thread(
@@ -200,11 +159,10 @@ impl ContextPickerCompletionProvider {
let new_text = MentionLink::for_thread(&thread_entry);
let new_text_len = new_text.len();
Completion {
replace_range: source_range.clone(),
old_range: source_range.clone(),
new_text,
label: CodeLabel::plain(thread_entry.summary.to_string(), None),
documentation: None,
insert_text_mode: None,
source: project::CompletionSource::Custom,
icon_path: Some(icon_for_completion.path().into()),
confirm: Some(confirm_completion_callback(
@@ -245,13 +203,12 @@ impl ContextPickerCompletionProvider {
let new_text = MentionLink::for_fetch(&url_to_fetch);
let new_text_len = new_text.len();
Completion {
replace_range: source_range.clone(),
old_range: source_range.clone(),
new_text,
label: CodeLabel::plain(url_to_fetch.to_string(), None),
documentation: None,
source: project::CompletionSource::Custom,
icon_path: Some(IconName::Globe.path().into()),
insert_text_mode: None,
confirm: Some(confirm_completion_callback(
IconName::Globe.path().into(),
url_to_fetch.clone(),
@@ -275,8 +232,8 @@ impl ContextPickerCompletionProvider {
url_to_fetch.to_string(),
))
.await?;
context_store.update(cx, |context_store, cx| {
context_store.add_fetched_url(url_to_fetch.to_string(), content, cx)
context_store.update(cx, |context_store, _| {
context_store.add_fetched_url(url_to_fetch.to_string(), content)
})
})
.detach_and_log_err(cx);
@@ -301,8 +258,11 @@ impl ContextPickerCompletionProvider {
path_prefix,
);
let label =
build_code_label_for_full_path(&file_name, directory.as_ref().map(|s| s.as_ref()), cx);
let label = Self::build_code_label_for_full_path(
&file_name,
directory.as_ref().map(|s| s.as_ref()),
cx,
);
let full_path = if let Some(directory) = directory {
format!("{}{}", directory, file_name)
} else {
@@ -324,13 +284,12 @@ impl ContextPickerCompletionProvider {
let new_text = MentionLink::for_file(&file_name, &full_path);
let new_text_len = new_text.len();
Completion {
replace_range: source_range.clone(),
old_range: source_range.clone(),
new_text,
label,
documentation: None,
source: project::CompletionSource::Custom,
icon_path: Some(completion_icon_path),
insert_text_mode: None,
confirm: Some(confirm_completion_callback(
crease_icon_path,
file_name,
@@ -387,13 +346,12 @@ impl ContextPickerCompletionProvider {
let new_text = MentionLink::for_symbol(&symbol.name, &full_path);
let new_text_len = new_text.len();
Some(Completion {
replace_range: source_range.clone(),
old_range: source_range.clone(),
new_text,
label,
documentation: None,
source: project::CompletionSource::Custom,
icon_path: Some(IconName::Code.path().into()),
insert_text_mode: None,
confirm: Some(confirm_completion_callback(
IconName::Code.path().into(),
symbol.name.clone().into(),
@@ -419,22 +377,6 @@ impl ContextPickerCompletionProvider {
}
}
fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
let mut label = CodeLabel::default();
label.push_str(&file_name, None);
label.push_str(" ", None);
if let Some(directory) = directory {
label.push_str(&directory, comment_id);
}
label.filter_range = 0..label.text().len();
label
}
impl CompletionProvider for ContextPickerCompletionProvider {
fn completions(
&self,
@@ -457,9 +399,10 @@ impl CompletionProvider for ContextPickerCompletionProvider {
return Task::ready(Ok(None));
};
let Some((workspace, context_store)) =
self.workspace.upgrade().zip(self.context_store.upgrade())
else {
let Some(workspace) = self.workspace.upgrade() else {
return Task::ready(Ok(None));
};
let Some(context_store) = self.context_store.upgrade() else {
return Task::ready(Ok(None));
};
@@ -471,89 +414,154 @@ impl CompletionProvider for ContextPickerCompletionProvider {
let editor = self.editor.clone();
let http_client = workspace.read(cx).client().http_client().clone();
let MentionCompletion { mode, argument, .. } = state;
let query = argument.unwrap_or_else(|| "".to_string());
let recent_entries = recent_context_picker_entries(
context_store.clone(),
thread_store.clone(),
workspace.clone(),
cx,
);
let search_task = search(
mode,
query,
Arc::<AtomicBool>::default(),
recent_entries,
thread_store.clone(),
workspace.clone(),
cx,
);
cx.spawn(async move |_, cx| {
let matches = search_task.await;
let Some(editor) = editor.upgrade() else {
return Ok(None);
};
let mut completions = Vec::new();
Ok(Some(cx.update(|cx| {
matches
.into_iter()
.filter_map(|mat| match mat {
Match::File(FileMatch { mat, is_recent }) => {
Some(Self::completion_for_path(
ProjectPath {
worktree_id: WorktreeId::from_usize(mat.worktree_id),
path: mat.path.clone(),
},
&mat.path_prefix,
is_recent,
mat.is_dir,
excerpt_id,
source_range.clone(),
editor.clone(),
context_store.clone(),
let MentionCompletion { mode, argument, .. } = state;
let query = argument.unwrap_or_else(|| "".to_string());
match mode {
Some(ContextPickerMode::File) => {
let path_matches = cx
.update(|cx| {
super::file_context_picker::search_paths(
query,
Arc::<AtomicBool>::default(),
&workspace,
cx,
))
}
Match::Symbol(SymbolMatch { symbol, .. }) => Self::completion_for_symbol(
symbol,
excerpt_id,
source_range.clone(),
editor.clone(),
context_store.clone(),
workspace.clone(),
cx,
),
Match::Thread(ThreadMatch {
thread, is_recent, ..
}) => {
let thread_store = thread_store.as_ref().and_then(|t| t.upgrade())?;
Some(Self::completion_for_thread(
thread,
excerpt_id,
)
})?
.await;
if let Some(editor) = editor.upgrade() {
completions.reserve(path_matches.len());
cx.update(|cx| {
completions.extend(path_matches.iter().map(|mat| {
Self::completion_for_path(
ProjectPath {
worktree_id: WorktreeId::from_usize(mat.worktree_id),
path: mat.path.clone(),
},
&mat.path_prefix,
false,
mat.is_dir,
excerpt_id,
source_range.clone(),
editor.clone(),
context_store.clone(),
cx,
)
}));
})?;
}
}
Some(ContextPickerMode::Symbol) => {
if let Some(editor) = editor.upgrade() {
let symbol_matches = cx
.update(|cx| {
super::symbol_context_picker::search_symbols(
query,
Arc::new(AtomicBool::default()),
&workspace,
cx,
)
})?
.await?;
cx.update(|cx| {
completions.extend(symbol_matches.into_iter().filter_map(
|(_, symbol)| {
Self::completion_for_symbol(
symbol,
excerpt_id,
source_range.clone(),
editor.clone(),
context_store.clone(),
workspace.clone(),
cx,
)
},
));
})?;
}
}
Some(ContextPickerMode::Fetch) => {
if let Some(editor) = editor.upgrade() {
if !query.is_empty() {
completions.push(Self::completion_for_fetch(
source_range.clone(),
is_recent,
query.into(),
excerpt_id,
editor.clone(),
context_store.clone(),
thread_store,
))
http_client.clone(),
));
}
Match::Fetch(url) => Some(Self::completion_for_fetch(
source_range.clone(),
url,
excerpt_id,
editor.clone(),
context_store.clone(),
http_client.clone(),
)),
Match::Mode(mode) => {
Some(Self::completion_for_mode(source_range.clone(), mode))
context_store.update(cx, |store, _| {
let urls = store.context().iter().filter_map(|context| {
if let AssistantContext::FetchedUrl(context) = context {
Some(context.url.clone())
} else {
None
}
});
for url in urls {
completions.push(Self::completion_for_fetch(
source_range.clone(),
url,
excerpt_id,
editor.clone(),
context_store.clone(),
http_client.clone(),
));
}
})?;
}
}
Some(ContextPickerMode::Thread) => {
if let Some((thread_store, editor)) = thread_store
.and_then(|thread_store| thread_store.upgrade())
.zip(editor.upgrade())
{
let threads = cx
.update(|cx| {
super::thread_context_picker::search_threads(
query,
thread_store.clone(),
cx,
)
})?
.await;
for thread in threads {
completions.push(Self::completion_for_thread(
thread.clone(),
excerpt_id,
source_range.clone(),
false,
editor.clone(),
context_store.clone(),
thread_store.clone(),
));
}
})
.collect()
})?))
}
}
None => {
cx.update(|cx| {
if let Some(editor) = editor.upgrade() {
completions.extend(Self::default_completions(
excerpt_id,
source_range.clone(),
context_store.clone(),
thread_store.clone(),
editor,
workspace.clone(),
cx,
));
}
})?;
}
}
Ok(Some(completions))
})
}
@@ -663,12 +671,7 @@ impl MentionCompletion {
let mut end = last_mention_start + 1;
if let Some(mode_text) = parts.next() {
end += mode_text.len();
if let Some(parsed_mode) = ContextPickerMode::try_from(mode_text).ok() {
mode = Some(parsed_mode);
} else {
argument = Some(mode_text.to_string());
}
mode = ContextPickerMode::try_from(mode_text).ok();
match rest_of_line[mode_text.len()..].find(|c: char| !c.is_whitespace()) {
Some(whitespace_count) => {
if let Some(argument_text) = parts.next() {
@@ -694,13 +697,13 @@ impl MentionCompletion {
#[cfg(test)]
mod tests {
use super::*;
use gpui::{EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext};
use gpui::{Focusable, TestAppContext, VisualTestContext};
use project::{Project, ProjectPath};
use serde_json::json;
use settings::SettingsStore;
use std::ops::Deref;
use std::{ops::Deref, path::PathBuf};
use util::{path, separator};
use workspace::{AppState, Item};
use workspace::AppState;
#[test]
fn test_mention_completion_parse() {
@@ -760,42 +763,9 @@ mod tests {
})
);
assert_eq!(
MentionCompletion::try_parse("Lorem @main", 0),
Some(MentionCompletion {
source_range: 6..11,
mode: None,
argument: Some("main".to_string()),
})
);
assert_eq!(MentionCompletion::try_parse("test@", 0), None);
}
struct AtMentionEditor(Entity<Editor>);
impl Item for AtMentionEditor {
type Event = ();
fn include_in_nav_history() -> bool {
false
}
}
impl EventEmitter<()> for AtMentionEditor {}
impl Focusable for AtMentionEditor {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.0.read(cx).focus_handle(cx).clone()
}
}
impl Render for AtMentionEditor {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
self.0.clone().into_any_element()
}
}
#[gpui::test]
async fn test_context_completion_provider(cx: &mut TestAppContext) {
init_test(cx);
@@ -871,30 +841,28 @@ mod tests {
.unwrap();
}
let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
let editor = cx.new(|cx| {
Editor::new(
editor::EditorMode::Full,
multi_buffer::MultiBuffer::build_simple("", cx),
let item = workspace
.update_in(&mut cx, |workspace, window, cx| {
workspace.open_path(
ProjectPath {
worktree_id,
path: PathBuf::from("editor").into(),
},
None,
true,
window,
cx,
)
});
workspace.active_pane().update(cx, |pane, cx| {
pane.add_item(
Box::new(cx.new(|_| AtMentionEditor(editor.clone()))),
true,
true,
None,
window,
cx,
);
});
editor
})
.await
.expect("Could not open test file");
let editor = cx.update(|_, cx| {
item.act_as::<Editor>(cx)
.expect("Opened test file wasn't an editor")
});
let context_store = cx.new(|_| ContextStore::new(project.downgrade(), None));
let context_store = cx.new(|_| ContextStore::new(workspace.downgrade(), None));
let editor_entity = editor.downgrade();
editor.update_in(&mut cx, |editor, window, cx| {
@@ -922,10 +890,10 @@ mod tests {
assert_eq!(
current_completion_labels(editor),
&[
"editor dir/",
"seven.txt dir/b/",
"six.txt dir/b/",
"five.txt dir/b/",
"four.txt dir/a/",
"Files & Directories",
"Symbols",
"Fetch"
@@ -1020,14 +988,14 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt)"
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@editor](@file:dir/editor)"
);
assert!(!editor.has_visible_completions_menu());
assert_eq!(
crease_ranges(editor, cx),
vec![
Point::new(0, 6)..Point::new(0, 37),
Point::new(0, 44)..Point::new(0, 79)
Point::new(0, 44)..Point::new(0, 71)
]
);
});
@@ -1037,14 +1005,14 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt)\n@"
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@editor](@file:dir/editor)\n@"
);
assert!(editor.has_visible_completions_menu());
assert_eq!(
crease_ranges(editor, cx),
vec![
Point::new(0, 6)..Point::new(0, 37),
Point::new(0, 44)..Point::new(0, 79)
Point::new(0, 44)..Point::new(0, 71)
]
);
});
@@ -1058,15 +1026,15 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt)\n[@six.txt](@file:dir/b/six.txt)"
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@editor](@file:dir/editor)\n[@seven.txt](@file:dir/b/seven.txt)"
);
assert!(!editor.has_visible_completions_menu());
assert_eq!(
crease_ranges(editor, cx),
vec![
Point::new(0, 6)..Point::new(0, 37),
Point::new(0, 44)..Point::new(0, 79),
Point::new(1, 0)..Point::new(1, 31)
Point::new(0, 44)..Point::new(0, 71),
Point::new(1, 0)..Point::new(1, 35)
]
);
});

View File

@@ -213,8 +213,8 @@ impl PickerDelegate for FetchContextPickerDelegate {
this.update_in(cx, |this, window, cx| {
this.delegate
.context_store
.update(cx, |context_store, cx| {
context_store.add_fetched_url(url, text, cx)
.update(cx, |context_store, _cx| {
context_store.add_fetched_url(url, text);
})?;
match confirm_behavior {

View File

@@ -58,7 +58,7 @@ pub struct FileContextPickerDelegate {
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
confirm_behavior: ConfirmBehavior,
matches: Vec<FileMatch>,
matches: Vec<PathMatch>,
selected_index: usize,
}
@@ -114,7 +114,7 @@ impl PickerDelegate for FileContextPickerDelegate {
return Task::ready(());
};
let search_task = search_files(query, Arc::<AtomicBool>::default(), &workspace, cx);
let search_task = search_paths(query, Arc::<AtomicBool>::default(), &workspace, cx);
cx.spawn_in(window, async move |this, cx| {
// TODO: This should be probably be run in the background.
@@ -128,7 +128,7 @@ impl PickerDelegate for FileContextPickerDelegate {
}
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
let Some(FileMatch { mat, .. }) = self.matches.get(self.selected_index) else {
let Some(mat) = self.matches.get(self.selected_index) else {
return;
};
@@ -181,7 +181,7 @@ impl PickerDelegate for FileContextPickerDelegate {
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let FileMatch { mat, .. } = &self.matches[ix];
let path_match = &self.matches[ix];
Some(
ListItem::new(ix)
@@ -189,10 +189,9 @@ impl PickerDelegate for FileContextPickerDelegate {
.toggle_state(selected)
.child(render_file_context_entry(
ElementId::NamedInteger("file-ctx-picker".into(), ix),
WorktreeId::from_usize(mat.worktree_id),
&mat.path,
&mat.path_prefix,
mat.is_dir,
&path_match.path,
&path_match.path_prefix,
path_match.is_dir,
self.context_store.clone(),
cx,
)),
@@ -200,17 +199,12 @@ impl PickerDelegate for FileContextPickerDelegate {
}
}
pub struct FileMatch {
pub mat: PathMatch,
pub is_recent: bool,
}
pub(crate) fn search_files(
pub(crate) fn search_paths(
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &Entity<Workspace>,
cx: &App,
) -> Task<Vec<FileMatch>> {
) -> Task<Vec<PathMatch>> {
if query.is_empty() {
let workspace = workspace.read(cx);
let project = workspace.project().read(cx);
@@ -219,34 +213,28 @@ pub(crate) fn search_files(
.into_iter()
.filter_map(|(project_path, _)| {
let worktree = project.worktree_for_id(project_path.worktree_id, cx)?;
Some(FileMatch {
mat: PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: project_path.worktree_id.to_usize(),
path: project_path.path,
path_prefix: worktree.read(cx).root_name().into(),
distance_to_relative_ancestor: 0,
is_dir: false,
},
is_recent: true,
Some(PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: project_path.worktree_id.to_usize(),
path: project_path.path,
path_prefix: worktree.read(cx).root_name().into(),
distance_to_relative_ancestor: 0,
is_dir: false,
})
});
let file_matches = project.worktrees(cx).flat_map(|worktree| {
let worktree = worktree.read(cx);
let path_prefix: Arc<str> = worktree.root_name().into();
worktree.entries(false, 0).map(move |entry| FileMatch {
mat: PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: worktree.id().to_usize(),
path: entry.path.clone(),
path_prefix: path_prefix.clone(),
distance_to_relative_ancestor: 0,
is_dir: entry.is_dir(),
},
is_recent: false,
worktree.entries(false, 0).map(move |entry| PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: worktree.id().to_usize(),
path: entry.path.clone(),
path_prefix: path_prefix.clone(),
distance_to_relative_ancestor: 0,
is_dir: entry.is_dir(),
})
});
@@ -281,12 +269,6 @@ pub(crate) fn search_files(
executor,
)
.await
.into_iter()
.map(|mat| FileMatch {
mat,
is_recent: false,
})
.collect::<Vec<_>>()
})
}
}
@@ -329,26 +311,19 @@ pub fn extract_file_name_and_directory(
pub fn render_file_context_entry(
id: ElementId,
worktree_id: WorktreeId,
path: &Arc<Path>,
path: &Path,
path_prefix: &Arc<str>,
is_directory: bool,
context_store: WeakEntity<ContextStore>,
cx: &App,
) -> Stateful<Div> {
let (file_name, directory) = extract_file_name_and_directory(&path, path_prefix);
let (file_name, directory) = extract_file_name_and_directory(path, path_prefix);
let added = context_store.upgrade().and_then(|context_store| {
let project_path = ProjectPath {
worktree_id,
path: path.clone(),
};
if is_directory {
context_store.read(cx).includes_directory(&project_path)
context_store.read(cx).includes_directory(path)
} else {
context_store
.read(cx)
.will_include_file_path(&project_path, cx)
context_store.read(cx).will_include_file_path(path, cx)
}
});
@@ -388,9 +363,8 @@ pub fn render_file_context_entry(
)
.child(Label::new("Added").size(LabelSize::Small)),
),
FileInclusion::InDirectory(directory_project_path) => {
// TODO: Consider using worktree full_path to include worktree name.
let directory_path = directory_project_path.path.to_string_lossy().into_owned();
FileInclusion::InDirectory(dir_name) => {
let dir_name = dir_name.to_string_lossy().into_owned();
el.child(
h_flex()
@@ -404,7 +378,7 @@ pub fn render_file_context_entry(
)
.child(Label::new("Included").size(LabelSize::Small)),
)
.tooltip(Tooltip::text(format!("in {directory_path}")))
.tooltip(Tooltip::text(format!("in {dir_name}")))
}
})
}

View File

@@ -2,7 +2,7 @@ use std::cmp::Reverse;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use anyhow::Result;
use anyhow::{Context as _, Result};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
@@ -119,7 +119,11 @@ impl PickerDelegate for SymbolContextPickerDelegate {
let search_task = search_symbols(query, Arc::<AtomicBool>::default(), &workspace, cx);
let context_store = self.context_store.clone();
cx.spawn_in(window, async move |this, cx| {
let symbols = search_task.await;
let symbols = search_task
.await
.context("Failed to load symbols")
.log_err()
.unwrap_or_default();
let symbol_entries = context_store
.read_with(cx, |context_store, cx| {
@@ -281,16 +285,12 @@ fn find_matching_symbol(symbol: &Symbol, candidates: &[DocumentSymbol]) -> Optio
}
}
pub struct SymbolMatch {
pub symbol: Symbol,
}
pub(crate) fn search_symbols(
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &Entity<Workspace>,
cx: &mut App,
) -> Task<Vec<SymbolMatch>> {
) -> Task<Result<Vec<(StringMatch, Symbol)>>> {
let symbols_task = workspace.update(cx, |workspace, cx| {
workspace
.project()
@@ -298,28 +298,19 @@ pub(crate) fn search_symbols(
});
let project = workspace.read(cx).project().clone();
cx.spawn(async move |cx| {
let Some(symbols) = symbols_task.await.log_err() else {
return Vec::new();
};
let Some((visible_match_candidates, external_match_candidates)): Option<(Vec<_>, Vec<_>)> =
project
.update(cx, |project, cx| {
symbols
.iter()
.enumerate()
.map(|(id, symbol)| {
StringMatchCandidate::new(id, &symbol.label.filter_text())
})
.partition(|candidate| {
project
.entry_for_path(&symbols[candidate.id].path, cx)
.map_or(false, |e| !e.is_ignored)
})
})
.log_err()
else {
return Vec::new();
};
let symbols = symbols_task.await?;
let (visible_match_candidates, external_match_candidates): (Vec<_>, Vec<_>) = project
.update(cx, |project, cx| {
symbols
.iter()
.enumerate()
.map(|(id, symbol)| StringMatchCandidate::new(id, &symbol.label.filter_text()))
.partition(|candidate| {
project
.entry_for_path(&symbols[candidate.id].path, cx)
.map_or(false, |e| !e.is_ignored)
})
})?;
const MAX_MATCHES: usize = 100;
let mut visible_matches = cx.background_executor().block(fuzzy::match_strings(
@@ -348,7 +339,7 @@ pub(crate) fn search_symbols(
let mut matches = visible_matches;
matches.append(&mut external_matches);
matches
Ok(matches
.into_iter()
.map(|mut mat| {
let symbol = symbols[mat.candidate_id].clone();
@@ -356,19 +347,19 @@ pub(crate) fn search_symbols(
for position in &mut mat.positions {
*position += filter_start;
}
SymbolMatch { symbol }
(mat, symbol)
})
.collect()
.collect())
})
}
fn compute_symbol_entries(
symbols: Vec<SymbolMatch>,
symbols: Vec<(StringMatch, Symbol)>,
context_store: &ContextStore,
cx: &App,
) -> Vec<SymbolEntry> {
let mut symbol_entries = Vec::with_capacity(symbols.len());
for SymbolMatch { symbol, .. } in symbols {
for (_, symbol) in symbols {
let symbols_for_path = context_store.included_symbols_by_path().get(&symbol.path);
let is_included = if let Some(symbols_for_path) = symbols_for_path {
let mut is_included = false;

View File

@@ -1,5 +1,4 @@
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use fuzzy::StringMatchCandidate;
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
@@ -115,11 +114,11 @@ impl PickerDelegate for ThreadContextPickerDelegate {
return Task::ready(());
};
let search_task = search_threads(query, Arc::new(AtomicBool::default()), threads, cx);
let search_task = search_threads(query, threads, cx);
cx.spawn_in(window, async move |this, cx| {
let matches = search_task.await;
this.update(cx, |this, cx| {
this.delegate.matches = matches.into_iter().map(|mat| mat.thread).collect();
this.delegate.matches = matches;
this.delegate.selected_index = 0;
cx.notify();
})
@@ -218,18 +217,11 @@ pub fn render_thread_context_entry(
})
}
#[derive(Clone)]
pub struct ThreadMatch {
pub thread: ThreadContextEntry,
pub is_recent: bool,
}
pub(crate) fn search_threads(
query: String,
cancellation_flag: Arc<AtomicBool>,
thread_store: Entity<ThreadStore>,
cx: &mut App,
) -> Task<Vec<ThreadMatch>> {
) -> Task<Vec<ThreadContextEntry>> {
let threads = thread_store.update(cx, |this, _cx| {
this.threads()
.into_iter()
@@ -244,12 +236,6 @@ pub(crate) fn search_threads(
cx.background_spawn(async move {
if query.is_empty() {
threads
.into_iter()
.map(|thread| ThreadMatch {
thread,
is_recent: false,
})
.collect()
} else {
let candidates = threads
.iter()
@@ -261,17 +247,14 @@ pub(crate) fn search_threads(
&query,
false,
100,
&cancellation_flag,
&Default::default(),
executor,
)
.await;
matches
.into_iter()
.map(|mat| ThreadMatch {
thread: threads[mat.candidate_id].clone(),
is_recent: false,
})
.map(|mat| threads[mat.candidate_id].clone())
.collect()
}
})

View File

@@ -1,5 +1,5 @@
use std::ops::Range;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::{Context as _, Result, anyhow};
@@ -8,10 +8,11 @@ use futures::future::join_all;
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 project::{ProjectItem, ProjectPath, Worktree};
use rope::Rope;
use text::{Anchor, BufferId, OffsetRangeExt};
use util::{ResultExt as _, maybe};
use workspace::Workspace;
use crate::ThreadStore;
use crate::context::{
@@ -22,13 +23,13 @@ use crate::context_strip::SuggestedContext;
use crate::thread::{Thread, ThreadId};
pub struct ContextStore {
project: WeakEntity<Project>,
workspace: WeakEntity<Workspace>,
context: Vec<AssistantContext>,
thread_store: Option<WeakEntity<ThreadStore>>,
// TODO: If an EntityId is used for all context types (like BufferId), can remove ContextId.
next_context_id: ContextId,
files: BTreeMap<BufferId, ContextId>,
directories: HashMap<ProjectPath, ContextId>,
directories: HashMap<PathBuf, ContextId>,
symbols: HashMap<ContextSymbolId, ContextId>,
symbol_buffers: HashMap<ContextSymbolId, Entity<Buffer>>,
symbols_by_path: HashMap<ProjectPath, Vec<ContextSymbolId>>,
@@ -39,11 +40,11 @@ pub struct ContextStore {
impl ContextStore {
pub fn new(
project: WeakEntity<Project>,
workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<ThreadStore>>,
) -> Self {
Self {
project,
workspace,
thread_store,
context: Vec::new(),
next_context_id: ContextId(0),
@@ -80,7 +81,12 @@ impl ContextStore {
remove_if_exists: bool,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let Some(project) = self.project.upgrade() else {
let workspace = self.workspace.clone();
let Some(project) = workspace
.upgrade()
.map(|workspace| workspace.read(cx).project().clone())
else {
return Task::ready(Err(anyhow!("failed to read project")));
};
@@ -92,11 +98,11 @@ impl ContextStore {
let buffer = open_buffer_task.await?;
let buffer_id = this.update(cx, |_, cx| buffer.read(cx).remote_id())?;
let already_included = this.update(cx, |this, cx| {
match this.will_include_buffer(buffer_id, &project_path) {
let already_included = this.update(cx, |this, _cx| {
match this.will_include_buffer(buffer_id, &project_path.path) {
Some(FileInclusion::Direct(context_id)) => {
if remove_if_exists {
this.remove_context(context_id, cx);
this.remove_context(context_id);
}
true
}
@@ -114,8 +120,8 @@ impl ContextStore {
let text = text_task.await;
this.update(cx, |this, cx| {
this.insert_file(make_context_buffer(buffer_info, text), cx);
this.update(cx, |this, _cx| {
this.insert_file(make_context_buffer(buffer_info, text));
})?;
anyhow::Ok(())
@@ -133,20 +139,19 @@ impl ContextStore {
let text = text_task.await;
this.update(cx, |this, cx| {
this.insert_file(make_context_buffer(buffer_info, text), cx)
this.update(cx, |this, _cx| {
this.insert_file(make_context_buffer(buffer_info, text))
})?;
anyhow::Ok(())
})
}
fn insert_file(&mut self, context_buffer: ContextBuffer, cx: &mut Context<Self>) {
fn insert_file(&mut self, context_buffer: ContextBuffer) {
let id = self.next_context_id.post_inc();
self.files.insert(context_buffer.id, id);
self.context
.push(AssistantContext::File(FileContext { id, context_buffer }));
cx.notify();
}
pub fn add_directory(
@@ -155,14 +160,18 @@ impl ContextStore {
remove_if_exists: bool,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let Some(project) = self.project.upgrade() else {
let workspace = self.workspace.clone();
let Some(project) = workspace
.upgrade()
.map(|workspace| workspace.read(cx).project().clone())
else {
return Task::ready(Err(anyhow!("failed to read project")));
};
let already_included = match self.includes_directory(&project_path) {
let already_included = match self.includes_directory(&project_path.path) {
Some(FileInclusion::Direct(context_id)) => {
if remove_if_exists {
self.remove_context(context_id, cx);
self.remove_context(context_id);
}
true
}
@@ -223,37 +232,30 @@ impl ContextStore {
.collect::<Vec<_>>();
if context_buffers.is_empty() {
let full_path = cx.update(|cx| worktree.read(cx).full_path(&project_path.path))?;
return Err(anyhow!("No text files found in {}", &full_path.display()));
return Err(anyhow!(
"No text files found in {}",
&project_path.path.display()
));
}
this.update(cx, |this, cx| {
this.insert_directory(worktree, project_path, context_buffers, cx);
this.update(cx, |this, _| {
this.insert_directory(project_path, context_buffers);
})?;
anyhow::Ok(())
})
}
fn insert_directory(
&mut self,
worktree: Entity<Worktree>,
project_path: ProjectPath,
context_buffers: Vec<ContextBuffer>,
cx: &mut Context<Self>,
) {
fn insert_directory(&mut self, project_path: ProjectPath, context_buffers: Vec<ContextBuffer>) {
let id = self.next_context_id.post_inc();
let path = project_path.path.clone();
self.directories.insert(project_path, id);
self.directories.insert(project_path.path.to_path_buf(), id);
self.context
.push(AssistantContext::Directory(DirectoryContext {
id,
worktree,
path,
project_path,
context_buffers,
}));
cx.notify();
}
pub fn add_symbol(
@@ -284,7 +286,7 @@ impl ContextStore {
if let Some(id) = matching_symbol_id {
if remove_if_exists {
self.remove_context(id, cx);
self.remove_context(id);
}
return Task::ready(Ok(false));
}
@@ -299,24 +301,21 @@ impl ContextStore {
cx.spawn(async move |this, cx| {
let content = collect_content_task.await;
this.update(cx, |this, cx| {
this.insert_symbol(
make_context_symbol(
buffer_info,
project_path,
symbol_name,
symbol_range,
symbol_enclosing_range,
content,
),
cx,
)
this.update(cx, |this, _cx| {
this.insert_symbol(make_context_symbol(
buffer_info,
project_path,
symbol_name,
symbol_range,
symbol_enclosing_range,
content,
))
})?;
anyhow::Ok(true)
})
}
fn insert_symbol(&mut self, context_symbol: ContextSymbol, cx: &mut Context<Self>) {
fn insert_symbol(&mut self, context_symbol: ContextSymbol) {
let id = self.next_context_id.post_inc();
self.symbols.insert(context_symbol.id.clone(), id);
self.symbols_by_path
@@ -329,7 +328,6 @@ impl ContextStore {
id,
context_symbol,
}));
cx.notify();
}
pub fn add_thread(
@@ -340,7 +338,7 @@ impl ContextStore {
) {
if let Some(context_id) = self.includes_thread(&thread.read(cx).id()) {
if remove_if_exists {
self.remove_context(context_id, cx);
self.remove_context(context_id);
}
} else {
self.insert_thread(thread, cx);
@@ -355,14 +353,14 @@ impl ContextStore {
})
}
fn insert_thread(&mut self, thread: Entity<Thread>, cx: &mut Context<Self>) {
fn insert_thread(&mut self, thread: Entity<Thread>, cx: &mut App) {
if let Some(summary_task) =
thread.update(cx, |thread, cx| thread.generate_detailed_summary(cx))
{
let thread = thread.clone();
let thread_store = self.thread_store.clone();
self.thread_summary_tasks.push(cx.spawn(async move |_, cx| {
self.thread_summary_tasks.push(cx.spawn(async move |cx| {
summary_task.await;
if let Some(thread_store) = thread_store {
@@ -384,26 +382,15 @@ impl ContextStore {
self.threads.insert(thread.read(cx).id().clone(), id);
self.context
.push(AssistantContext::Thread(ThreadContext { id, thread, text }));
cx.notify();
}
pub fn add_fetched_url(
&mut self,
url: String,
text: impl Into<SharedString>,
cx: &mut Context<ContextStore>,
) {
pub fn add_fetched_url(&mut self, url: String, text: impl Into<SharedString>) {
if self.includes_url(&url).is_none() {
self.insert_fetched_url(url, text, cx);
self.insert_fetched_url(url, text);
}
}
fn insert_fetched_url(
&mut self,
url: String,
text: impl Into<SharedString>,
cx: &mut Context<ContextStore>,
) {
fn insert_fetched_url(&mut self, url: String, text: impl Into<SharedString>) {
let id = self.next_context_id.post_inc();
self.fetched_urls.insert(url.clone(), id);
@@ -413,7 +400,6 @@ impl ContextStore {
url: url.into(),
text: text.into(),
}));
cx.notify();
}
pub fn accept_suggested_context(
@@ -440,7 +426,7 @@ impl ContextStore {
Task::ready(Ok(()))
}
pub fn remove_context(&mut self, id: ContextId, cx: &mut Context<Self>) {
pub fn remove_context(&mut self, id: ContextId) {
let Some(ix) = self.context.iter().position(|context| context.id() == id) else {
return;
};
@@ -472,38 +458,28 @@ impl ContextStore {
self.threads.retain(|_, context_id| *context_id != id);
}
}
cx.notify();
}
/// Returns whether the buffer is already included directly in the context, or if it will be
/// included in the context via a directory. Directory inclusion is based on paths rather than
/// buffer IDs as the directory will be re-scanned.
pub fn will_include_buffer(
&self,
buffer_id: BufferId,
project_path: &ProjectPath,
) -> Option<FileInclusion> {
pub fn will_include_buffer(&self, buffer_id: BufferId, path: &Path) -> Option<FileInclusion> {
if let Some(context_id) = self.files.get(&buffer_id) {
return Some(FileInclusion::Direct(*context_id));
}
self.will_include_file_path_via_directory(project_path)
self.will_include_file_path_via_directory(path)
}
/// Returns whether this file path is already included directly in the context, or if it will be
/// included in the context via a directory.
pub fn will_include_file_path(
&self,
project_path: &ProjectPath,
cx: &App,
) -> Option<FileInclusion> {
pub fn will_include_file_path(&self, path: &Path, cx: &App) -> Option<FileInclusion> {
if !self.files.is_empty() {
let found_file_context = self.context.iter().find(|context| match &context {
AssistantContext::File(file_context) => {
let buffer = file_context.context_buffer.buffer.read(cx);
if let Some(context_path) = buffer.project_path(cx) {
&context_path == project_path
if let Some(file_path) = buffer_path_log_err(buffer, cx) {
*file_path == *path
} else {
false
}
@@ -515,40 +491,31 @@ impl ContextStore {
}
}
self.will_include_file_path_via_directory(project_path)
self.will_include_file_path_via_directory(path)
}
fn will_include_file_path_via_directory(
&self,
project_path: &ProjectPath,
) -> Option<FileInclusion> {
fn will_include_file_path_via_directory(&self, path: &Path) -> Option<FileInclusion> {
if self.directories.is_empty() {
return None;
}
let mut path_buf = project_path.path.to_path_buf();
let mut buf = path.to_path_buf();
while path_buf.pop() {
// TODO: This isn't very efficient. Consider using a better representation of the
// directories map.
let directory_project_path = ProjectPath {
worktree_id: project_path.worktree_id,
path: path_buf.clone().into(),
};
if let Some(_) = self.directories.get(&directory_project_path) {
return Some(FileInclusion::InDirectory(directory_project_path));
while buf.pop() {
if let Some(_) = self.directories.get(&buf) {
return Some(FileInclusion::InDirectory(buf));
}
}
None
}
pub fn includes_directory(&self, project_path: &ProjectPath) -> Option<FileInclusion> {
if let Some(context_id) = self.directories.get(project_path) {
pub fn includes_directory(&self, path: &Path) -> Option<FileInclusion> {
if let Some(context_id) = self.directories.get(path) {
return Some(FileInclusion::Direct(*context_id));
}
self.will_include_file_path_via_directory(project_path)
self.will_include_file_path_via_directory(path)
}
pub fn included_symbol(&self, symbol_id: &ContextSymbolId) -> Option<ContextId> {
@@ -582,13 +549,13 @@ impl ContextStore {
}
}
pub fn file_paths(&self, cx: &App) -> HashSet<ProjectPath> {
pub fn file_paths(&self, cx: &App) -> HashSet<PathBuf> {
self.context
.iter()
.filter_map(|context| match context {
AssistantContext::File(file) => {
let buffer = file.context_buffer.buffer.read(cx);
buffer.project_path(cx)
buffer_path_log_err(buffer, cx).map(|p| p.to_path_buf())
}
AssistantContext::Directory(_)
| AssistantContext::Symbol(_)
@@ -605,7 +572,7 @@ impl ContextStore {
pub enum FileInclusion {
Direct(ContextId),
InDirectory(ProjectPath),
InDirectory(PathBuf),
}
// ContextBuffer without text.
@@ -672,6 +639,19 @@ fn collect_buffer_info_and_text(
Ok((buffer_info, text_task))
}
pub fn buffer_path_log_err(buffer: &Buffer, cx: &App) -> Option<Arc<Path>> {
if let Some(file) = buffer.file() {
let mut path = file.path().clone();
if path.as_os_str().is_empty() {
path = file.full_path(cx).into();
}
Some(path)
} else {
log::error!("Buffer that had a path unexpectedly no longer has a path.");
None
}
}
fn to_fenced_codeblock(path: &Path, content: Rope) -> SharedString {
let path_extension = path.extension().and_then(|ext| ext.to_str());
let path_string = path.to_string_lossy();
@@ -747,13 +727,13 @@ pub fn refresh_context_store_text(
}
}
AssistantContext::Directory(directory_context) => {
let directory_path = directory_context.project_path(cx);
let should_refresh = changed_buffers.is_empty()
|| changed_buffers.iter().any(|buffer| {
let Some(buffer_path) = buffer.read(cx).project_path(cx) else {
return false;
};
buffer_path.starts_with(&directory_path)
let buffer = buffer.read(cx);
buffer_path_log_err(&buffer, cx).map_or(false, |path| {
path.starts_with(&directory_context.project_path.path)
})
});
if should_refresh {
@@ -840,16 +820,14 @@ fn refresh_directory_text(
let context_buffers = future::join_all(futures);
let id = directory_context.id;
let worktree = directory_context.worktree.clone();
let path = directory_context.path.clone();
let project_path = directory_context.project_path.clone();
Some(cx.spawn(async move |cx| {
let context_buffers = context_buffers.await;
context_store
.update(cx, |context_store, _| {
let new_directory_context = DirectoryContext {
id,
worktree,
path,
project_path,
context_buffers,
};
context_store.replace_context(AssistantContext::Directory(new_directory_context));

View File

@@ -1,4 +1,3 @@
use std::path::Path;
use std::rc::Rc;
use collections::HashSet;
@@ -10,7 +9,6 @@ use gpui::{
};
use itertools::Itertools;
use language::Buffer;
use project::ProjectItem;
use ui::{KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
use workspace::{Workspace, notifications::NotifyResultExt};
@@ -61,7 +59,6 @@ impl ContextStrip {
let focus_handle = cx.focus_handle();
let subscriptions = vec![
cx.observe(&context_store, |_, _, cx| cx.notify()),
cx.subscribe_in(&context_picker, window, Self::handle_context_picker_event),
cx.on_focus(&focus_handle, window, Self::handle_focus),
cx.on_blur(&focus_handle, window, Self::handle_blur),
@@ -95,23 +92,26 @@ impl ContextStrip {
let active_buffer_entity = editor.buffer().read(cx).as_singleton()?;
let active_buffer = active_buffer_entity.read(cx);
let project_path = active_buffer.project_path(cx)?;
let path = active_buffer.file()?.full_path(cx);
if self
.context_store
.read(cx)
.will_include_buffer(active_buffer.remote_id(), &project_path)
.will_include_buffer(active_buffer.remote_id(), &path)
.is_some()
{
return None;
}
let file_name = active_buffer.file()?.file_name(cx);
let name = match path.file_name() {
Some(name) => name.to_string_lossy().into_owned().into(),
None => path.to_string_lossy().into_owned().into(),
};
let icon_path = FileIcons::get_icon(&Path::new(&file_name), cx);
let icon_path = FileIcons::get_icon(&path, cx);
Some(SuggestedContext::File {
name: file_name.to_string_lossy().into_owned().into(),
name,
buffer: active_buffer_entity.downgrade(),
icon_path,
})
@@ -290,9 +290,9 @@ impl ContextStrip {
if let Some(index) = self.focused_index {
let mut is_empty = false;
self.context_store.update(cx, |this, cx| {
self.context_store.update(cx, |this, _cx| {
if let Some(item) = this.context().get(index) {
this.remove_context(item.id(), cx);
this.remove_context(item.id());
}
is_empty = this.context().is_empty();
@@ -475,8 +475,8 @@ impl Render for ContextStrip {
Some({
let context_store = self.context_store.clone();
Rc::new(cx.listener(move |_this, _event, _window, cx| {
context_store.update(cx, |this, cx| {
this.remove_context(id, cx);
context_store.update(cx, |this, _cx| {
this.remove_context(id);
});
cx.notify();
}))

View File

@@ -28,18 +28,17 @@ use language_model::{LanguageModelRegistry, report_assistant_event};
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
use project::LspAction;
use project::Project;
use project::{CodeAction, ProjectTransaction};
use prompt_store::PromptBuilder;
use settings::{Settings, SettingsStore};
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
use text::{OffsetRangeExt, ToPoint as _};
use ui::prelude::*;
use util::RangeExt;
use util::ResultExt;
use workspace::{ItemHandle, Toast, Workspace, dock::Panel, notifications::NotificationId};
use zed_actions::agent::OpenConfiguration;
use workspace::{ItemHandle, Toast, Workspace, notifications::NotificationId};
use workspace::{ShowConfiguration, dock::Panel};
use crate::AssistantPanel;
use crate::buffer_codegen::{BufferCodegen, CodegenAlternative, CodegenEvent};
@@ -255,7 +254,6 @@ impl InlineAssistant {
assistant.assist(
&active_editor,
cx.entity().downgrade(),
workspace.project().downgrade(),
thread_store,
window,
cx,
@@ -267,7 +265,6 @@ impl InlineAssistant {
assistant.assist(
&active_terminal,
cx.entity().downgrade(),
workspace.project().downgrade(),
thread_store,
window,
cx,
@@ -298,7 +295,7 @@ impl InlineAssistant {
if let Some(answer) = answer {
if answer == 0 {
cx.update(|window, cx| {
window.dispatch_action(Box::new(OpenConfiguration), cx)
window.dispatch_action(Box::new(ShowConfiguration), cx)
})
.ok();
}
@@ -321,7 +318,6 @@ impl InlineAssistant {
&mut self,
editor: &Entity<Editor>,
workspace: WeakEntity<Workspace>,
project: WeakEntity<Project>,
thread_store: Option<WeakEntity<ThreadStore>>,
window: &mut Window,
cx: &mut App,
@@ -406,7 +402,7 @@ impl InlineAssistant {
codegen_ranges.push(anchor_range);
if let Some(model) = LanguageModelRegistry::read_global(cx).inline_assistant_model() {
self.telemetry.report_assistant_event(AssistantEventData {
self.telemetry.report_assistant_event(AssistantEvent {
conversation_id: None,
kind: AssistantKind::Inline,
phase: AssistantPhase::Invoked,
@@ -429,7 +425,7 @@ impl InlineAssistant {
for range in codegen_ranges {
let assist_id = self.next_assist_id.post_inc();
let context_store =
cx.new(|_cx| ContextStore::new(project.clone(), thread_store.clone()));
cx.new(|_cx| ContextStore::new(workspace.clone(), thread_store.clone()));
let codegen = cx.new(|cx| {
BufferCodegen::new(
editor.read(cx).buffer().clone(),
@@ -523,7 +519,7 @@ impl InlineAssistant {
initial_prompt: String,
initial_transaction_id: Option<TransactionId>,
focus: bool,
workspace: Entity<Workspace>,
workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<ThreadStore>>,
window: &mut Window,
cx: &mut App,
@@ -541,8 +537,8 @@ impl InlineAssistant {
range.end = range.end.bias_right(&snapshot);
}
let project = workspace.read(cx).project().downgrade();
let context_store = cx.new(|_cx| ContextStore::new(project, thread_store.clone()));
let context_store =
cx.new(|_cx| ContextStore::new(workspace.clone(), thread_store.clone()));
let codegen = cx.new(|cx| {
BufferCodegen::new(
@@ -566,7 +562,7 @@ impl InlineAssistant {
codegen.clone(),
self.fs.clone(),
context_store,
workspace.downgrade(),
workspace.clone(),
thread_store,
window,
cx,
@@ -593,7 +589,7 @@ impl InlineAssistant {
end_block_id,
range,
codegen.clone(),
workspace.downgrade(),
workspace.clone(),
window,
cx,
),
@@ -625,14 +621,14 @@ impl InlineAssistant {
BlockProperties {
style: BlockStyle::Sticky,
placement: BlockPlacement::Above(range.start),
height: Some(prompt_editor_height),
height: prompt_editor_height,
render: build_assist_editor_renderer(prompt_editor),
priority: 0,
},
BlockProperties {
style: BlockStyle::Sticky,
placement: BlockPlacement::Below(range.end),
height: None,
height: 0,
render: Arc::new(|cx| {
v_flex()
.h_full()
@@ -991,7 +987,7 @@ impl InlineAssistant {
.map(|language| language.name())
});
report_assistant_event(
AssistantEventData {
AssistantEvent {
conversation_id: None,
kind: AssistantKind::Inline,
message_id,
@@ -1396,7 +1392,7 @@ impl InlineAssistant {
deleted_lines_editor.update(cx, |editor, cx| editor.max_point(cx).row().0 + 1);
new_blocks.push(BlockProperties {
placement: BlockPlacement::Above(new_row),
height: Some(height),
height,
style: BlockStyle::Flex,
render: Arc::new(move |cx| {
div()
@@ -1783,7 +1779,6 @@ impl CodeActionProvider for AssistantCodeActionProvider {
let workspace = self.workspace.clone();
let thread_store = self.thread_store.clone();
window.spawn(cx, async move |cx| {
let workspace = workspace.upgrade().context("workspace was released")?;
let editor = editor.upgrade().context("editor was released")?;
let range = editor
.update(cx, |editor, cx| {

View File

@@ -3,16 +3,14 @@ use std::sync::Arc;
use crate::assistant_model_selector::ModelType;
use collections::HashSet;
use editor::actions::MoveUp;
use editor::{
ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorStyle, MultiBuffer,
};
use editor::{ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorStyle};
use file_icons::FileIcons;
use fs::Fs;
use gpui::{
Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,
WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
WeakEntity, linear_color_stop, linear_gradient, point,
};
use language::{Buffer, Language};
use language::Buffer;
use language_model::{ConfiguredModel, LanguageModelRegistry};
use language_model_selector::ToggleModelSelector;
use multi_buffer;
@@ -20,8 +18,12 @@ use project::Project;
use settings::Settings;
use std::time::Duration;
use theme::ThemeSettings;
use ui::{Disclosure, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
use ui::{
ButtonLike, Disclosure, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle, Tooltip,
prelude::*,
};
use util::ResultExt as _;
use vim_mode_setting::VimModeSetting;
use workspace::Workspace;
use crate::assistant_model_selector::AssistantModelSelector;
@@ -68,24 +70,8 @@ impl MessageEditor {
let inline_context_picker_menu_handle = PopoverMenuHandle::default();
let model_selector_menu_handle = PopoverMenuHandle::default();
let language = Language::new(
language::LanguageConfig {
completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']),
..Default::default()
},
None,
);
let editor = cx.new(|cx| {
let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
let mut editor = Editor::new(
editor::EditorMode::AutoHeight { max_lines: 10 },
buffer,
None,
window,
cx,
);
let mut editor = Editor::auto_height(10, window, cx);
editor.set_placeholder_text("Ask anything, @ to mention, ↑ to select", cx);
editor.set_show_indent_guides(false, cx);
editor.set_context_menu_options(ContextMenuOptions {
@@ -93,6 +79,7 @@ impl MessageEditor {
max_entries_visible: 12,
placement: Some(ContextMenuPlacement::Above),
});
editor
});
@@ -201,7 +188,7 @@ impl MessageEditor {
}
fn is_editor_empty(&self, cx: &App) -> bool {
self.editor.read(cx).text(cx).trim().is_empty()
self.editor.read(cx).text(cx).is_empty()
}
fn is_model_selected(&self, cx: &App) -> bool {
@@ -239,8 +226,7 @@ impl MessageEditor {
let thread = self.thread.clone();
let context_store = self.context_store.clone();
let git_store = self.project.read(cx).git_store().clone();
let checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx));
let checkpoint = self.project.read(cx).git_store().read(cx).checkpoint(cx);
cx.spawn(async move |this, cx| {
let checkpoint = checkpoint.await.ok();
@@ -254,14 +240,14 @@ impl MessageEditor {
cx.emit(ThreadEvent::ShowError(load_error));
}
})
.log_err();
.ok();
thread
.update(cx, |thread, cx| {
let context = context_store.read(cx).context().clone();
thread.insert_user_message(user_message, context, checkpoint, cx);
})
.log_err();
.ok();
if let Some(wait_for_summaries) = context_store
.update(cx, |context_store, cx| context_store.wait_for_summaries(cx))
@@ -271,7 +257,7 @@ impl MessageEditor {
this.waiting_for_summaries_to_send = true;
cx.notify();
})
.log_err();
.ok();
wait_for_summaries.await;
@@ -279,7 +265,7 @@ impl MessageEditor {
this.waiting_for_summaries_to_send = false;
cx.notify();
})
.log_err();
.ok();
}
// Send to model after summaries are done
@@ -287,7 +273,7 @@ impl MessageEditor {
.update(cx, |thread, cx| {
thread.send_to_model(model, request_kind, cx);
})
.log_err();
.ok();
})
.detach();
}
@@ -368,7 +354,24 @@ impl Render for MessageEditor {
let total_token_usage = thread.total_token_usage(cx);
let is_model_selected = self.is_model_selected(cx);
let is_editor_empty = self.is_editor_empty(cx);
let is_edit_changes_expanded = self.edits_expanded;
let needs_confirmation =
thread.has_pending_tool_uses() && thread.tools_needing_confirmation().next().is_some();
let submit_label_color = if is_editor_empty {
Color::Muted
} else {
Color::Default
};
let vim_mode_enabled = VimModeSetting::get_global(cx).0;
let platform = PlatformStyle::platform();
let linux = platform == PlatformStyle::Linux;
let windows = platform == PlatformStyle::Windows;
let button_width = if linux || windows || vim_mode_enabled {
px(82.)
} else {
px(64.)
};
let action_log = self.thread.read(cx).action_log();
let changed_buffers = action_log.read(cx).changed_buffers(cx);
@@ -416,6 +419,70 @@ impl Render for MessageEditor {
),
)
})
.when(is_generating, |parent| {
let focus_handle = self.editor.focus_handle(cx).clone();
parent.child(
h_flex().py_3().w_full().justify_center().child(
h_flex()
.flex_none()
.pl_2()
.pr_1()
.py_1()
.bg(editor_bg_color)
.border_1()
.border_color(cx.theme().colors().border_variant)
.rounded_lg()
.shadow_md()
.gap_1()
.child(
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Muted)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(gpui::Transformation::rotate(
gpui::percentage(delta),
))
},
),
)
.child({
Label::new(if needs_confirmation {
"Waiting for confirmation…"
} else {
"Generating…"
})
.size(LabelSize::XSmall)
.color(Color::Muted)
})
.child(ui::Divider::vertical())
.child(
Button::new("cancel-generation", "Cancel")
.label_size(LabelSize::XSmall)
.key_binding(
KeyBinding::for_action_in(
&editor::actions::Cancel,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.))),
)
.on_click(move |_event, window, cx| {
focus_handle.dispatch_action(
&editor::actions::Cancel,
window,
cx,
);
}),
),
),
)
})
.when(changed_buffers_count > 0, |parent| {
parent.child(
v_flex()
@@ -434,12 +501,12 @@ impl Render for MessageEditor {
.child(
h_flex()
.id("edits-container")
.cursor_pointer()
.p_1p5()
.justify_between()
.when(is_edit_changes_expanded, |this| {
.when(self.edits_expanded, |this| {
this.border_b_1().border_color(border_color)
})
.cursor_pointer()
.on_click(cx.listener(|this, _, window, cx| {
this.handle_review_click(window, cx)
}))
@@ -449,7 +516,7 @@ impl Render for MessageEditor {
.child(
Disclosure::new(
"edits-disclosure",
is_edit_changes_expanded,
self.edits_expanded,
)
.on_click(
cx.listener(|this, _ev, _window, cx| {
@@ -499,9 +566,9 @@ impl Render for MessageEditor {
})),
),
)
.when(is_edit_changes_expanded, |parent| {
.when(self.edits_expanded, |parent| {
parent.child(
v_flex().children(
v_flex().bg(cx.theme().colors().editor_background).children(
changed_buffers.into_iter().enumerate().flat_map(
|(index, (buffer, _diff))| {
let file = buffer.read(cx).file()?;
@@ -515,7 +582,7 @@ impl Render for MessageEditor {
} else {
Some(
Label::new(format!(
"/{}{}",
"{}{}",
parent_str,
std::path::MAIN_SEPARATOR_STR
))
@@ -543,105 +610,70 @@ impl Render for MessageEditor {
.size(IconSize::Small)
});
let hover_color = cx.theme()
.colors()
.element_background
.blend(cx.theme().colors().editor_foreground.opacity(0.025));
let overlay_gradient = linear_gradient(
90.,
linear_color_stop(
editor_bg_color,
1.,
),
linear_color_stop(
editor_bg_color
.opacity(0.2),
0.,
),
);
let overlay_gradient_hover = linear_gradient(
90.,
linear_color_stop(
hover_color,
1.,
),
linear_color_stop(
hover_color
.opacity(0.2),
0.,
),
);
let element = h_flex()
.group("edited-code")
.id(("file-container", index))
.cursor_pointer()
let element = div()
.relative()
.py_1()
.pl_2()
.pr_1()
.gap_2()
.justify_between()
.bg(cx.theme().colors().editor_background)
.hover(|style| style.bg(hover_color))
.px_2()
.when(index + 1 < changed_buffers_count, |parent| {
parent.border_color(border_color).border_b_1()
})
.child(
h_flex()
.id("file-name")
.pr_8()
.gap_1p5()
.max_w_full()
.overflow_x_scroll()
.child(file_icon)
.gap_2()
.justify_between()
.child(
h_flex()
.gap_0p5()
.children(name_label)
.children(parent_label)
) // TODO: show lines changed
.child(
Label::new("+")
.color(Color::Created),
.id(("file-container", index))
.pr_8()
.gap_1p5()
.max_w_full()
.overflow_x_scroll()
.cursor_pointer()
.on_click({
let buffer = buffer.clone();
cx.listener(move |this, _, window, cx| {
this.handle_file_click(buffer.clone(), window, cx);
})
})
.tooltip(
Tooltip::text(format!("Review {}", path.display()))
)
.child(file_icon)
.child(
h_flex()
.children(parent_label)
.children(name_label),
) // TODO: show lines changed
.child(
Label::new("+")
.color(Color::Created),
)
.child(
Label::new("-")
.color(Color::Deleted),
),
)
.child(
Label::new("-")
.color(Color::Deleted),
div()
.h_full()
.absolute()
.w_8()
.bottom_0()
.right_0()
.bg(linear_gradient(
90.,
linear_color_stop(
editor_bg_color,
1.,
),
linear_color_stop(
editor_bg_color
.opacity(0.2),
0.,
),
)),
),
)
.child(
div().visible_on_hover("edited-code").child(
Button::new("review", "Review")
.label_size(LabelSize::Small)
.on_click({
let buffer = buffer.clone();
cx.listener(move |this, _, window, cx| {
this.handle_file_click(buffer.clone(), window, cx);
})
})
)
)
.child(
div()
.id("gradient-overlay")
.absolute()
.h_5_6()
.w_12()
.bottom_0()
.right(px(52.))
.bg(overlay_gradient)
.group_hover("edited-code", |style| style.bg(overlay_gradient_hover))
,
)
.on_click({
let buffer = buffer.clone();
cx.listener(move |this, _, window, cx| {
this.handle_file_click(buffer.clone(), window, cx);
})
});
);
Some(element)
},
@@ -701,6 +733,7 @@ impl Render for MessageEditor {
..Default::default()
},
).into_any()
})
.child(
PopoverMenu::new("inline-context-picker")
@@ -708,6 +741,7 @@ impl Render for MessageEditor {
inline_context_picker.update(cx, |this, cx| {
this.init(window, cx);
});
Some(inline_context_picker.clone())
})
.attach(gpui::Corner::TopLeft)
@@ -724,72 +758,60 @@ impl Render for MessageEditor {
.justify_between()
.child(h_flex().gap_2().child(self.profile_selector.clone()))
.child(
h_flex().gap_1().child(self.model_selector.clone())
.map(|parent| {
if is_generating {
parent.child(
IconButton::new("stop-generation", IconName::StopFilled)
.icon_color(Color::Error)
.style(ButtonStyle::Tinted(ui::TintColor::Error))
.tooltip(move |window, cx| {
Tooltip::for_action(
"Stop Generation",
&editor::actions::Cancel,
window,
cx,
)
})
.on_click(move |_event, window, cx| {
focus_handle.dispatch_action(
&editor::actions::Cancel,
window,
cx,
);
})
.with_animation(
"pulsating-label",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 1.0)),
|icon_button, delta| icon_button.alpha(delta),
),
)
} else {
parent.child(
IconButton::new("send-message", IconName::Send)
.icon_color(Color::Accent)
.style(ButtonStyle::Filled)
.disabled(
is_editor_empty
|| !is_model_selected
|| self.waiting_for_summaries_to_send
h_flex().gap_1().child(self.model_selector.clone()).child(
ButtonLike::new("submit-message")
.width(button_width.into())
.style(ButtonStyle::Filled)
.disabled(
is_editor_empty
|| !is_model_selected
|| is_generating
|| self.waiting_for_summaries_to_send
)
.child(
h_flex()
.w_full()
.justify_between()
.child(
Label::new("Submit")
.size(LabelSize::Small)
.color(submit_label_color),
)
.children(
KeyBinding::for_action_in(
&Chat,
&focus_handle,
window,
cx,
)
.on_click(move |_event, window, cx| {
focus_handle.dispatch_action(&Chat, window, cx);
})
.when(!is_editor_empty && is_model_selected, |button| {
button.tooltip(move |window, cx| {
Tooltip::for_action(
"Send",
&Chat,
window,
cx,
)
})
})
.when(is_editor_empty, |button| {
button.tooltip(Tooltip::text(
"Type a message to submit",
))
})
.when(!is_model_selected, |button| {
button.tooltip(Tooltip::text(
"Select a model to continue",
))
})
)
}
})
.map(|binding| {
binding
.when(vim_mode_enabled, |kb| {
kb.size(rems_from_px(12.))
})
.into_any_element()
}),
),
)
.on_click(move |_event, window, cx| {
focus_handle.dispatch_action(&Chat, window, cx);
})
.when(is_editor_empty, |button| {
button.tooltip(Tooltip::text(
"Type a message to submit",
))
})
.when(is_generating, |button| {
button.tooltip(Tooltip::text(
"Cancel to submit a new message",
))
})
.when(!is_model_selected, |button| {
button.tooltip(Tooltip::text(
"Select a model to continue",
))
}),
),
),
),
)

View File

@@ -6,7 +6,7 @@ use language_model::{
ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, report_assistant_event,
};
use std::{sync::Arc, time::Instant};
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal::Terminal;
pub struct TerminalCodegen {
@@ -79,7 +79,7 @@ impl TerminalCodegen {
let error_message = result.as_ref().err().map(|error| error.to_string());
report_assistant_event(
AssistantEventData {
AssistantEvent {
conversation_id: None,
kind: AssistantKind::InlineTerminal,
message_id,

View File

@@ -16,10 +16,9 @@ use language_model::{
ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
Role, report_assistant_event,
};
use project::Project;
use prompt_store::PromptBuilder;
use std::sync::Arc;
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal_view::TerminalView;
use ui::prelude::*;
use util::ResultExt;
@@ -68,7 +67,6 @@ impl TerminalInlineAssistant {
&mut self,
terminal_view: &Entity<TerminalView>,
workspace: WeakEntity<Workspace>,
project: WeakEntity<Project>,
thread_store: Option<WeakEntity<ThreadStore>>,
window: &mut Window,
cx: &mut App,
@@ -77,7 +75,8 @@ impl TerminalInlineAssistant {
let assist_id = self.next_assist_id.post_inc();
let prompt_buffer =
cx.new(|cx| MultiBuffer::singleton(cx.new(|cx| Buffer::local(String::new(), cx)), cx));
let context_store = cx.new(|_cx| ContextStore::new(project, thread_store.clone()));
let context_store =
cx.new(|_cx| ContextStore::new(workspace.clone(), thread_store.clone()));
let codegen = cx.new(|_| TerminalCodegen::new(terminal, self.telemetry.clone()));
let prompt_editor = cx.new(|cx| {
@@ -293,7 +292,7 @@ impl TerminalInlineAssistant {
let codegen = assist.codegen.read(cx);
let executor = cx.background_executor().clone();
report_assistant_event(
AssistantEventData {
AssistantEvent {
conversation_id: None,
kind: AssistantKind::InlineTerminal,
message_id: codegen.message_id.clone(),

View File

@@ -3,10 +3,9 @@ use std::io::Write;
use std::ops::Range;
use std::sync::Arc;
use agent_rules::load_worktree_rules_file;
use anyhow::{Context as _, Result, anyhow};
use assistant_settings::AssistantSettings;
use assistant_tool::{ActionLog, ResponseDest, Tool, ToolWorkingSet};
use assistant_tool::{ActionLog, Tool, ToolWorkingSet};
use chrono::{DateTime, Utc};
use collections::{BTreeMap, HashMap};
use fs::Fs;
@@ -22,11 +21,13 @@ use language_model::{
};
use project::git_store::{GitStore, GitStoreCheckpoint, RepositoryState};
use project::{Project, Worktree};
use prompt_store::{AssistantSystemPromptContext, PromptBuilder, WorktreeInfoForSystemPrompt};
use prompt_store::{
AssistantSystemPromptContext, PromptBuilder, RulesFile, WorktreeInfoForSystemPrompt,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
use util::{ResultExt as _, TryFutureExt as _, post_inc};
use util::{ResultExt as _, TryFutureExt as _, maybe, post_inc};
use uuid::Uuid;
use crate::context::{AssistantContext, ContextId, format_context_as_string};
@@ -182,7 +183,7 @@ pub struct ThreadCheckpoint {
git_checkpoint: GitStoreCheckpoint,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[derive(Copy, Clone, Debug)]
pub enum ThreadFeedback {
Positive,
Negative,
@@ -260,8 +261,6 @@ pub struct Thread {
initial_project_snapshot: Shared<Task<Option<Arc<ProjectSnapshot>>>>,
cumulative_token_usage: TokenUsage,
feedback: Option<ThreadFeedback>,
message_feedback: HashMap<MessageId, ThreadFeedback>,
response_dest: ResponseDest,
}
impl Thread {
@@ -300,8 +299,6 @@ impl Thread {
},
cumulative_token_usage: TokenUsage::default(),
feedback: None,
message_feedback: HashMap::default(),
response_dest: ResponseDest::default(),
}
}
@@ -365,8 +362,6 @@ impl Thread {
initial_project_snapshot: Task::ready(serialized.initial_project_snapshot).shared(),
cumulative_token_usage: serialized.cumulative_token_usage,
feedback: None,
message_feedback: HashMap::default(),
response_dest: ResponseDest::default(),
}
}
@@ -390,28 +385,14 @@ impl Thread {
self.summary.clone()
}
pub const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread");
pub fn summary_or_default(&self) -> SharedString {
self.summary.clone().unwrap_or(Self::DEFAULT_SUMMARY)
const DEFAULT: SharedString = SharedString::new_static("New Thread");
self.summary.clone().unwrap_or(DEFAULT)
}
pub fn set_summary(&mut self, new_summary: impl Into<SharedString>, cx: &mut Context<Self>) {
let Some(current_summary) = &self.summary else {
// Don't allow setting summary until generated
return;
};
let mut new_summary = new_summary.into();
if new_summary.is_empty() {
new_summary = Self::DEFAULT_SUMMARY;
}
if current_summary != &new_summary {
self.summary = Some(new_summary);
cx.emit(ThreadEvent::SummaryChanged);
}
pub fn set_summary(&mut self, summary: impl Into<SharedString>, cx: &mut Context<Self>) {
self.summary = Some(summary.into());
cx.emit(ThreadEvent::SummaryChanged);
}
pub fn latest_detailed_summary_or_text(&self) -> SharedString {
@@ -461,10 +442,6 @@ impl Thread {
!self.tool_use.pending_tool_uses().is_empty()
}
pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
self.tool_use.pending_tool_uses()
}
pub fn checkpoint_for_message(&self, id: MessageId) -> Option<ThreadCheckpoint> {
self.checkpoints_by_message.get(&id).cloned()
}
@@ -480,11 +457,11 @@ impl Thread {
cx.emit(ThreadEvent::CheckpointChanged);
cx.notify();
let git_store = self.project().read(cx).git_store().clone();
let restore = git_store.update(cx, |git_store, cx| {
git_store.restore_checkpoint(checkpoint.git_checkpoint.clone(), cx)
});
let project = self.project.read(cx);
let restore = project
.git_store()
.read(cx)
.restore_checkpoint(checkpoint.git_checkpoint.clone(), cx);
cx.spawn(async move |this, cx| {
let result = restore.await;
this.update(cx, |this, cx| {
@@ -515,11 +492,11 @@ impl Thread {
};
let git_store = self.project.read(cx).git_store().clone();
let final_checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx));
let final_checkpoint = git_store.read(cx).checkpoint(cx);
cx.spawn(async move |this, cx| match final_checkpoint.await {
Ok(final_checkpoint) => {
let equal = git_store
.update(cx, |store, cx| {
.read_with(cx, |store, cx| {
store.compare_checkpoints(
pending_checkpoint.git_checkpoint.clone(),
final_checkpoint.clone(),
@@ -531,7 +508,7 @@ impl Thread {
if equal {
git_store
.update(cx, |store, cx| {
.read_with(cx, |store, cx| {
store.delete_checkpoint(pending_checkpoint.git_checkpoint, cx)
})?
.detach();
@@ -542,7 +519,7 @@ impl Thread {
}
git_store
.update(cx, |store, cx| {
.read_with(cx, |store, cx| {
store.delete_checkpoint(final_checkpoint, cx)
})?
.detach();
@@ -863,36 +840,67 @@ impl Thread {
let root_name = worktree.root_name().into();
let abs_path = worktree.abs_path();
let rules_task = load_worktree_rules_file(fs, worktree, cx);
let Some(rules_task) = rules_task else {
return Task::ready((
// Note that Cline supports `.clinerules` being a directory, but that is not currently
// supported. This doesn't seem to occur often in GitHub repositories.
const RULES_FILE_NAMES: [&'static str; 6] = [
".rules",
".cursorrules",
".windsurfrules",
".clinerules",
".github/copilot-instructions.md",
"CLAUDE.md",
];
let selected_rules_file = RULES_FILE_NAMES
.into_iter()
.filter_map(|name| {
worktree
.entry_for_path(name)
.filter(|entry| entry.is_file())
.map(|entry| (entry.path.clone(), worktree.absolutize(&entry.path)))
})
.next();
if let Some((rel_rules_path, abs_rules_path)) = selected_rules_file {
cx.spawn(async move |_| {
let rules_file_result = maybe!(async move {
let abs_rules_path = abs_rules_path?;
let text = fs.load(&abs_rules_path).await.with_context(|| {
format!("Failed to load assistant rules file {:?}", abs_rules_path)
})?;
anyhow::Ok(RulesFile {
rel_path: rel_rules_path,
abs_path: abs_rules_path.into(),
text: text.trim().to_string(),
})
})
.await;
let (rules_file, rules_file_error) = match rules_file_result {
Ok(rules_file) => (Some(rules_file), None),
Err(err) => (
None,
Some(ThreadError::Message {
header: "Error loading rules file".into(),
message: format!("{err}").into(),
}),
),
};
let worktree_info = WorktreeInfoForSystemPrompt {
root_name,
abs_path,
rules_file,
};
(worktree_info, rules_file_error)
})
} else {
Task::ready((
WorktreeInfoForSystemPrompt {
root_name,
abs_path,
rules_file: None,
},
None,
));
};
cx.spawn(async move |_| {
let (rules_file, rules_file_error) = match rules_task.await {
Ok(rules_file) => (Some(rules_file), None),
Err(err) => (
None,
Some(ThreadError::Message {
header: "Error loading rules file".into(),
message: format!("{err}").into(),
}),
),
};
let worktree_info = WorktreeInfoForSystemPrompt {
root_name,
abs_path,
rules_file,
};
(worktree_info, rules_file_error)
})
))
}
}
pub fn send_to_model(
@@ -1098,20 +1106,6 @@ impl Thread {
current_token_usage = token_usage;
}
LanguageModelCompletionEvent::Text(chunk) => {
match &thread.response_dest {
ResponseDest::File { path } => {
log::info!(
"Emitting {}B StreamedFileChunk for {path}",
chunk.len()
);
cx.emit(ThreadEvent::StreamedFileChunk {
path: path.clone(),
chunk: chunk.clone(),
});
}
ResponseDest::TextOnly => {}
}
if let Some(last_message) = thread.messages.last_mut() {
if last_message.role == Role::Assistant {
last_message.push_text(&chunk);
@@ -1127,7 +1121,7 @@ impl Thread {
// will result in duplicating the text of the chunk in the rendered Markdown.
thread.insert_message(
Role::Assistant,
vec![MessageSegment::Text(chunk)],
vec![MessageSegment::Text(chunk.to_string())],
cx,
);
};
@@ -1202,8 +1196,7 @@ impl Thread {
match result.as_ref() {
Ok(stop_reason) => match stop_reason {
StopReason::ToolUse => {
let tool_uses = thread.use_pending_tools(cx);
cx.emit(ThreadEvent::UsePendingTools { tool_uses });
cx.emit(ThreadEvent::UsePendingTools);
}
StopReason::EndTurn => {}
StopReason::MaxTokens => {}
@@ -1300,7 +1293,7 @@ impl Thread {
this.summary = Some(new_summary.into());
}
cx.emit(ThreadEvent::SummaryGenerated);
cx.emit(ThreadEvent::SummaryChanged);
})?;
anyhow::Ok(())
@@ -1391,7 +1384,10 @@ impl Thread {
)
}
pub fn use_pending_tools(&mut self, cx: &mut Context<Self>) -> Vec<PendingToolUse> {
pub fn use_pending_tools(
&mut self,
cx: &mut Context<Self>,
) -> impl IntoIterator<Item = PendingToolUse> + use<> {
let request = self.to_completion_request(RequestKind::Chat, cx);
let messages = Arc::new(request.messages);
let pending_tool_uses = self
@@ -1404,7 +1400,7 @@ impl Thread {
for tool_use in pending_tool_uses.iter() {
if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
if tool.needs_confirmation(&tool_use.input, cx)
if tool.needs_confirmation()
&& !AssistantSettings::get_global(cx).always_allow_tool_actions
{
self.tool_use.confirm_tool_use(
@@ -1469,24 +1465,7 @@ impl Thread {
cx.spawn({
async move |thread: WeakEntity<Thread>, cx| {
let new_response_dest;
let output = match run_tool.await {
Ok((response_dest, output)) => {
new_response_dest = response_dest;
Ok(output)
}
Err(err) => {
new_response_dest = ResponseDest::default();
Err(err)
}
};
thread.upgrade().map(|thread| {
thread.update(cx, |thread, _cx| {
thread.response_dest = new_response_dest;
})
});
let output = run_tool.await;
thread
.update(cx, |thread, cx| {
@@ -1494,38 +1473,19 @@ impl Thread {
tool_use_id.clone(),
tool_name,
output,
cx,
);
thread.tool_finished(tool_use_id, pending_tool_use, false, cx);
cx.emit(ThreadEvent::ToolFinished {
tool_use_id,
pending_tool_use,
canceled: false,
});
})
.ok();
}
})
}
fn tool_finished(
&mut self,
tool_use_id: LanguageModelToolUseId,
pending_tool_use: Option<PendingToolUse>,
canceled: bool,
cx: &mut Context<Self>,
) {
if self.all_tools_finished() {
let model_registry = LanguageModelRegistry::read_global(cx);
if let Some(ConfiguredModel { model, .. }) = model_registry.default_model() {
self.attach_tool_results(cx);
if !canceled {
self.send_to_model(model, RequestKind::Chat, cx);
}
}
}
cx.emit(ThreadEvent::ToolFinished {
tool_use_id,
pending_tool_use,
});
}
pub fn attach_tool_results(&mut self, cx: &mut Context<Self>) {
// Insert a user message to contain the tool results.
self.insert_user_message(
@@ -1549,12 +1509,11 @@ impl Thread {
let mut canceled = false;
for pending_tool_use in self.tool_use.cancel_pending() {
canceled = true;
self.tool_finished(
pending_tool_use.id.clone(),
Some(pending_tool_use),
true,
cx,
);
cx.emit(ThreadEvent::ToolFinished {
tool_use_id: pending_tool_use.id.clone(),
pending_tool_use: Some(pending_tool_use),
canceled: true,
});
}
canceled
};
@@ -1562,45 +1521,24 @@ impl Thread {
canceled
}
/// Returns the feedback given to the thread, if any.
pub fn feedback(&self) -> Option<ThreadFeedback> {
self.feedback
}
pub fn message_feedback(&self, message_id: MessageId) -> Option<ThreadFeedback> {
self.message_feedback.get(&message_id).copied()
}
pub fn report_message_feedback(
/// Reports feedback about the thread and stores it in our telemetry backend.
pub fn report_feedback(
&mut self,
message_id: MessageId,
feedback: ThreadFeedback,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
if self.message_feedback.get(&message_id) == Some(&feedback) {
return Task::ready(Ok(()));
}
let final_project_snapshot = Self::project_snapshot(self.project.clone(), cx);
let serialized_thread = self.serialize(cx);
let thread_id = self.id().clone();
let client = self.project.read(cx).client();
let enabled_tool_names: Vec<String> = self
.tools()
.enabled_tools(cx)
.iter()
.map(|tool| tool.name().to_string())
.collect();
self.message_feedback.insert(message_id, feedback);
self.feedback = Some(feedback);
cx.notify();
let message_content = self
.message(message_id)
.map(|msg| msg.to_string())
.unwrap_or_default();
cx.background_spawn(async move {
let final_project_snapshot = final_project_snapshot.await;
let serialized_thread = serialized_thread.await?;
@@ -1615,9 +1553,6 @@ impl Thread {
"Assistant Thread Rated",
rating,
thread_id,
enabled_tool_names,
message_id = message_id.0,
message_content,
thread_data,
final_project_snapshot
);
@@ -1627,52 +1562,6 @@ impl Thread {
})
}
pub fn report_feedback(
&mut self,
feedback: ThreadFeedback,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let last_assistant_message_id = self
.messages
.iter()
.rev()
.find(|msg| msg.role == Role::Assistant)
.map(|msg| msg.id);
if let Some(message_id) = last_assistant_message_id {
self.report_message_feedback(message_id, feedback, cx)
} else {
let final_project_snapshot = Self::project_snapshot(self.project.clone(), cx);
let serialized_thread = self.serialize(cx);
let thread_id = self.id().clone();
let client = self.project.read(cx).client();
self.feedback = Some(feedback);
cx.notify();
cx.background_spawn(async move {
let final_project_snapshot = final_project_snapshot.await;
let serialized_thread = serialized_thread.await?;
let thread_data = serde_json::to_value(serialized_thread)
.unwrap_or_else(|_| serde_json::Value::Null);
let rating = match feedback {
ThreadFeedback::Positive => "positive",
ThreadFeedback::Negative => "negative",
};
telemetry::event!(
"Assistant Thread Rated",
rating,
thread_id,
thread_data,
final_project_snapshot
);
client.telemetry().flush_events();
Ok(())
})
}
}
/// Create a snapshot of the current project state including git information and unsaved buffers.
fn project_snapshot(
project: Entity<Project>,
@@ -1747,10 +1636,10 @@ impl Thread {
.ok()
.flatten()
.map(|repo| {
repo.update(cx, |repo, _| {
repo.read_with(cx, |repo, _| {
let current_branch =
repo.branch.as_ref().map(|branch| branch.name.to_string());
repo.send_job(None, |state, _| async move {
repo.send_job(|state, _| async move {
let RepositoryState::Local { backend, .. } = state else {
return GitState {
remote_url: None,
@@ -1928,8 +1817,13 @@ impl Thread {
));
self.tool_use
.insert_tool_output(tool_use_id.clone(), tool_name, err, cx);
self.tool_finished(tool_use_id.clone(), None, true, cx);
.insert_tool_output(tool_use_id.clone(), tool_name, err);
cx.emit(ThreadEvent::ToolFinished {
tool_use_id,
pending_tool_use: None,
canceled: true,
});
}
}
@@ -1949,24 +1843,19 @@ pub enum ThreadEvent {
StreamedCompletion,
StreamedAssistantText(MessageId, String),
StreamedAssistantThinking(MessageId, String),
StreamedFileChunk {
path: Arc<str>,
chunk: String,
},
DoneStreaming,
MessageAdded(MessageId),
MessageEdited(MessageId),
MessageDeleted(MessageId),
SummaryGenerated,
SummaryChanged,
UsePendingTools {
tool_uses: Vec<PendingToolUse>,
},
UsePendingTools,
ToolFinished {
#[allow(unused)]
tool_use_id: LanguageModelToolUseId,
/// The pending tool use that corresponds to this tool.
pending_tool_use: Option<PendingToolUse>,
/// Whether the tool was canceled by the user.
canceled: bool,
},
CheckpointChanged,
ToolConfirmationNeeded,
@@ -2361,7 +2250,7 @@ fn main() {{
});
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
let context_store = cx.new(|_cx| ContextStore::new(project.downgrade(), None));
let context_store = cx.new(|_cx| ContextStore::new(workspace.downgrade(), None));
(workspace, thread_store, thread, context_store)
}

View File

@@ -17,7 +17,6 @@ use crate::{AssistantPanel, RemoveSelectedThread};
pub struct ThreadHistory {
assistant_panel: WeakEntity<AssistantPanel>,
history_store: Entity<HistoryStore>,
scroll_handle: UniformListScrollHandle,
selected_index: usize,
search_query: SharedString,
@@ -41,26 +40,33 @@ impl ThreadHistory {
editor
});
let search_editor_subscription =
cx.subscribe(&search_editor, |this, search_editor, event, cx| {
let search_editor_subscription = cx.subscribe_in(
&search_editor,
window,
|this, search_editor, event, window, cx| {
if let EditorEvent::BufferEdited = event {
let query = search_editor.read(cx).text(cx);
this.search_query = query.into();
this.update_search(cx);
this.update_search(window, cx);
}
});
},
);
let entries: Arc<Vec<_>> = history_store
.update(cx, |store, cx| store.entries(cx))
.into();
let history_store_subscription = cx.observe(&history_store, |this, _, cx| {
this.update_all_entries(cx);
});
let history_store_subscription =
cx.observe_in(&history_store, window, |this, history_store, window, cx| {
this.all_entries = history_store
.update(cx, |store, cx| store.entries(cx))
.into();
this.matches.clear();
this.update_search(window, cx);
});
Self {
assistant_panel,
history_store,
scroll_handle: UniformListScrollHandle::default(),
selected_index: 0,
search_query: SharedString::new_static(""),
@@ -72,16 +78,7 @@ impl ThreadHistory {
}
}
fn update_all_entries(&mut self, cx: &mut Context<Self>) {
self.all_entries = self
.history_store
.update(cx, |store, cx| store.entries(cx))
.into();
self.matches.clear();
self.update_search(cx);
}
fn update_search(&mut self, cx: &mut Context<Self>) {
fn update_search(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
self._search_task.take();
if self.has_search_query() {
@@ -225,15 +222,17 @@ impl ThreadHistory {
let task_result = match entry {
HistoryEntry::Thread(thread) => self
.assistant_panel
.update(cx, move |this, cx| this.open_thread(&thread.id, window, cx)),
HistoryEntry::Context(context) => {
self.assistant_panel.update(cx, move |this, cx| {
.update(cx, move |this, cx| this.open_thread(&thread.id, window, cx))
.ok(),
HistoryEntry::Context(context) => self
.assistant_panel
.update(cx, move |this, cx| {
this.open_saved_prompt_editor(context.path.clone(), window, cx)
})
}
.ok(),
};
if let Some(task) = task_result.log_err() {
if let Some(task) = task_result {
task.detach_and_log_err(cx);
};
@@ -244,22 +243,28 @@ impl ThreadHistory {
fn remove_selected_thread(
&mut self,
_: &RemoveSelectedThread,
_window: &mut Window,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(entry) = self.get_match(self.selected_index) {
let task_result = match entry {
HistoryEntry::Thread(thread) => self
.assistant_panel
.update(cx, |this, cx| this.delete_thread(&thread.id, cx)),
HistoryEntry::Context(context) => self
.assistant_panel
.update(cx, |this, cx| this.delete_context(context.path.clone(), cx)),
};
match entry {
HistoryEntry::Thread(thread) => {
self.assistant_panel
.update(cx, |this, cx| {
this.delete_thread(&thread.id, cx);
})
.ok();
}
HistoryEntry::Context(context) => {
self.assistant_panel
.update(cx, |this, cx| {
this.delete_context(context.path.clone(), cx);
})
.ok();
}
}
if let Some(task) = task_result.log_err() {
task.detach_and_log_err(cx);
};
self.update_search(window, cx);
cx.notify();
}
@@ -431,6 +436,17 @@ impl RenderOnce for PastThread {
.end_slot(
h_flex()
.gap_1p5()
.child(
Label::new("Thread")
.color(Color::Muted)
.size(LabelSize::XSmall),
)
.child(
div()
.size(px(3.))
.rounded_full()
.bg(cx.theme().colors().text_disabled),
)
.child(
Label::new(thread_timestamp)
.color(Color::Muted)
@@ -440,16 +456,14 @@ impl RenderOnce for PastThread {
IconButton::new("delete", IconName::TrashAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.tooltip(move |window, cx| {
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
})
.tooltip(Tooltip::text("Delete Thread"))
.on_click({
let assistant_panel = self.assistant_panel.clone();
let id = self.thread.id.clone();
move |_event, _window, cx| {
assistant_panel
.update(cx, |this, cx| {
this.delete_thread(&id, cx).detach_and_log_err(cx);
this.delete_thread(&id, cx);
})
.ok();
}
@@ -522,6 +536,17 @@ impl RenderOnce for PastContext {
.end_slot(
h_flex()
.gap_1p5()
.child(
Label::new("Prompt Editor")
.color(Color::Muted)
.size(LabelSize::XSmall),
)
.child(
div()
.size(px(3.))
.rounded_full()
.bg(cx.theme().colors().text_disabled),
)
.child(
Label::new(context_timestamp)
.color(Color::Muted)
@@ -531,17 +556,14 @@ impl RenderOnce for PastContext {
IconButton::new("delete", IconName::TrashAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.tooltip(move |window, cx| {
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
})
.tooltip(Tooltip::text("Delete Prompt Editor"))
.on_click({
let assistant_panel = self.assistant_panel.clone();
let path = self.context.path.clone();
move |_event, _window, cx| {
assistant_panel
.update(cx, |this, cx| {
this.delete_context(path.clone(), cx)
.detach_and_log_err(cx);
this.delete_context(path.clone(), cx);
})
.ok();
}

View File

@@ -174,9 +174,8 @@ impl ThreadStore {
let database = database_future.await.map_err(|err| anyhow!(err))?;
database.delete_thread(id.clone()).await?;
this.update(cx, |this, cx| {
this.threads.retain(|thread| thread.id != id);
cx.notify();
this.update(cx, |this, _cx| {
this.threads.retain(|thread| thread.id != id)
})
})
}

View File

@@ -7,11 +7,10 @@ use futures::FutureExt as _;
use futures::future::Shared;
use gpui::{App, SharedString, Task};
use language_model::{
LanguageModelRegistry, LanguageModelRequestMessage, LanguageModelToolResult,
LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role,
LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse,
LanguageModelToolUseId, MessageContent, Role,
};
use ui::IconName;
use util::truncate_lines_to_byte_limit;
use crate::thread::MessageId;
use crate::thread_store::SerializedMessage;
@@ -36,18 +35,6 @@ pub enum ToolUseStatus {
Error(SharedString),
}
impl ToolUseStatus {
pub fn text(&self) -> SharedString {
match self {
ToolUseStatus::NeedsConfirmation => "".into(),
ToolUseStatus::Pending => "".into(),
ToolUseStatus::Running => "".into(),
ToolUseStatus::Finished(out) => out.clone(),
ToolUseStatus::Error(out) => out.clone(),
}
}
}
pub struct ToolUseState {
tools: Arc<ToolWorkingSet>,
tool_uses_by_assistant_message: HashMap<MessageId, Vec<LanguageModelToolUse>>,
@@ -201,7 +188,7 @@ impl ToolUseState {
let (icon, needs_confirmation) = if let Some(tool) = self.tools.tool(&tool_use.name, cx)
{
(tool.icon(), tool.needs_confirmation(&tool_use.input, cx))
(tool.icon(), tool.needs_confirmation())
} else {
(IconName::Cog, false)
};
@@ -332,34 +319,9 @@ impl ToolUseState {
tool_use_id: LanguageModelToolUseId,
tool_name: Arc<str>,
output: Result<String>,
cx: &App,
) -> Option<PendingToolUse> {
telemetry::event!("Agent Tool Finished", tool_name, success = output.is_ok());
match output {
Ok(tool_result) => {
let model_registry = LanguageModelRegistry::read_global(cx);
const BYTES_PER_TOKEN_ESTIMATE: usize = 3;
// Protect from clearly large output
let tool_output_limit = model_registry
.default_model()
.map(|model| model.model.max_token_count() * BYTES_PER_TOKEN_ESTIMATE)
.unwrap_or(usize::MAX);
let tool_result = if tool_result.len() <= tool_output_limit {
tool_result
} else {
let truncated = truncate_lines_to_byte_limit(&tool_result, tool_output_limit);
format!(
"Tool result too long. The first {} bytes:\n\n{}",
truncated.len(),
truncated
)
};
self.tool_results.insert(
tool_use_id.clone(),
LanguageModelToolResult {

View File

@@ -280,10 +280,9 @@ impl AddedContext {
}
AssistantContext::Directory(directory_context) => {
let full_path = directory_context
.worktree
.read(cx)
.full_path(&directory_context.path);
// TODO: handle worktree disambiguation. Maybe by storing an `Arc<dyn File>` to also
// handle renames?
let full_path = &directory_context.project_path.path;
let full_path_string: SharedString =
full_path.to_string_lossy().into_owned().into();
let name = full_path

View File

@@ -1,25 +0,0 @@
[package]
name = "agent_rules"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/agent_rules.rs"
doctest = false
[dependencies]
anyhow.workspace = true
fs.workspace = true
gpui.workspace = true
prompt_store.workspace = true
util.workspace = true
worktree.workspace = true
workspace-hack = { version = "0.1", path = "../../tooling/workspace-hack" }
[dev-dependencies]
indoc.workspace = true

View File

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

View File

@@ -1,51 +0,0 @@
use std::sync::Arc;
use anyhow::{Context as _, Result};
use fs::Fs;
use gpui::{App, AppContext, Task};
use prompt_store::SystemPromptRulesFile;
use util::maybe;
use worktree::Worktree;
const RULES_FILE_NAMES: [&'static str; 6] = [
".rules",
".cursorrules",
".windsurfrules",
".clinerules",
".github/copilot-instructions.md",
"CLAUDE.md",
];
pub fn load_worktree_rules_file(
fs: Arc<dyn Fs>,
worktree: &Worktree,
cx: &App,
) -> Option<Task<Result<SystemPromptRulesFile>>> {
let selected_rules_file = RULES_FILE_NAMES
.into_iter()
.filter_map(|name| {
worktree
.entry_for_path(name)
.filter(|entry| entry.is_file())
.map(|entry| (entry.path.clone(), worktree.absolutize(&entry.path)))
})
.next();
// Note that Cline supports `.clinerules` being a directory, but that is not currently
// supported. This doesn't seem to occur often in GitHub repositories.
selected_rules_file.map(|(path_in_worktree, abs_path)| {
let fs = fs.clone();
cx.background_spawn(maybe!(async move {
let abs_path = abs_path?;
let text = fs
.load(&abs_path)
.await
.with_context(|| format!("Failed to load assistant rules file {:?}", abs_path))?;
anyhow::Ok(SystemPromptRulesFile {
path_in_worktree,
abs_path: abs_path.into(),
text: text.trim().to_string(),
})
}))
})
}

View File

@@ -321,54 +321,38 @@ pub async fn stream_completion(
.map(|output| output.0)
}
/// An individual rate limit.
#[derive(Debug)]
pub struct RateLimit {
pub limit: usize,
pub remaining: usize,
pub reset: DateTime<Utc>,
}
impl RateLimit {
fn from_headers(resource: &str, headers: &HeaderMap<HeaderValue>) -> Result<Self> {
let limit =
get_header(&format!("anthropic-ratelimit-{resource}-limit"), headers)?.parse()?;
let remaining = get_header(
&format!("anthropic-ratelimit-{resource}-remaining"),
headers,
)?
.parse()?;
let reset = DateTime::parse_from_rfc3339(get_header(
&format!("anthropic-ratelimit-{resource}-reset"),
headers,
)?)?
.to_utc();
Ok(Self {
limit,
remaining,
reset,
})
}
}
/// <https://docs.anthropic.com/en/api/rate-limits#response-headers>
#[derive(Debug)]
pub struct RateLimitInfo {
pub requests: Option<RateLimit>,
pub tokens: Option<RateLimit>,
pub input_tokens: Option<RateLimit>,
pub output_tokens: Option<RateLimit>,
pub requests_limit: usize,
pub requests_remaining: usize,
pub requests_reset: DateTime<Utc>,
pub tokens_limit: usize,
pub tokens_remaining: usize,
pub tokens_reset: DateTime<Utc>,
}
impl RateLimitInfo {
fn from_headers(headers: &HeaderMap<HeaderValue>) -> Self {
Self {
requests: RateLimit::from_headers("requests", headers).log_err(),
tokens: RateLimit::from_headers("tokens", headers).log_err(),
input_tokens: RateLimit::from_headers("input-tokens", headers).log_err(),
output_tokens: RateLimit::from_headers("output-tokens", headers).log_err(),
}
fn from_headers(headers: &HeaderMap<HeaderValue>) -> Result<Self> {
let tokens_limit = get_header("anthropic-ratelimit-tokens-limit", headers)?.parse()?;
let requests_limit = get_header("anthropic-ratelimit-requests-limit", headers)?.parse()?;
let tokens_remaining =
get_header("anthropic-ratelimit-tokens-remaining", headers)?.parse()?;
let requests_remaining =
get_header("anthropic-ratelimit-requests-remaining", headers)?.parse()?;
let requests_reset = get_header("anthropic-ratelimit-requests-reset", headers)?;
let tokens_reset = get_header("anthropic-ratelimit-tokens-reset", headers)?;
let requests_reset = DateTime::parse_from_rfc3339(requests_reset)?.to_utc();
let tokens_reset = DateTime::parse_from_rfc3339(tokens_reset)?.to_utc();
Ok(Self {
requests_limit,
tokens_limit,
requests_remaining,
tokens_remaining,
requests_reset,
tokens_reset,
})
}
}
@@ -434,7 +418,7 @@ pub async fn stream_completion_with_rate_limit_info(
}
})
.boxed();
Ok((stream, Some(rate_limits)))
Ok((stream, rate_limits.log_err()))
} else {
let mut body = Vec::new();
response

View File

@@ -37,11 +37,11 @@ use ui::{ContextMenu, PopoverMenu, Tooltip, prelude::*};
use util::{ResultExt, maybe};
use workspace::DraggedTab;
use workspace::{
DraggedSelection, Pane, ToggleZoom, Workspace,
DraggedSelection, Pane, ShowConfiguration, ToggleZoom, Workspace,
dock::{DockPosition, Panel, PanelEvent},
pane,
};
use zed_actions::assistant::{InlineAssist, OpenPromptLibrary, ShowConfiguration, ToggleFocus};
use zed_actions::assistant::{InlineAssist, OpenPromptLibrary, ToggleFocus};
pub fn init(cx: &mut App) {
workspace::FollowableViewRegistry::register::<ContextEditor>(cx);
@@ -54,15 +54,7 @@ pub fn init(cx: &mut App) {
.register_action(ContextEditor::insert_dragged_files)
.register_action(AssistantPanel::show_configuration)
.register_action(AssistantPanel::create_new_context)
.register_action(AssistantPanel::restart_context_servers)
.register_action(|workspace, _: &OpenPromptLibrary, window, cx| {
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)
});
}
});
.register_action(AssistantPanel::restart_context_servers);
},
)
.detach();

View File

@@ -57,7 +57,7 @@ use std::{
time::{Duration, Instant},
};
use streaming_diff::{CharOperation, LineDiff, LineOperation, StreamingDiff};
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal_view::terminal_panel::TerminalPanel;
use text::{OffsetRangeExt, ToPoint as _};
use theme::ThemeSettings;
@@ -315,7 +315,7 @@ impl InlineAssistant {
if let Some(ConfiguredModel { model, .. }) =
LanguageModelRegistry::read_global(cx).default_model()
{
self.telemetry.report_assistant_event(AssistantEventData {
self.telemetry.report_assistant_event(AssistantEvent {
conversation_id: None,
kind: AssistantKind::Inline,
phase: AssistantPhase::Invoked,
@@ -527,14 +527,14 @@ impl InlineAssistant {
BlockProperties {
style: BlockStyle::Sticky,
placement: BlockPlacement::Above(range.start),
height: Some(prompt_editor_height),
height: prompt_editor_height,
render: build_assist_editor_renderer(prompt_editor),
priority: 0,
},
BlockProperties {
style: BlockStyle::Sticky,
placement: BlockPlacement::Below(range.end),
height: None,
height: 0,
render: Arc::new(|cx| {
v_flex()
.h_full()
@@ -892,7 +892,7 @@ impl InlineAssistant {
.map(|language| language.name())
});
report_assistant_event(
AssistantEventData {
AssistantEvent {
conversation_id: None,
kind: AssistantKind::Inline,
message_id,
@@ -1301,7 +1301,7 @@ impl InlineAssistant {
deleted_lines_editor.update(cx, |editor, cx| editor.max_point(cx).row().0 + 1);
new_blocks.push(BlockProperties {
placement: BlockPlacement::Above(new_row),
height: Some(height),
height,
style: BlockStyle::Flex,
render: Arc::new(move |cx| {
div()
@@ -3148,7 +3148,7 @@ impl CodegenAlternative {
let error_message = result.as_ref().err().map(|error| error.to_string());
report_assistant_event(
AssistantEventData {
AssistantEvent {
conversation_id: None,
message_id,
kind: AssistantKind::Inline,

View File

@@ -27,7 +27,7 @@ use std::{
sync::Arc,
time::{Duration, Instant},
};
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal::Terminal;
use terminal_view::TerminalView;
use theme::ThemeSettings;
@@ -324,7 +324,7 @@ impl TerminalInlineAssistant {
let codegen = assist.codegen.read(cx);
let executor = cx.background_executor().clone();
report_assistant_event(
AssistantEventData {
AssistantEvent {
conversation_id: None,
kind: AssistantKind::InlineTerminal,
message_id: codegen.message_id.clone(),
@@ -1183,7 +1183,7 @@ impl Codegen {
let error_message = result.as_ref().err().map(|error| error.to_string());
report_assistant_event(
AssistantEventData {
AssistantEvent {
conversation_id: None,
kind: AssistantKind::InlineTerminal,
message_id,

View File

@@ -22,7 +22,6 @@ clock.workspace = true
collections.workspace = true
context_server.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
@@ -54,9 +53,8 @@ theme.workspace = true
ui.workspace = true
util.workspace = true
uuid.workspace = true
workspace-hack.workspace = true
workspace.workspace = true
zed_actions.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
language_model = { workspace = true, features = ["test-support"] }

View File

@@ -40,7 +40,7 @@ use std::{
sync::Arc,
time::{Duration, Instant},
};
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use text::{BufferSnapshot, ToPoint};
use ui::IconName;
use util::{ResultExt, TryFutureExt, post_inc};
@@ -2498,7 +2498,7 @@ impl AssistantContext {
.language()
.map(|language| language.name());
report_assistant_event(
AssistantEventData {
AssistantEvent {
conversation_id: Some(this.id.0.clone()),
kind: AssistantKind::Panel,
phase: AssistantPhase::Response,

View File

@@ -10,7 +10,7 @@ use collections::{BTreeSet, HashMap, HashSet, hash_map};
use editor::{
Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, ProposedChangeLocation,
ProposedChangesEditor, RowExt, ToOffset as _, ToPoint,
actions::{MoveToEndOfLine, Newline, ShowCompletions},
actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
display_map::{
BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata,
CustomBlockId, FoldId, RenderBlock, ToDisplayPoint,
@@ -18,7 +18,6 @@ use editor::{
scroll::Autoscroll,
};
use editor::{FoldPlaceholder, display_map::CreaseId};
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt as _};
use fs::Fs;
use futures::FutureExt;
use gpui::{
@@ -57,7 +56,8 @@ use ui::{
use util::{ResultExt, maybe};
use workspace::searchable::{Direction, SearchableItemHandle};
use workspace::{
Save, Toast, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
Save, ShowConfiguration, Toast, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
Workspace,
item::{self, FollowableItem, Item, ItemHandle},
notifications::NotificationId,
pane::{self, SaveIntent},
@@ -1053,7 +1053,7 @@ impl ContextEditor {
let creases = editor.insert_creases(creases, cx);
for buffer_row in buffer_rows_to_fold.into_iter().rev() {
editor.fold_at(buffer_row, window, cx);
editor.fold_at(&FoldAt { buffer_row }, window, cx);
}
creases
@@ -1109,7 +1109,7 @@ impl ContextEditor {
buffer_rows_to_fold.clear();
}
for buffer_row in buffer_rows_to_fold.into_iter().rev() {
editor.fold_at(buffer_row, window, cx);
editor.fold_at(&FoldAt { buffer_row }, window, cx);
}
});
}
@@ -1562,7 +1562,7 @@ impl ContextEditor {
})
};
let create_block_properties = |message: &Message| BlockProperties {
height: Some(2),
height: 2,
style: BlockStyle::Sticky,
placement: BlockPlacement::Above(
buffer
@@ -1844,7 +1844,13 @@ impl ContextEditor {
|_, _, _, _| Empty.into_any(),
);
editor.insert_creases(vec![crease], cx);
editor.fold_at(start_row, window, cx);
editor.fold_at(
&FoldAt {
buffer_row: start_row,
},
window,
cx,
);
}
})
}
@@ -2036,7 +2042,7 @@ impl ContextEditor {
cx,
);
for buffer_row in buffer_rows_to_fold.into_iter().rev() {
editor.fold_at(buffer_row, window, cx);
editor.fold_at(&FoldAt { buffer_row }, window, cx);
}
}
});
@@ -2105,7 +2111,7 @@ impl ContextEditor {
let image = render_image.clone();
anchor.is_valid(&buffer).then(|| BlockProperties {
placement: BlockPlacement::Above(anchor),
height: Some(MAX_HEIGHT_IN_LINES),
height: MAX_HEIGHT_IN_LINES,
style: BlockStyle::Sticky,
render: Arc::new(move |cx| {
let image_size = size_for_image(
@@ -2353,19 +2359,7 @@ impl ContextEditor {
.on_click({
let focus_handle = self.focus_handle(cx).clone();
move |_event, window, cx| {
if cx.has_flag::<Assistant2FeatureFlag>() {
focus_handle.dispatch_action(
&zed_actions::agent::OpenConfiguration,
window,
cx,
);
} else {
focus_handle.dispatch_action(
&zed_actions::assistant::ShowConfiguration,
window,
cx,
);
};
focus_handle.dispatch_action(&ShowConfiguration, window, cx);
}
}),
)
@@ -2787,7 +2781,7 @@ fn render_thought_process_fold_icon_button(
let button = match status {
ThoughtProcessStatus::Pending => button
.child(
Icon::new(IconName::LightBulb)
Icon::new(IconName::Brain)
.size(IconSize::Small)
.color(Color::Muted),
)
@@ -2802,7 +2796,7 @@ fn render_thought_process_fold_icon_button(
),
ThoughtProcessStatus::Completed => button
.style(ButtonStyle::Filled)
.child(Icon::new(IconName::LightBulb).size(IconSize::Small))
.child(Icon::new(IconName::Brain).size(IconSize::Small))
.child(Label::new("Thought Process").single_line()),
};
@@ -2814,7 +2808,7 @@ fn render_thought_process_fold_icon_button(
.start
.to_point(&editor.buffer().read(cx).read(cx));
let buffer_row = MultiBufferRow(buffer_start.row);
editor.unfold_at(buffer_row, window, cx);
editor.unfold_at(&UnfoldAt { buffer_row }, window, cx);
})
.ok();
})
@@ -2841,7 +2835,7 @@ fn render_fold_icon_button(
.start
.to_point(&editor.buffer().read(cx).read(cx));
let buffer_row = MultiBufferRow(buffer_start.row);
editor.unfold_at(buffer_row, window, cx);
editor.unfold_at(&UnfoldAt { buffer_row }, window, cx);
})
.ok();
})
@@ -2901,7 +2895,7 @@ fn quote_selection_fold_placeholder(title: String, editor: WeakEntity<Editor>) -
.start
.to_point(&editor.buffer().read(cx).read(cx));
let buffer_row = MultiBufferRow(buffer_start.row);
editor.unfold_at(buffer_row, window, cx);
editor.unfold_at(&UnfoldAt { buffer_row }, window, cx);
})
.ok();
})

View File

@@ -120,14 +120,13 @@ impl SlashCommandCompletionProvider {
) as Arc<_>
});
Some(project::Completion {
replace_range: name_range.clone(),
old_range: name_range.clone(),
documentation: Some(CompletionDocumentation::SingleLine(
command.description().into(),
)),
new_text,
label: command.label(cx),
icon_path: None,
insert_text_mode: None,
confirm,
source: CompletionSource::Custom,
})
@@ -219,7 +218,7 @@ impl SlashCommandCompletionProvider {
}
project::Completion {
replace_range: if new_argument.replace_previous_arguments {
old_range: if new_argument.replace_previous_arguments {
argument_range.clone()
} else {
last_argument_range.clone()
@@ -229,7 +228,6 @@ impl SlashCommandCompletionProvider {
new_text,
documentation: None,
confirm,
insert_text_mode: None,
source: CompletionSource::Custom,
}
})

View File

@@ -1,5 +1,5 @@
[package]
name = "agent_eval"
name = "assistant_eval"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
@@ -9,7 +9,7 @@ license = "GPL-3.0-or-later"
workspace = true
[[bin]]
name = "agent_eval"
name = "assistant_eval"
path = "src/main.rs"
[dependencies]

View File

@@ -0,0 +1,68 @@
# Tool Evals
A framework for evaluating and benchmarking the agent panel generations.
## Overview
Tool Evals provides a headless environment for running assistants evaluations on code repositories. It automates the process of:
1. Setting up test code and repositories
2. Sending prompts to language models
3. Allowing the assistant to use tools to modify code
4. Collecting metrics on performance and tool usage
5. Evaluating results against known good solutions
## How It Works
The system consists of several key components:
- **Eval**: Loads exercises from the zed-ace-framework repository, creates temporary repos, and executes evaluations
- **HeadlessAssistant**: Provides a headless environment for running the AI assistant
- **Judge**: Evaluates AI-generated solutions against reference implementations and assigns scores
- **Templates**: Defines evaluation frameworks for different tasks (Project Creation, Code Modification, Conversational Guidance)
## Setup Requirements
### Prerequisites
- Rust and Cargo
- Git
- Python (for report generation)
- Network access to clone repositories
- Appropriate API keys for language models and git services (Anthropic, GitHub, etc.)
### Environment Variables
Ensure you have the required API keys set, either from a dev run of Zed or via these environment variables:
- `ZED_ANTHROPIC_API_KEY` for Claude models
- `ZED_GITHUB_API_KEY` for GitHub API (or similar)
## Usage
### Running Evaluations
```bash
# Run all tests
cargo run -p assistant_eval -- --all
# Run only specific languages
cargo run -p assistant_eval -- --all --languages python,rust
# Limit concurrent evaluations
cargo run -p assistant_eval -- --all --concurrency 5
# Limit number of exercises per language
cargo run -p assistant_eval -- --all --max-exercises-per-language 3
```
### Evaluation Template Types
The system supports three types of evaluation templates:
1. **ProjectCreation**: Tests the model's ability to create new implementations from scratch
2. **CodeModification**: Tests the model's ability to modify existing code to meet new requirements
3. **ConversationalGuidance**: Tests the model's ability to provide guidance without writing code
### Support Repo
The [zed-industries/zed-ace-framework](https://github.com/zed-industries/zed-ace-framework) contains the analytics and reporting scripts.

View File

@@ -1,6 +1,6 @@
use crate::git_commands::{run_git, setup_temp_repo};
use crate::headless_assistant::{HeadlessAppState, HeadlessAssistant};
use crate::{get_exercise_language, get_exercise_name};
use crate::{get_exercise_language, get_exercise_name, templates_eval::Template};
use agent::RequestKind;
use anyhow::{Result, anyhow};
use collections::HashMap;
@@ -18,6 +18,8 @@ use std::{
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct EvalResult {
pub exercise_name: String,
pub template_name: String,
pub score: String,
pub diff: String,
pub assistant_response: String,
pub elapsed_time_ms: u128,
@@ -27,6 +29,7 @@ pub struct EvalResult {
pub output_tokens: usize,
pub total_tokens: usize,
pub tool_use_counts: usize,
pub judge_model_name: String, // Added field for judge model name
}
pub struct EvalOutput {
@@ -248,6 +251,29 @@ pub async fn read_instructions(exercise_path: &Path) -> Result<String> {
Ok(instructions)
}
pub async fn read_example_solution(exercise_path: &Path, language: &str) -> Result<String> {
// Map the language to the file extension
let language_extension = match language {
"python" => "py",
"go" => "go",
"rust" => "rs",
"typescript" => "ts",
"javascript" => "js",
"ruby" => "rb",
"php" => "php",
"bash" => "sh",
"multi" => "diff",
"internal" => "diff",
_ => return Err(anyhow!("Unsupported language: {}", language)),
};
let example_path = exercise_path
.join(".meta")
.join(format!("example.{}", language_extension));
println!("Reading example solution from: {}", example_path.display());
let example = smol::unblock(move || std::fs::read_to_string(&example_path)).await?;
Ok(example)
}
pub async fn save_eval_results(exercise_path: &Path, results: Vec<EvalResult>) -> Result<()> {
let eval_dir = exercise_path.join("evaluation");
fs::create_dir_all(&eval_dir)?;
@@ -285,8 +311,12 @@ pub async fn save_eval_results(exercise_path: &Path, results: Vec<EvalResult>) -
// Group the new results by test name (exercise name)
for result in results {
let exercise_name = &result.exercise_name;
let template_name = &result.template_name;
println!("Adding result: exercise={}", exercise_name);
println!(
"Adding result: exercise={}, template={}",
exercise_name, template_name
);
// Ensure the exercise entry exists
if eval_data.get(exercise_name).is_none() {
@@ -299,7 +329,7 @@ pub async fn save_eval_results(exercise_path: &Path, results: Vec<EvalResult>) -
}
// Add this result under the timestamp with template name as key
eval_data[exercise_name][&timestamp] = serde_json::to_value(&result)?;
eval_data[exercise_name][&timestamp][template_name] = serde_json::to_value(&result)?;
}
// Write back to file with pretty formatting
@@ -314,7 +344,9 @@ pub async fn save_eval_results(exercise_path: &Path, results: Vec<EvalResult>) -
pub async fn run_exercise_eval(
exercise_path: PathBuf,
template: Template,
model: Arc<dyn LanguageModel>,
judge_model: Arc<dyn LanguageModel>,
app_state: Arc<HeadlessAppState>,
base_sha: String,
_framework_path: PathBuf,
@@ -327,15 +359,68 @@ pub async fn run_exercise_eval(
"\n\nWhen writing the code for this prompt, use {} to achieve the goal.",
language
));
let example_solution = read_example_solution(&exercise_path, &language).await?;
println!("Running evaluation for exercise: {}", exercise_name);
println!(
"Running evaluation for exercise: {} with template: {}",
exercise_name, template.name
);
// Create temporary directory with exercise files
let temp_dir = setup_temp_repo(&exercise_path, &base_sha).await?;
let temp_path = temp_dir.path().to_path_buf();
if template.name == "ProjectCreation" {
for entry in fs::read_dir(&temp_path)? {
let entry = entry?;
let path = entry.path();
// Skip directories that start with dot (like .docs, .meta, .git)
if path.is_dir()
&& path
.file_name()
.and_then(|name| name.to_str())
.map(|name| name.starts_with("."))
.unwrap_or(false)
{
continue;
}
// Delete regular files
if path.is_file() {
println!(" Deleting file: {}", path.display());
fs::remove_file(path)?;
}
}
// Commit the deletion so it shows up in the diff
run_git(&temp_path, &["add", "."]).await?;
run_git(
&temp_path,
&["commit", "-m", "Remove root files for clean slate"],
)
.await?;
}
let local_commit_sha = run_git(&temp_path, &["rev-parse", "HEAD"]).await?;
// Prepare prompt based on template
let prompt = match template.name {
"ProjectCreation" => format!(
"I need to create a new implementation for this exercise. Please create all the necessary files in the best location.\n\n{}",
instructions
),
"CodeModification" => format!(
"I need help updating my code to meet these requirements. Please modify the appropriate files:\n\n{}",
instructions
),
"ConversationalGuidance" => format!(
"I'm trying to solve this coding exercise but I'm not sure where to start. Can you help me understand the requirements and guide me through the solution process without writing code for me?\n\n{}",
instructions
),
_ => instructions.clone(),
};
let start_time = SystemTime::now();
// Create a basic eval struct to work with the existing system
@@ -345,7 +430,7 @@ pub async fn run_exercise_eval(
url: format!("file://{}", temp_path.display()),
base_sha: local_commit_sha, // Use the local commit SHA instead of the framework base SHA
},
user_prompt: instructions.clone(),
user_prompt: prompt,
};
// Run the evaluation
@@ -356,6 +441,79 @@ pub async fn run_exercise_eval(
// Get diff from git
let diff = eval_output.diff.clone();
// For project creation template, we need to compare with reference implementation
let judge_output = if template.name == "ProjectCreation" {
let project_judge_prompt = template
.content
.replace(
"<!-- ```requirements go here``` -->",
&format!("```\n{}\n```", instructions),
)
.replace(
"<!-- ```reference code goes here``` -->",
&format!("```{}\n{}\n```", language, example_solution),
)
.replace(
"<!-- ```git diff goes here``` -->",
&format!("```\n{}\n```", diff),
);
// Use the run_with_prompt method which we'll add to judge.rs
let judge = crate::judge::Judge {
original_diff: None,
original_message: Some(project_judge_prompt),
model: judge_model.clone(),
};
cx.update(|cx| judge.run_with_prompt(cx))?.await?
} else if template.name == "CodeModification" {
// For CodeModification, we'll compare the example solution with the LLM-generated solution
let code_judge_prompt = template
.content
.replace(
"<!-- ```reference code goes here``` -->",
&format!("```{}\n{}\n```", language, example_solution),
)
.replace(
"<!-- ```git diff goes here``` -->",
&format!("```\n{}\n```", diff),
);
// Use the run_with_prompt method
let judge = crate::judge::Judge {
original_diff: None,
original_message: Some(code_judge_prompt),
model: judge_model.clone(),
};
cx.update(|cx| judge.run_with_prompt(cx))?.await?
} else {
// Conversational template
let conv_judge_prompt = template
.content
.replace(
"<!-- ```query goes here``` -->",
&format!("```\n{}\n```", instructions),
)
.replace(
"<!-- ```transcript goes here``` -->",
&format!("```\n{}\n```", eval_output.last_message),
)
.replace(
"<!-- ```git diff goes here``` -->",
&format!("```\n{}\n```", diff),
);
// Use the run_with_prompt method for consistency
let judge = crate::judge::Judge {
original_diff: None,
original_message: Some(conv_judge_prompt),
model: judge_model.clone(),
};
cx.update(|cx| judge.run_with_prompt(cx))?.await?
};
let elapsed_time = start_time.elapsed()?;
// Calculate total tokens as the sum of input and output tokens
@@ -364,9 +522,14 @@ pub async fn run_exercise_eval(
let tool_use_counts = eval_output.tool_use_counts.values().sum::<u32>();
let total_tokens = input_tokens + output_tokens;
// Get judge model name
let judge_model_name = judge_model.id().0.to_string();
// Save results to evaluation directory
let result = EvalResult {
exercise_name: exercise_name.clone(),
template_name: template.name.to_string(),
score: judge_output.trim().to_string(),
diff,
assistant_response: eval_output.last_message.clone(),
elapsed_time_ms: elapsed_time.as_millis(),
@@ -378,6 +541,7 @@ pub async fn run_exercise_eval(
output_tokens: output_tokens.try_into().unwrap(),
total_tokens: total_tokens.try_into().unwrap(),
tool_use_counts: tool_use_counts.try_into().unwrap(),
judge_model_name, // Add judge model name to result
};
Ok(result)

View File

@@ -4,10 +4,12 @@ use assistant_tool::ToolWorkingSet;
use client::{Client, UserStore};
use collections::HashMap;
use dap::DapRegistry;
use gpui::{App, Entity, SemanticVersion, Subscription, Task, prelude::*};
use futures::StreamExt;
use gpui::{App, AsyncApp, Entity, SemanticVersion, Subscription, Task, prelude::*};
use language::LanguageRegistry;
use language_model::{
AuthenticateError, LanguageModel, LanguageModelProviderId, LanguageModelRegistry,
LanguageModelRequest,
};
use node_runtime::NodeRuntime;
use project::{Project, RealFs};
@@ -95,7 +97,11 @@ impl HeadlessAssistant {
self.done_tx.send_blocking(Ok(())).unwrap()
}
}
ThreadEvent::UsePendingTools { .. } => {}
ThreadEvent::UsePendingTools => {
thread.update(cx, |thread, cx| {
thread.use_pending_tools(cx);
});
}
ThreadEvent::ToolConfirmationNeeded => {
// Automatically approve all tools that need confirmation in headless mode
println!("Tool confirmation needed - automatically approving in headless mode");
@@ -148,6 +154,19 @@ impl HeadlessAssistant {
if let Some(tool_result) = thread.read(cx).tool_result(tool_use_id) {
println!("Tool result: {:?}", tool_result);
}
if thread.read(cx).all_tools_finished() {
let model_registry = LanguageModelRegistry::read_global(cx);
if let Some(model) = model_registry.default_model() {
thread.update(cx, |thread, cx| {
thread.attach_tool_results(cx);
thread.send_to_model(model.model, RequestKind::Chat, cx);
});
} else {
println!(
"Warning: No active language model available to continue conversation"
);
}
}
}
_ => {}
}
@@ -227,3 +246,34 @@ pub fn authenticate_model_provider(
let model_provider = model_registry.provider(&provider_id).unwrap();
model_provider.authenticate(cx)
}
pub async fn send_language_model_request(
model: Arc<dyn LanguageModel>,
request: LanguageModelRequest,
cx: &mut AsyncApp,
) -> anyhow::Result<String> {
match model.stream_completion_text(request, &cx).await {
Ok(mut stream) => {
let mut full_response = String::new();
// Process the response stream
while let Some(chunk_result) = stream.stream.next().await {
match chunk_result {
Ok(chunk_str) => {
full_response.push_str(&chunk_str);
}
Err(err) => {
return Err(anyhow!(
"Error receiving response from language model: {err}"
));
}
}
}
Ok(full_response)
}
Err(err) => Err(anyhow!(
"Failed to get response from language model. Error was: {err}"
)),
}
}

View File

@@ -0,0 +1,37 @@
use crate::headless_assistant::send_language_model_request;
use anyhow::anyhow;
use gpui::{App, Task};
use language_model::{
LanguageModel, LanguageModelRequest, LanguageModelRequestMessage, MessageContent, Role,
};
use std::sync::Arc;
pub struct Judge {
#[allow(dead_code)]
pub original_diff: Option<String>,
pub original_message: Option<String>,
pub model: Arc<dyn LanguageModel>,
}
impl Judge {
pub fn run_with_prompt(&self, cx: &mut App) -> Task<anyhow::Result<String>> {
let Some(prompt) = self.original_message.as_ref() else {
return Task::ready(Err(anyhow!("No prompt provided in original_message")));
};
let request = LanguageModelRequest {
messages: vec![LanguageModelRequestMessage {
role: Role::User,
content: vec![MessageContent::Text(prompt.clone())],
cache: false,
}],
temperature: Some(0.0),
tools: Vec::new(),
stop: Vec::new(),
};
let model = self.model.clone();
let request = request.clone();
cx.spawn(async move |cx| send_language_model_request(model, request, cx).await)
}
}

View File

@@ -2,6 +2,8 @@ mod eval;
mod get_exercise;
mod git_commands;
mod headless_assistant;
mod judge;
mod templates_eval;
use clap::Parser;
use eval::{run_exercise_eval, save_eval_results};
@@ -13,10 +15,11 @@ use headless_assistant::{authenticate_model_provider, find_model};
use language_model::LanguageModelRegistry;
use reqwest_client::ReqwestClient;
use std::{path::PathBuf, sync::Arc};
use templates_eval::all_templates;
#[derive(Parser, Debug)]
#[command(
name = "agent_eval",
name = "assistant_eval",
disable_version_flag = true,
before_help = "Tool eval runner"
)]
@@ -34,17 +37,24 @@ struct Args {
/// Name of the model (default: "claude-3-7-sonnet-latest")
#[arg(long, default_value = "claude-3-7-sonnet-latest")]
model_name: String,
/// Name of the editor model (default: value of `--model_name`).
/// Name of the judge model (default: value of `--model_name`).
#[arg(long)]
editor_model_name: Option<String>,
judge_model_name: Option<String>,
/// Number of evaluations to run concurrently (default: 3)
#[arg(short, long, default_value = "5")]
#[arg(short, long, default_value = "3")]
concurrency: usize,
/// Maximum number of exercises to evaluate per language
#[arg(long)]
max_exercises_per_language: Option<usize>,
}
// First, let's define the order in which templates should be executed
const TEMPLATE_EXECUTION_ORDER: [&str; 3] = [
"ProjectCreation",
"CodeModification",
"ConversationalGuidance",
];
fn main() {
env_logger::init();
let args = Args::parse();
@@ -66,7 +76,7 @@ fn main() {
let app_state = headless_assistant::init(cx);
let model = find_model(&args.model_name, cx).unwrap();
let editor_model = if let Some(model_name) = &args.editor_model_name {
let judge_model = if let Some(model_name) = &args.judge_model_name {
find_model(model_name, cx).unwrap()
} else {
model.clone()
@@ -77,7 +87,7 @@ fn main() {
});
let model_provider_id = model.provider_id();
let editor_model_provider_id = editor_model.provider_id();
let judge_model_provider_id = judge_model.provider_id();
let framework_path_clone = framework_path.clone();
let languages_clone = languages.clone();
@@ -90,17 +100,15 @@ fn main() {
.unwrap()
.await
.unwrap();
cx.update(|cx| authenticate_model_provider(editor_model_provider_id.clone(), cx))
cx.update(|cx| authenticate_model_provider(judge_model_provider_id.clone(), cx))
.unwrap()
.await
.unwrap();
println!("framework path: {}", framework_path_clone.display());
// Read base SHA from setup.json
let base_sha = read_base_sha(&framework_path_clone).await.unwrap();
println!("base sha: {}", base_sha);
// Find all exercises for the specified languages
let all_exercises = find_exercises(
&framework_path_clone,
&languages_clone
@@ -132,12 +140,23 @@ fn main() {
println!("Will run {} exercises", exercises_to_run.len());
// Get all templates and sort them according to the execution order
let mut templates = all_templates();
templates.sort_by_key(|template| {
TEMPLATE_EXECUTION_ORDER
.iter()
.position(|&name| name == template.name)
.unwrap_or(usize::MAX)
});
// Create exercise eval tasks - each exercise is a single task that will run templates sequentially
let exercise_tasks: Vec<_> = exercises_to_run
.into_iter()
.map(|exercise_path| {
let exercise_name = get_exercise_name(&exercise_path);
let templates_clone = templates.clone();
let model_clone = model.clone();
let judge_model_clone = judge_model.clone();
let app_state_clone = app_state.clone();
let base_sha_clone = base_sha.clone();
let framework_path_clone = framework_path_clone.clone();
@@ -147,22 +166,56 @@ fn main() {
println!("Processing exercise: {}", exercise_name);
let mut exercise_results = Vec::new();
match run_exercise_eval(
exercise_path.clone(),
model_clone.clone(),
app_state_clone.clone(),
base_sha_clone.clone(),
framework_path_clone.clone(),
cx_clone.clone(),
)
.await
{
Ok(result) => {
println!("Completed {}", exercise_name);
exercise_results.push(result);
}
// Determine the language for this exercise
let language = match get_exercise_language(&exercise_path) {
Ok(lang) => lang,
Err(err) => {
println!("Error running {}: {}", exercise_name, err);
println!(
"Error determining language for {}: {}",
exercise_name, err
);
return exercise_results;
}
};
// Run each template sequentially for this exercise
for template in templates_clone {
// For "multi" or "internal" language, only run the CodeModification template
if (language == "multi" || language == "internal")
&& template.name != "CodeModification"
{
println!(
"Skipping {} template for {} language",
template.name, language
);
continue;
}
match run_exercise_eval(
exercise_path.clone(),
template.clone(),
model_clone.clone(),
judge_model_clone.clone(),
app_state_clone.clone(),
base_sha_clone.clone(),
framework_path_clone.clone(),
cx_clone.clone(),
)
.await
{
Ok(result) => {
println!(
"Completed {} with template {} - score: {}",
exercise_name, template.name, result.score
);
exercise_results.push(result);
}
Err(err) => {
println!(
"Error running {} with template {}: {}",
exercise_name, template.name, err
);
}
}
}

View File

@@ -0,0 +1,210 @@
#[derive(Clone, Debug)]
pub struct Template {
pub name: &'static str,
pub content: &'static str,
}
pub fn all_templates() -> Vec<Template> {
vec![
Template {
name: "ProjectCreation",
content: r#"
# Project Creation Evaluation Template
## Instructions
Evaluate how well the AI assistant created a new implementation from scratch. Score it between 0.0 and 1.0 based on quality and fulfillment of requirements.
- 1.0 = Perfect implementation that creates all necessary files with correct functionality.
- 0.0 = Completely fails to create working files or meet requirements.
Note: A git diff output is required. If no code changes are provided (i.e., no git diff output), the score must be 0.0.
## Evaluation Criteria
Please consider the following aspects in order of importance:
1. **File Creation (25%)**
- Did the assistant create all necessary files?
- Are the files appropriately named and organized?
- Did the assistant create a complete solution without missing components?
2. **Functional Correctness (40%)**
- Does the implementation fulfill all specified requirements?
- Does it handle edge cases properly?
- Is it free of logical errors and bugs?
- Do all components work together as expected?
3. **Code Quality (20%)**
- Is the code well-structured, readable and well-documented?
- Does it follow language-specific best practices?
- Is there proper error handling?
- Are naming conventions clear and consistent?
4. **Architecture Design (15%)**
- Is the code modular and extensible?
- Is there proper separation of concerns?
- Are appropriate design patterns used?
- Is the overall architecture appropriate for the requirements?
## Input
Requirements:
<!-- ```requirements go here``` -->
Reference Implementation:
<!-- ```reference code goes here``` -->
AI-Generated Implementation (git diff output):
<!-- ```git diff goes here``` -->
## Output Format
THE ONLY OUTPUT SHOULD BE A SCORE BETWEEN 0.0 AND 1.0.
EXAMPLE ONE:
0.92
EXAMPLE TWO:
0.85
EXAMPLE THREE:
0.78
"#,
},
Template {
name: "CodeModification",
content: r#"
# Code Modification Evaluation Template
## Instructions
Evaluate how well the AI assistant modified existing code to meet requirements. Score between 0.0 and 1.0 based on quality and appropriateness of changes.
- 1.0 = Perfect modifications that correctly implement all requirements.
- 0.0 = Failed to make appropriate changes or introduced serious errors.
## Evaluation Criteria
Please consider the following aspects in order of importance:
1. **Functional Correctness (50%)**
- Do the modifications correctly implement the requirements?
- Did the assistant modify the right files and code sections?
- Are the changes free of bugs and logical errors?
- Do the modifications maintain compatibility with existing code?
2. **Modification Approach (25%)**
- Are the changes minimal and focused on what needs to be changed?
- Did the assistant avoid unnecessary modifications?
- Are the changes integrated seamlessly with the existing codebase?
- Did the assistant preserve the original code style and patterns?
3. **Code Quality (15%)**
- Are the modifications well-structured and documented?
- Do they follow the same conventions as the original code?
- Is there proper error handling in the modified code?
- Are the changes readable and maintainable?
4. **Solution Completeness (10%)**
- Do the modifications completely address all requirements?
- Are there any missing changes or overlooked requirements?
- Did the assistant consider all necessary edge cases?
## Input
Original:
<!-- ```reference code goes here``` -->
New (git diff output):
<!-- ```git diff goes here``` -->
## Output Format
THE ONLY OUTPUT SHOULD BE A SCORE BETWEEN 0.0 AND 1.0.
EXAMPLE ONE:
0.92
EXAMPLE TWO:
0.85
EXAMPLE THREE:
0.78
"#,
},
Template {
name: "ConversationalGuidance",
content: r#"
# Conversational Guidance Evaluation Template
## Instructions
Evaluate the quality of the AI assistant's conversational guidance and score it between 0.0 and 1.0.
- 1.0 = Perfect guidance with ideal information gathering, clarification, and advice without writing code.
- 0.0 = Completely unhelpful, inappropriate guidance, or wrote code when it should not have.
## Evaluation Criteria
ABSOLUTE REQUIREMENT:
- The assistant should NOT generate complete code solutions in conversation mode.
- If the git diff shows the assistant wrote complete code, the score should be significantly reduced.
1. **Information Gathering Effectiveness (30%)**
- Did the assistant ask relevant and precise questions?
- Did it efficiently narrow down the problem scope?
- Did it avoid unnecessary or redundant questions?
- Was questioning appropriately paced and contextual?
2. **Conceptual Guidance (30%)**
- Did the assistant provide high-level approaches and strategies?
- Did it explain relevant concepts and algorithms?
- Did it offer planning advice without implementing the solution?
- Did it suggest a structured approach to solving the problem?
3. **Educational Value (20%)**
- Did the assistant help the user understand the problem better?
- Did it provide explanations that would help the user learn?
- Did it guide without simply giving away answers?
- Did it encourage the user to think through parts of the problem?
4. **Conversation Quality (20%)**
- Was the conversation logically structured and easy to follow?
- Did the assistant maintain appropriate context throughout?
- Was the interaction helpful without being condescending?
- Did the conversation reach a satisfactory conclusion with clear next steps?
## Input
Initial Query:
<!-- ```query goes here``` -->
Conversation Transcript:
<!-- ```transcript goes here``` -->
Git Diff:
<!-- ```git diff goes here``` -->
## Output Format
THE ONLY OUTPUT SHOULD BE A SCORE BETWEEN 0.0 AND 1.0.
EXAMPLE ONE:
0.92
EXAMPLE TWO:
0.85
EXAMPLE THREE:
0.78
"#,
},
]
}

View File

@@ -18,20 +18,6 @@ pub use crate::action_log::*;
pub use crate::tool_registry::*;
pub use crate::tool_working_set::*;
/// Where the streamed-in text should go.
/// For example, the file creation tool streams it to a file.
#[derive(Debug, Clone)]
pub enum ResponseDest {
File { path: Arc<str> },
TextOnly,
}
impl Default for ResponseDest {
fn default() -> Self {
Self::TextOnly
}
}
pub fn init(cx: &mut App) {
ToolRegistry::default_global(cx);
}
@@ -62,7 +48,7 @@ pub trait Tool: 'static + Send + Sync {
/// Returns true iff the tool needs the users's confirmation
/// before having permission to run.
fn needs_confirmation(&self, input: &serde_json::Value, cx: &App) -> bool;
fn needs_confirmation(&self) -> bool;
/// Returns the JSON schema that describes the tool's input.
fn input_schema(&self, _: LanguageModelToolSchemaFormat) -> serde_json::Value {
@@ -80,7 +66,7 @@ pub trait Tool: 'static + Send + Sync {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<(ResponseDest, String)>>;
) -> Task<Result<String>>;
}
impl Debug for dyn Tool {

View File

@@ -16,6 +16,7 @@ anyhow.workspace = true
assistant_tool.workspace = true
chrono.workspace = true
collections.workspace = true
feature_flags.workspace = true
futures.workspace = true
gpui.workspace = true
html_to_markdown.workspace = true
@@ -23,13 +24,19 @@ http_client.workspace = true
itertools.workspace = true
language.workspace = true
language_model.workspace = true
log.workspace = true
lsp.workspace = true
project.workspace = true
regex.workspace = true
release_channel.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
worktree.workspace = true
open = { workspace = true }
workspace-hack.workspace = true

View File

@@ -1,10 +1,13 @@
mod code_action_tool;
mod bash_tool;
mod batch_tool;
mod code_symbol_iter;
mod code_symbols_tool;
mod copy_path_tool;
mod create_directory_tool;
mod create_file_tool;
mod delete_path_tool;
mod diagnostics_tool;
mod edit_files_tool;
mod fetch_tool;
mod find_replace_file_tool;
mod list_directory_tool;
@@ -14,11 +17,9 @@ mod open_tool;
mod path_search_tool;
mod read_file_tool;
mod regex_search_tool;
mod rename_tool;
mod replace;
mod schema;
mod symbol_info_tool;
mod terminal_tool;
mod thinking_tool;
use std::sync::Arc;
@@ -29,12 +30,14 @@ use gpui::App;
use http_client::HttpClientWithUrl;
use move_path_tool::MovePathTool;
use crate::code_action_tool::CodeActionTool;
use crate::bash_tool::BashTool;
use crate::batch_tool::BatchTool;
use crate::code_symbols_tool::CodeSymbolsTool;
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_files_tool::EditFilesTool;
use crate::fetch_tool::FetchTool;
use crate::find_replace_file_tool::FindReplaceFileTool;
use crate::list_directory_tool::ListDirectoryTool;
@@ -43,25 +46,25 @@ 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 fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
assistant_tool::init(cx);
crate::edit_files_tool::log::init(cx);
let registry = ToolRegistry::global(cx);
registry.register_tool(TerminalTool);
registry.register_tool(BashTool);
registry.register_tool(BatchTool);
registry.register_tool(CreateDirectoryTool);
registry.register_tool(CreateFileTool);
registry.register_tool(CopyPathTool);
registry.register_tool(DeletePathTool);
registry.register_tool(FindReplaceFileTool);
registry.register_tool(SymbolInfoTool);
registry.register_tool(CodeActionTool);
registry.register_tool(MovePathTool);
registry.register_tool(DiagnosticsTool);
registry.register_tool(EditFilesTool);
registry.register_tool(ListDirectoryTool);
registry.register_tool(NowTool);
registry.register_tool(OpenTool);
@@ -69,7 +72,6 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
registry.register_tool(PathSearchTool);
registry.register_tool(ReadFileTool);
registry.register_tool(RegexSearchTool);
registry.register_tool(RenameTool);
registry.register_tool(ThinkingTool);
registry.register_tool(FetchTool::new(http_client));
}

View File

@@ -0,0 +1,153 @@
use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use gpui::{App, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::sync::Arc;
use ui::IconName;
use util::command::new_smol_command;
use util::markdown::MarkdownString;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct BashToolInput {
/// The bash command to execute as a one-liner.
command: String,
/// Working directory for the command. This must be one of the root directories of the project.
cd: String,
}
pub struct BashTool;
impl Tool for BashTool {
fn name(&self) -> String {
"bash".to_string()
}
fn needs_confirmation(&self) -> bool {
true
}
fn description(&self) -> String {
include_str!("./bash_tool/description.md").to_string()
}
fn icon(&self) -> IconName {
IconName::Terminal
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value {
json_schema_for::<BashToolInput>(format)
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<BashToolInput>(input.clone()) {
Ok(input) => {
let mut lines = input.command.lines();
let first_line = lines.next().unwrap_or_default();
let remaining_line_count = lines.count();
match remaining_line_count {
0 => MarkdownString::inline_code(&first_line).0,
1 => {
MarkdownString::inline_code(&format!(
"{} - {} more line",
first_line, remaining_line_count
))
.0
}
n => {
MarkdownString::inline_code(&format!("{} - {} more lines", first_line, n)).0
}
}
}
Err(_) => "Run bash command".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
let input: BashToolInput = match serde_json::from_value(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
};
let project = project.read(cx);
let input_path = Path::new(&input.cd);
let working_dir = if input.cd == "." {
// Accept "." as meaning "the one worktree" if we only have one worktree.
let mut worktrees = project.worktrees(cx);
let only_worktree = match worktrees.next() {
Some(worktree) => worktree,
None => return Task::ready(Err(anyhow!("No worktrees found in the project"))),
};
if worktrees.next().is_some() {
return Task::ready(Err(anyhow!(
"'.' is ambiguous in multi-root workspaces. Please specify a root directory explicitly."
)));
}
only_worktree.read(cx).abs_path()
} else if input_path.is_absolute() {
// Absolute paths are allowed, but only if they're in one of the project's worktrees.
if !project
.worktrees(cx)
.any(|worktree| input_path.starts_with(&worktree.read(cx).abs_path()))
{
return Task::ready(Err(anyhow!(
"The absolute path must be within one of the project's worktrees"
)));
}
input_path.into()
} else {
let Some(worktree) = project.worktree_for_root_name(&input.cd, cx) else {
return Task::ready(Err(anyhow!(
"`cd` directory {} not found in the project",
&input.cd
)));
};
worktree.read(cx).abs_path()
};
cx.spawn(async move |_| {
// Add 2>&1 to merge stderr into stdout for proper interleaving.
let command = format!("({}) 2>&1", input.command);
let output = new_smol_command("bash")
.arg("-c")
.arg(&command)
.current_dir(working_dir)
.output()
.await
.context("Failed to execute bash command")?;
let output_string = String::from_utf8_lossy(&output.stdout).to_string();
if output.status.success() {
if output_string.is_empty() {
Ok("Command executed successfully.".to_string())
} else {
Ok(output_string)
}
} else {
Ok(format!(
"Command failed with exit code {}\n{}",
output.status.code().unwrap_or(-1),
&output_string
))
}
})
}
}

View File

@@ -0,0 +1,7 @@
Executes a bash one-liner and returns the combined output.
This tool spawns a bash process, combines stdout and stderr into one interleaved stream as they are produced (preserving the order of writes), and captures that stream into a string which is returned.
Make sure you use the `cd` parameter to navigate to one of the root directories of the project. NEVER do it as part of the `command` itself, otherwise it will error.
Remember that each invocation of this tool will spawn a new bash process, so you can't rely on any state from previous invocations.

View File

@@ -0,0 +1,301 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolWorkingSet};
use futures::future::join_all;
use gpui::{App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use ui::IconName;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ToolInvocation {
/// The name of the tool to invoke
pub name: String,
/// The input to the tool in JSON format
pub input: serde_json::Value,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct BatchToolInput {
/// The tool invocations to run as a batch. These tools will be run either sequentially
/// or concurrently depending on the `run_tools_concurrently` flag.
///
/// <example>
/// Basic file operations (concurrent)
///
/// ```json
/// {
/// "invocations": [
/// {
/// "name": "read-file",
/// "input": {
/// "path": "src/main.rs"
/// }
/// },
/// {
/// "name": "list-directory",
/// "input": {
/// "path": "src/lib"
/// }
/// },
/// {
/// "name": "regex-search",
/// "input": {
/// "regex": "fn run\\("
/// }
/// }
/// ],
/// "run_tools_concurrently": true
/// }
/// ```
/// </example>
///
/// <example>
/// Multiple find-replace operations on the same file (sequential)
///
/// ```json
/// {
/// "invocations": [
/// {
/// "name": "find-replace-file",
/// "input": {
/// "path": "src/config.rs",
/// "display_description": "Update default timeout value",
/// "find": "pub const DEFAULT_TIMEOUT: u64 = 30;\n\npub const MAX_RETRIES: u32 = 3;\n\npub const SERVER_URL: &str = \"https://api.example.com\";",
/// "replace": "pub const DEFAULT_TIMEOUT: u64 = 60;\n\npub const MAX_RETRIES: u32 = 3;\n\npub const SERVER_URL: &str = \"https://api.example.com\";"
/// }
/// },
/// {
/// "name": "find-replace-file",
/// "input": {
/// "path": "src/config.rs",
/// "display_description": "Update API endpoint URL",
/// "find": "pub const MAX_RETRIES: u32 = 3;\n\npub const SERVER_URL: &str = \"https://api.example.com\";\n\npub const API_VERSION: &str = \"v1\";",
/// "replace": "pub const MAX_RETRIES: u32 = 3;\n\npub const SERVER_URL: &str = \"https://api.newdomain.com\";\n\npub const API_VERSION: &str = \"v1\";"
/// }
/// }
/// ],
/// "run_tools_concurrently": false
/// }
/// ```
/// </example>
///
/// <example>
/// Searching and analyzing code (concurrent)
///
/// ```json
/// {
/// "invocations": [
/// {
/// "name": "regex-search",
/// "input": {
/// "regex": "impl Database"
/// }
/// },
/// {
/// "name": "path-search",
/// "input": {
/// "glob": "**/*test*.rs"
/// }
/// }
/// ],
/// "run_tools_concurrently": true
/// }
/// ```
/// </example>
///
/// <example>
/// Multi-file refactoring (concurrent)
///
/// ```json
/// {
/// "invocations": [
/// {
/// "name": "find-replace-file",
/// "input": {
/// "path": "src/models/user.rs",
/// "display_description": "Add email field to User struct",
/// "find": "pub struct User {\n pub id: u64,\n pub username: String,\n pub created_at: DateTime<Utc>,\n}",
/// "replace": "pub struct User {\n pub id: u64,\n pub username: String,\n pub email: String,\n pub created_at: DateTime<Utc>,\n}"
/// }
/// },
/// {
/// "name": "find-replace-file",
/// "input": {
/// "path": "src/db/queries.rs",
/// "display_description": "Update user insertion query",
/// "find": "pub async fn insert_user(conn: &mut Connection, user: &User) -> Result<(), DbError> {\n conn.execute(\n \"INSERT INTO users (id, username, created_at) VALUES ($1, $2, $3)\",\n &[&user.id, &user.username, &user.created_at],\n ).await?;\n \n Ok(())\n}",
/// "replace": "pub async fn insert_user(conn: &mut Connection, user: &User) -> Result<(), DbError> {\n conn.execute(\n \"INSERT INTO users (id, username, email, created_at) VALUES ($1, $2, $3, $4)\",\n &[&user.id, &user.username, &user.email, &user.created_at],\n ).await?;\n \n Ok(())\n}"
/// }
/// }
/// ],
/// "run_tools_concurrently": true
/// }
/// ```
/// </example>
pub invocations: Vec<ToolInvocation>,
/// Whether to run the tools in this batch concurrently. If this is false (the default), the tools will run sequentially.
#[serde(default)]
pub run_tools_concurrently: bool,
}
pub struct BatchTool;
impl Tool for BatchTool {
fn name(&self) -> String {
"batch_tool".into()
}
fn needs_confirmation(&self) -> bool {
true
}
fn description(&self) -> String {
include_str!("./batch_tool/description.md").into()
}
fn icon(&self) -> IconName {
IconName::Cog
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value {
json_schema_for::<BatchToolInput>(format)
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<BatchToolInput>(input.clone()) {
Ok(input) => {
let count = input.invocations.len();
let mode = if input.run_tools_concurrently {
"concurrently"
} else {
"sequentially"
};
let first_tool_name = input
.invocations
.first()
.map(|inv| inv.name.clone())
.unwrap_or_default();
let all_same = input
.invocations
.iter()
.all(|invocation| invocation.name == first_tool_name);
if all_same {
format!(
"Run `{}` {} times {}",
first_tool_name,
input.invocations.len(),
mode
)
} else {
format!("Run {} tools {}", count, mode)
}
}
Err(_) => "Batch tools".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
let input = match serde_json::from_value::<BatchToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
};
if input.invocations.is_empty() {
return Task::ready(Err(anyhow!("No tool invocations provided")));
}
let run_tools_concurrently = input.run_tools_concurrently;
let foreground_task = {
let working_set = ToolWorkingSet::default();
let invocations = input.invocations;
let messages = messages.to_vec();
cx.spawn(async move |cx| {
let mut tasks = Vec::new();
let mut tool_names = Vec::new();
for invocation in invocations {
let tool_name = invocation.name.clone();
tool_names.push(tool_name.clone());
let tool = cx
.update(|cx| working_set.tool(&tool_name, cx))
.map_err(|err| {
anyhow!("Failed to look up tool '{}': {}", tool_name, err)
})?;
let Some(tool) = tool else {
return Err(anyhow!("Tool '{}' not found", tool_name));
};
let project = project.clone();
let action_log = action_log.clone();
let messages = messages.clone();
let task = cx
.update(|cx| tool.run(invocation.input, &messages, project, action_log, cx))
.map_err(|err| anyhow!("Failed to start tool '{}': {}", tool_name, err))?;
tasks.push(task);
}
Ok((tasks, tool_names))
})
};
cx.background_spawn(async move {
let (tasks, tool_names) = foreground_task.await?;
let mut results = Vec::with_capacity(tasks.len());
if run_tools_concurrently {
results.extend(join_all(tasks).await)
} else {
for task in tasks {
results.push(task.await);
}
};
let mut formatted_results = String::new();
let mut error_occurred = false;
for (i, result) in results.into_iter().enumerate() {
let tool_name = &tool_names[i];
match result {
Ok(output) => {
formatted_results
.push_str(&format!("Tool '{}' result:\n{}\n\n", tool_name, output));
}
Err(err) => {
error_occurred = true;
formatted_results
.push_str(&format!("Tool '{}' error: {}\n\n", tool_name, err));
}
}
}
if error_occurred {
formatted_results
.push_str("Note: Some tool invocations failed. See individual results above.");
}
Ok(formatted_results.trim().to_string())
})
}
}

View File

@@ -1,389 +0,0 @@
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, ResponseDest, Tool};
use gpui::{App, Entity, Task};
use language::{self, Anchor, Buffer, ToPointUtf16};
use language_model::LanguageModelRequestMessage;
use project::{self, LspAction, Project};
use regex::Regex;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{ops::Range, sync::Arc};
use ui::IconName;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CodeActionToolInput {
/// The relative path to the file containing the text range.
///
/// WARNING: you MUST start this path with one of the project's root directories.
pub path: String,
/// The specific code action to execute.
///
/// If this field is provided, the tool will execute the specified action.
/// If omitted, the tool will list all available code actions for the text range.
///
/// Here are some actions that are commonly supported (but may not be for this particular
/// text range; you can omit this field to list all the actions, if you want to know
/// what your options are, or you can just try an action and if it fails I'll tell you
/// what the available actions were instead):
/// - "quickfix.all" - applies all available quick fixes in the range
/// - "source.organizeImports" - sorts and cleans up import statements
/// - "source.fixAll" - applies all available auto fixes
/// - "refactor.extract" - extracts selected code into a new function or variable
/// - "refactor.inline" - inlines a variable by replacing references with its value
/// - "refactor.rewrite" - general code rewriting operations
/// - "source.addMissingImports" - adds imports for references that lack them
/// - "source.removeUnusedImports" - removes imports that aren't being used
/// - "source.implementInterface" - generates methods required by an interface/trait
/// - "source.generateAccessors" - creates getter/setter methods
/// - "source.convertToAsyncFunction" - converts callback-style code to async/await
///
/// Also, there is a special case: if you specify exactly "textDocument/rename" as the action,
/// then this will rename the symbol to whatever string you specified for the `arguments` field.
pub action: Option<String>,
/// Optional arguments to pass to the code action.
///
/// For rename operations (when action="textDocument/rename"), this should contain the new name.
/// For other code actions, these arguments may be passed to the language server.
pub arguments: Option<serde_json::Value>,
/// The text that comes immediately before the text range in the file.
pub context_before_range: String,
/// The text range. This text must appear in the file right between `context_before_range`
/// and `context_after_range`.
///
/// The file must contain exactly one occurrence of `context_before_range` followed by
/// `text_range` followed by `context_after_range`. If the file contains zero occurrences,
/// or if it contains more than one occurrence, the tool will fail, so it is absolutely
/// critical that you verify ahead of time that the string is unique. You can search
/// the file's contents to verify this ahead of time.
///
/// To make the string more likely to be unique, include a minimum of 1 line of context
/// before the text range, as well as a minimum of 1 line of context after the text range.
/// If these lines of context are not enough to obtain a string that appears only once
/// in the file, then double the number of context lines until the string becomes unique.
/// (Start with 1 line before and 1 line after though, because too much context is
/// needlessly costly.)
///
/// Do not alter the context lines of code in any way, and make sure to preserve all
/// whitespace and indentation for all lines of code. The combined string must be exactly
/// as it appears in the file, or else this tool call will fail.
pub text_range: String,
/// The text that comes immediately after the text range in the file.
pub context_after_range: String,
}
pub struct CodeActionTool;
impl Tool for CodeActionTool {
fn name(&self) -> String {
"code_actions".into()
}
fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
false
}
fn description(&self) -> String {
include_str!("./code_action_tool/description.md").into()
}
fn icon(&self) -> IconName {
IconName::Wand
}
fn input_schema(
&self,
_format: language_model::LanguageModelToolSchemaFormat,
) -> serde_json::Value {
let schema = schemars::schema_for!(CodeActionToolInput);
serde_json::to_value(&schema).unwrap()
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<CodeActionToolInput>(input.clone()) {
Ok(input) => {
if let Some(action) = &input.action {
if action == "textDocument/rename" {
let new_name = match &input.arguments {
Some(serde_json::Value::String(new_name)) => new_name.clone(),
Some(value) => {
if let Ok(new_name) =
serde_json::from_value::<String>(value.clone())
{
new_name
} else {
"invalid name".to_string()
}
}
None => "missing name".to_string(),
};
format!("Rename '{}' to '{}'", input.text_range, new_name)
} else {
format!(
"Execute code action '{}' for '{}'",
action, input.text_range
)
}
} else {
format!("List available code actions for '{}'", input.text_range)
}
}
Err(_) => "Perform code action".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<(ResponseDest, String)>> {
let input = match serde_json::from_value::<CodeActionToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
};
cx.spawn(async move |cx| {
let buffer = {
let project_path = project.read_with(cx, |project, cx| {
project
.find_project_path(&input.path, cx)
.context("Path not found in project")
})??;
project.update(cx, |project, cx| project.open_buffer(project_path, cx))?.await?
};
action_log.update(cx, |action_log, cx| {
action_log.buffer_read(buffer.clone(), cx);
})?;
let range = {
let Some(range) = buffer.read_with(cx, |buffer, _cx| {
find_text_range(&buffer, &input.context_before_range, &input.text_range, &input.context_after_range)
})? else {
return Err(anyhow!(
"Failed to locate the text specified by context_before_range, text_range, and context_after_range. Make sure context_before_range and context_after_range each match exactly once in the file."
));
};
range
};
if let Some(action_type) = &input.action {
// Special-case the `rename` operation
let response = if action_type == "textDocument/rename" {
let Some(new_name) = input.arguments.and_then(|args| serde_json::from_value::<String>(args).ok()) else {
return Err(anyhow!("For rename operations, 'arguments' must be a string containing the new name"));
};
let position = buffer.read_with(cx, |buffer, _| {
range.start.to_point_utf16(&buffer.snapshot())
})?;
project
.update(cx, |project, cx| {
project.perform_rename(buffer.clone(), position, new_name.clone(), cx)
})?
.await?;
format!("Renamed '{}' to '{}'", input.text_range, new_name)
} else {
// Get code actions for the range
let actions = project
.update(cx, |project, cx| {
project.code_actions(&buffer, range.clone(), None, cx)
})?
.await?;
if actions.is_empty() {
return Err(anyhow!("No code actions available for this range"));
}
// Find all matching actions
let regex = match Regex::new(action_type) {
Ok(regex) => regex,
Err(err) => return Err(anyhow!("Invalid regex pattern: {}", err)),
};
let mut matching_actions = actions
.into_iter()
.filter(|action| { regex.is_match(action.lsp_action.title()) });
let Some(action) = matching_actions.next() else {
return Err(anyhow!("No code actions match the pattern: {}", action_type));
};
// There should have been exactly one matching action.
if let Some(second) = matching_actions.next() {
let mut all_matches = vec![action, second];
all_matches.extend(matching_actions);
return Err(anyhow!(
"Pattern '{}' matches multiple code actions: {}",
action_type,
all_matches.into_iter().map(|action| action.lsp_action.title().to_string()).collect::<Vec<_>>().join(", ")
));
}
let title = action.lsp_action.title().to_string();
project
.update(cx, |project, cx| {
project.apply_code_action(buffer.clone(), action, true, cx)
})?
.await?;
format!("Completed code action: {}", title)
};
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
.await?;
action_log.update(cx, |log, cx| {
log.buffer_edited(buffer.clone(), cx)
})?;
Ok((ResponseDest::TextOnly, response))
} else {
// No action specified, so list the available ones.
let (position_start, position_end) = buffer.read_with(cx, |buffer, _| {
let snapshot = buffer.snapshot();
(
range.start.to_point_utf16(&snapshot),
range.end.to_point_utf16(&snapshot)
)
})?;
// Convert position to display coordinates (1-based)
let position_start_display = language::Point {
row: position_start.row + 1,
column: position_start.column + 1,
};
let position_end_display = language::Point {
row: position_end.row + 1,
column: position_end.column + 1,
};
// Get code actions for the range
let actions = project
.update(cx, |project, cx| {
project.code_actions(&buffer, range.clone(), None, cx)
})?
.await?;
let mut response = format!(
"Available code actions for text range '{}' at position {}:{} to {}:{} (UTF-16 coordinates):\n\n",
input.text_range,
position_start_display.row, position_start_display.column,
position_end_display.row, position_end_display.column
);
if actions.is_empty() {
response.push_str("No code actions available for this range.");
} else {
for (i, action) in actions.iter().enumerate() {
let title = match &action.lsp_action {
LspAction::Action(code_action) => code_action.title.as_str(),
LspAction::Command(command) => command.title.as_str(),
LspAction::CodeLens(code_lens) => {
if let Some(cmd) = &code_lens.command {
cmd.title.as_str()
} else {
"Unknown code lens"
}
},
};
let kind = match &action.lsp_action {
LspAction::Action(code_action) => {
if let Some(kind) = &code_action.kind {
kind.as_str()
} else {
"unknown"
}
},
LspAction::Command(_) => "command",
LspAction::CodeLens(_) => "code_lens",
};
response.push_str(&format!("{}. {title} ({kind})\n", i + 1));
}
}
Ok((ResponseDest::TextOnly, response))
}
})
}
}
/// Finds the range of the text in the buffer, if it appears between context_before_range
/// and context_after_range, and if that combined string has one unique result in the buffer.
///
/// If an exact match fails, it tries adding a newline to the end of context_before_range and
/// to the beginning of context_after_range to accommodate line-based context matching.
fn find_text_range(
buffer: &Buffer,
context_before_range: &str,
text_range: &str,
context_after_range: &str,
) -> Option<Range<Anchor>> {
let snapshot = buffer.snapshot();
let text = snapshot.text();
// First try with exact match
let search_string = format!("{context_before_range}{text_range}{context_after_range}");
let mut positions = text.match_indices(&search_string);
let position_result = positions.next();
if let Some(position) = position_result {
// Check if the matched string is unique
if positions.next().is_none() {
let range_start = position.0 + context_before_range.len();
let range_end = range_start + text_range.len();
let range_start_anchor = snapshot.anchor_before(snapshot.offset_to_point(range_start));
let range_end_anchor = snapshot.anchor_before(snapshot.offset_to_point(range_end));
return Some(range_start_anchor..range_end_anchor);
}
}
// If exact match fails or is not unique, try with line-based context
// Add a newline to the end of before context and beginning of after context
let line_based_before = if context_before_range.ends_with('\n') {
context_before_range.to_string()
} else {
format!("{context_before_range}\n")
};
let line_based_after = if context_after_range.starts_with('\n') {
context_after_range.to_string()
} else {
format!("\n{context_after_range}")
};
let line_search_string = format!("{line_based_before}{text_range}{line_based_after}");
let mut line_positions = text.match_indices(&line_search_string);
let line_position = line_positions.next()?;
// The line-based search string must also appear exactly once
if line_positions.next().is_some() {
return None;
}
let line_range_start = line_position.0 + line_based_before.len();
let line_range_end = line_range_start + text_range.len();
let line_range_start_anchor =
snapshot.anchor_before(snapshot.offset_to_point(line_range_start));
let line_range_end_anchor = snapshot.anchor_before(snapshot.offset_to_point(line_range_end));
Some(line_range_start_anchor..line_range_end_anchor)
}

View File

@@ -1,19 +0,0 @@
A tool for applying code actions to specific sections of your code. It uses language servers to provide refactoring capabilities similar to what you'd find in an IDE.
This tool can:
- List all available code actions for a selected text range
- Execute a specific code action on that range
- Rename symbols across your codebase. This tool is the preferred way to rename things, and you should always prefer to rename code symbols using this tool rather than using textual find/replace when both are available.
Use this tool when you want to:
- Discover what code actions are available for a piece of code
- Apply automatic fixes and code transformations
- Rename variables, functions, or other symbols consistently throughout your project
- Clean up imports, implement interfaces, or perform other language-specific operations
- If unsure what actions are available, call the tool without specifying an action to get a list
- For common operations, you can directly specify actions like "quickfix.all" or "source.organizeImports"
- For renaming, use the special "textDocument/rename" action and provide the new name in the arguments field
- Be specific with your text range and context to ensure the tool identifies the correct code location
The tool will automatically save any changes it makes to your files.

View File

@@ -0,0 +1,88 @@
use project::DocumentSymbol;
use regex::Regex;
#[derive(Debug, Clone)]
pub struct Entry {
pub name: String,
pub kind: lsp::SymbolKind,
pub depth: u32,
pub start_line: usize,
pub end_line: usize,
}
/// An iterator that filters document symbols based on a regex pattern.
/// This iterator recursively traverses the document symbol tree, incrementing depth for child symbols.
#[derive(Debug, Clone)]
pub struct CodeSymbolIterator<'a> {
symbols: &'a [DocumentSymbol],
regex: Option<Regex>,
// Stack of (symbol, depth) pairs to process
pending_symbols: Vec<(&'a DocumentSymbol, u32)>,
current_index: usize,
current_depth: u32,
}
impl<'a> CodeSymbolIterator<'a> {
pub fn new(symbols: &'a [DocumentSymbol], regex: Option<Regex>) -> Self {
Self {
symbols,
regex,
pending_symbols: Vec::new(),
current_index: 0,
current_depth: 0,
}
}
}
impl Iterator for CodeSymbolIterator<'_> {
type Item = Entry;
fn next(&mut self) -> Option<Self::Item> {
if let Some((symbol, depth)) = self.pending_symbols.pop() {
for child in symbol.children.iter().rev() {
self.pending_symbols.push((child, depth + 1));
}
return Some(Entry {
name: symbol.name.clone(),
kind: symbol.kind,
depth,
start_line: symbol.range.start.0.row as usize,
end_line: symbol.range.end.0.row as usize,
});
}
while self.current_index < self.symbols.len() {
let regex = self.regex.as_ref();
let symbol = &self.symbols[self.current_index];
self.current_index += 1;
if regex.is_none_or(|regex| regex.is_match(&symbol.name)) {
// Push in reverse order to maintain traversal order
for child in symbol.children.iter().rev() {
self.pending_symbols.push((child, self.current_depth + 1));
}
return Some(Entry {
name: symbol.name.clone(),
kind: symbol.kind,
depth: self.current_depth,
start_line: symbol.range.start.0.row as usize,
end_line: symbol.range.end.0.row as usize,
});
} else {
// Even if parent doesn't match, push children to check them later
for child in symbol.children.iter().rev() {
self.pending_symbols.push((child, self.current_depth + 1));
}
// Check if any pending children match our criteria
if let Some(result) = self.next() {
return Some(result);
}
}
}
None
}
}

View File

@@ -1,21 +1,24 @@
use std::fmt::Write;
use std::fmt::{self, Write};
use std::path::PathBuf;
use std::sync::Arc;
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, ResponseDest, Tool};
use assistant_tool::{ActionLog, Tool};
use collections::IndexMap;
use gpui::{App, AsyncApp, Entity, Task};
use language::{OutlineItem, ParseStatus, Point};
use language::{CodeLabel, Language, LanguageRegistry};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::{Project, Symbol};
use lsp::SymbolKind;
use project::{DocumentSymbol, Project, Symbol};
use regex::{Regex, RegexBuilder};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use ui::IconName;
use util::markdown::MarkdownString;
use crate::code_symbol_iter::{CodeSymbolIterator, Entry};
use crate::schema::json_schema_for;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CodeSymbolsInput {
/// The relative path of the source code file to read and get the symbols for.
@@ -79,7 +82,7 @@ impl Tool for CodeSymbolsTool {
"code_symbols".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self) -> bool {
false
}
@@ -129,7 +132,7 @@ impl Tool for CodeSymbolsTool {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<(ResponseDest, String)>> {
) -> Task<Result<String>> {
let input = match serde_json::from_value::<CodeSymbolsInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -146,19 +149,14 @@ impl Tool for CodeSymbolsTool {
None => None,
};
cx.spawn(async move |cx| {
match input.path {
Some(path) => {
file_outline(project, path, action_log, regex, input.offset, cx).await
}
None => project_symbols(project, regex, input.offset, cx).await,
}
.map(|output| (ResponseDest::TextOnly, output))
cx.spawn(async move |cx| match input.path {
Some(path) => file_outline(project, path, action_log, regex, input.offset, cx).await,
None => project_symbols(project, regex, input.offset, cx).await,
})
}
}
pub async fn file_outline(
async fn file_outline(
project: Entity<Project>,
path: String,
action_log: Entity<ActionLog>,
@@ -182,28 +180,24 @@ pub async fn file_outline(
action_log.buffer_read(buffer.clone(), cx);
})?;
// Wait until the buffer has been fully parsed, so that we can read its outline.
let mut parse_status = buffer.read_with(cx, |buffer, _| buffer.parse_status())?;
while parse_status
.recv()
.await
.map_or(false, |status| status != ParseStatus::Idle)
{}
let symbols = project
.update(cx, |project, cx| project.document_symbols(&buffer, cx))?
.await?;
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
let Some(outline) = snapshot.outline(None) else {
return Err(anyhow!("No outline information available for this file."));
};
if symbols.is_empty() {
return Err(
if buffer.read_with(cx, |buffer, _| buffer.snapshot().is_empty())? {
anyhow!("This file is empty.")
} else {
anyhow!("No outline information available for this file.")
},
);
}
render_outline(
outline
.items
.into_iter()
.map(|item| item.to_point(&snapshot)),
regex,
offset,
)
.await
let language = buffer.read_with(cx, |buffer, _| buffer.language().cloned())?;
let language_registry = project.read_with(cx, |project, _| project.languages().clone())?;
render_outline(&symbols, language, language_registry, regex, offset).await
}
async fn project_symbols(
@@ -298,27 +292,61 @@ async fn project_symbols(
}
async fn render_outline(
items: impl IntoIterator<Item = OutlineItem<Point>>,
symbols: &[DocumentSymbol],
language: Option<Arc<Language>>,
registry: Arc<LanguageRegistry>,
regex: Option<Regex>,
offset: u32,
) -> Result<String> {
const RESULTS_PER_PAGE_USIZE: usize = RESULTS_PER_PAGE as usize;
let entries = CodeSymbolIterator::new(symbols, regex.clone())
.skip(offset as usize)
// Take 1 more than RESULTS_PER_PAGE so we can tell if there are more results.
.take(RESULTS_PER_PAGE_USIZE.saturating_add(1))
.collect::<Vec<Entry>>();
let has_more = entries.len() > RESULTS_PER_PAGE_USIZE;
let mut items = items.into_iter().skip(offset as usize);
// Get language-specific labels, if available
let labels = match &language {
Some(lang) => {
let entries_for_labels: Vec<(String, SymbolKind)> = entries
.iter()
.take(RESULTS_PER_PAGE_USIZE)
.map(|entry| (entry.name.clone(), entry.kind))
.collect();
let entries = items
.by_ref()
.filter(|item| {
regex
.as_ref()
.is_none_or(|regex| regex.is_match(&item.text))
})
.take(RESULTS_PER_PAGE_USIZE)
.collect::<Vec<_>>();
let has_more = items.next().is_some();
let lang_name = lang.name();
if let Some(lsp_adapter) = registry.lsp_adapters(&lang_name).first().cloned() {
lsp_adapter
.labels_for_symbols(&entries_for_labels, lang)
.await
.ok()
} else {
None
}
}
None => None,
};
let mut output = String::new();
let entries_rendered = render_entries(&mut output, entries);
let entries_rendered = match &labels {
Some(label_list) => render_entries(
&mut output,
entries
.into_iter()
.take(RESULTS_PER_PAGE_USIZE)
.zip(label_list.iter())
.map(|(entry, label)| (entry, label.as_ref())),
),
None => render_entries(
&mut output,
entries
.into_iter()
.take(RESULTS_PER_PAGE_USIZE)
.map(|entry| (entry, None)),
),
};
// Calculate pagination information
let page_start = offset + 1;
@@ -344,19 +372,31 @@ async fn render_outline(
Ok(output)
}
fn render_entries(output: &mut String, items: impl IntoIterator<Item = OutlineItem<Point>>) -> u32 {
fn render_entries<'a>(
output: &mut String,
entries: impl IntoIterator<Item = (Entry, Option<&'a CodeLabel>)>,
) -> u32 {
let mut entries_rendered = 0;
for item in items {
for (entry, label) in entries {
// Indent based on depth ("" for level 0, " " for level 1, etc.)
for _ in 0..item.depth {
output.push(' ');
for _ in 0..entry.depth {
output.push_str(" ");
}
match label {
Some(label) => {
output.push_str(label.text());
}
None => {
write_symbol_kind(output, entry.kind).ok();
output.push_str(&entry.name);
}
}
output.push_str(&item.text);
// Add position information - convert to 1-based line numbers for display
let start_line = item.range.start.row + 1;
let end_line = item.range.end.row + 1;
let start_line = entry.start_line + 1;
let end_line = entry.end_line + 1;
if start_line == end_line {
writeln!(output, " [L{}]", start_line).ok();
@@ -368,3 +408,38 @@ fn render_entries(output: &mut String, items: impl IntoIterator<Item = OutlineIt
entries_rendered
}
// We may not have a language server adapter to have language-specific
// ways to translate SymbolKnd into a string. In that situation,
// fall back on some reasonable default strings to render.
fn write_symbol_kind(buf: &mut String, kind: SymbolKind) -> Result<(), fmt::Error> {
match kind {
SymbolKind::FILE => write!(buf, "file "),
SymbolKind::MODULE => write!(buf, "module "),
SymbolKind::NAMESPACE => write!(buf, "namespace "),
SymbolKind::PACKAGE => write!(buf, "package "),
SymbolKind::CLASS => write!(buf, "class "),
SymbolKind::METHOD => write!(buf, "method "),
SymbolKind::PROPERTY => write!(buf, "property "),
SymbolKind::FIELD => write!(buf, "field "),
SymbolKind::CONSTRUCTOR => write!(buf, "constructor "),
SymbolKind::ENUM => write!(buf, "enum "),
SymbolKind::INTERFACE => write!(buf, "interface "),
SymbolKind::FUNCTION => write!(buf, "function "),
SymbolKind::VARIABLE => write!(buf, "variable "),
SymbolKind::CONSTANT => write!(buf, "constant "),
SymbolKind::STRING => write!(buf, "string "),
SymbolKind::NUMBER => write!(buf, "number "),
SymbolKind::BOOLEAN => write!(buf, "boolean "),
SymbolKind::ARRAY => write!(buf, "array "),
SymbolKind::OBJECT => write!(buf, "object "),
SymbolKind::KEY => write!(buf, "key "),
SymbolKind::NULL => write!(buf, "null "),
SymbolKind::ENUM_MEMBER => write!(buf, "enum member "),
SymbolKind::STRUCT => write!(buf, "struct "),
SymbolKind::EVENT => write!(buf, "event "),
SymbolKind::OPERATOR => write!(buf, "operator "),
SymbolKind::TYPE_PARAMETER => write!(buf, "type parameter "),
_ => Ok(()),
}
}

View File

@@ -1,6 +1,5 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::ResponseDest;
use assistant_tool::{ActionLog, Tool};
use gpui::{App, AppContext, Entity, Task};
use language_model::LanguageModelRequestMessage;
@@ -44,7 +43,7 @@ impl Tool for CopyPathTool {
"copy_path".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self) -> bool {
true
}
@@ -78,7 +77,7 @@ impl Tool for CopyPathTool {
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<(ResponseDest, String)>> {
) -> Task<Result<String>> {
let input = match serde_json::from_value::<CopyPathToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -106,9 +105,9 @@ impl Tool for CopyPathTool {
cx.background_spawn(async move {
match copy_task.await {
Ok(_) => Ok((
ResponseDest::TextOnly,
format!("Copied {} to {}", input.source_path, input.destination_path),
Ok(_) => Ok(format!(
"Copied {} to {}",
input.source_path, input.destination_path
)),
Err(err) => Err(anyhow!(
"Failed to copy {} to {}: {}",

View File

@@ -1,6 +1,5 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::ResponseDest;
use assistant_tool::{ActionLog, Tool};
use gpui::{App, Entity, Task};
use language_model::LanguageModelRequestMessage;
@@ -34,7 +33,7 @@ impl Tool for CreateDirectoryTool {
"create_directory".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self) -> bool {
true
}
@@ -69,7 +68,7 @@ impl Tool for CreateDirectoryTool {
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<(ResponseDest, String)>> {
) -> Task<Result<String>> {
let input = match serde_json::from_value::<CreateDirectoryToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -88,10 +87,7 @@ impl Tool for CreateDirectoryTool {
.await
.map_err(|err| anyhow!("Unable to create directory {destination_path}: {err}"))?;
Ok((
ResponseDest::TextOnly,
format!("Created directory {destination_path}"),
))
Ok(format!("Created directory {destination_path}"))
})
}
}

View File

@@ -1,6 +1,5 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::ResponseDest;
use assistant_tool::{ActionLog, Tool};
use gpui::{App, Entity, Task};
use language_model::LanguageModelRequestMessage;
@@ -25,6 +24,13 @@ pub struct CreateFileToolInput {
/// You can create a new file by providing a path of "directory1/new_file.txt"
/// </example>
pub path: String,
/// The text contents of the file to create.
///
/// <example>
/// To create a file with the text "Hello, World!", provide contents of "Hello, World!"
/// </example>
pub contents: String,
}
pub struct CreateFileTool;
@@ -34,7 +40,7 @@ impl Tool for CreateFileTool {
"create_file".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self) -> bool {
false
}
@@ -67,7 +73,7 @@ impl Tool for CreateFileTool {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<(ResponseDest, String)>> {
) -> Task<Result<String>> {
let input = match serde_json::from_value::<CreateFileToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -76,6 +82,7 @@ impl Tool for CreateFileTool {
Some(project_path) => project_path,
None => return Task::ready(Err(anyhow!("Path to create was outside the project"))),
};
let contents: Arc<str> = input.contents.as_str().into();
let destination_path: Arc<str> = input.path.as_str().into();
cx.spawn(async move |cx| {
@@ -86,6 +93,7 @@ impl Tool for CreateFileTool {
.await
.map_err(|err| anyhow!("Unable to open buffer for {destination_path}: {err}"))?;
cx.update(|cx| {
buffer.update(cx, |buffer, cx| buffer.set_text(contents, cx));
action_log.update(cx, |action_log, cx| {
action_log.will_create_buffer(buffer.clone(), cx)
});
@@ -96,7 +104,7 @@ impl Tool for CreateFileTool {
.await
.map_err(|err| anyhow!("Unable to save buffer for {destination_path}: {err}"))?;
Ok((ResponseDest::File { path: destination_path.clone() }, format!("Created file {destination_path} - next, its exact contents will become your response:")))
Ok(format!("Created file {destination_path}"))
})
}
}

View File

@@ -1,5 +1,3 @@
Creates a new file at the specified path within the project. The entire message you respond with will then be streamed into the file.
Creates a new file at the specified path within the project, containing the given text content. Returns confirmation that the file was created.
This tool is the most efficient way to create new files within the project, so it should always be chosen whenever it's necessary to create a new file in the project with specific text content, or whenever a file in the project needs such a drastic change that you would prefer to replace the entire thing instead of making individual edits. This tool should not be used when making changes to parts of an existing file but not all of it.
Note that *all* the text you respond with will be streamed into the file, so you must ONLY respond with the contents of the file.
This tool is the most efficient way to create new files within the project, so it should always be chosen whenever it's necessary to create a new file in the project with specific text content, or whenever a file in the project needs such a drastic change that you would prefer to replace the entire thing instead of making individual edits. This tool should not be used when making changes to parts of an existing file but not all of it. In those cases, it's better to use another approach to edit the file.

View File

@@ -1,6 +1,6 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, ResponseDest, Tool};
use assistant_tool::{ActionLog, Tool};
use futures::{SinkExt, StreamExt, channel::mpsc};
use gpui::{App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -33,7 +33,7 @@ impl Tool for DeletePathTool {
"delete_path".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self) -> bool {
true
}
@@ -63,7 +63,7 @@ impl Tool for DeletePathTool {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<(ResponseDest, String)>> {
) -> Task<Result<String>> {
let path_str = match serde_json::from_value::<DeletePathToolInput>(input) {
Ok(input) => input.path,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -124,7 +124,7 @@ impl Tool for DeletePathTool {
match delete {
Some(deletion_task) => match deletion_task.await {
Ok(()) => Ok((ResponseDest::TextOnly, format!("Deleted {path_str}"))),
Ok(()) => Ok(format!("Deleted {path_str}")),
Err(err) => Err(anyhow!("Failed to delete {path_str}: {err}")),
},
None => Err(anyhow!(

View File

@@ -1,6 +1,6 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, ResponseDest, Tool};
use assistant_tool::{ActionLog, Tool};
use gpui::{App, Entity, Task};
use language::{DiagnosticSeverity, OffsetRangeExt};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -46,7 +46,7 @@ impl Tool for DiagnosticsTool {
"diagnostics".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self) -> bool {
false
}
@@ -83,7 +83,7 @@ impl Tool for DiagnosticsTool {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<(ResponseDest, String)>> {
) -> Task<Result<String>> {
match serde_json::from_value::<DiagnosticsToolInput>(input)
.ok()
.and_then(|input| input.path)
@@ -119,14 +119,11 @@ impl Tool for DiagnosticsTool {
)?;
}
Ok((
ResponseDest::TextOnly,
if output.is_empty() {
"File doesn't have errors or warnings!".to_string()
} else {
output
},
))
if output.is_empty() {
Ok("File doesn't have errors or warnings!".to_string())
} else {
Ok(output)
}
})
}
_ => {
@@ -157,14 +154,11 @@ impl Tool for DiagnosticsTool {
action_log.checked_project_diagnostics();
});
Task::ready(Ok((
ResponseDest::TextOnly,
if has_diagnostics {
output
} else {
"No errors or warnings found in the project.".to_string()
},
)))
if has_diagnostics {
Task::ready(Ok(output))
} else {
Task::ready(Ok("No errors or warnings found in the project.".to_string()))
}
}
}
}

View File

@@ -0,0 +1,559 @@
mod edit_action;
pub mod log;
use crate::replace::{replace_exact, replace_with_flexible_indent};
use crate::schema::json_schema_for;
use anyhow::{Context, Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use collections::HashSet;
use edit_action::{EditAction, EditActionParser, edit_model_prompt};
use futures::{SinkExt, StreamExt, channel::mpsc};
use gpui::{App, AppContext, AsyncApp, Entity, Task};
use language_model::{ConfiguredModel, LanguageModelToolSchemaFormat};
use language_model::{
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, MessageContent, Role,
};
use log::{EditToolLog, EditToolRequestId};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt::Write;
use std::sync::Arc;
use ui::IconName;
use util::ResultExt;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct EditFilesToolInput {
/// High-level edit instructions. These will be interpreted by a smaller
/// model, so explain the changes you want that model to make and which
/// file paths need changing. The description should be concise and clear.
///
/// WARNING: When specifying which file paths need changing, you MUST
/// start each path with one of the project's root directories.
///
/// WARNING: NEVER include code blocks or snippets in edit instructions.
/// Only provide natural language descriptions of the changes needed! The tool will
/// reject any instructions that contain code blocks or snippets.
///
/// The following examples assume we have two root directories in the project:
/// - root-1
/// - root-2
///
/// <example>
/// If you want to introduce a new quit function to kill the process, your
/// instructions should be: "Add a new `quit` function to
/// `root-1/src/main.rs` to kill the process".
///
/// Notice how the file path starts with root-1. Without that, the path
/// would be ambiguous and the call would fail!
/// </example>
///
/// <example>
/// If you want to change documentation to always start with a capital
/// letter, your instructions should be: "In `root-2/db.js`,
/// `root-2/inMemory.js` and `root-2/sql.js`, change all the documentation
/// to start with a capital letter".
///
/// Notice how we never specify code snippets in the instructions!
/// </example>
pub edit_instructions: String,
/// A user-friendly description of what changes are being made.
/// This will be shown to the user in the UI to describe the edit operation. The screen real estate for this UI will be extremely
/// constrained, so make the description extremely terse.
///
/// <example>
/// For fixing a broken authentication system:
/// "Fix auth bug in login flow"
/// </example>
///
/// <example>
/// For adding unit tests to a module:
/// "Add tests for user profile logic"
/// </example>
pub display_description: String,
}
pub struct EditFilesTool;
impl Tool for EditFilesTool {
fn name(&self) -> String {
"edit_files".into()
}
fn needs_confirmation(&self) -> bool {
false
}
fn description(&self) -> String {
include_str!("./edit_files_tool/description.md").into()
}
fn icon(&self) -> IconName {
IconName::Pencil
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value {
json_schema_for::<EditFilesToolInput>(format)
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<EditFilesToolInput>(input.clone()) {
Ok(input) => input.display_description,
Err(_) => "Edit files".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
let input = match serde_json::from_value::<EditFilesToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
};
match EditToolLog::try_global(cx) {
Some(log) => {
let req_id = log.update(cx, |log, cx| {
log.new_request(input.edit_instructions.clone(), cx)
});
let task = EditToolRequest::new(
input,
messages,
project,
action_log,
Some((log.clone(), req_id)),
cx,
);
cx.spawn(async move |cx| {
let result = task.await;
let str_result = match &result {
Ok(out) => Ok(out.clone()),
Err(err) => Err(err.to_string()),
};
log.update(cx, |log, cx| log.set_tool_output(req_id, str_result, cx))
.log_err();
result
})
}
None => EditToolRequest::new(input, messages, project, action_log, None, cx),
}
}
}
struct EditToolRequest {
parser: EditActionParser,
editor_response: EditorResponse,
project: Entity<Project>,
action_log: Entity<ActionLog>,
tool_log: Option<(Entity<EditToolLog>, EditToolRequestId)>,
}
enum EditorResponse {
/// The editor model hasn't produced any actions yet.
/// If we don't have any by the end, we'll return its message to the architect model.
Message(String),
/// The editor model produced at least one action.
Actions {
applied: Vec<AppliedAction>,
search_errors: Vec<SearchError>,
},
}
struct AppliedAction {
source: String,
buffer: Entity<language::Buffer>,
}
#[derive(Debug)]
enum DiffResult {
Diff(language::Diff),
SearchError(SearchError),
}
#[derive(Debug)]
enum SearchError {
NoMatch {
file_path: String,
search: String,
},
EmptyBuffer {
file_path: String,
search: String,
exists: bool,
},
}
impl EditToolRequest {
fn new(
input: EditFilesToolInput,
messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
tool_log: Option<(Entity<EditToolLog>, EditToolRequestId)>,
cx: &mut App,
) -> Task<Result<String>> {
let model_registry = LanguageModelRegistry::read_global(cx);
let Some(ConfiguredModel { model, .. }) = model_registry.default_model() else {
return Task::ready(Err(anyhow!("No model configured")));
};
let mut messages = messages.to_vec();
// Remove the last tool use (this run) to prevent an invalid request
'outer: for message in messages.iter_mut().rev() {
for (index, content) in message.content.iter().enumerate().rev() {
match content {
MessageContent::ToolUse(_) => {
message.content.remove(index);
break 'outer;
}
MessageContent::ToolResult(_) => {
// If we find any tool results before a tool use, the request is already valid
break 'outer;
}
MessageContent::Text(_) | MessageContent::Image(_) => {}
}
}
}
messages.push(LanguageModelRequestMessage {
role: Role::User,
content: vec![edit_model_prompt().into(), input.edit_instructions.into()],
cache: false,
});
cx.spawn(async move |cx| {
let llm_request = LanguageModelRequest {
messages,
tools: vec![],
stop: vec![],
temperature: Some(0.0),
};
let (mut tx, mut rx) = mpsc::channel::<String>(32);
let stream = model.stream_completion_text(llm_request, &cx);
let reader_task = cx.background_spawn(async move {
let mut chunks = stream.await?;
while let Some(chunk) = chunks.stream.next().await {
if let Some(chunk) = chunk.log_err() {
// we don't process here because the API fails
// if we take too long between reads
tx.send(chunk).await?
}
}
tx.close().await?;
anyhow::Ok(())
});
let mut request = Self {
parser: EditActionParser::new(),
editor_response: EditorResponse::Message(String::with_capacity(256)),
action_log,
project,
tool_log,
};
while let Some(chunk) = rx.next().await {
request.process_response_chunk(&chunk, cx).await?;
}
reader_task.await?;
request.finalize(cx).await
})
}
async fn process_response_chunk(&mut self, chunk: &str, cx: &mut AsyncApp) -> Result<()> {
let new_actions = self.parser.parse_chunk(chunk);
if let EditorResponse::Message(ref mut message) = self.editor_response {
if new_actions.is_empty() {
message.push_str(chunk);
}
}
if let Some((ref log, req_id)) = self.tool_log {
log.update(cx, |log, cx| {
log.push_editor_response_chunk(req_id, chunk, &new_actions, cx)
})
.log_err();
}
for action in new_actions {
self.apply_action(action, cx).await?;
}
Ok(())
}
async fn apply_action(
&mut self,
(action, source): (EditAction, String),
cx: &mut AsyncApp,
) -> Result<()> {
let project_path = self.project.read_with(cx, |project, cx| {
project
.find_project_path(action.file_path(), cx)
.context("Path not found in project")
})??;
let buffer = self
.project
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
.await?;
let result = match action {
EditAction::Replace {
old,
new,
file_path,
} => {
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
cx.background_executor()
.spawn(Self::replace_diff(old, new, file_path, snapshot))
.await
}
EditAction::Write { content, .. } => Ok(DiffResult::Diff(
buffer
.read_with(cx, |buffer, cx| buffer.diff(content, cx))?
.await,
)),
}?;
match result {
DiffResult::SearchError(error) => {
self.push_search_error(error);
}
DiffResult::Diff(diff) => {
cx.update(|cx| {
self.action_log
.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
buffer.update(cx, |buffer, cx| {
buffer.finalize_last_transaction();
buffer.apply_diff(diff, cx);
buffer.finalize_last_transaction();
});
self.action_log
.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
})?;
self.push_applied_action(AppliedAction { source, buffer });
}
}
anyhow::Ok(())
}
fn push_search_error(&mut self, error: SearchError) {
match &mut self.editor_response {
EditorResponse::Message(_) => {
self.editor_response = EditorResponse::Actions {
applied: Vec::new(),
search_errors: vec![error],
};
}
EditorResponse::Actions { search_errors, .. } => {
search_errors.push(error);
}
}
}
fn push_applied_action(&mut self, action: AppliedAction) {
match &mut self.editor_response {
EditorResponse::Message(_) => {
self.editor_response = EditorResponse::Actions {
applied: vec![action],
search_errors: Vec::new(),
};
}
EditorResponse::Actions { applied, .. } => {
applied.push(action);
}
}
}
async fn replace_diff(
old: String,
new: String,
file_path: std::path::PathBuf,
snapshot: language::BufferSnapshot,
) -> Result<DiffResult> {
if snapshot.is_empty() {
let exists = snapshot
.file()
.map_or(false, |file| file.disk_state().exists());
let error = SearchError::EmptyBuffer {
file_path: file_path.display().to_string(),
exists,
search: old,
};
return Ok(DiffResult::SearchError(error));
}
let replace_result =
// Try to match exactly
replace_exact(&old, &new, &snapshot)
.await
// If that fails, try being flexible about indentation
.or_else(|| replace_with_flexible_indent(&old, &new, &snapshot));
let Some(diff) = replace_result else {
let error = SearchError::NoMatch {
search: old,
file_path: file_path.display().to_string(),
};
return Ok(DiffResult::SearchError(error));
};
Ok(DiffResult::Diff(diff))
}
async fn finalize(self, cx: &mut AsyncApp) -> Result<String> {
match self.editor_response {
EditorResponse::Message(message) => Err(anyhow!(
"No edits were applied! You might need to provide more context.\n\n{}",
message
)),
EditorResponse::Actions {
applied,
search_errors,
} => {
let mut output = String::with_capacity(1024);
let parse_errors = self.parser.errors();
let has_errors = !search_errors.is_empty() || !parse_errors.is_empty();
if has_errors {
let error_count = search_errors.len() + parse_errors.len();
if applied.is_empty() {
writeln!(
&mut output,
"{} errors occurred! No edits were applied.",
error_count,
)?;
} else {
writeln!(
&mut output,
"{} errors occurred, but {} edits were correctly applied.",
error_count,
applied.len(),
)?;
writeln!(
&mut output,
"# {} SEARCH/REPLACE block(s) applied:\n\nDo not re-send these since they are already applied!\n",
applied.len()
)?;
}
} else {
write!(
&mut output,
"Successfully applied! Here's a list of applied edits:"
)?;
}
let mut changed_buffers = HashSet::default();
for action in applied {
changed_buffers.insert(action.buffer.clone());
write!(&mut output, "\n\n{}", action.source)?;
}
for buffer in &changed_buffers {
self.project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
.await?;
}
if !search_errors.is_empty() {
writeln!(
&mut output,
"\n\n## {} SEARCH/REPLACE block(s) failed to match:\n",
search_errors.len()
)?;
for error in search_errors {
match error {
SearchError::NoMatch { file_path, search } => {
writeln!(
&mut output,
"### No exact match in: `{}`\n```\n{}\n```\n",
file_path, search,
)?;
}
SearchError::EmptyBuffer {
file_path,
exists: true,
search,
} => {
writeln!(
&mut output,
"### No match because `{}` is empty:\n```\n{}\n```\n",
file_path, search,
)?;
}
SearchError::EmptyBuffer {
file_path,
exists: false,
search,
} => {
writeln!(
&mut output,
"### No match because `{}` does not exist:\n```\n{}\n```\n",
file_path, search,
)?;
}
}
}
write!(
&mut output,
"The SEARCH section must exactly match an existing block of lines including all white \
space, comments, indentation, docstrings, etc."
)?;
}
if !parse_errors.is_empty() {
writeln!(
&mut output,
"\n\n## {} SEARCH/REPLACE blocks failed to parse:",
parse_errors.len()
)?;
for error in parse_errors {
writeln!(&mut output, "- {}", error)?;
}
}
if has_errors {
writeln!(
&mut output,
"\n\nYou can fix errors by running the tool again. You can include instructions, \
but errors are part of the conversation so you don't need to repeat them.",
)?;
Err(anyhow!(output))
} else {
Ok(output)
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
Edit files in the current project by specifying instructions in natural language.
IMPORTANT NOTE: If there is a find-replace tool, use that instead of this tool! This tool is only to be used as a fallback in case that tool is unavailable. Always prefer that tool if it is available.
When using this tool, you should suggest one coherent edit that can be made to the codebase.
When the set of edits you want to make is large or complex, feel free to invoke this tool multiple times, each time focusing on a specific change you wanna make.
You should use this tool when you want to edit a subset of a file's contents, but not the entire file. You should not use this tool when you want to replace the entire contents of a file with completely different contents, and you absolutely must never use this tool to create new files from scratch. If you ever consider using this tool to create a new file from scratch, for any reason, instead you must reconsider and choose a different approach.
DO NOT call this tool until the code to be edited appears in the conversation! You must use the `read-files` tool or ask the user to add it to context first.

View File

@@ -0,0 +1,967 @@
use std::{
mem::take,
ops::Range,
path::{Path, PathBuf},
};
use util::ResultExt;
/// Represents an edit action to be performed on a file.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EditAction {
/// Replace specific content in a file with new content
Replace {
file_path: PathBuf,
old: String,
new: String,
},
/// Write content to a file (create or overwrite)
Write { file_path: PathBuf, content: String },
}
impl EditAction {
pub fn file_path(&self) -> &Path {
match self {
EditAction::Replace { file_path, .. } => file_path,
EditAction::Write { file_path, .. } => file_path,
}
}
}
/// Parses edit actions from an LLM response.
/// See system.md for more details on the format.
#[derive(Debug)]
pub struct EditActionParser {
state: State,
line: usize,
column: usize,
marker_ix: usize,
action_source: Vec<u8>,
fence_start_offset: usize,
block_range: Range<usize>,
old_range: Range<usize>,
new_range: Range<usize>,
errors: Vec<ParseError>,
}
#[derive(Debug, PartialEq, Eq)]
enum State {
/// Anywhere outside an action
Default,
/// After opening ```, in optional language tag
OpenFence,
/// In SEARCH marker
SearchMarker,
/// In search block or divider
SearchBlock,
/// In replace block or REPLACE marker
ReplaceBlock,
/// In closing ```
CloseFence,
}
/// used to avoid having source code that looks like git-conflict markers
macro_rules! marker_sym {
($char:expr) => {
concat!($char, $char, $char, $char, $char, $char, $char)
};
}
const SEARCH_MARKER: &str = concat!(marker_sym!('<'), " SEARCH");
const DIVIDER: &str = marker_sym!('=');
const NL_DIVIDER: &str = concat!("\n", marker_sym!('='));
const REPLACE_MARKER: &str = concat!(marker_sym!('>'), " REPLACE");
const NL_REPLACE_MARKER: &str = concat!("\n", marker_sym!('>'), " REPLACE");
const FENCE: &str = "```";
impl EditActionParser {
/// Creates a new `EditActionParser`
pub fn new() -> Self {
Self {
state: State::Default,
line: 1,
column: 0,
action_source: Vec::new(),
fence_start_offset: 0,
marker_ix: 0,
block_range: Range::default(),
old_range: Range::default(),
new_range: Range::default(),
errors: Vec::new(),
}
}
/// Processes a chunk of input text and returns any completed edit actions.
///
/// This method can be called repeatedly with fragments of input. The parser
/// maintains its state between calls, allowing you to process streaming input
/// as it becomes available. Actions are only inserted once they are fully parsed.
///
/// If a block fails to parse, it will simply be skipped and an error will be recorded.
/// All errors can be accessed through the `EditActionsParser::errors` method.
pub fn parse_chunk(&mut self, input: &str) -> Vec<(EditAction, String)> {
use State::*;
let mut actions = Vec::new();
for byte in input.bytes() {
// Update line and column tracking
if byte == b'\n' {
self.line += 1;
self.column = 0;
} else {
self.column += 1;
}
let action_offset = self.action_source.len();
match &self.state {
Default => match self.match_marker(byte, FENCE, false) {
MarkerMatch::Complete => {
self.fence_start_offset = action_offset + 1 - FENCE.len();
self.to_state(OpenFence);
}
MarkerMatch::Partial => {}
MarkerMatch::None => {
if self.marker_ix > 0 {
self.marker_ix = 0;
} else if self.action_source.ends_with(b"\n") {
self.action_source.clear();
}
}
},
OpenFence => {
// skip language tag
if byte == b'\n' {
self.to_state(SearchMarker);
}
}
SearchMarker => {
if self.expect_marker(byte, SEARCH_MARKER, true) {
self.to_state(SearchBlock);
}
}
SearchBlock => {
if self.extend_block_range(byte, DIVIDER, NL_DIVIDER) {
self.old_range = take(&mut self.block_range);
self.to_state(ReplaceBlock);
}
}
ReplaceBlock => {
if self.extend_block_range(byte, REPLACE_MARKER, NL_REPLACE_MARKER) {
self.new_range = take(&mut self.block_range);
self.to_state(CloseFence);
}
}
CloseFence => {
if self.expect_marker(byte, FENCE, false) {
self.action_source.push(byte);
if let Some(action) = self.action() {
actions.push(action);
}
self.errors();
self.reset();
continue;
}
}
};
self.action_source.push(byte);
}
actions
}
/// Returns a reference to the errors encountered during parsing.
pub fn errors(&self) -> &[ParseError] {
&self.errors
}
fn action(&mut self) -> Option<(EditAction, String)> {
let old_range = take(&mut self.old_range);
let new_range = take(&mut self.new_range);
let action_source = take(&mut self.action_source);
let action_source = String::from_utf8(action_source).log_err()?;
let mut file_path_bytes = action_source[..self.fence_start_offset].to_owned();
if file_path_bytes.ends_with("\n") {
file_path_bytes.pop();
if file_path_bytes.ends_with("\r") {
file_path_bytes.pop();
}
}
let file_path = PathBuf::from(file_path_bytes);
if old_range.is_empty() {
return Some((
EditAction::Write {
file_path,
content: action_source[new_range].to_owned(),
},
action_source,
));
}
let old = action_source[old_range].to_owned();
let new = action_source[new_range].to_owned();
let action = EditAction::Replace {
file_path,
old,
new,
};
Some((action, action_source))
}
fn to_state(&mut self, state: State) {
self.state = state;
self.marker_ix = 0;
}
fn reset(&mut self) {
self.action_source.clear();
self.block_range = Range::default();
self.old_range = Range::default();
self.new_range = Range::default();
self.fence_start_offset = 0;
self.marker_ix = 0;
self.to_state(State::Default);
}
fn expect_marker(&mut self, byte: u8, marker: &'static str, trailing_newline: bool) -> bool {
match self.match_marker(byte, marker, trailing_newline) {
MarkerMatch::Complete => true,
MarkerMatch::Partial => false,
MarkerMatch::None => {
self.errors.push(ParseError {
line: self.line,
column: self.column,
expected: marker,
found: byte,
});
self.reset();
false
}
}
}
fn extend_block_range(&mut self, byte: u8, marker: &str, nl_marker: &str) -> bool {
let marker = if self.block_range.is_empty() {
// do not require another newline if block is empty
marker
} else {
nl_marker
};
let offset = self.action_source.len();
match self.match_marker(byte, marker, true) {
MarkerMatch::Complete => {
if self.action_source[self.block_range.clone()].ends_with(b"\r") {
self.block_range.end -= 1;
}
true
}
MarkerMatch::Partial => false,
MarkerMatch::None => {
if self.marker_ix > 0 {
self.marker_ix = 0;
self.block_range.end = offset;
// The beginning of marker might match current byte
match self.match_marker(byte, marker, true) {
MarkerMatch::Complete => return true,
MarkerMatch::Partial => return false,
MarkerMatch::None => { /* no match, keep collecting */ }
}
}
if self.block_range.is_empty() {
self.block_range.start = offset;
}
self.block_range.end = offset + 1;
false
}
}
}
fn match_marker(&mut self, byte: u8, marker: &str, trailing_newline: bool) -> MarkerMatch {
if trailing_newline && self.marker_ix >= marker.len() {
if byte == b'\n' {
MarkerMatch::Complete
} else if byte == b'\r' {
MarkerMatch::Partial
} else {
MarkerMatch::None
}
} else if byte == marker.as_bytes()[self.marker_ix] {
self.marker_ix += 1;
if self.marker_ix < marker.len() || trailing_newline {
MarkerMatch::Partial
} else {
MarkerMatch::Complete
}
} else {
MarkerMatch::None
}
}
}
#[derive(Debug)]
enum MarkerMatch {
None,
Partial,
Complete,
}
#[derive(Debug, PartialEq, Eq)]
pub struct ParseError {
line: usize,
column: usize,
expected: &'static str,
found: u8,
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"input:{}:{}: Expected marker {:?}, found {:?}",
self.line, self.column, self.expected, self.found as char
)
}
}
pub fn edit_model_prompt() -> String {
include_str!("edit_prompt.md")
.to_string()
.replace("{{SEARCH_MARKER}}", SEARCH_MARKER)
.replace("{{DIVIDER}}", DIVIDER)
.replace("{{REPLACE_MARKER}}", REPLACE_MARKER)
}
#[cfg(test)]
mod tests {
use super::*;
use rand::prelude::*;
use util::line_endings;
const WRONG_MARKER: &str = concat!(marker_sym!('<'), " WRONG_MARKER");
#[test]
fn test_simple_edit_action() {
// Construct test input using format with multiline string literals
let input = format!(
r#"src/main.rs
```
{}
fn original() {{}}
{}
fn replacement() {{}}
{}
```
"#,
SEARCH_MARKER, DIVIDER, REPLACE_MARKER
);
let mut parser = EditActionParser::new();
let actions = parser.parse_chunk(&input);
assert_no_errors(&parser);
assert_eq!(actions.len(), 1);
assert_eq!(
actions[0].0,
EditAction::Replace {
file_path: PathBuf::from("src/main.rs"),
old: "fn original() {}".to_string(),
new: "fn replacement() {}".to_string(),
}
);
}
#[test]
fn test_with_language_tag() {
// Construct test input using format with multiline string literals
let input = format!(
r#"src/main.rs
```rust
{}
fn original() {{}}
{}
fn replacement() {{}}
{}
```
"#,
SEARCH_MARKER, DIVIDER, REPLACE_MARKER
);
let mut parser = EditActionParser::new();
let actions = parser.parse_chunk(&input);
assert_no_errors(&parser);
assert_eq!(actions.len(), 1);
assert_eq!(
actions[0].0,
EditAction::Replace {
file_path: PathBuf::from("src/main.rs"),
old: "fn original() {}".to_string(),
new: "fn replacement() {}".to_string(),
}
);
}
#[test]
fn test_with_surrounding_text() {
// Construct test input using format with multiline string literals
let input = format!(
r#"Here's a modification I'd like to make to the file:
src/main.rs
```rust
{}
fn original() {{}}
{}
fn replacement() {{}}
{}
```
This change makes the function better.
"#,
SEARCH_MARKER, DIVIDER, REPLACE_MARKER
);
let mut parser = EditActionParser::new();
let actions = parser.parse_chunk(&input);
assert_no_errors(&parser);
assert_eq!(actions.len(), 1);
assert_eq!(
actions[0].0,
EditAction::Replace {
file_path: PathBuf::from("src/main.rs"),
old: "fn original() {}".to_string(),
new: "fn replacement() {}".to_string(),
}
);
}
#[test]
fn test_multiple_edit_actions() {
// Construct test input using format with multiline string literals
let input = format!(
r#"First change:
src/main.rs
```
{}
fn original() {{}}
{}
fn replacement() {{}}
{}
```
Second change:
src/utils.rs
```rust
{}
fn old_util() -> bool {{ false }}
{}
fn new_util() -> bool {{ true }}
{}
```
"#,
SEARCH_MARKER, DIVIDER, REPLACE_MARKER, SEARCH_MARKER, DIVIDER, REPLACE_MARKER
);
let mut parser = EditActionParser::new();
let actions = parser.parse_chunk(&input);
assert_no_errors(&parser);
assert_eq!(actions.len(), 2);
let (action, _) = &actions[0];
assert_eq!(
action,
&EditAction::Replace {
file_path: PathBuf::from("src/main.rs"),
old: "fn original() {}".to_string(),
new: "fn replacement() {}".to_string(),
}
);
let (action2, _) = &actions[1];
assert_eq!(
action2,
&EditAction::Replace {
file_path: PathBuf::from("src/utils.rs"),
old: "fn old_util() -> bool { false }".to_string(),
new: "fn new_util() -> bool { true }".to_string(),
}
);
}
#[test]
fn test_multiline() {
// Construct test input using format with multiline string literals
let input = format!(
r#"src/main.rs
```rust
{}
fn original() {{
println!("This is the original function");
let x = 42;
if x > 0 {{
println!("Positive number");
}}
}}
{}
fn replacement() {{
println!("This is the replacement function");
let x = 100;
if x > 50 {{
println!("Large number");
}} else {{
println!("Small number");
}}
}}
{}
```
"#,
SEARCH_MARKER, DIVIDER, REPLACE_MARKER
);
let mut parser = EditActionParser::new();
let actions = parser.parse_chunk(&input);
assert_no_errors(&parser);
assert_eq!(actions.len(), 1);
let (action, _) = &actions[0];
assert_eq!(
action,
&EditAction::Replace {
file_path: PathBuf::from("src/main.rs"),
old: "fn original() {\n println!(\"This is the original function\");\n let x = 42;\n if x > 0 {\n println!(\"Positive number\");\n }\n}".to_string(),
new: "fn replacement() {\n println!(\"This is the replacement function\");\n let x = 100;\n if x > 50 {\n println!(\"Large number\");\n } else {\n println!(\"Small number\");\n }\n}".to_string(),
}
);
}
#[test]
fn test_write_action() {
// Construct test input using format with multiline string literals
let input = format!(
r#"Create a new main.rs file:
src/main.rs
```rust
{}
{}
fn new_function() {{
println!("This function is being added");
}}
{}
```
"#,
SEARCH_MARKER, DIVIDER, REPLACE_MARKER
);
let mut parser = EditActionParser::new();
let actions = parser.parse_chunk(&input);
assert_no_errors(&parser);
assert_eq!(actions.len(), 1);
assert_eq!(
actions[0].0,
EditAction::Write {
file_path: PathBuf::from("src/main.rs"),
content: "fn new_function() {\n println!(\"This function is being added\");\n}"
.to_string(),
}
);
}
#[test]
fn test_empty_replace() {
// Construct test input using format with multiline string literals
let input = format!(
r#"src/main.rs
```rust
{}
fn this_will_be_deleted() {{
println!("Deleting this function");
}}
{}
{}
```
"#,
SEARCH_MARKER, DIVIDER, REPLACE_MARKER
);
let mut parser = EditActionParser::new();
let actions = parser.parse_chunk(&input);
assert_no_errors(&parser);
assert_eq!(actions.len(), 1);
assert_eq!(
actions[0].0,
EditAction::Replace {
file_path: PathBuf::from("src/main.rs"),
old: "fn this_will_be_deleted() {\n println!(\"Deleting this function\");\n}"
.to_string(),
new: "".to_string(),
}
);
let mut parser = EditActionParser::new();
let actions = parser.parse_chunk(&input.replace("\n", "\r\n"));
assert_no_errors(&parser);
assert_eq!(actions.len(), 1);
assert_eq!(
actions[0].0,
EditAction::Replace {
file_path: PathBuf::from("src/main.rs"),
old:
"fn this_will_be_deleted() {\r\n println!(\"Deleting this function\");\r\n}"
.to_string(),
new: "".to_string(),
}
);
}
#[test]
fn test_empty_both() {
// Construct test input using format with multiline string literals
let input = format!(
r#"src/main.rs
```rust
{}
{}
{}
```
"#,
SEARCH_MARKER, DIVIDER, REPLACE_MARKER
);
let mut parser = EditActionParser::new();
let actions = parser.parse_chunk(&input);
assert_eq!(actions.len(), 1);
assert_eq!(
actions[0].0,
EditAction::Write {
file_path: PathBuf::from("src/main.rs"),
content: String::new(),
}
);
assert_no_errors(&parser);
}
#[test]
fn test_resumability() {
// Construct test input using format with multiline string literals
let input_part1 = format!("src/main.rs\n```rust\n{}\nfn ori", SEARCH_MARKER);
let input_part2 = format!("ginal() {{}}\n{}\nfn replacement() {{}}", DIVIDER);
let input_part3 = format!("\n{}\n```\n", REPLACE_MARKER);
let mut parser = EditActionParser::new();
let actions1 = parser.parse_chunk(&input_part1);
assert_no_errors(&parser);
assert_eq!(actions1.len(), 0);
let actions2 = parser.parse_chunk(&input_part2);
// No actions should be complete yet
assert_no_errors(&parser);
assert_eq!(actions2.len(), 0);
let actions3 = parser.parse_chunk(&input_part3);
// The third chunk should complete the action
assert_no_errors(&parser);
assert_eq!(actions3.len(), 1);
let (action, _) = &actions3[0];
assert_eq!(
action,
&EditAction::Replace {
file_path: PathBuf::from("src/main.rs"),
old: "fn original() {}".to_string(),
new: "fn replacement() {}".to_string(),
}
);
}
#[test]
fn test_parser_state_preservation() {
let mut parser = EditActionParser::new();
let first_chunk = format!("src/main.rs\n```rust\n{}\n", SEARCH_MARKER);
let actions1 = parser.parse_chunk(&first_chunk);
// Check parser is in the correct state
assert_no_errors(&parser);
assert_eq!(parser.state, State::SearchBlock);
assert_eq!(parser.action_source, first_chunk.as_bytes());
// Continue parsing
let second_chunk = format!("original code\n{}\n", DIVIDER);
let actions2 = parser.parse_chunk(&second_chunk);
assert_no_errors(&parser);
assert_eq!(parser.state, State::ReplaceBlock);
assert_eq!(
&parser.action_source[parser.old_range.clone()],
b"original code"
);
let third_chunk = format!("replacement code\n{}\n```\n", REPLACE_MARKER);
let actions3 = parser.parse_chunk(&third_chunk);
// After complete parsing, state should reset
assert_no_errors(&parser);
assert_eq!(parser.state, State::Default);
assert_eq!(parser.action_source, b"\n");
assert!(parser.old_range.is_empty());
assert!(parser.new_range.is_empty());
assert_eq!(actions1.len(), 0);
assert_eq!(actions2.len(), 0);
assert_eq!(actions3.len(), 1);
}
#[test]
fn test_invalid_search_marker() {
let input = format!(
r#"src/main.rs
```rust
{}
fn original() {{}}
{}
fn replacement() {{}}
{}
```
"#,
WRONG_MARKER, DIVIDER, REPLACE_MARKER
);
let mut parser = EditActionParser::new();
let actions = parser.parse_chunk(&input);
assert_eq!(actions.len(), 0);
assert_eq!(parser.errors().len(), 1);
let error = &parser.errors()[0];
assert_eq!(
error.to_string(),
format!(
"input:3:9: Expected marker \"{}\", found 'W'",
SEARCH_MARKER
)
);
}
#[test]
fn test_missing_closing_fence() {
// Construct test input using format with multiline string literals
let input = format!(
r#"src/main.rs
```rust
{}
fn original() {{}}
{}
fn replacement() {{}}
{}
<!-- Missing closing fence -->
src/utils.rs
```rust
{}
fn utils_func() {{}}
{}
fn new_utils_func() {{}}
{}
```
"#,
SEARCH_MARKER, DIVIDER, REPLACE_MARKER, SEARCH_MARKER, DIVIDER, REPLACE_MARKER
);
let mut parser = EditActionParser::new();
let actions = parser.parse_chunk(&input);
// Only the second block should be parsed
assert_eq!(actions.len(), 1);
let (action, _) = &actions[0];
assert_eq!(
action,
&EditAction::Replace {
file_path: PathBuf::from("src/utils.rs"),
old: "fn utils_func() {}".to_string(),
new: "fn new_utils_func() {}".to_string(),
}
);
assert_eq!(parser.errors().len(), 1);
assert_eq!(
parser.errors()[0].to_string(),
"input:8:1: Expected marker \"```\", found '<'"
);
// The parser should continue after an error
assert_eq!(parser.state, State::Default);
}
#[test]
fn test_parse_examples_in_edit_prompt() {
let mut parser = EditActionParser::new();
let actions = parser.parse_chunk(&edit_model_prompt());
assert_examples_in_edit_prompt(&actions, parser.errors());
}
#[gpui::test(iterations = 10)]
fn test_random_chunking_of_edit_prompt(mut rng: StdRng) {
let mut parser = EditActionParser::new();
let mut remaining: &str = &edit_model_prompt();
let mut actions = Vec::with_capacity(5);
while !remaining.is_empty() {
let chunk_size = rng.gen_range(1..=std::cmp::min(remaining.len(), 100));
let (chunk, rest) = remaining.split_at(chunk_size);
let chunk_actions = parser.parse_chunk(chunk);
actions.extend(chunk_actions);
remaining = rest;
}
assert_examples_in_edit_prompt(&actions, parser.errors());
}
fn assert_examples_in_edit_prompt(actions: &[(EditAction, String)], errors: &[ParseError]) {
assert_eq!(actions.len(), 5);
assert_eq!(
actions[0].0,
EditAction::Replace {
file_path: PathBuf::from("mathweb/flask/app.py"),
old: "from flask import Flask".to_string(),
new: line_endings!("import math\nfrom flask import Flask").to_string(),
},
);
assert_eq!(
actions[1].0,
EditAction::Replace {
file_path: PathBuf::from("mathweb/flask/app.py"),
old: line_endings!("def factorial(n):\n \"compute factorial\"\n\n if n == 0:\n return 1\n else:\n return n * factorial(n-1)\n").to_string(),
new: "".to_string(),
}
);
assert_eq!(
actions[2].0,
EditAction::Replace {
file_path: PathBuf::from("mathweb/flask/app.py"),
old: " return str(factorial(n))".to_string(),
new: " return str(math.factorial(n))".to_string(),
},
);
assert_eq!(
actions[3].0,
EditAction::Write {
file_path: PathBuf::from("hello.py"),
content: line_endings!(
"def hello():\n \"print a greeting\"\n\n print(\"hello\")"
)
.to_string(),
},
);
assert_eq!(
actions[4].0,
EditAction::Replace {
file_path: PathBuf::from("main.py"),
old: line_endings!(
"def hello():\n \"print a greeting\"\n\n print(\"hello\")"
)
.to_string(),
new: "from hello import hello".to_string(),
},
);
// The system prompt includes some text that would produce errors
assert_eq!(
errors[0].to_string(),
format!(
"input:102:1: Expected marker \"{}\", found '3'",
SEARCH_MARKER
)
);
#[cfg(not(windows))]
assert_eq!(
errors[1].to_string(),
format!(
"input:109:0: Expected marker \"{}\", found '\\n'",
SEARCH_MARKER
)
);
#[cfg(windows)]
assert_eq!(
errors[1].to_string(),
format!(
"input:108:1: Expected marker \"{}\", found '\\r'",
SEARCH_MARKER
)
);
}
#[test]
fn test_print_error() {
let input = format!(
r#"src/main.rs
```rust
{}
fn original() {{}}
{}
fn replacement() {{}}
{}
```
"#,
WRONG_MARKER, DIVIDER, REPLACE_MARKER
);
let mut parser = EditActionParser::new();
parser.parse_chunk(&input);
assert_eq!(parser.errors().len(), 1);
let error = &parser.errors()[0];
let expected_error = format!(
r#"input:3:9: Expected marker "{}", found 'W'"#,
SEARCH_MARKER
);
assert_eq!(format!("{}", error), expected_error);
}
// helpers
fn assert_no_errors(parser: &EditActionParser) {
let errors = parser.errors();
assert!(
errors.is_empty(),
"Expected no errors, but found:\n\n{}",
errors
.iter()
.map(|e| e.to_string())
.collect::<Vec<String>>()
.join("\n")
);
}
}

View File

@@ -0,0 +1,134 @@
Act as an expert software developer.
Always use best practices when coding.
Respect and use existing conventions, libraries, etc that are already present in the code base.
Take requests for changes to the supplied code.
Always reply to the user in the same language they are using.
Once you understand the request, you MUST:
1. Decide if you need to propose *SEARCH/REPLACE* edits to any files that haven't been added to the chat. You can create new files without asking!
2. Think step-by-step and explain the needed changes in a few short sentences.
3. Describe each change with a *SEARCH/REPLACE block* per the examples below.
All changes to files must use this *SEARCH/REPLACE block* format.
ONLY EVER RETURN CODE IN A *SEARCH/REPLACE BLOCK*!
# Example conversations:
## USER: Change get_factorial() to use math.factorial
## ASSISTANT: To make this change we need to modify `mathweb/flask/app.py` to:
1. Import the math package.
2. Remove the existing factorial() function.
3. Update get_factorial() to call math.factorial instead.
Here are the *SEARCH/REPLACE* blocks:
mathweb/flask/app.py
```python
{{SEARCH_MARKER}}
from flask import Flask
{{DIVIDER}}
import math
from flask import Flask
{{REPLACE_MARKER}}
```
mathweb/flask/app.py
```python
{{SEARCH_MARKER}}
def factorial(n):
"compute factorial"
if n == 0:
return 1
else:
return n * factorial(n-1)
{{DIVIDER}}
{{REPLACE_MARKER}}
```
mathweb/flask/app.py
```python
{{SEARCH_MARKER}}
return str(factorial(n))
{{DIVIDER}}
return str(math.factorial(n))
{{REPLACE_MARKER}}
```
## USER: Refactor hello() into its own file.
## ASSISTANT: To make this change we need to modify `main.py` and make a new file `hello.py`:
1. Make a new hello.py file with hello() in it.
2. Remove hello() from main.py and replace it with an import.
Here are the *SEARCH/REPLACE* blocks:
hello.py
```python
{{SEARCH_MARKER}}
{{DIVIDER}}
def hello():
"print a greeting"
print("hello")
{{REPLACE_MARKER}}
```
main.py
```python
{{SEARCH_MARKER}}
def hello():
"print a greeting"
print("hello")
{{DIVIDER}}
from hello import hello
{{REPLACE_MARKER}}
```
# *SEARCH/REPLACE block* Rules:
Every *SEARCH/REPLACE block* must use this format:
1. The *FULL* file path alone on a line, verbatim. No bold asterisks, no quotes around it, no escaping of characters, etc.
2. The opening fence and code language, eg: ```python
3. The start of search block: {{SEARCH_MARKER}}
4. A contiguous chunk of lines to search for in the existing source code
5. The dividing line: {{DIVIDER}}
6. The lines to replace into the source code
7. The end of the replace block: {{REPLACE_MARKER}}
8. The closing fence: ```
Use the *FULL* file path, as shown to you by the user. Make sure to include the project's root directory name at the start of the path. *NEVER* specify the absolute path of the file!
Every *SEARCH* section must *EXACTLY MATCH* the existing file content, character for character, including all comments, docstrings, etc.
If the file contains code or other data wrapped/escaped in json/xml/quotes or other containers, you need to propose edits to the literal contents of the file, including the container markup.
*SEARCH/REPLACE* blocks will *only* replace the first match occurrence.
Including multiple unique *SEARCH/REPLACE* blocks if needed.
Include enough lines in each SEARCH section to uniquely match each set of lines that need to change.
Keep *SEARCH/REPLACE* blocks concise.
Break large *SEARCH/REPLACE* blocks into a series of smaller blocks that each change a small portion of the file.
Include just the changing lines, and a few surrounding lines if needed for uniqueness.
Do not include long runs of unchanging lines in *SEARCH/REPLACE* blocks.
Only create *SEARCH/REPLACE* blocks for files that have been read! Even though the conversation includes `read-file` tool results, you *CANNOT* issue your own reads. If the conversation doesn't include the code you need to edit, ask for it to be read explicitly.
To move code within a file, use 2 *SEARCH/REPLACE* blocks: 1 to delete it from its current location, 1 to insert it in the new location.
Pay attention to which filenames the user wants you to edit, especially if they are asking you to create a new file.
If you want to put code in a new file, use a *SEARCH/REPLACE block* with:
- A new file path, including dir name if needed
- An empty `SEARCH` section
- The new file's contents in the `REPLACE` section
ONLY EVER RETURN CODE IN A *SEARCH/REPLACE BLOCK*!

View File

@@ -0,0 +1,417 @@
use std::path::Path;
use collections::HashSet;
use feature_flags::FeatureFlagAppExt;
use gpui::{
App, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment, ListState,
SharedString, Subscription, Window, actions, list, prelude::*,
};
use release_channel::ReleaseChannel;
use settings::Settings;
use ui::prelude::*;
use workspace::{Item, Workspace, WorkspaceId, item::ItemEvent};
use super::edit_action::EditAction;
actions!(debug, [EditTool]);
pub fn init(cx: &mut App) {
if cx.is_staff() || ReleaseChannel::global(cx) == ReleaseChannel::Dev {
// Track events even before opening the log
EditToolLog::global(cx);
}
cx.observe_new(|workspace: &mut Workspace, _, _| {
workspace.register_action(|workspace, _: &EditTool, window, cx| {
let viewer = cx.new(EditToolLogViewer::new);
workspace.add_item_to_active_pane(Box::new(viewer), None, true, window, cx)
});
})
.detach();
}
pub struct GlobalEditToolLog(Entity<EditToolLog>);
impl Global for GlobalEditToolLog {}
#[derive(Default)]
pub struct EditToolLog {
requests: Vec<EditToolRequest>,
}
#[derive(Clone, Copy, Hash, Eq, PartialEq)]
pub struct EditToolRequestId(u32);
impl EditToolLog {
pub fn global(cx: &mut App) -> Entity<Self> {
match Self::try_global(cx) {
Some(entity) => entity,
None => {
let entity = cx.new(|_cx| Self::default());
cx.set_global(GlobalEditToolLog(entity.clone()));
entity
}
}
}
pub fn try_global(cx: &App) -> Option<Entity<Self>> {
cx.try_global::<GlobalEditToolLog>()
.map(|log| log.0.clone())
}
pub fn new_request(
&mut self,
instructions: String,
cx: &mut Context<Self>,
) -> EditToolRequestId {
let id = EditToolRequestId(self.requests.len() as u32);
self.requests.push(EditToolRequest {
id,
instructions,
editor_response: None,
tool_output: None,
parsed_edits: Vec::new(),
});
cx.emit(EditToolLogEvent::Inserted);
id
}
pub fn push_editor_response_chunk(
&mut self,
id: EditToolRequestId,
chunk: &str,
new_actions: &[(EditAction, String)],
cx: &mut Context<Self>,
) {
if let Some(request) = self.requests.get_mut(id.0 as usize) {
match &mut request.editor_response {
None => {
request.editor_response = Some(chunk.to_string());
}
Some(response) => {
response.push_str(chunk);
}
}
request
.parsed_edits
.extend(new_actions.iter().cloned().map(|(action, _)| action));
cx.emit(EditToolLogEvent::Updated);
}
}
pub fn set_tool_output(
&mut self,
id: EditToolRequestId,
tool_output: Result<String, String>,
cx: &mut Context<Self>,
) {
if let Some(request) = self.requests.get_mut(id.0 as usize) {
request.tool_output = Some(tool_output);
cx.emit(EditToolLogEvent::Updated);
}
}
}
enum EditToolLogEvent {
Inserted,
Updated,
}
impl EventEmitter<EditToolLogEvent> for EditToolLog {}
pub struct EditToolRequest {
id: EditToolRequestId,
instructions: String,
// we don't use a result here because the error might have occurred after we got a response
editor_response: Option<String>,
parsed_edits: Vec<EditAction>,
tool_output: Option<Result<String, String>>,
}
pub struct EditToolLogViewer {
focus_handle: FocusHandle,
log: Entity<EditToolLog>,
list_state: ListState,
expanded_edits: HashSet<(EditToolRequestId, usize)>,
_subscription: Subscription,
}
impl EditToolLogViewer {
pub fn new(cx: &mut Context<Self>) -> Self {
let log = EditToolLog::global(cx);
let subscription = cx.subscribe(&log, Self::handle_log_event);
Self {
focus_handle: cx.focus_handle(),
log: log.clone(),
list_state: ListState::new(
log.read(cx).requests.len(),
ListAlignment::Bottom,
px(1024.),
{
let this = cx.entity().downgrade();
move |ix, window: &mut Window, cx: &mut App| {
this.update(cx, |this, cx| this.render_request(ix, window, cx))
.unwrap()
}
},
),
expanded_edits: HashSet::default(),
_subscription: subscription,
}
}
fn handle_log_event(
&mut self,
_: Entity<EditToolLog>,
event: &EditToolLogEvent,
cx: &mut Context<Self>,
) {
match event {
EditToolLogEvent::Inserted => {
let count = self.list_state.item_count();
self.list_state.splice(count..count, 1);
}
EditToolLogEvent::Updated => {}
}
cx.notify();
}
fn render_request(
&self,
index: usize,
_window: &mut Window,
cx: &mut Context<Self>,
) -> AnyElement {
let requests = &self.log.read(cx).requests;
let request = &requests[index];
v_flex()
.gap_3()
.child(Self::render_section(IconName::ArrowRight, "Tool Input"))
.child(request.instructions.clone())
.py_5()
.when(index + 1 < requests.len(), |element| {
element
.border_b_1()
.border_color(cx.theme().colors().border)
})
.map(|parent| match &request.editor_response {
None => {
if request.tool_output.is_none() {
parent.child("...")
} else {
parent
}
}
Some(response) => parent
.child(Self::render_section(
IconName::ZedAssistant,
"Editor Response",
))
.child(Label::new(response.clone()).buffer_font(cx)),
})
.when(!request.parsed_edits.is_empty(), |parent| {
parent
.child(Self::render_section(IconName::Microscope, "Parsed Edits"))
.child(
v_flex()
.gap_2()
.children(request.parsed_edits.iter().enumerate().map(
|(index, edit)| {
self.render_edit_action(edit, request.id, index, cx)
},
)),
)
})
.when_some(request.tool_output.as_ref(), |parent, output| {
parent
.child(Self::render_section(IconName::ArrowLeft, "Tool Output"))
.child(match output {
Ok(output) => Label::new(output.clone()).color(Color::Success),
Err(error) => Label::new(error.clone()).color(Color::Error),
})
})
.into_any()
}
fn render_section(icon: IconName, title: &'static str) -> AnyElement {
h_flex()
.gap_1()
.child(Icon::new(icon).color(Color::Muted))
.child(Label::new(title).size(LabelSize::Small).color(Color::Muted))
.into_any()
}
fn render_edit_action(
&self,
edit_action: &EditAction,
request_id: EditToolRequestId,
index: usize,
cx: &Context<Self>,
) -> AnyElement {
let expanded_id = (request_id, index);
match edit_action {
EditAction::Replace {
file_path,
old,
new,
} => self
.render_edit_action_container(
expanded_id,
&file_path,
[
Self::render_block(IconName::MagnifyingGlass, "Search", old.clone(), cx)
.border_r_1()
.border_color(cx.theme().colors().border)
.into_any(),
Self::render_block(IconName::Replace, "Replace", new.clone(), cx)
.into_any(),
],
cx,
)
.into_any(),
EditAction::Write { file_path, content } => self
.render_edit_action_container(
expanded_id,
&file_path,
[
Self::render_block(IconName::Pencil, "Write", content.clone(), cx)
.into_any(),
],
cx,
)
.into_any(),
}
}
fn render_edit_action_container(
&self,
expanded_id: (EditToolRequestId, usize),
file_path: &Path,
content: impl IntoIterator<Item = AnyElement>,
cx: &Context<Self>,
) -> AnyElement {
let is_expanded = self.expanded_edits.contains(&expanded_id);
v_flex()
.child(
h_flex()
.bg(cx.theme().colors().element_background)
.border_1()
.border_color(cx.theme().colors().border)
.rounded_t_md()
.when(!is_expanded, |el| el.rounded_b_md())
.py_1()
.px_2()
.gap_1()
.child(
ui::Disclosure::new(ElementId::Integer(expanded_id.1), is_expanded)
.on_click(cx.listener(move |this, _ev, _window, cx| {
if is_expanded {
this.expanded_edits.remove(&expanded_id);
} else {
this.expanded_edits.insert(expanded_id);
}
cx.notify();
})),
)
.child(Label::new(file_path.display().to_string()).size(LabelSize::Small)),
)
.child(if is_expanded {
h_flex()
.border_1()
.border_t_0()
.border_color(cx.theme().colors().border)
.rounded_b_md()
.children(content)
.into_any()
} else {
Empty.into_any()
})
.into_any()
}
fn render_block(icon: IconName, title: &'static str, content: String, cx: &App) -> Div {
v_flex()
.p_1()
.gap_1()
.flex_1()
.h_full()
.child(
h_flex()
.gap_1()
.child(Icon::new(icon).color(Color::Muted))
.child(Label::new(title).size(LabelSize::Small).color(Color::Muted)),
)
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
.text_sm()
.child(content)
.child(div().flex_1())
}
}
impl EventEmitter<()> for EditToolLogViewer {}
impl Focusable for EditToolLogViewer {
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl Item for EditToolLogViewer {
type Event = ();
fn to_item_events(_: &Self::Event, _: impl FnMut(ItemEvent)) {}
fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
Some("Edit Tool Log".into())
}
fn telemetry_event_text(&self) -> Option<&'static str> {
None
}
fn clone_on_split(
&self,
_workspace_id: Option<WorkspaceId>,
_window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Entity<Self>>
where
Self: Sized,
{
Some(cx.new(Self::new))
}
}
impl Render for EditToolLogViewer {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if self.list_state.item_count() == 0 {
return v_flex()
.justify_center()
.size_full()
.gap_1()
.bg(cx.theme().colors().editor_background)
.text_center()
.text_lg()
.child("No requests yet")
.child(
div()
.text_ui(cx)
.child("Go ask the assistant to perform some edits"),
);
}
v_flex()
.p_4()
.bg(cx.theme().colors().editor_background)
.size_full()
.child(list(self.list_state.clone()).flex_grow())
}
}

View File

@@ -4,7 +4,7 @@ use std::sync::Arc;
use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow, bail};
use assistant_tool::{ActionLog, ResponseDest, Tool};
use assistant_tool::{ActionLog, Tool};
use futures::AsyncReadExt as _;
use gpui::{App, AppContext as _, Entity, Task};
use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
@@ -116,7 +116,7 @@ impl Tool for FetchTool {
"fetch".to_string()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self) -> bool {
true
}
@@ -146,7 +146,7 @@ impl Tool for FetchTool {
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<(ResponseDest, String)>> {
) -> Task<Result<String>> {
let input = match serde_json::from_value::<FetchToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -164,7 +164,7 @@ impl Tool for FetchTool {
bail!("no textual content found");
}
Ok((ResponseDest::TextOnly, text))
Ok(text)
})
}
}

View File

@@ -1,6 +1,6 @@
use crate::{replace::replace_with_flexible_indent, schema::json_schema_for};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, ResponseDest, Tool};
use assistant_tool::{ActionLog, Tool};
use gpui::{App, AppContext, AsyncApp, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -63,16 +63,6 @@ pub struct FindReplaceFileToolInput {
/// even one character in this string is different in any way from how it appears
/// in the file, then the tool call will fail.
///
/// If you get an error that the `find` string was not found, this means that either
/// you made a mistake, or that the file has changed since you last looked at it.
/// Either way, when this happens, you should retry doing this tool call until it
/// succeeds, up to 3 times. Each time you retry, you should take another look at
/// the exact text of the file in question, to make sure that you are searching for
/// exactly the right string. Regardless of whether it was because you made a mistake
/// or because the file changed since you last looked at it, you should be extra
/// careful when retrying in this way. It's a bad experience for the user if
/// this `find` string isn't found, so be super careful to get it exactly right!
///
/// <example>
/// If a file contains this code:
///
@@ -139,7 +129,7 @@ impl Tool for FindReplaceFileTool {
"find_replace_file".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self) -> bool {
false
}
@@ -169,7 +159,7 @@ impl Tool for FindReplaceFileTool {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<(ResponseDest, String)>> {
) -> Task<Result<String>> {
let input = match serde_json::from_value::<FindReplaceFileToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -261,7 +251,8 @@ impl Tool for FindReplaceFileTool {
}).await;
Ok((ResponseDest::TextOnly,format!("Edited {}:\n\n```diff\n{}\n```", input.path.display(), diff_str)))
Ok(format!("Edited {}:\n\n```diff\n{}\n```", input.path.display(), diff_str))
})
}
}

View File

@@ -1,13 +1,9 @@
Find one unique part of a file in the project and replace that text with new text.
This tool is the preferred way to make edits to files *except* when making a rename. When making a rename specifically, the rename tool must always be used instead.
This tool is the preferred way to make edits to files. If you have multiple edits to make, including edits across multiple files, then make a plan to respond with a single message containing multiple calls to this tool - one call for each find/replace operation.
If you have multiple edits to make, including edits across multiple files, then make a plan to respond with a single message containing a batch of calls to this tool - one call for each find/replace operation.
You should only use this tool when you want to edit a subset of a file's contents, but not the entire file. You should not use this tool when you want to replace the entire contents of a file with completely different contents. You also should not use this tool when you want to move or rename a file. You absolutely must NEVER use this tool to create new files from scratch. If you ever consider using this tool to create a new file from scratch, for any reason, instead you must reconsider and choose a different approach.
You should use this tool when you want to edit a subset of a file's contents, but not the entire file. You should not use this tool when you want to replace the entire contents of a file with completely different contents. You also should not use this tool when you want to move or rename a file. You absolutely must NEVER use this tool to create new files from scratch. If you ever consider using this tool to create a new file from scratch, for any reason, instead you must reconsider and choose a different approach.
DO NOT call this tool until the code to be edited appears in the conversation! You must use another tool to read the file's contents into the conversation, or ask the user to add it to context first.
Never call this tool with identical "find" and "replace" strings. Instead, stop and think about what you actually want to do.
REMEMBER: You can use this tool after you just used the `create_file` tool. It's better to edit the file you just created than to recreate a new file from scratch.

View File

@@ -1,6 +1,6 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, ResponseDest, Tool};
use assistant_tool::{ActionLog, Tool};
use gpui::{App, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -44,7 +44,7 @@ impl Tool for ListDirectoryTool {
"list_directory".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self) -> bool {
false
}
@@ -77,7 +77,7 @@ impl Tool for ListDirectoryTool {
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<(ResponseDest, String)>> {
) -> Task<Result<String>> {
let input = match serde_json::from_value::<ListDirectoryToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -101,7 +101,7 @@ impl Tool for ListDirectoryTool {
.collect::<Vec<_>>()
.join("\n");
return Task::ready(Ok((ResponseDest::TextOnly, output)));
return Task::ready(Ok(output));
}
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
@@ -132,13 +132,9 @@ impl Tool for ListDirectoryTool {
)
.unwrap();
}
Task::ready(Ok((
ResponseDest::TextOnly,
if output.is_empty() {
format!("{} is empty.", input.path)
} else {
output
},
)))
if output.is_empty() {
return Task::ready(Ok(format!("{} is empty.", input.path)));
}
Task::ready(Ok(output))
}
}

View File

@@ -1,6 +1,6 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, ResponseDest, Tool};
use assistant_tool::{ActionLog, Tool};
use gpui::{App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -42,7 +42,7 @@ impl Tool for MovePathTool {
"move_path".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self) -> bool {
true
}
@@ -90,7 +90,7 @@ impl Tool for MovePathTool {
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<(ResponseDest, String)>> {
) -> Task<Result<String>> {
let input = match serde_json::from_value::<MovePathToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -116,9 +116,9 @@ impl Tool for MovePathTool {
cx.background_spawn(async move {
match rename_task.await {
Ok(_) => Ok((
ResponseDest::TextOnly,
format!("Moved {} to {}", input.source_path, input.destination_path),
Ok(_) => Ok(format!(
"Moved {} to {}",
input.source_path, input.destination_path
)),
Err(err) => Err(anyhow!(
"Failed to move {} to {}: {}",

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