Compare commits

..

22 Commits

Author SHA1 Message Date
Thomas Mickley-Doyle
6f5d0cfe00 Call shell for each env 2025-04-21 10:19:34 -05:00
Thomas Mickley-Doyle
a41a4fb145 Run with bash 2025-04-21 08:26:54 -05:00
Thomas Mickley-Doyle
1ef424f1aa Remove running with bash 2025-04-21 08:25:58 -05:00
Thomas Mickley-Doyle
b36b205a6f Run eval with bash 2025-04-21 08:25:17 -05:00
Thomas Mickley-Doyle
83e4cd1553 Run evals in GA 2025-04-18 22:01:36 -05:00
Thomas Mickley-Doyle
65502c0689 Running evals from github actions 2025-04-18 21:47:54 -05:00
Nathan Sobo
61315388cf Replace todo! with TODO: to avoid formatting issue
We want to fix this soon.

Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2025-04-18 20:27:25 -06:00
Nathan Sobo
01f9bff428 Temporarily restore the old attach_tool_results implementation
Thinking models are broken with the reverted change, but I'd still like
to not do it this way.

Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2025-04-18 20:07:39 -06:00
Nathan Sobo
0a9e5e4586 Merge remote-tracking branch 'origin/main' into reboot-agent-loop 2025-04-18 19:45:33 -06:00
Max Brunsfeld
bc7723b5fe Remove extra code fence lines from tool output in eval example files
Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2025-04-18 18:13:35 -07:00
Agus Zubiaga
128b2d4bff Remove abs_path from system prompt
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-04-18 18:12:37 -07:00
Agus Zubiaga
c7b4e5e1ac eval: Run judges concurrently (#29039)
Release Notes:

- N/A
2025-04-18 10:20:43 -03:00
Antonio Scandurra
8aea590646 Avoid telling the model to provide an offset if results fit in a page 2025-04-18 13:49:20 +02:00
Antonio Scandurra
9cf88d21aa Always invoke request callback, even when there's an error
This lets us inspect what went wrong
2025-04-18 13:39:48 +02:00
Antonio Scandurra
50418ab113 Show "no matches found" when path_search_tool doesn't find anything 2025-04-18 13:13:15 +02:00
Antonio Scandurra
4fe6ae52f8 Test read_file and return an error when file does not exist 2025-04-18 12:29:14 +02:00
Max Brunsfeld
e9bb15b906 Normalize line endings in judge prompt tests 2025-04-17 17:41:33 -07:00
Max Brunsfeld
f77e752908 Remove duplicate critera and judge_prompt files 2025-04-17 17:24:51 -07:00
Agus Zubiaga
28b1e08c0c eval: Improve lang server idle detection (#29013)
More accurately detect when the language server is idle in the eval.
This helps ensure that the diagnostics tool will work and that we will
accurately report them to the judge.

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
2025-04-17 21:16:08 -03:00
Max Brunsfeld
69b6aef396 Don't check typos within any eval examples 2025-04-17 17:14:53 -07:00
Max Brunsfeld
6e825281de Remove todo 2025-04-17 16:35:10 -07:00
Nathan Sobo
5d5377d838 Reboot agent loop
Co-authored-by: Max <max@zed.dev>
Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Agus <agus@zed.dev>
Co-authored-by: Richard <richard@zed.dev>
2025-04-17 16:32:04 -07:00
691 changed files with 18327 additions and 38373 deletions

View File

@@ -20,6 +20,7 @@ platforms = [
[traversal-excludes]
workspace-members = [
"remote_server",
"eval",
]
third-party = [
{ name = "reqwest", version = "0.11.27" },

View File

@@ -325,7 +325,7 @@ jobs:
cache-provider: "buildjet"
- name: Install Clang & Mold
run: ./script/remote-server && ./script/install-mold 2.34.0
run: ./script/headless && ./script/install-mold 2.34.0
- name: Configure CI
run: |

View File

@@ -1,80 +0,0 @@
name: Run Agent Eval
on:
schedule:
- cron: "0 0 * * *"
pull_request:
branches:
- "**"
types: [opened, synchronize, reopened, labeled]
workflow_dispatch:
concurrency:
# Allow only one workflow per any non-`main` branch.
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_EVAL_TELEMETRY: 1
jobs:
# This is a no-op job that we run to prevent GitHub from marking the workflow
# as failed for PRs that don't have the `run-eval` label.
noop:
name: No-op
runs-on: ubuntu-latest
steps:
- name: No-op
run: echo "Nothing to do"
run_eval:
timeout-minutes: 60
name: Run Agent Eval
if: >
github.repository_owner == 'zed-industries' &&
(github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-eval'))
runs-on:
- buildjet-16vcpu-ubuntu-2204
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Cache dependencies
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "buildjet"
- name: Install Linux dependencies
run: ./script/linux
- name: Configure CI
run: |
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- name: Compile eval
run: cargo build --package=eval
- name: Run eval
run: cargo run --package=eval -- --repetitions=3 --concurrency=1
# Even the Linux runner is not stateful, in theory there is no need to do this cleanup.
# But, to avoid potential issues in the future if we choose to use a stateful Linux runner and forget to add code
# to clean up the config file, Ive included the cleanup code here as a precaution.
# While its not strictly necessary at this moment, I believe its better to err on the side of caution.
- name: Clean CI config file
if: always()
run: rm -rf ./../.cargo

View File

@@ -170,6 +170,55 @@ jobs:
- name: Upload Zed Nightly
run: script/upload-nightly linux-targz
bundle-nix:
timeout-minutes: 60
name: (${{ matrix.system.os }}) Nix Build
continue-on-error: true
strategy:
fail-fast: false
matrix:
system:
- os: x86 Linux
runner: buildjet-16vcpu-ubuntu-2204
install_nix: true
- os: arm Mac
runner: [macOS, ARM64, test]
install_nix: false
if: github.repository_owner == 'zed-industries'
runs-on: ${{ matrix.system.runner }}
needs: tests
env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
GIT_LFS_SKIP_SMUDGE: 1 # breaks the livekit rust sdk examples which we don't actually depend on
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
# on our macs we manually install nix. for some reason the cachix action is running
# under a non-login /bin/bash shell which doesn't source the proper script to add the
# nix profile to PATH, so we manually add them here
- name: Set path
if: ${{ ! matrix.system.install_nix }}
run: |
echo "/nix/var/nix/profiles/default/bin" >> $GITHUB_PATH
echo "/Users/administrator/.nix-profile/bin" >> $GITHUB_PATH
- uses: cachix/install-nix-action@d1ca217b388ee87b2507a9a93bf01368bde7cec2 # v31
if: ${{ matrix.system.install_nix }}
with:
github_access_token: ${{ secrets.GITHUB_TOKEN }}
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: zed-industries
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- run: nix build
- name: Limit /nix/store to 50GB
run: '[ $(du -sm /nix/store | cut -f1) -gt 50000 ] && nix-collect-garbage -d'
update-nightly-tag:
name: Update nightly tag
if: github.repository_owner == 'zed-industries'

View File

@@ -0,0 +1,92 @@
name: Run Eval Daily
on:
schedule:
- cron: "0 2 * * *"
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1
jobs:
build_binary:
name: Build Eval Binary
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Install Dependencies
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxcb-shape0-dev libxcb-xfixes0-dev libxcb1-dev libxcb-render0-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-keysyms1-dev libxext-dev libxrender-dev libxrandr-dev libxtst-dev libxfixes-dev pkg-config libasound2-dev libx11-xcb-dev libxkbcommon-dev libxkbcommon-x11-dev
./script/headless && ./script/install-mold 2.34.0
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Build entire Zed application
run: cargo build --release
- name: Build eval binary separately to ensure it's available
run: cargo build --release -p eval
- name: Upload eval binary
uses: actions/upload-artifact@v4
with:
name: eval-binary
path: target/release/eval
run_eval:
name: Run Eval - ${{ matrix.exercise }}
needs: build_binary
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
exercise:
- find_and_replace_diff_card
- metal_i64_support
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Install Dependencies
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxcb-shape0-dev libxcb-xfixes0-dev libxcb1-dev libxcb-render0-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-keysyms1-dev libxext-dev libxrender-dev libxrandr-dev libxtst-dev libxfixes-dev pkg-config libasound2-dev libx11-xcb-dev libxkbcommon-dev libxkbcommon-x11-dev
./script/headless && ./script/install-mold 2.34.0
- name: Create required directories
run: |
mkdir -p /home/runner/.config/zed
touch /home/runner/.config/zed/settings.json
mkdir -p ./crates/eval/repos
mkdir -p ./crates/eval/worktrees
mkdir -p ./crates/eval/runs
- name: Set up Anthropic API key
run: echo "ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_API_KEY }}" >> $GITHUB_ENV
- name: Download eval binary
uses: actions/download-artifact@v4
with:
name: eval-binary
path: ./target/release/
- name: Make eval binary executable
run: chmod +x ./target/release/eval
- name: Run eval for ${{ matrix.exercise }}
env:
SHELL: /bin/bash
run: ./target/release/eval --cohort-id dailyrun${{ github.run_id }} ${{ matrix.exercise }}

2
.gitignore vendored
View File

@@ -23,7 +23,6 @@
/crates/theme/schemas/theme.json
/crates/zed/resources/flatpak/flatpak-cargo-sources.json
/dev.zed.Zed*.json
/node_modules/
/plugins/bin
/script/node_modules
/snap
@@ -33,5 +32,4 @@ Packages
xcuserdata/
# Don't commit any secrets to the repo.
.env
.env.secret.toml

16
.rules
View File

@@ -5,7 +5,6 @@
* Prefer implementing functionality in existing files unless it is a new logical component. Avoid creating many small files.
* Avoid using functions that panic like `unwrap()`, instead use mechanisms like `?` to propagate errors.
* Be careful with operations like indexing which may panic if the indexes are out of bounds.
* Never create files with `mod.rs` paths - prefer `src/some_module.rs` instead of `src/some_module/mod.rs`.
# GPUI
@@ -109,18 +108,3 @@ When a view's state has changed in a way that may affect its rendering, it shoul
While updating an entity (`cx: Context<T>`), it can emit an event using `cx.emit(event)`. Entities register which events they can emit by declaring `impl EventEmittor<EventType> for EntityType {}`.
Other entities can then register a callback to handle these events by doing `cx.subscribe(other_entity, |this, other_entity, event, cx| ...)`. This will return a `Subscription` which deregisters the callback when dropped. Typically `cx.subscribe` happens when creating a new entity and the subscriptions are stored in a `_subscriptions: Vec<Subscription>` field.
## Recent API changes
GPUI has had some changes to its APIs. Always write code using the new APIs:
* `spawn` methods now take async closures (`AsyncFn`), and so should be called like `cx.spawn(async move |cx| ...)`.
* Use `Entity<T>`. This replaces `Model<T>` and `View<T>` which longer exists and should NEVER be used.
* Use `App` references. This replaces `AppContext` which no longer exists and should NEVER be used.
* Use `Context<T>` references. This replaces `ModelContext<T>` which no longer exists and should NEVER be used.
* `Window` is now passed around explicitly. The new interface adds a `Window` reference parameter to some methods, and adds some new "*_in" methods for plumbing `Window`. The old types `WindowContext` and `ViewContext<T>` should NEVER be used.
## General guidelines
- Use `./script/clippy` instead of `cargo clippy`

View File

@@ -45,6 +45,5 @@
"hard_tabs": false,
"formatter": "auto",
"remove_trailing_whitespace_on_save": true,
"ensure_final_newline_on_save": true,
"file_scan_exclusions": ["crates/eval/worktrees/", "crates/eval/repos/"]
"ensure_final_newline_on_save": true
}

663
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -39,9 +39,9 @@ members = [
"crates/credentials_provider",
"crates/dap",
"crates/dap_adapters",
"crates/db",
"crates/debugger_tools",
"crates/debugger_ui",
"crates/db",
"crates/deepseek",
"crates/diagnostics",
"crates/docs_preprocessor",
@@ -109,6 +109,7 @@ members = [
"crates/project",
"crates/project_panel",
"crates/project_symbols",
"crates/prompt_library",
"crates/prompt_store",
"crates/proto",
"crates/recent_projects",
@@ -122,7 +123,6 @@ members = [
"crates/rich_text",
"crates/rope",
"crates/rpc",
"crates/rules_library",
"crates/schema_generator",
"crates/search",
"crates/semantic_index",
@@ -229,7 +229,6 @@ auto_update_ui = { path = "crates/auto_update_ui" }
aws_http_client = { path = "crates/aws_http_client" }
bedrock = { path = "crates/bedrock" }
breadcrumbs = { path = "crates/breadcrumbs" }
buffer_diff = { path = "crates/buffer_diff" }
call = { path = "crates/call" }
channel = { path = "crates/channel" }
cli = { path = "crates/cli" }
@@ -249,10 +248,11 @@ credentials_provider = { path = "crates/credentials_provider" }
dap = { path = "crates/dap" }
dap_adapters = { path = "crates/dap_adapters" }
db = { path = "crates/db" }
debugger_tools = { path = "crates/debugger_tools" }
debugger_ui = { path = "crates/debugger_ui" }
debugger_tools = { path = "crates/debugger_tools" }
deepseek = { path = "crates/deepseek" }
diagnostics = { path = "crates/diagnostics" }
buffer_diff = { path = "crates/buffer_diff" }
editor = { path = "crates/editor" }
extension = { path = "crates/extension" }
extension_host = { path = "crates/extension_host" }
@@ -309,8 +309,8 @@ ollama = { path = "crates/ollama" }
open_ai = { path = "crates/open_ai" }
outline = { path = "crates/outline" }
outline_panel = { path = "crates/outline_panel" }
panel = { path = "crates/panel" }
paths = { path = "crates/paths" }
panel = { path = "crates/panel" }
picker = { path = "crates/picker" }
plugin = { path = "crates/plugin" }
plugin_macros = { path = "crates/plugin_macros" }
@@ -318,6 +318,7 @@ prettier = { path = "crates/prettier" }
project = { path = "crates/project" }
project_panel = { path = "crates/project_panel" }
project_symbols = { path = "crates/project_symbols" }
prompt_library = { path = "crates/prompt_library" }
prompt_store = { path = "crates/prompt_store" }
proto = { path = "crates/proto" }
recent_projects = { path = "crates/recent_projects" }
@@ -330,7 +331,6 @@ reqwest_client = { path = "crates/reqwest_client" }
rich_text = { path = "crates/rich_text" }
rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" }
rules_library = { path = "crates/rules_library" }
search = { path = "crates/search" }
semantic_index = { path = "crates/semantic_index" }
semantic_version = { path = "crates/semantic_version" }
@@ -417,6 +417,7 @@ bitflags = "2.6.0"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
blade-util = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
naga = { version = "23.1.0", features = ["wgsl-in"] }
blake3 = "1.5.3"
bytes = "1.0"
cargo_metadata = "0.19"
@@ -426,16 +427,15 @@ circular-buffer = "1.0"
clap = { version = "4.4", features = ["derive"] }
cocoa = "0.26"
cocoa-foundation = "0.2.0"
core-video = { version = "0.4.3", features = ["metal"] }
convert_case = "0.8.0"
core-foundation = "0.10.0"
core-foundation-sys = "0.8.6"
core-video = { version = "0.4.3", features = ["metal"] }
ctor = "0.4.0"
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "be69a016ba710191b9fdded28c8b042af4b617f7" }
dashmap = "6.0"
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "be69a016ba710191b9fdded28c8b042af4b617f7" }
derive_more = "0.99.17"
dirs = "4.0"
dotenv = "0.15.0"
ec4rs = "1.1"
emojis = "0.6.1"
env_logger = "0.11"
@@ -452,8 +452,8 @@ heck = "0.5"
heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
hex = "0.4.3"
html5ever = "0.27.0"
http = "1.1"
hyper = "0.14"
http = "1.1"
ignore = "0.4.22"
image = "0.25.1"
imara-diff = "0.1.8"
@@ -469,27 +469,23 @@ libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0"
linkme = "0.3.31"
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "c9c189f1c5dd53c624a419ce35bc77ad6a908d18" }
markup5ever_rcdom = "0.3.0"
metal = "0.29"
mlua = { version = "0.10", features = ["lua54", "vendored", "async", "send"] }
naga = { version = "23.1.0", features = ["wgsl-in"] }
nanoid = "0.4"
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
nix = "0.29"
num-format = "0.4.4"
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"
partial-json-fixer = "0.5.3"
pathdiff = "0.2"
pet = { 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-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" }
postage = { version = "0.5", features = ["futures-traits"] }
@@ -503,10 +499,9 @@ pulldown-cmark = { version = "0.12.0", default-features = false }
quote = "1.0.9"
rand = "0.8.5"
rayon = "1.8"
ref-cast = "1.0.24"
regex = "1.5"
repair_json = "0.1.0"
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c770a32f1998d6e999cef3e59e0013e6c4415", default-features = false, features = [
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f6998da16bbca97b6dddda9be7827c50e29", default-features = false, features = [
"charset",
"http2",
"macos-system-configuration",
@@ -518,8 +513,8 @@ rsa = "0.9.6"
runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [
"async-dispatcher-runtime",
] }
rust-embed = { version = "8.4", features = ["include-exclude"] }
rustc-demangle = "0.1.23"
rust-embed = { version = "8.4", features = ["include-exclude"] }
rustc-hash = "2.1.0"
rustls = { version = "0.23.26" }
rustls-platform-verifier = "0.5.0"
@@ -561,16 +556,15 @@ time = { version = "0.3", features = [
"formatting",
] }
tiny_http = "0.8"
toml = "0.8"
tokio = { version = "1" }
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"] }
toml = "0.8"
tower-http = "0.4.4"
tree-sitter = { version = "0.25.3", features = ["wasm"] }
tree-sitter-bash = "0.23"
tree-sitter-c = "0.23"
tree-sitter-cpp = "0.23"
tree-sitter-css = "0.23"
tree-sitter-diff = "0.1.0"
tree-sitter-elixir = "0.3"
tree-sitter-embedded-template = "0.23.0"
tree-sitter-gitcommit = { git = "https://github.com/zed-industries/tree-sitter-git-commit", rev = "88309716a69dd13ab83443721ba6e0b491d37ee9" }
@@ -578,6 +572,7 @@ tree-sitter-go = "0.23"
tree-sitter-go-mod = { git = "https://github.com/camdencheek/tree-sitter-go-mod", rev = "6efb59652d30e0e9cd5f3b3a669afd6f1a926d3c", package = "tree-sitter-gomod" }
tree-sitter-gowork = { git = "https://github.com/zed-industries/tree-sitter-go-work", rev = "acb0617bf7f4fda02c6217676cc64acb89536dc7" }
tree-sitter-heex = { git = "https://github.com/zed-industries/tree-sitter-heex", rev = "1dd45142fbb05562e35b2040c6129c9bca346592" }
tree-sitter-diff = "0.1.0"
tree-sitter-html = "0.23"
tree-sitter-jsdoc = "0.23"
tree-sitter-json = "0.24"
@@ -589,15 +584,15 @@ tree-sitter-rust = "0.24"
tree-sitter-typescript = "0.23"
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "baff0b51c64ef6a1fb1f8390f3ad6015b83ec13a" }
unicase = "2.6"
unicode-script = "0.5.7"
unicode-segmentation = "1.10"
unindent = "0.2.0"
unicode-segmentation = "1.10"
unicode-script = "0.5.7"
url = "2.2"
urlencoding = "2.1.2"
uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
walkdir = "2.3"
wasm-encoder = "0.221"
wasmparser = "0.221"
wasm-encoder = "0.221"
wasmtime = { version = "29", default-features = false, features = [
"async",
"demangle",
@@ -609,8 +604,9 @@ wasmtime-wasi = "29"
which = "6.0.0"
wit-component = "0.221"
workspace-hack = "0.1.0"
zed_llm_client = "0.7.1"
zed_llm_client = "0.6.1"
zstd = "0.11"
metal = "0.29"
[workspace.dependencies.async-stripe]
git = "https://github.com/zed-industries/async-stripe"

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-image-icon lucide-image"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>

Before

Width:  |  Height:  |  Size: 372 B

View File

@@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 12H16" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 6H20" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 18H12" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 402 B

View File

@@ -1,14 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2489_484)">
<path d="M11 8.9V11C8.51716 11 7.48284 11 5 11V10.4L11 5.6V5H5V7.1" stroke="black" stroke-width="1.5"/>
<path d="M1.5 5.5V1.5H5" stroke="black" stroke-opacity="0.5" stroke-width="1.5"/>
<path d="M14.5 5.5V1.5H11" stroke="black" stroke-opacity="0.5" stroke-width="1.5"/>
<path d="M1.5 10.5V14.5H5" stroke="black" stroke-opacity="0.5" stroke-width="1.5"/>
<path d="M14.5 10.5V14.5H11" stroke="black" stroke-opacity="0.5" stroke-width="1.5"/>
</g>
<defs>
<clipPath id="clip0_2489_484">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 687 B

View File

@@ -49,6 +49,15 @@
"down": "menu::SelectNext"
}
},
{
"context": "Prompt",
"bindings": {
"left": "menu::SelectPrevious",
"right": "menu::SelectNext",
"h": "menu::SelectPrevious",
"l": "menu::SelectNext"
}
},
{
"context": "Editor",
"bindings": {
@@ -60,6 +69,8 @@
"shift-tab": "editor::Backtab",
"ctrl-k": "editor::CutToEndOfLine",
// "ctrl-t": "editor::Transpose",
"ctrl-k ctrl-q": "editor::Rewrap",
"ctrl-k q": "editor::Rewrap",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd",
"cut": "editor::Cut",
@@ -104,7 +115,46 @@
"ctrl-shift-end": "editor::SelectToEnd",
"ctrl-a": "editor::SelectAll",
"ctrl-l": "editor::SelectLine",
"ctrl-alt-space": "editor::ShowCharacterPalette"
"ctrl-shift-i": "editor::Format",
"alt-shift-o": "editor::OrganizeImports",
// "cmd-shift-left": ["editor::SelectToBeginningOfLine", {"stop_at_soft_wraps": true, "stop_at_indent": true }],
// "ctrl-shift-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
"shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
// "cmd-shift-right": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
// "ctrl-shift-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
"shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
// "alt-v": ["editor::MovePageUp", { "center_cursor": true }],
"ctrl-alt-space": "editor::ShowCharacterPalette",
"ctrl-;": "editor::ToggleLineNumbers",
"ctrl-'": "editor::ToggleSelectedDiffHunks",
"ctrl-\"": "editor::ExpandAllDiffHunks",
"ctrl-i": "editor::ShowSignatureHelp",
"alt-g b": "editor::ToggleGitBlame",
"menu": "editor::OpenContextMenu",
"shift-f10": "editor::OpenContextMenu",
"ctrl-shift-e": "editor::ToggleEditPrediction",
"f9": "editor::ToggleBreakpoint",
"shift-f9": "editor::EditLogBreakpoint",
"ctrl-shift-backspace": "editor::GoToPreviousChange",
"ctrl-shift-alt-backspace": "editor::GoToNextChange"
}
},
{
"context": "Editor && !agent_diff",
"bindings": {
"ctrl-k ctrl-r": "git::Restore",
"ctrl-alt-y": "git::ToggleStaged",
"alt-y": "git::StageAndNext",
"alt-shift-y": "git::UnstageAndNext"
}
},
{
"context": "AgentDiff",
"bindings": {
"ctrl-y": "agent::Keep",
"ctrl-n": "agent::Reject",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll"
}
},
{
@@ -123,30 +173,7 @@
"ctrl->": "assistant::QuoteSelection",
"ctrl-<": "assistant::InsertIntoEditor",
"ctrl-alt-e": "editor::SelectEnclosingSymbol",
"alt-enter": "editor::OpenSelectionsInMultibuffer",
"ctrl-k ctrl-q": "editor::Rewrap",
"ctrl-k q": "editor::Rewrap",
"ctrl-shift-i": "editor::Format",
"alt-shift-o": "editor::OrganizeImports",
// "cmd-shift-left": ["editor::SelectToBeginningOfLine", {"stop_at_soft_wraps": true, "stop_at_indent": true }],
// "ctrl-shift-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
"shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
// "cmd-shift-right": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
// "ctrl-shift-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
"shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
// "alt-v": ["editor::MovePageUp", { "center_cursor": true }],
"ctrl-;": "editor::ToggleLineNumbers",
"ctrl-'": "editor::ToggleSelectedDiffHunks",
"ctrl-\"": "editor::ExpandAllDiffHunks",
"ctrl-i": "editor::ShowSignatureHelp",
"alt-g b": "editor::ToggleGitBlame",
"menu": "editor::OpenContextMenu",
"shift-f10": "editor::OpenContextMenu",
"ctrl-shift-e": "editor::ToggleEditPrediction",
"f9": "editor::ToggleBreakpoint",
"shift-f9": "editor::EditLogBreakpoint",
"ctrl-shift-backspace": "editor::GoToPreviousChange",
"ctrl-shift-alt-backspace": "editor::GoToNextChange"
"alt-enter": "editor::OpenSelectionsInMultibuffer"
}
},
{
@@ -178,31 +205,6 @@
"ctrl-c": "markdown::Copy"
}
},
{
"context": "Editor && jupyter && !ContextEditor",
"bindings": {
"ctrl-shift-enter": "repl::Run",
"ctrl-alt-enter": "repl::RunInPlace"
}
},
{
"context": "Editor && !agent_diff",
"bindings": {
"ctrl-k ctrl-r": "git::Restore",
"ctrl-alt-y": "git::ToggleStaged",
"alt-y": "git::StageAndNext",
"alt-shift-y": "git::UnstageAndNext"
}
},
{
"context": "AgentDiff",
"bindings": {
"ctrl-y": "agent::Keep",
"ctrl-n": "agent::Reject",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll"
}
},
{
"context": "AssistantPanel",
"bindings": {
@@ -212,113 +214,18 @@
"ctrl-shift-g": "search::SelectPreviousMatch",
"ctrl-alt-/": "assistant::ToggleModelSelector",
"ctrl-k h": "assistant::DeployHistory",
"ctrl-k l": "assistant::OpenRulesLibrary",
"ctrl-k l": "assistant::OpenPromptLibrary",
"new": "assistant::NewChat",
"ctrl-t": "assistant::NewChat",
"ctrl-n": "assistant::NewChat"
}
},
{
"context": "ContextEditor > Editor",
"bindings": {
"ctrl-enter": "assistant::Assist",
"ctrl-shift-enter": "assistant::Edit",
"ctrl-s": "workspace::Save",
"save": "workspace::Save",
"ctrl->": "assistant::QuoteSelection",
"ctrl-<": "assistant::InsertIntoEditor",
"ctrl-alt-/": "assistant::ToggleModelSelector",
"shift-enter": "assistant::Split",
"ctrl-r": "assistant::CycleMessageRole",
"enter": "assistant::ConfirmCommand",
"alt-enter": "editor::Newline"
}
},
{
"context": "AgentPanel",
"bindings": {
"ctrl-n": "agent::NewThread",
"ctrl-alt-n": "agent::NewTextThread",
"ctrl-shift-h": "agent::OpenHistory",
"ctrl-alt-c": "agent::OpenConfiguration",
"ctrl-alt-p": "assistant::OpenRulesLibrary",
"ctrl-i": "agent::ToggleProfileSelector",
"ctrl-alt-/": "assistant::ToggleModelSelector",
"ctrl-shift-a": "agent::ToggleContextPicker",
"ctrl-shift-o": "agent::ToggleNavigationMenu",
"ctrl-shift-i": "agent::ToggleOptionsMenu",
"shift-escape": "agent::ExpandMessageEditor",
"ctrl-e": "agent::ChatMode",
"ctrl-alt-e": "agent::RemoveAllContext"
}
},
{
"context": "AgentPanel > NavigationMenu",
"bindings": {
"shift-backspace": "agent::DeleteRecentlyOpenThread"
}
},
{
"context": "AgentPanel > Markdown",
"bindings": {
"copy": "markdown::CopyAsMarkdown",
"ctrl-c": "markdown::CopyAsMarkdown"
}
},
{
"context": "AgentPanel && prompt_editor",
"bindings": {
"cmd-n": "agent::NewTextThread",
"cmd-alt-t": "agent::NewThread"
}
},
{
"context": "MessageEditor > Editor",
"bindings": {
"enter": "agent::Chat",
"ctrl-i": "agent::ToggleProfileSelector",
"shift-ctrl-r": "agent::OpenAgentDiff"
}
},
{
"context": "EditMessageEditor > Editor",
"bindings": {
"escape": "menu::Cancel",
"enter": "menu::Confirm",
"alt-enter": "editor::Newline"
}
},
{
"context": "AgentFeedbackMessageEditor > Editor",
"bindings": {
"escape": "menu::Cancel",
"enter": "menu::Confirm",
"alt-enter": "editor::Newline"
}
},
{
"context": "ContextStrip",
"bindings": {
"up": "agent::FocusUp",
"right": "agent::FocusRight",
"left": "agent::FocusLeft",
"down": "agent::FocusDown",
"backspace": "agent::RemoveFocusedContext",
"enter": "agent::AcceptSuggestedContext"
}
},
{
"context": "ThreadHistory",
"bindings": {
"backspace": "agent::RemoveSelectedThread"
}
},
{
"context": "PromptLibrary",
"bindings": {
"new": "rules_library::NewRule",
"ctrl-n": "rules_library::NewRule",
"ctrl-shift-s": "rules_library::ToggleDefaultRule"
"new": "prompt_library::NewPrompt",
"ctrl-n": "prompt_library::NewPrompt",
"ctrl-shift-s": "prompt_library::ToggleDefaultPrompt"
}
},
{
@@ -445,6 +352,8 @@
"alt-down": "editor::MoveLineDown",
"ctrl-alt-shift-up": "editor::DuplicateLineUp",
"ctrl-alt-shift-down": "editor::DuplicateLineDown",
"alt-shift-right": "editor::SelectLargerSyntaxNode", // Expand Selection
"alt-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
"ctrl-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
"ctrl-f2": "editor::SelectAllMatches", // Select all occurrences of current word
"ctrl-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand
@@ -452,20 +361,10 @@
"ctrl-shift-up": ["editor::SelectPrevious", { "replace_newest": false }], // editor.action.addSelectionToPreviousFindMatch
"ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }], // editor.action.moveSelectionToNextFindMatch / find_under_expand_skip
"ctrl-k ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch
"ctrl-u": "editor::UndoSelection",
"ctrl-shift-u": "editor::RedoSelection",
"ctrl-\\": "pane::SplitRight"
}
},
{
"context": "Editor && mode == full",
"bindings": {
"ctrl-shift-o": "outline::Toggle",
"ctrl-g": "go_to_line::Toggle",
"alt-shift-right": "editor::SelectLargerSyntaxNode", // Expand Selection
"alt-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
"ctrl-k ctrl-i": "editor::Hover",
"ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }],
"ctrl-u": "editor::UndoSelection",
"ctrl-shift-u": "editor::RedoSelection",
"f8": "editor::GoToDiagnostic",
"shift-f8": "editor::GoToPreviousDiagnostic",
"f2": "editor::Rename",
@@ -499,6 +398,7 @@
"ctrl-.": "editor::ToggleCodeActions",
"ctrl-k r": "editor::RevealInFileManager",
"ctrl-k p": "editor::CopyPath",
"ctrl-\\": "pane::SplitRight",
"ctrl-k v": "markdown::OpenPreviewToTheSide",
"ctrl-shift-v": "markdown::OpenPreview",
"ctrl-alt-shift-c": "editor::DisplayCursorNames",
@@ -506,6 +406,13 @@
"alt-,": "editor::GoToPreviousHunk"
}
},
{
"context": "Editor && mode == full",
"bindings": {
"ctrl-shift-o": "outline::Toggle",
"ctrl-g": "go_to_line::Toggle"
}
},
{
"context": "Workspace",
"bindings": {
@@ -528,7 +435,6 @@
"shift-new": "workspace::NewWindow",
"ctrl-shift-n": "workspace::NewWindow",
"ctrl-`": "terminal_panel::ToggleFocus",
"f10": ["app_menu::OpenApplicationMenu", "Zed"],
"alt-1": ["workspace::ActivatePane", 0],
"alt-2": ["workspace::ActivatePane", 1],
"alt-3": ["workspace::ActivatePane", 2],
@@ -587,7 +493,6 @@
{
"context": "ApplicationMenu",
"bindings": {
"f10": "menu::Cancel",
"left": "app_menu::ActivateMenuLeft",
"right": "app_menu::ActivateMenuRight"
}
@@ -685,7 +590,7 @@
}
},
{
"context": "!ContextEditor > Editor && mode == full",
"context": "Editor && mode == full",
"bindings": {
"alt-enter": "editor::OpenExcerpts",
"shift-enter": "editor::ExpandExcerpts",
@@ -697,6 +602,100 @@
"ctrl-:": "editor::ToggleInlayHints"
}
},
{
"context": "Editor && jupyter && !ContextEditor",
"bindings": {
"ctrl-shift-enter": "repl::Run",
"ctrl-alt-enter": "repl::RunInPlace"
}
},
{
"context": "ContextEditor > Editor",
"bindings": {
"ctrl-enter": "assistant::Assist",
"ctrl-shift-enter": "assistant::Edit",
"ctrl-s": "workspace::Save",
"save": "workspace::Save",
"ctrl->": "assistant::QuoteSelection",
"ctrl-<": "assistant::InsertIntoEditor",
"ctrl-alt-/": "assistant::ToggleModelSelector",
"shift-enter": "assistant::Split",
"ctrl-r": "assistant::CycleMessageRole",
"enter": "assistant::ConfirmCommand",
"alt-enter": "editor::Newline"
}
},
{
"context": "AgentPanel",
"bindings": {
"ctrl-n": "agent::NewThread",
"ctrl-alt-n": "agent::NewTextThread",
"ctrl-shift-h": "agent::OpenHistory",
"ctrl-alt-c": "agent::OpenConfiguration",
"ctrl-alt-p": "assistant::OpenPromptLibrary",
"ctrl-i": "agent::ToggleProfileSelector",
"ctrl-alt-/": "assistant::ToggleModelSelector",
"ctrl-shift-a": "agent::ToggleContextPicker",
"shift-escape": "agent::ExpandMessageEditor",
"ctrl-e": "agent::ChatMode",
"ctrl-alt-e": "agent::RemoveAllContext"
}
},
{
"context": "AgentPanel > Markdown",
"bindings": {
"copy": "markdown::CopyAsMarkdown",
"ctrl-c": "markdown::CopyAsMarkdown"
}
},
{
"context": "AgentPanel && prompt_editor",
"bindings": {
"cmd-n": "agent::NewTextThread",
"cmd-alt-t": "agent::NewThread"
}
},
{
"context": "MessageEditor > Editor",
"bindings": {
"enter": "agent::Chat",
"ctrl-i": "agent::ToggleProfileSelector",
"shift-ctrl-r": "agent::OpenAgentDiff"
}
},
{
"context": "EditMessageEditor > Editor",
"bindings": {
"escape": "menu::Cancel",
"enter": "menu::Confirm",
"alt-enter": "editor::Newline"
}
},
{
"context": "AgentFeedbackMessageEditor > Editor",
"bindings": {
"escape": "menu::Cancel",
"enter": "menu::Confirm",
"alt-enter": "editor::Newline"
}
},
{
"context": "ContextStrip",
"bindings": {
"up": "agent::FocusUp",
"right": "agent::FocusRight",
"left": "agent::FocusLeft",
"down": "agent::FocusDown",
"backspace": "agent::RemoveFocusedContext",
"enter": "agent::AcceptSuggestedContext"
}
},
{
"context": "ThreadHistory",
"bindings": {
"backspace": "agent::RemoveSelectedThread"
}
},
{
"context": "PromptEditor",
"bindings": {
@@ -705,15 +704,6 @@
"ctrl-alt-e": "agent::RemoveAllContext"
}
},
{
"context": "Prompt",
"bindings": {
"left": "menu::SelectPrevious",
"right": "menu::SelectNext",
"h": "menu::SelectPrevious",
"l": "menu::SelectNext"
}
},
{
"context": "ProjectSearchBar && !in_replace",
"bindings": {
@@ -930,7 +920,6 @@
"ctrl-enter": "assistant::InlineAssist",
"alt-b": ["terminal::SendText", "\u001bb"],
"alt-f": ["terminal::SendText", "\u001bf"],
"alt-.": ["terminal::SendText", "\u001b."],
// Overrides for conflicting keybindings
"ctrl-b": ["terminal::SendKeystroke", "ctrl-b"],
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],

View File

@@ -5,8 +5,8 @@
"context": "PromptLibrary",
"use_key_equivalents": true,
"bindings": {
"cmd-n": "rules_library::NewRule",
"cmd-shift-s": "rules_library::ToggleDefaultRule",
"cmd-n": "prompt_library::NewPrompt",
"cmd-shift-s": "prompt_library::ToggleDefaultPrompt",
"cmd-w": "workspace::CloseWindow"
}
},
@@ -62,10 +62,6 @@
}
},
{
// `Editor` context applies to all editor modes
// - auto_height (multi-line: inline assistant, git commit messages, etc)
// - single_line (command palette, renaming a file, etc)
// - full (main editor buffers, assistant text threads)
"context": "Editor",
"use_key_equivalents": true,
"bindings": {
@@ -80,6 +76,8 @@
"ctrl-t": "editor::Transpose",
"ctrl-k": "editor::KillRingCut",
"ctrl-y": "editor::KillRingYank",
"cmd-k cmd-q": "editor::Rewrap",
"cmd-k q": "editor::Rewrap",
"cmd-backspace": "editor::DeleteToBeginningOfLine",
"cmd-delete": "editor::DeleteToEndOfLine",
"alt-backspace": "editor::DeleteToPreviousWordStart",
@@ -108,6 +106,7 @@
"left": "editor::MoveLeft",
"ctrl-f": "editor::MoveRight",
"right": "editor::MoveRight",
"ctrl-l": "editor::ScrollCursorCenter",
"alt-left": "editor::MoveToPreviousWordStart",
"alt-right": "editor::MoveToNextWordEnd",
"cmd-left": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
@@ -136,6 +135,8 @@
"cmd-shift-down": "editor::SelectToEnd",
"cmd-a": "editor::SelectAll",
"cmd-l": "editor::SelectLine",
"cmd-shift-i": "editor::Format",
"alt-shift-o": "editor::OrganizeImports",
"cmd-shift-left": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
"shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
"ctrl-shift-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
@@ -144,7 +145,17 @@
"ctrl-shift-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
"ctrl-v": ["editor::MovePageDown", { "center_cursor": true }],
"ctrl-shift-v": ["editor::MovePageUp", { "center_cursor": true }],
"ctrl-cmd-space": "editor::ShowCharacterPalette"
"ctrl-cmd-space": "editor::ShowCharacterPalette",
"cmd-;": "editor::ToggleLineNumbers",
"cmd-'": "editor::ToggleSelectedDiffHunks",
"cmd-\"": "editor::ExpandAllDiffHunks",
"cmd-alt-g b": "editor::ToggleGitBlame",
"cmd-i": "editor::ShowSignatureHelp",
"f9": "editor::ToggleBreakpoint",
"shift-f9": "editor::EditLogBreakpoint",
"ctrl-f12": "editor::GoToDeclaration",
"alt-ctrl-f12": "editor::GoToDeclarationSplit",
"ctrl-cmd-e": "editor::ToggleEditPrediction"
}
},
{
@@ -163,22 +174,7 @@
"cmd->": "assistant::QuoteSelection",
"cmd-<": "assistant::InsertIntoEditor",
"cmd-alt-e": "editor::SelectEnclosingSymbol",
"cmd-k cmd-q": "editor::Rewrap",
"cmd-k q": "editor::Rewrap",
"ctrl-l": "editor::ScrollCursorCenter",
"cmd-i": "editor::ShowSignatureHelp",
"cmd-shift-i": "editor::Format",
"alt-shift-o": "editor::OrganizeImports",
"ctrl-cmd-e": "editor::ToggleEditPrediction",
"f9": "editor::ToggleBreakpoint",
"shift-f9": "editor::EditLogBreakpoint",
"ctrl-f12": "editor::GoToDeclaration",
"alt-ctrl-f12": "editor::GoToDeclarationSplit",
"alt-enter": "editor::OpenSelectionsInMultibuffer",
"cmd-;": "editor::ToggleLineNumbers",
"cmd-'": "editor::ToggleSelectedDiffHunks",
"cmd-\"": "editor::ExpandAllDiffHunks",
"cmd-alt-g b": "editor::ToggleGitBlame"
"alt-enter": "editor::OpenSelectionsInMultibuffer"
}
},
{
@@ -261,7 +257,7 @@
"cmd-shift-g": "search::SelectPreviousMatch",
"cmd-alt-/": "assistant::ToggleModelSelector",
"cmd-k h": "assistant::DeployHistory",
"cmd-k l": "assistant::OpenRulesLibrary",
"cmd-k l": "assistant::OpenPromptLibrary",
"cmd-t": "assistant::NewChat",
"cmd-n": "assistant::NewChat"
}
@@ -290,23 +286,15 @@
"cmd-alt-n": "agent::NewTextThread",
"cmd-shift-h": "agent::OpenHistory",
"cmd-alt-c": "agent::OpenConfiguration",
"cmd-alt-p": "assistant::OpenRulesLibrary",
"cmd-alt-p": "assistant::OpenPromptLibrary",
"cmd-i": "agent::ToggleProfileSelector",
"cmd-alt-/": "assistant::ToggleModelSelector",
"cmd-shift-a": "agent::ToggleContextPicker",
"cmd-shift-o": "agent::ToggleNavigationMenu",
"cmd-shift-i": "agent::ToggleOptionsMenu",
"shift-escape": "agent::ExpandMessageEditor",
"cmd-e": "agent::ChatMode",
"cmd-alt-e": "agent::RemoveAllContext"
}
},
{
"context": "AgentPanel > NavigationMenu",
"bindings": {
"shift-backspace": "agent::DeleteRecentlyOpenThread"
}
},
{
"context": "AgentPanel > Markdown",
"use_key_equivalents": true,
@@ -502,6 +490,8 @@
"alt-down": "editor::MoveLineDown",
"alt-shift-up": "editor::DuplicateLineUp",
"alt-shift-down": "editor::DuplicateLineDown",
"ctrl-shift-right": "editor::SelectLargerSyntaxNode", // Expand Selection
"ctrl-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
"cmd-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand
"cmd-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
"cmd-f2": "editor::SelectAllMatches", // Select all occurrences of current word
@@ -511,18 +501,21 @@
// defaults write com.apple.symbolichotkeys AppleSymbolicHotKeys -dict-add 70 '<dict><key>enabled</key><false/></dict>'
"ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": false }], // editor.action.addSelectionToPreviousFindMatch
"cmd-k ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch
"cmd-k cmd-i": "editor::Hover",
"cmd-/": ["editor::ToggleComments", { "advance_downwards": false }],
"cmd-u": "editor::UndoSelection",
"cmd-shift-u": "editor::RedoSelection",
"f8": "editor::GoToDiagnostic",
"shift-f8": "editor::GoToPreviousDiagnostic",
"f2": "editor::Rename",
"f12": "editor::GoToDefinition",
"alt-f12": "editor::GoToDefinitionSplit",
"cmd-f12": "editor::GoToTypeDefinition",
"shift-f12": "editor::GoToImplementation",
"alt-cmd-f12": "editor::GoToTypeDefinitionSplit",
"alt-shift-f12": "editor::FindAllReferences",
"cmd-|": "editor::MoveToEnclosingBracket",
"ctrl-m": "editor::MoveToEnclosingBracket",
"cmd-\\": "pane::SplitRight"
}
},
// VSCode Bindings full Editors (e.g. buffers)
{
"context": "Editor && mode == full",
"use_key_equivalents": true,
"bindings": {
"alt-cmd-[": "editor::Fold",
"alt-cmd-]": "editor::UnfoldLines",
"cmd-k cmd-l": "editor::ToggleFold",
@@ -546,28 +539,22 @@
"cmd-.": "editor::ToggleCodeActions",
"cmd-k r": "editor::RevealInFileManager",
"cmd-k p": "editor::CopyPath",
"ctrl-shift-right": "editor::SelectLargerSyntaxNode", // Expand Selection
"ctrl-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
"cmd-k cmd-i": "editor::Hover",
"cmd-/": ["editor::ToggleComments", { "advance_downwards": false }],
"f8": "editor::GoToDiagnostic",
"shift-f8": "editor::GoToPreviousDiagnostic",
"f2": "editor::Rename",
"f12": "editor::GoToDefinition",
"alt-f12": "editor::GoToDefinitionSplit",
"cmd-f12": "editor::GoToTypeDefinition",
"shift-f12": "editor::GoToImplementation",
"alt-cmd-f12": "editor::GoToTypeDefinitionSplit",
"alt-shift-f12": "editor::FindAllReferences",
"cmd-\\": "pane::SplitRight",
"cmd-k v": "markdown::OpenPreviewToTheSide",
"cmd-shift-v": "markdown::OpenPreview",
"ctrl-cmd-c": "editor::DisplayCursorNames",
"cmd-shift-o": "outline::Toggle",
"ctrl-g": "go_to_line::Toggle",
"cmd-shift-backspace": "editor::GoToPreviousChange",
"cmd-shift-alt-backspace": "editor::GoToNextChange"
}
},
{
"context": "Editor && mode == full",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-o": "outline::Toggle",
"ctrl-g": "go_to_line::Toggle"
}
},
{
"context": "Pane",
"use_key_equivalents": true,
@@ -751,7 +738,7 @@
}
},
{
"context": "!ContextEditor > Editor && mode == full",
"context": "Editor && mode == full",
"use_key_equivalents": true,
"bindings": {
"alt-enter": "editor::OpenExcerpts",
@@ -1018,7 +1005,6 @@
"alt-right": ["terminal::SendText", "\u001bf"],
"alt-b": ["terminal::SendText", "\u001bb"],
"alt-f": ["terminal::SendText", "\u001bf"],
"alt-.": ["terminal::SendText", "\u001b."],
// There are conflicting bindings for these keys in the global context.
// these bindings override them, remove at your own risk:
"up": ["terminal::SendKeystroke", "up"],
@@ -1041,10 +1027,10 @@
// 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-alt-up": "pane::SplitUp",
"ctrl-alt-down": "pane::SplitDown",
"ctrl-alt-left": "pane::SplitLeft",
"ctrl-alt-right": "pane::SplitRight"
"ctrl-k up": "pane::SplitUp",
"ctrl-k down": "pane::SplitDown",
"ctrl-k left": "pane::SplitLeft",
"ctrl-k right": "pane::SplitRight"
}
},
{

View File

@@ -10,6 +10,11 @@
{
"context": "Editor",
"bindings": {
"ctrl-shift-l": "language_selector::Toggle", // grammar-selector:show
"ctrl-|": "pane::RevealInProjectPanel", // tree-view:reveal-active-file
"ctrl-b": "editor::GoToDefinition", // fuzzy-finder:toggle-buffer-finder
"ctrl-alt-b": "editor::GoToDefinitionSplit", // N/A: From JetBrains
"ctrl-<": "editor::ScrollCursorCenter", // editor:scroll-to-cursor
"f3": ["editor::SelectNext", { "replace_newest": true }], // find-and-replace:find-next
"shift-f3": ["editor::SelectPrevious", { "replace_newest": true }], //find-and-replace:find-previous
"alt-shift-down": "editor::AddSelectionBelow", // editor:add-selection-below
@@ -20,19 +25,14 @@
"ctrl-shift-d": "editor::DuplicateLineDown", // editor:duplicate-lines
"ctrl-up": "editor::MoveLineUp", // editor:move-line-up
"ctrl-down": "editor::MoveLineDown", // editor:move-line-down
"ctrl-\\": "workspace::ToggleLeftDock" // tree-view:toggle (overrides bind in default keymap)
"ctrl-\\": "workspace::ToggleLeftDock", // tree-view:toggle
"ctrl-shift-m": "markdown::OpenPreviewToTheSide" // markdown-preview:toggle
}
},
{
"context": "Editor && mode == full",
"bindings": {
"ctrl-shift-l": "language_selector::Toggle", // grammar-selector:show
"ctrl-|": "pane::RevealInProjectPanel", // tree-view:reveal-active-file
"ctrl-b": "editor::GoToDefinition", // fuzzy-finder:toggle-buffer-finder
"ctrl-alt-b": "editor::GoToDefinitionSplit", // N/A: From JetBrains
"ctrl-<": "editor::ScrollCursorCenter", // editor:scroll-to-cursor
"ctrl-r": "outline::Toggle", // symbols-view:toggle-project-symbols
"ctrl-shift-m": "markdown::OpenPreviewToTheSide" // markdown-preview:toggle
"ctrl-r": "outline::Toggle" // symbols-view:toggle-project-symbols
}
},
{

View File

@@ -32,15 +32,24 @@
"ctrl-alt-down": "editor::AddSelectionBelow",
"ctrl-shift-up": "editor::MoveLineUp",
"ctrl-shift-down": "editor::MoveLineDown",
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
"ctrl-shift-l": "editor::SplitSelectionIntoLines",
"ctrl-shift-a": "editor::SelectLargerSyntaxNode",
"ctrl-shift-d": "editor::DuplicateSelection",
"alt-f3": "editor::SelectAllMatches", // find_all_under
// "ctrl-f3": "", // find_under (cancels any selections)
// "cmd-alt-shift-g": "" // find_under_prev (cancels any selections)
"f9": "editor::SortLinesCaseSensitive",
"ctrl-f9": "editor::SortLinesCaseInsensitive",
"f12": "editor::GoToDefinition",
"ctrl-f12": "editor::GoToDefinitionSplit",
"shift-f12": "editor::FindAllReferences",
"ctrl-shift-f12": "editor::FindAllReferences",
"ctrl-.": "editor::GoToHunk",
"ctrl-,": "editor::GoToPreviousHunk",
"ctrl-k ctrl-u": "editor::ConvertToUpperCase",
"ctrl-k ctrl-l": "editor::ConvertToLowerCase",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd",
"f3": "editor::FindNextMatch",
@@ -50,16 +59,7 @@
{
"context": "Editor && mode == full",
"bindings": {
"ctrl-r": "outline::Toggle",
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
"ctrl-shift-a": "editor::SelectLargerSyntaxNode",
"f12": "editor::GoToDefinition",
"ctrl-f12": "editor::GoToDefinitionSplit",
"shift-f12": "editor::FindAllReferences",
"ctrl-shift-f12": "editor::FindAllReferences",
"ctrl-.": "editor::GoToHunk",
"ctrl-,": "editor::GoToPreviousHunk",
"shift-alt-m": "markdown::OpenPreviewToTheSide"
"ctrl-r": "outline::Toggle"
}
},
{

View File

@@ -10,6 +10,11 @@
{
"context": "Editor",
"bindings": {
"ctrl-shift-l": "language_selector::Toggle",
"cmd-|": "pane::RevealInProjectPanel",
"cmd-b": "editor::GoToDefinition",
"alt-cmd-b": "editor::GoToDefinitionSplit",
"cmd-<": "editor::ScrollCursorCenter",
"cmd-g": ["editor::SelectNext", { "replace_newest": true }],
"cmd-shift-g": ["editor::SelectPrevious", { "replace_newest": true }],
"ctrl-shift-down": "editor::AddSelectionBelow",
@@ -21,18 +26,13 @@
"cmd-shift-d": "editor::DuplicateLineDown",
"ctrl-cmd-up": "editor::MoveLineUp",
"ctrl-cmd-down": "editor::MoveLineDown",
"cmd-\\": "workspace::ToggleLeftDock", // overrides bind in default keymap
"cmd-\\": "workspace::ToggleLeftDock",
"ctrl-shift-m": "markdown::OpenPreviewToTheSide"
}
},
{
"context": "Editor && mode == full",
"bindings": {
"ctrl-shift-l": "language_selector::Toggle",
"cmd-|": "pane::RevealInProjectPanel",
"cmd-b": "editor::GoToDefinition",
"alt-cmd-b": "editor::GoToDefinitionSplit",
"cmd-<": "editor::ScrollCursorCenter",
"cmd-r": "outline::Toggle"
}
},

View File

@@ -50,12 +50,6 @@
"] -": "vim::NextLesserIndent",
"] +": "vim::NextGreaterIndent",
"] =": "vim::NextSameIndent",
"] b": "pane::ActivateNextItem",
"[ b": "pane::ActivatePreviousItem",
"] shift-b": "pane::ActivateLastItem",
"[ shift-b": ["pane::ActivateItem", 0],
"] space": "vim::InsertEmptyLineBelow",
"[ space": "vim::InsertEmptyLineAbove",
// Word motions
"w": "vim::NextWordStart",
"e": "vim::NextWordEnd",
@@ -114,11 +108,7 @@
"ctrl-e": "vim::LineDown",
"ctrl-y": "vim::LineUp",
// "g" commands
"g shift-r": "vim::PushReplaceWithRegister",
"g r n": "editor::Rename",
"g r r": "editor::FindAllReferences",
"g r i": "editor::GoToImplementation",
"g r a": "editor::ToggleCodeActions",
"g r": "vim::PushReplaceWithRegister",
"g g": "vim::StartOfDocument",
"g h": "editor::Hover",
"g t": "pane::ActivateNextItem",
@@ -137,7 +127,6 @@
"g <": ["editor::SelectPrevious", { "replace_newest": true }],
"g a": "editor::SelectAllMatches",
"g s": "outline::Toggle",
"g shift-o": "outline::Toggle",
"g shift-s": "project_symbols::Toggle",
"g .": "editor::ToggleCodeActions", // zed specific
"g shift-a": "editor::FindAllReferences", // zed specific
@@ -316,7 +305,7 @@
"!": "vim::ShellCommand",
"i": ["vim::PushObject", { "around": false }],
"a": ["vim::PushObject", { "around": true }],
"g shift-r": ["vim::Paste", { "preserve_clipboard": true }],
"g r": ["vim::Paste", { "preserve_clipboard": true }],
"g c": "vim::ToggleComments",
"g q": "vim::Rewrap",
"g ?": "vim::ConvertToRot13",
@@ -350,8 +339,7 @@
"ctrl-shift-q": ["vim::PushLiteral", {}],
"ctrl-r": "vim::PushRegister",
"insert": "vim::ToggleReplace",
"ctrl-o": "vim::TemporaryNormal",
"ctrl-s": "editor::ShowSignatureHelp"
"ctrl-o": "vim::TemporaryNormal"
}
},
{
@@ -516,14 +504,12 @@
"'": "vim::Quotes",
"`": "vim::BackQuotes",
"\"": "vim::DoubleQuotes",
// "q": "vim::AnyQuotes",
"q": "vim::MiniQuotes",
"q": "vim::AnyQuotes",
"|": "vim::VerticalBars",
"(": "vim::Parentheses",
")": "vim::Parentheses",
"b": "vim::Parentheses",
// "b": "vim::AnyBrackets",
// "b": "vim::MiniBrackets",
"[": "vim::SquareBrackets",
"]": "vim::SquareBrackets",
"r": "vim::SquareBrackets",
@@ -644,10 +630,9 @@
}
},
{
"context": "vim_operator == gR",
"context": "vim_operator == gr",
"bindings": {
"r": "vim::CurrentLine",
"shift-r": "vim::CurrentLine"
"r": "vim::CurrentLine"
}
},
{
@@ -708,7 +693,7 @@
}
},
{
"context": "GitPanel || ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || DebugPanel",
"context": "GitPanel || ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView",
"bindings": {
// window related commands (ctrl-w X)
"ctrl-w": null,
@@ -845,13 +830,5 @@
// and Windows.
"alt-l": "editor::AcceptEditPrediction"
}
},
{
// Fixes https://github.com/zed-industries/zed/issues/29095 by ensuring that
// the last binding for editor::ToggleComments is not ctrl-c.
"context": "hack_to_fix_ctrl-c",
"bindings": {
"g c": "editor::ToggleComments"
}
}
]

View File

@@ -8,6 +8,16 @@ You are a highly skilled software engineer with extensive knowledge in many prog
4. NEVER lie or make things up.
5. Refrain from apologizing all the time when results are unexpected. Instead, just try your best to proceed or explain the circumstances to the user without apologizing.
## Searching and Reading
If you are unsure about the answer to the user's request or how to satiate their request, you should gather more information.
This can be done with additional tool calls, asking clarifying questions, etc.
For example, if you've performed a semantic search, and the results may not fully answer the user's request, or merit gathering more information, feel free to call more tools. Similarly, if you've performed an edit that may partially
satiate the user's query, but you're not confident, gather more information or use more tools before ending your turn.
Bias towards not asking the user for help if you can find the answer yourself.
## Tool Use
1. Make sure to adhere to the tools schema.
@@ -15,102 +25,7 @@ You are a highly skilled software engineer with extensive knowledge in many prog
3. DO NOT use tools to access items that are already available in the context section.
4. Use only the tools that are currently available.
5. DO NOT use a tool that is not available just because it appears in the conversation. This means the user turned it off.
6. NEVER run commands that don't terminate on their own such as web servers (like `npm run start`, `npm run dev`, `python -m http.server`, etc) or file watchers.
## Searching and Reading
If you are unsure how to fulfill the user's request, gather more information with tool calls and/or clarifying questions.
{{! TODO: If there are files, we should mention it but otherwise omit that fact }}
If appropriate, use tool calls to explore the current project, which contains the following root directories:
{{#each worktrees}}
- `{{root_name}}`
{{/each}}
- Bias towards not asking the user for help if you can find the answer yourself.
- When providing paths to tools, the path should always begin with a path that starts with a project root directory listed above.
- Before you read or edit a file, you must first find the full path. DO NOT ever guess a file path!
{{# if (has_tool 'grep') }}
- When looking for symbols in the project, prefer the `grep` tool.
- As you learn about the structure of the project, use that information to scope `grep` searches to targeted subtrees of the project.
- The user might specify a partial file path. If you don't know the full path, use `find_path` (not `grep`) before you read the file.
{{/if}}
## Code Block Formatting
Whenever you mention a code block, you MUST use ONLY use the following format:
```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.
## Fixing Diagnostics
1. Make 1-2 attempts at fixing diagnostics, then defer to the user.
@@ -135,6 +50,12 @@ Otherwise, follow debugging best practices:
Operating System: {{os}}
Default Shell: {{shell}}
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}}
- `{{root_name}}`
{{/each}}
{{#if (or has_rules has_default_user_rules)}}
## User's Custom Instructions
@@ -152,9 +73,9 @@ There are project rules that apply to these root directories:
{{/each}}
{{/if}}
{{#if has_user_rules}}
{{#if has_default_user_rules}}
The user has specified the following rules that should be applied:
{{#each user_rules}}
{{#each default_user_rules}}
{{#if title}}
Rules title: {{title}}

View File

@@ -167,23 +167,7 @@
// Default: not set, defaults to "bar"
"cursor_shape": null,
// Determines when the mouse cursor should be hidden in an editor or input box.
//
// 1. Never hide the mouse cursor:
// "never"
// 2. Hide only when typing:
// "on_typing"
// 3. Hide on both typing and cursor movement:
// "on_typing_and_movement"
"hide_mouse": "on_typing_and_movement",
// Determines how snippets are sorted relative to other completion items.
//
// 1. Place snippets at the top of the completion list:
// "top"
// 2. Place snippets normally without any preference:
// "inline"
// 3. Place snippets at the bottom of the completion list:
// "bottom"
"snippet_sort_order": "inline",
// How to highlight the current line in the editor.
//
// 1. Don't highlight the current line:
@@ -197,6 +181,8 @@
"current_line_highlight": "all",
// Whether to highlight all occurrences of the selected text in an editor.
"selection_highlight": true,
// The debounce delay before querying highlights based on the selected text.
"selection_highlight_debounce": 50,
// The debounce delay before querying highlights from the language
// server based on the current cursor location.
"lsp_highlight_debounce": 75,
@@ -226,9 +212,16 @@
// Hide the values of in variables from visual display in private files
"redact_private_values": false,
// The default number of lines to expand excerpts in the multibuffer by.
"expand_excerpt_lines": 5,
"expand_excerpt_lines": 3,
// Globs to match against file paths to determine if a file is private.
"private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"],
"private_files": [
"**/.env*",
"**/*.pem",
"**/*.key",
"**/*.cert",
"**/*.crt",
"**/secrets.yml"
],
// Whether to use additional LSP queries to format (and amend) the code after
// every "trigger" symbol input, defined by LSP server capabilities.
"use_on_type_format": true,
@@ -601,13 +594,6 @@
//
// Default: main
"fallback_branch_name": "main",
// Whether to sort entries in the panel by path
// or by status (the default).
//
// Default: false
"sort_by_path": false,
"scrollbar": {
// When to show the scrollbar in the git panel.
//
@@ -669,9 +655,9 @@
"fetch": true,
"list_directory": false,
"now": true,
"find_path": true,
"path_search": true,
"read_file": true,
"grep": true,
"regex_search": true,
"thinking": true,
"web_search": true
}
@@ -693,20 +679,15 @@
"list_directory": true,
"move_path": false,
"now": false,
"find_path": true,
"path_search": true,
"read_file": true,
"grep": true,
"regex_search": true,
"rename": false,
"symbol_info": false,
"terminal": true,
"thinking": true,
"web_search": true
}
},
"manual": {
"name": "Manual",
"enable_all_context_servers": false,
"tools": {}
}
},
// Where to show notifications when an agent has either completed
@@ -740,7 +721,9 @@
// The list of language servers to use (or disable) for all languages.
//
// This is typically customized on a per-language basis.
"language_servers": ["..."],
"language_servers": [
"..."
],
// When to automatically save edited buffers. This setting can
// take four values.
//
@@ -936,7 +919,9 @@
// for files that are not tracked by git, but are still important to your project. Note that globs
// that are overly broad can slow down Zed's file scanning. `file_scan_exclusions` takes
// precedence over these inclusions.
"file_scan_inclusions": [".env*"],
"file_scan_inclusions": [
".env*"
],
// Git gutter behavior configuration.
"git": {
// Control whether the git gutter is shown. May take 2 values:
@@ -988,7 +973,15 @@
// Any addition to this list will be merged with the default list.
// Globs are matched relative to the worktree root,
// except when starting with a slash (/) or equivalent in Windows.
"disabled_globs": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/.dev.vars", "**/secrets.yml"],
"disabled_globs": [
"**/.env*",
"**/*.pem",
"**/*.key",
"**/*.cert",
"**/*.crt",
"**/.dev.vars",
"**/secrets.yml"
],
// When to show edit predictions previews in buffer.
// This setting takes two possible values:
// 1. Display predictions inline when there are no language server completions available.
@@ -1121,7 +1114,12 @@
// Default directories to search for virtual environments, relative
// to the current working directory. We recommend overriding this
// in your project's settings, rather than globally.
"directories": [".env", "env", ".venv", "venv"],
"directories": [
".env",
"env",
".venv",
"venv"
],
// Can also be `csh`, `fish`, `nushell` and `power_shell`
"activate_script": "default"
}
@@ -1185,8 +1183,15 @@
// }
//
"file_types": {
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json"],
"Shell Script": [".env.*"]
"JSONC": [
"**/.zed/**/*.json",
"**/zed/**/*.json",
"**/Zed/**/*.json",
"**/.vscode/**/*.json"
],
"Shell Script": [
".env.*"
]
},
// By default use a recent system version of node, or install our own.
// You can override this to use a version of node that is not in $PATH with:
@@ -1259,10 +1264,15 @@
// Different settings for specific languages.
"languages": {
"Astro": {
"language_servers": ["astro-language-server", "..."],
"language_servers": [
"astro-language-server",
"..."
],
"prettier": {
"allowed": true,
"plugins": ["prettier-plugin-astro"]
"plugins": [
"prettier-plugin-astro"
]
}
},
"Blade": {
@@ -1298,10 +1308,19 @@
"ensure_final_newline_on_save": false
},
"Elixir": {
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
"language_servers": [
"elixir-ls",
"!next-ls",
"!lexical",
"..."
]
},
"Erlang": {
"language_servers": ["erlang-ls", "!elp", "..."]
"language_servers": [
"erlang-ls",
"!elp",
"..."
]
},
"Git Commit": {
"allow_rewrap": "anywhere"
@@ -1317,7 +1336,12 @@
}
},
"HEEX": {
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
"language_servers": [
"elixir-ls",
"!next-ls",
"!lexical",
"..."
]
},
"HTML": {
"prettier": {
@@ -1327,11 +1351,17 @@
"Java": {
"prettier": {
"allowed": true,
"plugins": ["prettier-plugin-java"]
"plugins": [
"prettier-plugin-java"
]
}
},
"JavaScript": {
"language_servers": ["!typescript-language-server", "vtsls", "..."],
"language_servers": [
"!typescript-language-server",
"vtsls",
"..."
],
"prettier": {
"allowed": true
}
@@ -1349,7 +1379,10 @@
"LaTeX": {
"format_on_save": "on",
"formatter": "language_server",
"language_servers": ["texlab", "..."],
"language_servers": [
"texlab",
"..."
],
"prettier": {
"allowed": false
}
@@ -1364,10 +1397,16 @@
}
},
"PHP": {
"language_servers": ["phpactor", "!intelephense", "..."],
"language_servers": [
"phpactor",
"!intelephense",
"..."
],
"prettier": {
"allowed": true,
"plugins": ["@prettier/plugin-php"],
"plugins": [
"@prettier/plugin-php"
],
"parser": "php"
}
},
@@ -1375,7 +1414,12 @@
"allow_rewrap": "anywhere"
},
"Ruby": {
"language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "..."]
"language_servers": [
"solargraph",
"!ruby-lsp",
"!rubocop",
"..."
]
},
"SCSS": {
"prettier": {
@@ -1385,21 +1429,36 @@
"SQL": {
"prettier": {
"allowed": true,
"plugins": ["prettier-plugin-sql"]
"plugins": [
"prettier-plugin-sql"
]
}
},
"Starlark": {
"language_servers": ["starpls", "!buck2-lsp", "..."]
"language_servers": [
"starpls",
"!buck2-lsp",
"..."
]
},
"Svelte": {
"language_servers": ["svelte-language-server", "..."],
"language_servers": [
"svelte-language-server",
"..."
],
"prettier": {
"allowed": true,
"plugins": ["prettier-plugin-svelte"]
"plugins": [
"prettier-plugin-svelte"
]
}
},
"TSX": {
"language_servers": ["!typescript-language-server", "vtsls", "..."],
"language_servers": [
"!typescript-language-server",
"vtsls",
"..."
],
"prettier": {
"allowed": true
}
@@ -1410,13 +1469,20 @@
}
},
"TypeScript": {
"language_servers": ["!typescript-language-server", "vtsls", "..."],
"language_servers": [
"!typescript-language-server",
"vtsls",
"..."
],
"prettier": {
"allowed": true
}
},
"Vue.js": {
"language_servers": ["vue-language-server", "..."],
"language_servers": [
"vue-language-server",
"..."
],
"prettier": {
"allowed": true
}
@@ -1424,7 +1490,9 @@
"XML": {
"prettier": {
"allowed": true,
"plugins": ["@prettier/plugin-xml"]
"plugins": [
"@prettier/plugin-xml"
]
}
},
"YAML": {
@@ -1433,7 +1501,10 @@
}
},
"Zig": {
"language_servers": ["zls", "..."]
"language_servers": [
"zls",
"..."
]
}
},
// Different settings for specific language models.
@@ -1517,12 +1588,7 @@
"use_multiline_find": false,
"use_smartcase_find": false,
"highlight_on_yank_duration": 200,
"custom_digraphs": {},
// Cursor shape for the each mode.
// Specify the mode as the key and the shape as the value.
// The mode can be one of the following: "normal", "replace", "insert", "visual".
// The shape can be one of the following: "block", "bar", "underline", "hollow".
"cursor_shape": {}
"custom_digraphs": {}
},
// The server to connect to. If the environment variable
// ZED_SERVER_URL is set, it will override this setting.

View File

@@ -1,7 +1,2 @@
allow-private-module-inception = true
avoid-breaking-exported-api = false
ignore-interior-mutability = [
# Suppresses clippy::mutable_key_type, which is a false positive as the Eq
# and Hash impls do not use fields with interior mutability.
"agent::context::AgentContextKey"
]

View File

@@ -28,6 +28,7 @@ async-watch.workspace = true
buffer_diff.workspace = true
chrono.workspace = true
client.workspace = true
clock.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
component.workspace = true
@@ -60,12 +61,10 @@ ordered-float.workspace = true
parking_lot.workspace = true
paths.workspace = true
picker.workspace = true
postage.workspace = true
project.workspace = true
rules_library.workspace = true
prompt_library.workspace = true
prompt_store.workspace = true
proto.workspace = true
ref-cast.workspace = true
release_channel.workspace = true
rope.workspace = true
schemars.workspace = true

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
use crate::{Keep, KeepAll, Reject, RejectAll, Thread, ThreadEvent, ui::AnimatedLabel};
use crate::{Keep, KeepAll, Reject, RejectAll, Thread, ThreadEvent};
use anyhow::Result;
use buffer_diff::DiffHunkStatus;
use collections::{HashMap, HashSet};
@@ -8,8 +8,8 @@ use editor::{
scroll::Autoscroll,
};
use gpui::{
Action, AnyElement, AnyView, App, Empty, Entity, EventEmitter, FocusHandle, Focusable,
SharedString, Subscription, Task, WeakEntity, Window, prelude::*,
Action, AnyElement, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, SharedString,
Subscription, Task, WeakEntity, Window, prelude::*,
};
use language::{Capability, DiskState, OffsetRangeExt, Point};
use multi_buffer::PathKey;
@@ -307,10 +307,6 @@ impl AgentDiff {
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.thread.read(cx).is_generating() {
return;
}
let snapshot = self.multibuffer.read(cx).snapshot(cx);
let diff_hunks_in_ranges = self
.editor
@@ -343,10 +339,6 @@ impl AgentDiff {
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.thread.read(cx).is_generating() {
return;
}
let snapshot = self.multibuffer.read(cx).snapshot(cx);
let diff_hunks_in_ranges = self
.editor
@@ -597,10 +589,6 @@ impl Item for AgentDiff {
editor.added_to_workspace(workspace, window, cx)
});
}
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
"Agent Diff".into()
}
}
impl Render for AgentDiff {
@@ -662,11 +650,6 @@ fn render_diff_hunk_controls(
cx: &mut App,
) -> AnyElement {
let editor = editor.clone();
if agent_diff.read(cx).thread.read(cx).is_generating() {
return Empty.into_any();
}
h_flex()
.h(line_height)
.mr_0p5()
@@ -874,14 +857,8 @@ impl Render for AgentDiffToolbar {
None => return div(),
};
let is_generating = agent_diff.read(cx).thread.read(cx).is_generating();
if is_generating {
return div()
.w(rems(6.5625)) // Arbitrary 105px size—so the label doesn't dance around
.child(AnimatedLabel::new("Generating"));
}
let is_empty = agent_diff.read(cx).multibuffer.read(cx).is_empty();
if is_empty {
return div();
}
@@ -951,7 +928,6 @@ mod tests {
ThemeSettings::register(cx);
ContextServerSettings::register(cx);
EditorSettings::register(cx);
language_model::init_settings(cx);
});
let fs = FakeFs::new(cx.executor());
@@ -967,13 +943,11 @@ mod tests {
})
.unwrap();
let prompt_store = None;
let thread_store = cx
.update(|cx| {
ThreadStore::load(
project.clone(),
cx.new(|_| ToolWorkingSet::default()),
prompt_store,
Arc::new(PromptBuilder::new(None).unwrap()),
cx,
)
@@ -995,7 +969,7 @@ mod tests {
.await
.unwrap();
cx.update(|_, cx| {
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
buffer.update(cx, |buffer, cx| {
buffer
.edit(

View File

@@ -39,9 +39,8 @@ use thread::ThreadId;
pub use crate::active_thread::ActiveThread;
use crate::assistant_configuration::{AddContextServerModal, ManageProfilesModal};
pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate};
pub use crate::context::{ContextLoadResult, LoadedContext};
pub use crate::inline_assistant::InlineAssistant;
pub use crate::thread::{Message, MessageSegment, Thread, ThreadEvent};
pub use crate::thread::{Message, RequestKind, Thread, ThreadEvent};
pub use crate::thread_store::ThreadStore;
pub use agent_diff::{AgentDiff, AgentDiffToolbar};
@@ -50,9 +49,6 @@ actions!(
[
NewTextThread,
ToggleContextPicker,
ToggleNavigationMenu,
ToggleOptionsMenu,
DeleteRecentlyOpenThread,
ToggleProfileSelector,
RemoveAllContext,
ExpandMessageEditor,

View File

@@ -16,7 +16,7 @@ use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageMod
use settings::{Settings, update_settings_file};
use ui::{
Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Scrollbar, ScrollbarState,
Switch, SwitchColor, Tooltip, prelude::*,
Switch, Tooltip, prelude::*,
};
use util::ResultExt as _;
use zed_actions::ExtensionCategoryFilter;
@@ -236,7 +236,6 @@ impl AssistantConfiguration {
"always-allow-tool-actions-switch",
always_allow_tool_actions.into(),
)
.color(SwitchColor::Accent)
.on_click({
let fs = self.fs.clone();
move |state, _window, cx| {
@@ -333,44 +332,41 @@ impl AssistantConfiguration {
),
)
.child(
Switch::new("context-server-switch", is_running.into())
.color(SwitchColor::Accent)
.on_click({
let context_server_manager =
self.context_server_manager.clone();
let context_server = context_server.clone();
move |state, _window, cx| match state {
ToggleState::Unselected
| ToggleState::Indeterminate => {
context_server_manager.update(cx, |this, cx| {
this.stop_server(context_server.clone(), cx)
.log_err();
});
}
ToggleState::Selected => {
cx.spawn({
let context_server_manager =
context_server_manager.clone();
let context_server = context_server.clone();
async move |cx| {
if let Some(start_server_task) =
context_server_manager
.update(cx, |this, cx| {
this.start_server(
context_server,
cx,
)
})
.log_err()
{
start_server_task.await.log_err();
}
}
})
.detach();
}
Switch::new("context-server-switch", is_running.into()).on_click({
let context_server_manager =
self.context_server_manager.clone();
let context_server = context_server.clone();
move |state, _window, cx| match state {
ToggleState::Unselected | ToggleState::Indeterminate => {
context_server_manager.update(cx, |this, cx| {
this.stop_server(context_server.clone(), cx)
.log_err();
});
}
}),
ToggleState::Selected => {
cx.spawn({
let context_server_manager =
context_server_manager.clone();
let context_server = context_server.clone();
async move |cx| {
if let Some(start_server_task) =
context_server_manager
.update(cx, |this, cx| {
this.start_server(
context_server,
cx,
)
})
.log_err()
{
start_server_task.await.log_err();
}
}
})
.detach();
}
}
}),
),
)
.map(|parent| {
@@ -408,7 +404,7 @@ impl AssistantConfiguration {
.gap_2()
.child(
h_flex().w_full().child(
Button::new("add-context-server", "Add Custom Server")
Button::new("add-context-server", "Add MCPs Directly")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.full_width()

View File

@@ -2,7 +2,7 @@ use context_server::{ContextServerSettings, ServerCommand, ServerConfig};
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity, prelude::*};
use serde_json::json;
use settings::update_settings_file;
use ui::{KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
use ui::{Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
use ui_input::SingleLineInput;
use workspace::{ModalView, Workspace};
@@ -34,9 +34,9 @@ impl AddContextServerModal {
cx: &mut Context<Self>,
) -> Self {
let name_editor =
cx.new(|cx| SingleLineInput::new(window, cx, "my-custom-server").label("Name"));
cx.new(|cx| SingleLineInput::new(window, cx, "Your server name").label("Name"));
let command_editor = cx.new(|cx| {
SingleLineInput::new(window, cx, "Command").label("Command to run the MCP server")
SingleLineInput::new(window, cx, "Command").label("Command to run the context server")
});
Self {
@@ -46,7 +46,7 @@ impl AddContextServerModal {
}
}
fn confirm(&mut self, _: &menu::Confirm, cx: &mut Context<Self>) {
fn confirm(&mut self, cx: &mut Context<Self>) {
let name = self
.name_editor
.read(cx)
@@ -96,7 +96,7 @@ impl AddContextServerModal {
cx.emit(DismissEvent);
}
fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context<Self>) {
fn cancel(&mut self, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
}
@@ -112,68 +112,38 @@ impl Focusable for AddContextServerModal {
impl EventEmitter<DismissEvent> for AddContextServerModal {}
impl Render for AddContextServerModal {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let is_name_empty = self.name_editor.read(cx).is_empty(cx);
let is_command_empty = self.command_editor.read(cx).is_empty(cx);
let focus_handle = self.focus_handle(cx);
div()
.elevation_3(cx)
.w(rems(34.))
.key_context("AddContextServerModal")
.on_action(
cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(&menu::Cancel, cx)),
)
.on_action(
cx.listener(|this, _: &menu::Confirm, _window, cx| {
this.confirm(&menu::Confirm, cx)
}),
)
.on_action(cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(cx)))
.on_action(cx.listener(|this, _: &menu::Confirm, _window, cx| this.confirm(cx)))
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
this.focus_handle(cx).focus(window);
}))
.on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
.child(
Modal::new("add-context-server", None)
.header(ModalHeader::new().headline("Add MCP Server"))
.header(ModalHeader::new().headline("Add Context Server"))
.section(
Section::new().child(
v_flex()
.gap_2()
.child(self.name_editor.clone())
.child(self.command_editor.clone()),
),
Section::new()
.child(self.name_editor.clone())
.child(self.command_editor.clone()),
)
.footer(
ModalFooter::new()
.start_slot(
Button::new("cancel", "Cancel")
.key_binding(
KeyBinding::for_action_in(
&menu::Cancel,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(cx.listener(|this, _event, _window, cx| {
this.cancel(&menu::Cancel, cx)
})),
Button::new("cancel", "Cancel").on_click(
cx.listener(|this, _event, _window, cx| this.cancel(cx)),
),
)
.end_slot(
Button::new("add-server", "Add Server")
.disabled(is_name_empty || is_command_empty)
.key_binding(
KeyBinding::for_action_in(
&menu::Confirm,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.map(|button| {
if is_name_empty {
button.tooltip(Tooltip::text("Name is required"))
@@ -183,9 +153,9 @@ impl Render for AddContextServerModal {
button
}
})
.on_click(cx.listener(|this, _event, _window, cx| {
this.confirm(&menu::Confirm, cx)
})),
.on_click(
cx.listener(|this, _event, _window, cx| this.confirm(cx)),
),
),
),
)

View File

@@ -2,7 +2,7 @@ use std::sync::Arc;
use assistant_settings::{
AgentProfile, AgentProfileContent, AgentProfileId, AssistantSettings, AssistantSettingsContent,
ContextServerPresetContent,
ContextServerPresetContent, VersionedAssistantSettingsContent,
};
use assistant_tool::{ToolSource, ToolWorkingSet};
use fs::Fs;
@@ -201,10 +201,10 @@ impl PickerDelegate for ToolPickerDelegate {
let profile_id = self.profile_id.clone();
let default_profile = self.profile.clone();
let tool = tool.clone();
move |settings: &mut AssistantSettingsContent, _cx| {
settings
.v2_setting(|v2_settings| {
let profiles = v2_settings.profiles.get_or_insert_default();
move |settings, _cx| match settings {
AssistantSettingsContent::Versioned(boxed) => {
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
let profiles = settings.profiles.get_or_insert_default();
let profile =
profiles
.entry(profile_id)
@@ -240,10 +240,9 @@ impl PickerDelegate for ToolPickerDelegate {
*preset.tools.entry(tool.name.clone()).or_default() = is_enabled;
}
}
Ok(())
})
.ok();
}
}
_ => {}
}
});
}
@@ -272,7 +271,7 @@ impl PickerDelegate for ToolPickerDelegate {
.get(id.as_ref())
.and_then(|preset| preset.tools.get(&tool.name))
.copied()
.unwrap_or(self.profile.enable_all_context_servers),
.unwrap_or(false),
};
Some(

View File

@@ -1,9 +1,7 @@
use assistant_settings::AssistantSettings;
use fs::Fs;
use gpui::{Entity, FocusHandle, SharedString};
use crate::Thread;
use language_model::{ConfiguredModel, LanguageModelRegistry};
use language_model::LanguageModelRegistry;
use language_model_selector::{
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
};
@@ -11,9 +9,9 @@ use settings::update_settings_file;
use std::sync::Arc;
use ui::{ButtonLike, PopoverMenuHandle, Tooltip, prelude::*};
#[derive(Clone)]
#[derive(Clone, Copy)]
pub enum ModelType {
Default(Entity<Thread>),
Default,
InlineAssistant,
}
@@ -21,6 +19,7 @@ pub struct AssistantModelSelector {
selector: Entity<LanguageModelSelector>,
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
focus_handle: FocusHandle,
model_type: ModelType,
}
impl AssistantModelSelector {
@@ -30,39 +29,18 @@ impl AssistantModelSelector {
focus_handle: FocusHandle,
model_type: ModelType,
window: &mut Window,
cx: &mut Context<Self>,
cx: &mut App,
) -> Self {
Self {
selector: cx.new(move |cx| {
selector: cx.new(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
{
let model_type = model_type.clone();
move |cx| match &model_type {
ModelType::Default(thread) => thread.read(cx).configured_model(),
ModelType::InlineAssistant => {
LanguageModelRegistry::read_global(cx).inline_assistant_model()
}
}
},
move |model, cx| {
let provider = model.provider_id().0.to_string();
let model_id = model.id().0.to_string();
match &model_type {
ModelType::Default(thread) => {
thread.update(cx, |thread, cx| {
let registry = LanguageModelRegistry::read_global(cx);
if let Some(provider) = registry.provider(&model.provider_id())
{
thread.set_configured_model(
Some(ConfiguredModel {
provider,
model: model.clone(),
}),
cx,
);
}
});
match model_type {
ModelType::Default => {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
@@ -91,6 +69,7 @@ impl AssistantModelSelector {
}),
menu_handle,
focus_handle,
model_type,
}
}
@@ -103,7 +82,11 @@ impl Render for AssistantModelSelector {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle.clone();
let model = self.selector.read(cx).active_model(cx);
let model_registry = LanguageModelRegistry::read_global(cx);
let model = match self.model_type {
ModelType::Default => model_registry.default_model(),
ModelType::InlineAssistant => model_registry.inline_assistant_model(),
};
let (model_name, model_icon) = match model {
Some(model) => (model.model.name().0, Some(model.provider.icon())),
_ => (SharedString::from("No model selected"), None),

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,12 @@
use crate::context::ContextLoadResult;
use crate::context::attach_context_to_message;
use crate::context_store::ContextStore;
use crate::inline_prompt_editor::CodegenStatus;
use crate::{context::load_context, context_store::ContextStore};
use anyhow::Result;
use anyhow::{Context as _, Result};
use client::telemetry::Telemetry;
use collections::HashSet;
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
use futures::{
SinkExt, Stream, StreamExt, TryStreamExt as _, channel::mpsc, future::LocalBoxFuture, join,
};
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription, Task, WeakEntity};
use futures::{SinkExt, Stream, StreamExt, channel::mpsc, future::LocalBoxFuture, join};
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription, Task};
use language::{Buffer, IndentKind, Point, TransactionId, line_diff};
use language_model::{
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
@@ -16,9 +14,7 @@ use language_model::{
};
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
use project::Project;
use prompt_store::PromptBuilder;
use prompt_store::PromptStore;
use rope::Rope;
use smol::future::FutureExt;
use std::{
@@ -43,8 +39,6 @@ pub struct BufferCodegen {
range: Range<Anchor>,
initial_transaction_id: Option<TransactionId>,
context_store: Entity<ContextStore>,
project: WeakEntity<Project>,
prompt_store: Option<Entity<PromptStore>>,
telemetry: Arc<Telemetry>,
builder: Arc<PromptBuilder>,
pub is_insertion: bool,
@@ -56,8 +50,6 @@ impl BufferCodegen {
range: Range<Anchor>,
initial_transaction_id: Option<TransactionId>,
context_store: Entity<ContextStore>,
project: WeakEntity<Project>,
prompt_store: Option<Entity<PromptStore>>,
telemetry: Arc<Telemetry>,
builder: Arc<PromptBuilder>,
cx: &mut Context<Self>,
@@ -68,8 +60,6 @@ impl BufferCodegen {
range.clone(),
false,
Some(context_store.clone()),
project.clone(),
prompt_store.clone(),
Some(telemetry.clone()),
builder.clone(),
cx,
@@ -85,8 +75,6 @@ impl BufferCodegen {
range,
initial_transaction_id,
context_store,
project,
prompt_store,
telemetry,
builder,
};
@@ -143,12 +131,7 @@ impl BufferCodegen {
cx.notify();
}
pub fn start(
&mut self,
primary_model: Arc<dyn LanguageModel>,
user_prompt: String,
cx: &mut Context<Self>,
) -> Result<()> {
pub fn start(&mut self, user_prompt: String, cx: &mut Context<Self>) -> Result<()> {
let alternative_models = LanguageModelRegistry::read_global(cx)
.inline_alternative_models()
.to_vec();
@@ -165,8 +148,6 @@ impl BufferCodegen {
self.range.clone(),
false,
Some(self.context_store.clone()),
self.project.clone(),
self.prompt_store.clone(),
Some(self.telemetry.clone()),
self.builder.clone(),
cx,
@@ -174,6 +155,11 @@ impl BufferCodegen {
}));
}
let primary_model = LanguageModelRegistry::read_global(cx)
.default_model()
.context("no active model")?
.model;
for (model, alternative) in iter::once(primary_model)
.chain(alternative_models)
.zip(&self.alternatives)
@@ -243,14 +229,13 @@ pub struct CodegenAlternative {
generation: Task<()>,
diff: Diff,
context_store: Option<Entity<ContextStore>>,
project: WeakEntity<Project>,
prompt_store: Option<Entity<PromptStore>>,
telemetry: Option<Arc<Telemetry>>,
_subscription: gpui::Subscription,
builder: Arc<PromptBuilder>,
active: bool,
edits: Vec<(Range<Anchor>, String)>,
line_operations: Vec<LineOperation>,
request: Option<LanguageModelRequest>,
elapsed_time: Option<f64>,
completion: Option<String>,
pub message_id: Option<String>,
@@ -264,8 +249,6 @@ impl CodegenAlternative {
range: Range<Anchor>,
active: bool,
context_store: Option<Entity<ContextStore>>,
project: WeakEntity<Project>,
prompt_store: Option<Entity<PromptStore>>,
telemetry: Option<Arc<Telemetry>>,
builder: Arc<PromptBuilder>,
cx: &mut Context<Self>,
@@ -307,8 +290,6 @@ impl CodegenAlternative {
generation: Task::ready(()),
diff: Diff::default(),
context_store,
project,
prompt_store,
telemetry,
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
builder,
@@ -316,6 +297,7 @@ impl CodegenAlternative {
edits: Vec::new(),
line_operations: Vec::new(),
range,
request: None,
elapsed_time: None,
completion: None,
}
@@ -384,18 +366,16 @@ impl CodegenAlternative {
async { Ok(LanguageModelTextStream::default()) }.boxed_local()
} else {
let request = self.build_request(user_prompt, cx)?;
cx.spawn(async move |_, cx| model.stream_completion_text(request.await, &cx).await)
self.request = Some(request.clone());
cx.spawn(async move |_, cx| model.stream_completion_text(request, &cx).await)
.boxed_local()
};
self.handle_stream(telemetry_id, provider_id.to_string(), api_key, stream, cx);
Ok(())
}
fn build_request(
&self,
user_prompt: String,
cx: &mut App,
) -> Result<Task<LanguageModelRequest>> {
fn build_request(&self, user_prompt: String, cx: &mut App) -> Result<LanguageModelRequest> {
let buffer = self.buffer.read(cx).snapshot(cx);
let language = buffer.language_at(self.range.start);
let language_name = if let Some(language) = language.as_ref() {
@@ -428,45 +408,30 @@ impl CodegenAlternative {
.generate_inline_transformation_prompt(user_prompt, language_name, buffer, range)
.map_err(|e| anyhow::anyhow!("Failed to generate content prompt: {}", e))?;
let context_task = self.context_store.as_ref().map(|context_store| {
if let Some(project) = self.project.upgrade() {
let context = context_store
.read(cx)
.context()
.cloned()
.collect::<Vec<_>>();
load_context(context, &project, &self.prompt_store, cx)
} else {
Task::ready(ContextLoadResult::default())
}
});
let mut request_message = LanguageModelRequestMessage {
role: Role::User,
content: Vec::new(),
cache: false,
};
Ok(cx.spawn(async move |_cx| {
let mut request_message = LanguageModelRequestMessage {
role: Role::User,
content: Vec::new(),
cache: false,
};
if let Some(context_store) = &self.context_store {
attach_context_to_message(
&mut request_message,
context_store.read(cx).context().iter(),
cx,
);
}
if let Some(context_task) = context_task {
context_task
.await
.loaded_context
.add_to_request_message(&mut request_message);
}
request_message.content.push(prompt.into());
request_message.content.push(prompt.into());
LanguageModelRequest {
thread_id: None,
prompt_id: None,
mode: None,
tools: Vec::new(),
stop: Vec::new(),
temperature: None,
messages: vec![request_message],
}
}))
Ok(LanguageModelRequest {
thread_id: None,
prompt_id: None,
tools: Vec::new(),
stop: Vec::new(),
temperature: None,
messages: vec![request_message],
})
}
pub fn handle_stream(
@@ -504,7 +469,7 @@ impl CodegenAlternative {
}
}
let http_client = cx.http_client();
let http_client = cx.http_client().clone();
let telemetry = self.telemetry.clone();
let language_name = {
let multibuffer = self.buffer.read(cx);
@@ -543,9 +508,7 @@ impl CodegenAlternative {
let mut response_latency = None;
let request_start = Instant::now();
let diff = async {
let chunks = StripInvalidSpans::new(
stream?.stream.map_err(|error| error.into()),
);
let chunks = StripInvalidSpans::new(stream?.stream);
futures::pin_mut!(chunks);
let mut diff = StreamingDiff::new(selected_text.to_string());
let mut line_diff = LineDiff::default();
@@ -1071,7 +1034,6 @@ impl Diff {
#[cfg(test)]
mod tests {
use super::*;
use fs::FakeFs;
use futures::{
Stream,
stream::{self},
@@ -1114,16 +1076,12 @@ mod tests {
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
});
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, vec![], cx).await;
let codegen = cx.new(|cx| {
CodegenAlternative::new(
buffer.clone(),
range.clone(),
true,
None,
project.downgrade(),
None,
None,
prompt_builder,
cx,
@@ -1182,16 +1140,12 @@ mod tests {
snapshot.anchor_before(Point::new(1, 6))..snapshot.anchor_after(Point::new(1, 6))
});
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, vec![], cx).await;
let codegen = cx.new(|cx| {
CodegenAlternative::new(
buffer.clone(),
range.clone(),
true,
None,
project.downgrade(),
None,
None,
prompt_builder,
cx,
@@ -1253,16 +1207,12 @@ mod tests {
snapshot.anchor_before(Point::new(1, 2))..snapshot.anchor_after(Point::new(1, 2))
});
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, vec![], cx).await;
let codegen = cx.new(|cx| {
CodegenAlternative::new(
buffer.clone(),
range.clone(),
true,
None,
project.downgrade(),
None,
None,
prompt_builder,
cx,
@@ -1324,16 +1274,12 @@ mod tests {
snapshot.anchor_before(Point::new(0, 0))..snapshot.anchor_after(Point::new(4, 2))
});
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, vec![], cx).await;
let codegen = cx.new(|cx| {
CodegenAlternative::new(
buffer.clone(),
range.clone(),
true,
None,
project.downgrade(),
None,
None,
prompt_builder,
cx,
@@ -1383,16 +1329,12 @@ mod tests {
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(1, 14))
});
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, vec![], cx).await;
let codegen = cx.new(|cx| {
CodegenAlternative::new(
buffer.clone(),
range.clone(),
false,
None,
project.downgrade(),
None,
None,
prompt_builder,
cx,

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
mod completion_provider;
mod fetch_context_picker;
mod file_context_picker;
mod rules_context_picker;
mod symbol_context_picker;
mod thread_context_picker;
@@ -10,96 +9,37 @@ use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{Result, anyhow};
pub use completion_provider::ContextPickerCompletionProvider;
use editor::display_map::{Crease, FoldId};
use editor::{Anchor, AnchorRangeExt as _, Editor, ExcerptId, FoldPlaceholder, ToOffset};
use fetch_context_picker::FetchContextPicker;
use file_context_picker::FileContextPicker;
use file_context_picker::render_file_context_entry;
use gpui::{
App, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task,
WeakEntity,
};
use language::Buffer;
use multi_buffer::MultiBufferRow;
use project::{Entry, ProjectPath};
use prompt_store::{PromptStore, UserPromptId};
use rules_context_picker::{RulesContextEntry, RulesContextPicker};
use symbol_context_picker::SymbolContextPicker;
use thread_context_picker::{ThreadContextEntry, ThreadContextPicker, render_thread_context_entry};
use thread_context_picker::{ThreadContextEntry, render_thread_context_entry};
use ui::{
ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*,
};
use uuid::Uuid;
use workspace::{Workspace, notifications::NotifyResultExt};
use crate::AssistantPanel;
use crate::context::RULES_ICON;
pub use crate::context_picker::completion_provider::ContextPickerCompletionProvider;
use crate::context_picker::fetch_context_picker::FetchContextPicker;
use crate::context_picker::file_context_picker::FileContextPicker;
use crate::context_picker::thread_context_picker::ThreadContextPicker;
use crate::context_store::ContextStore;
use crate::thread::ThreadId;
use crate::thread_store::ThreadStore;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ContextPickerEntry {
Mode(ContextPickerMode),
Action(ContextPickerAction),
}
impl ContextPickerEntry {
pub fn keyword(&self) -> &'static str {
match self {
Self::Mode(mode) => mode.keyword(),
Self::Action(action) => action.keyword(),
}
}
pub fn label(&self) -> &'static str {
match self {
Self::Mode(mode) => mode.label(),
Self::Action(action) => action.label(),
}
}
pub fn icon(&self) -> IconName {
match self {
Self::Mode(mode) => mode.icon(),
Self::Action(action) => action.icon(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ContextPickerMode {
File,
Symbol,
Fetch,
Thread,
Rules,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ContextPickerAction {
AddSelections,
}
impl ContextPickerAction {
pub fn keyword(&self) -> &'static str {
match self {
Self::AddSelections => "selection",
}
}
pub fn label(&self) -> &'static str {
match self {
Self::AddSelections => "Selection",
}
}
pub fn icon(&self) -> IconName {
match self {
Self::AddSelections => IconName::Context,
}
}
}
impl TryFrom<&str> for ContextPickerMode {
@@ -111,20 +51,18 @@ impl TryFrom<&str> for ContextPickerMode {
"symbol" => Ok(Self::Symbol),
"fetch" => Ok(Self::Fetch),
"thread" => Ok(Self::Thread),
"rules" => Ok(Self::Rules),
_ => Err(format!("Invalid context picker mode: {}", value)),
}
}
}
impl ContextPickerMode {
pub fn keyword(&self) -> &'static str {
pub fn mention_prefix(&self) -> &'static str {
match self {
Self::File => "file",
Self::Symbol => "symbol",
Self::Fetch => "fetch",
Self::Thread => "thread",
Self::Rules => "rules",
}
}
@@ -134,7 +72,6 @@ impl ContextPickerMode {
Self::Symbol => "Symbols",
Self::Fetch => "Fetch",
Self::Thread => "Threads",
Self::Rules => "Rules",
}
}
@@ -144,7 +81,6 @@ impl ContextPickerMode {
Self::Symbol => IconName::Code,
Self::Fetch => IconName::Globe,
Self::Thread => IconName::MessageBubbles,
Self::Rules => RULES_ICON,
}
}
}
@@ -156,7 +92,6 @@ enum ContextPickerState {
Symbol(Entity<SymbolContextPicker>),
Fetch(Entity<FetchContextPicker>),
Thread(Entity<ThreadContextPicker>),
Rules(Entity<RulesContextPicker>),
}
pub(super) struct ContextPicker {
@@ -164,7 +99,6 @@ pub(super) struct ContextPicker {
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
thread_store: Option<WeakEntity<ThreadStore>>,
prompt_store: Option<Entity<PromptStore>>,
_subscriptions: Vec<Subscription>,
}
@@ -192,13 +126,6 @@ impl ContextPicker {
)
.collect::<Vec<Subscription>>();
let prompt_store = thread_store.as_ref().and_then(|thread_store| {
thread_store
.read_with(cx, |thread_store, _cx| thread_store.prompt_store().clone())
.ok()
.flatten()
});
ContextPicker {
mode: ContextPickerState::Default(ContextMenu::build(
window,
@@ -208,7 +135,6 @@ impl ContextPicker {
workspace,
context_store,
thread_store,
prompt_store,
_subscriptions: subscriptions,
}
}
@@ -229,18 +155,7 @@ impl ContextPicker {
.enumerate()
.map(|(ix, entry)| self.recent_menu_item(context_picker.clone(), ix, entry));
let entries = self
.workspace
.upgrade()
.map(|workspace| {
available_context_picker_entries(
&self.prompt_store,
&self.thread_store,
&workspace,
cx,
)
})
.unwrap_or_default();
let modes = supported_context_picker_modes(&self.thread_store);
menu.when(has_recent, |menu| {
menu.custom_row(|_, _| {
@@ -256,18 +171,18 @@ impl ContextPicker {
})
.extend(recent_entries)
.when(has_recent, |menu| menu.separator())
.extend(entries.into_iter().map(|entry| {
.extend(modes.into_iter().map(|mode| {
let context_picker = context_picker.clone();
ContextMenuEntry::new(entry.label())
.icon(entry.icon())
ContextMenuEntry::new(mode.label())
.icon(mode.icon())
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.handler(move |window, cx| {
context_picker.update(cx, |this, cx| this.select_entry(entry, window, cx))
context_picker.update(cx, |this, cx| this.select_mode(mode, window, cx))
})
}))
.keep_open_on_confirm(true)
.keep_open_on_confirm()
});
cx.subscribe(&menu, move |_, _, _: &DismissEvent, cx| {
@@ -283,87 +198,61 @@ impl ContextPicker {
self.thread_store.is_some()
}
fn select_entry(
fn select_mode(
&mut self,
entry: ContextPickerEntry,
mode: ContextPickerMode,
window: &mut Window,
cx: &mut Context<Self>,
) {
let context_picker = cx.entity().downgrade();
match entry {
ContextPickerEntry::Mode(mode) => match mode {
ContextPickerMode::File => {
self.mode = ContextPickerState::File(cx.new(|cx| {
FileContextPicker::new(
match mode {
ContextPickerMode::File => {
self.mode = ContextPickerState::File(cx.new(|cx| {
FileContextPicker::new(
context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
window,
cx,
)
}));
}
ContextPickerMode::Symbol => {
self.mode = ContextPickerState::Symbol(cx.new(|cx| {
SymbolContextPicker::new(
context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
window,
cx,
)
}));
}
ContextPickerMode::Fetch => {
self.mode = ContextPickerState::Fetch(cx.new(|cx| {
FetchContextPicker::new(
context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
window,
cx,
)
}));
}
ContextPickerMode::Thread => {
if let Some(thread_store) = self.thread_store.as_ref() {
self.mode = ContextPickerState::Thread(cx.new(|cx| {
ThreadContextPicker::new(
thread_store.clone(),
context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
window,
cx,
)
}));
}
ContextPickerMode::Symbol => {
self.mode = ContextPickerState::Symbol(cx.new(|cx| {
SymbolContextPicker::new(
context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
window,
cx,
)
}));
}
ContextPickerMode::Rules => {
if let Some(prompt_store) = self.prompt_store.as_ref() {
self.mode = ContextPickerState::Rules(cx.new(|cx| {
RulesContextPicker::new(
prompt_store.clone(),
context_picker.clone(),
self.context_store.clone(),
window,
cx,
)
}));
}
}
ContextPickerMode::Fetch => {
self.mode = ContextPickerState::Fetch(cx.new(|cx| {
FetchContextPicker::new(
context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
window,
cx,
)
}));
}
ContextPickerMode::Thread => {
if let Some(thread_store) = self.thread_store.as_ref() {
self.mode = ContextPickerState::Thread(cx.new(|cx| {
ThreadContextPicker::new(
thread_store.clone(),
context_picker.clone(),
self.context_store.clone(),
window,
cx,
)
}));
}
}
},
ContextPickerEntry::Action(action) => match action {
ContextPickerAction::AddSelections => {
if let Some((context_store, workspace)) =
self.context_store.upgrade().zip(self.workspace.upgrade())
{
add_selections_as_context(&context_store, &workspace, cx);
}
cx.emit(DismissEvent);
}
},
}
}
cx.notify();
@@ -388,7 +277,7 @@ impl ContextPicker {
ContextMenuItem::custom_entry(
move |_window, cx| {
render_file_context_entry(
ElementId::named_usize("ctx-recent", ix),
ElementId::NamedInteger("ctx-recent".into(), ix),
worktree_id,
&path,
&path_prefix,
@@ -492,7 +381,6 @@ impl ContextPicker {
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()),
ContextPickerState::Rules(entity) => entity.update(cx, |_, cx| cx.notify()),
}
}
}
@@ -507,7 +395,6 @@ impl Focusable for ContextPicker {
ContextPickerState::Symbol(symbol_picker) => symbol_picker.focus_handle(cx),
ContextPickerState::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
ContextPickerState::Thread(thread_picker) => thread_picker.focus_handle(cx),
ContextPickerState::Rules(user_rules_picker) => user_rules_picker.focus_handle(cx),
}
}
}
@@ -523,9 +410,6 @@ impl Render for ContextPicker {
ContextPickerState::Symbol(symbol_picker) => parent.child(symbol_picker.clone()),
ContextPickerState::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
ContextPickerState::Thread(thread_picker) => parent.child(thread_picker.clone()),
ContextPickerState::Rules(user_rules_picker) => {
parent.child(user_rules_picker.clone())
}
})
}
}
@@ -537,41 +421,18 @@ enum RecentEntry {
Thread(ThreadContextEntry),
}
fn available_context_picker_entries(
prompt_store: &Option<Entity<PromptStore>>,
fn supported_context_picker_modes(
thread_store: &Option<WeakEntity<ThreadStore>>,
workspace: &Entity<Workspace>,
cx: &mut App,
) -> Vec<ContextPickerEntry> {
let mut entries = vec![
ContextPickerEntry::Mode(ContextPickerMode::File),
ContextPickerEntry::Mode(ContextPickerMode::Symbol),
) -> Vec<ContextPickerMode> {
let mut modes = vec![
ContextPickerMode::File,
ContextPickerMode::Symbol,
ContextPickerMode::Fetch,
];
let has_selection = workspace
.read(cx)
.active_item(cx)
.and_then(|item| item.downcast::<Editor>())
.map_or(false, |editor| {
editor.update(cx, |editor, cx| editor.has_non_empty_selection(cx))
});
if has_selection {
entries.push(ContextPickerEntry::Action(
ContextPickerAction::AddSelections,
));
}
if thread_store.is_some() {
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Thread));
modes.push(ContextPickerMode::Thread);
}
if prompt_store.is_some() {
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules));
}
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch));
entries
modes
}
fn recent_context_picker_entries(
@@ -601,21 +462,22 @@ fn recent_context_picker_entries(
}),
);
let current_threads = context_store.read(cx).thread_ids();
let mut current_threads = context_store.read(cx).thread_ids();
let active_thread_id = workspace
if let Some(active_thread) = workspace
.panel::<AssistantPanel>(cx)
.map(|panel| panel.read(cx).active_thread(cx).read(cx).id());
.map(|panel| panel.read(cx).active_thread(cx))
{
current_threads.insert(active_thread.read(cx).id().clone());
}
if let Some(thread_store) = thread_store.and_then(|thread_store| thread_store.upgrade()) {
recent.extend(
thread_store
.read(cx)
.reverse_chronological_threads()
.threads()
.into_iter()
.filter(|thread| {
Some(&thread.id) != active_thread_id && !current_threads.contains(&thread.id)
})
.filter(|thread| !current_threads.contains(&thread.id))
.take(2)
.map(|thread| {
RecentEntry::Thread(ThreadContextEntry {
@@ -629,52 +491,6 @@ fn recent_context_picker_entries(
recent
}
fn add_selections_as_context(
context_store: &Entity<ContextStore>,
workspace: &Entity<Workspace>,
cx: &mut App,
) {
let selection_ranges = selection_ranges(workspace, cx);
context_store.update(cx, |context_store, cx| {
for (buffer, range) in selection_ranges {
context_store.add_selection(buffer, range, cx);
}
})
}
fn selection_ranges(
workspace: &Entity<Workspace>,
cx: &mut App,
) -> Vec<(Entity<Buffer>, Range<text::Anchor>)> {
let Some(editor) = workspace
.read(cx)
.active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx))
else {
return Vec::new();
};
editor.update(cx, |editor, cx| {
let selections = editor.selections.all_adjusted(cx);
let buffer = editor.buffer().clone().read(cx);
let snapshot = buffer.snapshot(cx);
selections
.into_iter()
.map(|s| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
.flat_map(|range| {
let (start_buffer, start) = buffer.text_anchor_for_position(range.start, cx)?;
let (end_buffer, end) = buffer.text_anchor_for_position(range.end, cx)?;
if start_buffer != end_buffer {
return None;
}
Some((start_buffer, start..end))
})
.collect::<Vec<_>>()
})
}
pub(crate) fn insert_fold_for_mention(
excerpt_id: ExcerptId,
crease_start: text::Anchor,
@@ -694,11 +510,24 @@ pub(crate) fn insert_fold_for_mention(
let start = start.bias_right(&snapshot);
let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
let crease = crease_for_mention(
crease_label,
crease_icon_path,
let placeholder = FoldPlaceholder {
render: render_fold_icon_button(
crease_icon_path,
crease_label,
editor_entity.downgrade(),
),
merge_adjacent: false,
..Default::default()
};
let render_trailer =
move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
let crease = Crease::inline(
start..end,
editor_entity.downgrade(),
placeholder.clone(),
fold_toggle("mention"),
render_trailer,
);
editor.display_map.update(cx, |display_map, cx| {
@@ -707,29 +536,6 @@ pub(crate) fn insert_fold_for_mention(
});
}
pub fn crease_for_mention(
label: SharedString,
icon_path: SharedString,
range: Range<Anchor>,
editor_entity: WeakEntity<Editor>,
) -> Crease<Anchor> {
let placeholder = FoldPlaceholder {
render: render_fold_icon_button(icon_path, label, editor_entity),
merge_adjacent: false,
..Default::default()
};
let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
let crease = Crease::inline(
range,
placeholder.clone(),
fold_toggle("mention"),
render_trailer,
);
crease
}
fn render_fold_icon_button(
icon_path: SharedString,
label: SharedString,
@@ -818,19 +624,15 @@ fn fold_toggle(
pub enum MentionLink {
File(ProjectPath, Entry),
Symbol(ProjectPath, String),
Selection(ProjectPath, Range<usize>),
Fetch(String),
Thread(ThreadId),
Rules(UserPromptId),
}
impl MentionLink {
const FILE: &str = "@file";
const SYMBOL: &str = "@symbol";
const SELECTION: &str = "@selection";
const THREAD: &str = "@thread";
const FETCH: &str = "@fetch";
const RULES: &str = "@rules";
const SEPARATOR: &str = ":";
@@ -838,9 +640,7 @@ impl MentionLink {
url.starts_with(Self::FILE)
|| url.starts_with(Self::SYMBOL)
|| url.starts_with(Self::FETCH)
|| url.starts_with(Self::SELECTION)
|| url.starts_with(Self::THREAD)
|| url.starts_with(Self::RULES)
}
pub fn for_file(file_name: &str, full_path: &str) -> String {
@@ -857,29 +657,12 @@ impl MentionLink {
)
}
pub fn for_selection(file_name: &str, full_path: &str, line_range: Range<usize>) -> String {
format!(
"[@{} ({}-{})]({}:{}:{}-{})",
file_name,
line_range.start,
line_range.end,
Self::SELECTION,
full_path,
line_range.start,
line_range.end
)
}
pub fn for_thread(thread: &ThreadContextEntry) -> String {
format!("[@{}]({}:{})", thread.summary, Self::THREAD, thread.id)
}
pub fn for_fetch(url: &str) -> String {
format!("[@{}]({}:{})", url, Self::FETCH, url)
}
pub fn for_rules(rules: &RulesContextEntry) -> String {
format!("[@{}]({}:{})", rules.title, Self::RULES, rules.prompt_id.0)
pub fn for_thread(thread: &ThreadContextEntry) -> String {
format!("[@{}]({}:{})", thread.summary, Self::THREAD, thread.id)
}
pub fn try_parse(link: &str, workspace: &Entity<Workspace>, cx: &App) -> Option<Self> {
@@ -918,29 +701,11 @@ impl MentionLink {
let project_path = extract_project_path_from_link(path, workspace, cx)?;
Some(MentionLink::Symbol(project_path, symbol.to_string()))
}
Self::SELECTION => {
let (path, line_args) = argument.split_once(Self::SEPARATOR)?;
let project_path = extract_project_path_from_link(path, workspace, cx)?;
let line_range = {
let (start, end) = line_args
.trim_start_matches('(')
.trim_end_matches(')')
.split_once('-')?;
start.parse::<usize>().ok()?..end.parse::<usize>().ok()?
};
Some(MentionLink::Selection(project_path, line_range))
}
Self::THREAD => {
let thread_id = ThreadId::from(argument);
Some(MentionLink::Thread(thread_id))
}
Self::FETCH => Some(MentionLink::Fetch(argument.to_string())),
Self::RULES => {
let prompt_id = UserPromptId(Uuid::try_parse(argument).ok()?);
Some(MentionLink::Rules(prompt_id))
}
_ => None,
}
}

View File

@@ -1,64 +1,59 @@
use std::cell::RefCell;
use std::ops::Range;
use std::path::{Path, PathBuf};
use std::path::Path;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use anyhow::Result;
use editor::{CompletionProvider, Editor, ExcerptId, ToOffset as _};
use editor::{CompletionProvider, Editor, ExcerptId};
use file_icons::FileIcons;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{App, Entity, Task, WeakEntity};
use http_client::HttpClientWithUrl;
use itertools::Itertools;
use language::{Buffer, CodeLabel, HighlightId};
use lsp::CompletionContext;
use project::{Completion, CompletionIntent, ProjectPath, Symbol, WorktreeId};
use prompt_store::PromptStore;
use rope::Point;
use text::{Anchor, OffsetRangeExt, ToPoint};
use text::{Anchor, ToPoint};
use ui::prelude::*;
use workspace::Workspace;
use crate::context::RULES_ICON;
use crate::context_picker::file_context_picker::search_files;
use crate::context_picker::symbol_context_picker::search_symbols;
use crate::context_store::ContextStore;
use crate::thread_store::ThreadStore;
use super::fetch_context_picker::fetch_url_content;
use super::file_context_picker::{FileMatch, search_files};
use super::rules_context_picker::{RulesContextEntry, search_rules};
use super::file_context_picker::FileMatch;
use super::symbol_context_picker::SymbolMatch;
use super::symbol_context_picker::search_symbols;
use super::thread_context_picker::{ThreadContextEntry, ThreadMatch, search_threads};
use super::{
ContextPickerAction, ContextPickerEntry, ContextPickerMode, MentionLink, RecentEntry,
available_context_picker_entries, recent_context_picker_entries, selection_ranges,
ContextPickerMode, MentionLink, RecentEntry, recent_context_picker_entries,
supported_context_picker_modes,
};
pub(crate) enum Match {
File(FileMatch),
Symbol(SymbolMatch),
File(FileMatch),
Thread(ThreadMatch),
Fetch(SharedString),
Rules(RulesContextEntry),
Entry(EntryMatch),
Mode(ModeMatch),
}
pub struct EntryMatch {
pub struct ModeMatch {
mat: Option<StringMatch>,
entry: ContextPickerEntry,
mode: ContextPickerMode,
}
impl Match {
pub fn score(&self) -> f64 {
match self {
Match::File(file) => file.mat.score,
Match::Entry(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.),
Match::Mode(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.),
Match::Thread(_) => 1.,
Match::Symbol(_) => 1.,
Match::Fetch(_) => 1.,
Match::Rules(_) => 1.,
}
}
}
@@ -68,7 +63,6 @@ fn search(
query: String,
cancellation_flag: Arc<AtomicBool>,
recent_entries: Vec<RecentEntry>,
prompt_store: Option<Entity<PromptStore>>,
thread_store: Option<WeakEntity<ThreadStore>>,
workspace: Entity<Workspace>,
cx: &mut App,
@@ -85,7 +79,6 @@ fn search(
.collect()
})
}
Some(ContextPickerMode::Symbol) => {
let search_symbols_task =
search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx);
@@ -97,7 +90,6 @@ fn search(
.collect()
})
}
Some(ContextPickerMode::Thread) => {
if let Some(thread_store) = thread_store.as_ref().and_then(|t| t.upgrade()) {
let search_threads_task =
@@ -113,7 +105,6 @@ fn search(
Task::ready(Vec::new())
}
}
Some(ContextPickerMode::Fetch) => {
if !query.is_empty() {
Task::ready(vec![Match::Fetch(query.into())])
@@ -121,23 +112,6 @@ fn search(
Task::ready(Vec::new())
}
}
Some(ContextPickerMode::Rules) => {
if let Some(prompt_store) = prompt_store.as_ref() {
let search_rules_task =
search_rules(query.clone(), cancellation_flag.clone(), prompt_store, cx);
cx.background_spawn(async move {
search_rules_task
.await
.into_iter()
.map(Match::Rules)
.collect::<Vec<_>>()
})
} else {
Task::ready(Vec::new())
}
}
None => {
if query.is_empty() {
let mut matches = recent_entries
@@ -168,14 +142,9 @@ fn search(
.collect::<Vec<_>>();
matches.extend(
available_context_picker_entries(&prompt_store, &thread_store, &workspace, cx)
supported_context_picker_modes(&thread_store)
.into_iter()
.map(|mode| {
Match::Entry(EntryMatch {
entry: mode,
mat: None,
})
}),
.map(|mode| Match::Mode(ModeMatch { mode, mat: None })),
);
Task::ready(matches)
@@ -185,12 +154,11 @@ fn search(
let search_files_task =
search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
let entries =
available_context_picker_entries(&prompt_store, &thread_store, &workspace, cx);
let entry_candidates = entries
let modes = supported_context_picker_modes(&thread_store);
let mode_candidates = modes
.iter()
.enumerate()
.map(|(ix, entry)| StringMatchCandidate::new(ix, entry.keyword()))
.map(|(ix, mode)| StringMatchCandidate::new(ix, mode.mention_prefix()))
.collect::<Vec<_>>();
cx.background_spawn(async move {
@@ -200,8 +168,8 @@ fn search(
.map(Match::File)
.collect::<Vec<_>>();
let entry_matches = fuzzy::match_strings(
&entry_candidates,
let mode_matches = fuzzy::match_strings(
&mode_candidates,
&query,
false,
100,
@@ -210,9 +178,9 @@ fn search(
)
.await;
matches.extend(entry_matches.into_iter().map(|mat| {
Match::Entry(EntryMatch {
entry: entries[mat.candidate_id],
matches.extend(mode_matches.into_iter().map(|mat| {
Match::Mode(ModeMatch {
mode: modes[mat.candidate_id],
mat: Some(mat),
})
}));
@@ -252,139 +220,19 @@ impl ContextPickerCompletionProvider {
}
}
fn completion_for_entry(
entry: ContextPickerEntry,
excerpt_id: ExcerptId,
source_range: Range<Anchor>,
editor: Entity<Editor>,
context_store: Entity<ContextStore>,
workspace: &Entity<Workspace>,
cx: &mut App,
) -> Option<Completion> {
match entry {
ContextPickerEntry::Mode(mode) => Some(Completion {
replace_range: source_range.clone(),
new_text: format!("@{} ", mode.keyword()),
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)),
}),
ContextPickerEntry::Action(action) => {
let (new_text, on_action) = match action {
ContextPickerAction::AddSelections => {
let selections = selection_ranges(workspace, cx);
let selection_infos = selections
.iter()
.map(|(buffer, range)| {
let full_path = buffer
.read(cx)
.file()
.map(|file| file.full_path(cx))
.unwrap_or_else(|| PathBuf::from("untitled"));
let file_name = full_path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let line_range = range.to_point(&buffer.read(cx).snapshot());
let link = MentionLink::for_selection(
&file_name,
&full_path.to_string_lossy(),
line_range.start.row as usize..line_range.end.row as usize,
);
(file_name, link, line_range)
})
.collect::<Vec<_>>();
let new_text = selection_infos.iter().map(|(_, link, _)| link).join(" ");
let callback = Arc::new({
let context_store = context_store.clone();
let selections = selections.clone();
let selection_infos = selection_infos.clone();
move |_, _: &mut Window, cx: &mut App| {
context_store.update(cx, |context_store, cx| {
for (buffer, range) in &selections {
context_store.add_selection(
buffer.clone(),
range.clone(),
cx,
);
}
});
let editor = editor.clone();
let selection_infos = selection_infos.clone();
cx.defer(move |cx| {
let mut current_offset = 0;
for (file_name, link, line_range) in selection_infos.iter() {
let snapshot =
editor.read(cx).buffer().read(cx).snapshot(cx);
let Some(start) = snapshot
.anchor_in_excerpt(excerpt_id, source_range.start)
else {
return;
};
let offset = start.to_offset(&snapshot) + current_offset;
let text_len = link.len();
let range = snapshot.anchor_after(offset)
..snapshot.anchor_after(offset + text_len);
let crease = super::crease_for_mention(
format!(
"{} ({}-{})",
file_name,
line_range.start.row + 1,
line_range.end.row + 1
)
.into(),
IconName::Context.path().into(),
range,
editor.downgrade(),
);
editor.update(cx, |editor, cx| {
editor.display_map.update(cx, |display_map, cx| {
display_map.fold(vec![crease], cx);
});
});
current_offset += text_len + 1;
}
});
false
}
});
(new_text, callback)
}
};
Some(Completion {
replace_range: source_range.clone(),
new_text,
label: CodeLabel::plain(action.label().to_string(), None),
icon_path: Some(action.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(on_action),
})
}
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)),
}
}
@@ -439,40 +287,6 @@ impl ContextPickerCompletionProvider {
}
}
fn completion_for_rules(
rules: RulesContextEntry,
excerpt_id: ExcerptId,
source_range: Range<Anchor>,
editor: Entity<Editor>,
context_store: Entity<ContextStore>,
) -> Completion {
let new_text = MentionLink::for_rules(&rules);
let new_text_len = new_text.len();
Completion {
replace_range: source_range.clone(),
new_text,
label: CodeLabel::plain(rules.title.to_string(), None),
documentation: None,
insert_text_mode: None,
source: project::CompletionSource::Custom,
icon_path: Some(RULES_ICON.path().into()),
confirm: Some(confirm_completion_callback(
RULES_ICON.path().into(),
rules.title.clone(),
excerpt_id,
source_range.start,
new_text_len,
editor.clone(),
move |cx| {
let user_prompt_id = rules.prompt_id;
context_store.update(cx, |context_store, cx| {
context_store.add_rules(user_prompt_id, false, cx);
});
},
)),
}
}
fn completion_for_fetch(
source_range: Range<Anchor>,
url_to_fetch: SharedString,
@@ -504,7 +318,7 @@ impl ContextPickerCompletionProvider {
let url_to_fetch = url_to_fetch.clone();
cx.spawn(async move |cx| {
if context_store.update(cx, |context_store, _| {
context_store.includes_url(&url_to_fetch)
context_store.includes_url(&url_to_fetch).is_some()
})? {
return Ok(());
}
@@ -580,7 +394,7 @@ impl ContextPickerCompletionProvider {
move |cx| {
context_store.update(cx, |context_store, cx| {
let task = if is_directory {
Task::ready(context_store.add_directory(&project_path, false, cx))
context_store.add_directory(project_path.clone(), false, cx)
} else {
context_store.add_file_from_path(project_path.clone(), false, cx)
};
@@ -708,7 +522,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
let thread_store = self.thread_store.clone();
let editor = self.editor.clone();
let http_client = workspace.read(cx).client().http_client();
let http_client = workspace.read(cx).client().http_client().clone();
let MentionCompletion { mode, argument, .. } = state;
let query = argument.unwrap_or_else(|| "".to_string());
@@ -720,19 +534,11 @@ impl CompletionProvider for ContextPickerCompletionProvider {
cx,
);
let prompt_store = thread_store.as_ref().and_then(|thread_store| {
thread_store
.read_with(cx, |thread_store, _cx| thread_store.prompt_store().clone())
.ok()
.flatten()
});
let search_task = search(
mode,
query,
Arc::<AtomicBool>::default(),
recent_entries,
prompt_store,
thread_store.clone(),
workspace.clone(),
cx,
@@ -764,7 +570,6 @@ impl CompletionProvider for ContextPickerCompletionProvider {
cx,
))
}
Match::Symbol(SymbolMatch { symbol, .. }) => Self::completion_for_symbol(
symbol,
excerpt_id,
@@ -774,7 +579,6 @@ impl CompletionProvider for ContextPickerCompletionProvider {
workspace.clone(),
cx,
),
Match::Thread(ThreadMatch {
thread, is_recent, ..
}) => {
@@ -789,15 +593,6 @@ impl CompletionProvider for ContextPickerCompletionProvider {
thread_store,
))
}
Match::Rules(user_rules) => Some(Self::completion_for_rules(
user_rules,
excerpt_id,
source_range.clone(),
editor.clone(),
context_store.clone(),
)),
Match::Fetch(url) => Some(Self::completion_for_fetch(
source_range.clone(),
url,
@@ -806,16 +601,9 @@ impl CompletionProvider for ContextPickerCompletionProvider {
context_store.clone(),
http_client.clone(),
)),
Match::Entry(EntryMatch { entry, .. }) => Self::completion_for_entry(
entry,
excerpt_id,
source_range.clone(),
editor.clone(),
context_store.clone(),
&workspace,
cx,
),
Match::Mode(ModeMatch { mode, .. }) => {
Some(Self::completion_for_mode(source_range.clone(), mode))
}
})
.collect()
})?))
@@ -1045,10 +833,6 @@ mod tests {
fn include_in_nav_history() -> bool {
false
}
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
"Test".into()
}
}
impl EventEmitter<()> for AtMentionEditor {}

View File

@@ -193,7 +193,7 @@ impl PickerDelegate for FetchContextPickerDelegate {
return;
};
let http_client = workspace.read(cx).client().http_client();
let http_client = workspace.read(cx).client().http_client().clone();
let url = self.url.clone();
cx.spawn_in(window, async move |this, cx| {
let text = cx
@@ -227,7 +227,7 @@ impl PickerDelegate for FetchContextPickerDelegate {
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let added = self.context_store.upgrade().map_or(false, |context_store| {
context_store.read(cx).includes_url(&self.url)
context_store.read(cx).includes_url(&self.url).is_some()
});
Some(

View File

@@ -134,9 +134,9 @@ impl PickerDelegate for FileContextPickerDelegate {
.context_store
.update(cx, |context_store, cx| {
if is_directory {
Task::ready(context_store.add_directory(&project_path, true, cx))
context_store.add_directory(project_path, true, cx)
} else {
context_store.add_file_from_path(project_path.clone(), true, cx)
context_store.add_file_from_path(project_path, true, cx)
}
})
.ok()
@@ -169,7 +169,7 @@ impl PickerDelegate for FileContextPickerDelegate {
.inset(true)
.toggle_state(selected)
.child(render_file_context_entry(
ElementId::named_usize("file-ctx-picker", ix),
ElementId::NamedInteger("file-ctx-picker".into(), ix),
WorktreeId::from_usize(mat.worktree_id),
&mat.path,
&mat.path_prefix,
@@ -325,11 +325,11 @@ pub fn render_file_context_entry(
path: path.clone(),
};
if is_directory {
context_store.read(cx).includes_directory(&project_path)
} else {
context_store
.read(cx)
.path_included_in_directory(&project_path, cx)
} else {
context_store.read(cx).file_path_included(&project_path, cx)
.will_include_file_path(&project_path, cx)
}
});
@@ -357,7 +357,7 @@ pub fn render_file_context_entry(
})),
)
.when_some(added, |el, added| match added {
FileInclusion::Direct => el.child(
FileInclusion::Direct(_) => el.child(
h_flex()
.w_full()
.justify_end()
@@ -369,8 +369,9 @@ pub fn render_file_context_entry(
)
.child(Label::new("Added").size(LabelSize::Small)),
),
FileInclusion::InDirectory { full_path } => {
let directory_full_path = full_path.to_string_lossy().into_owned();
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();
el.child(
h_flex()
@@ -384,7 +385,7 @@ pub fn render_file_context_entry(
)
.child(Label::new("Included").size(LabelSize::Small)),
)
.tooltip(Tooltip::text(format!("in {directory_full_path}")))
.tooltip(Tooltip::text(format!("in {directory_path}")))
}
})
}

View File

@@ -1,224 +0,0 @@
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
use picker::{Picker, PickerDelegate};
use prompt_store::{PromptId, PromptStore, UserPromptId};
use ui::{ListItem, prelude::*};
use util::ResultExt as _;
use crate::context::RULES_ICON;
use crate::context_picker::ContextPicker;
use crate::context_store::{self, ContextStore};
pub struct RulesContextPicker {
picker: Entity<Picker<RulesContextPickerDelegate>>,
}
impl RulesContextPicker {
pub fn new(
prompt_store: Entity<PromptStore>,
context_picker: WeakEntity<ContextPicker>,
context_store: WeakEntity<context_store::ContextStore>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let delegate = RulesContextPickerDelegate::new(prompt_store, context_picker, context_store);
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
RulesContextPicker { picker }
}
}
impl Focusable for RulesContextPicker {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for RulesContextPicker {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
self.picker.clone()
}
}
#[derive(Debug, Clone)]
pub struct RulesContextEntry {
pub prompt_id: UserPromptId,
pub title: SharedString,
}
pub struct RulesContextPickerDelegate {
prompt_store: Entity<PromptStore>,
context_picker: WeakEntity<ContextPicker>,
context_store: WeakEntity<context_store::ContextStore>,
matches: Vec<RulesContextEntry>,
selected_index: usize,
}
impl RulesContextPickerDelegate {
pub fn new(
prompt_store: Entity<PromptStore>,
context_picker: WeakEntity<ContextPicker>,
context_store: WeakEntity<context_store::ContextStore>,
) -> Self {
RulesContextPickerDelegate {
prompt_store,
context_picker,
context_store,
matches: Vec::new(),
selected_index: 0,
}
}
}
impl PickerDelegate for RulesContextPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Search available rules…".into()
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let search_task = search_rules(
query,
Arc::new(AtomicBool::default()),
&self.prompt_store,
cx,
);
cx.spawn_in(window, async move |this, cx| {
let matches = search_task.await;
this.update(cx, |this, cx| {
this.delegate.matches = matches;
this.delegate.selected_index = 0;
cx.notify();
})
.ok();
})
}
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
let Some(entry) = self.matches.get(self.selected_index) else {
return;
};
self.context_store
.update(cx, |context_store, cx| {
context_store.add_rules(entry.prompt_id, true, cx)
})
.log_err();
}
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
self.context_picker
.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
.ok();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let thread = &self.matches[ix];
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
render_thread_context_entry(thread, self.context_store.clone(), cx),
))
}
}
pub fn render_thread_context_entry(
user_rules: &RulesContextEntry,
context_store: WeakEntity<ContextStore>,
cx: &mut App,
) -> Div {
let added = context_store.upgrade().map_or(false, |context_store| {
context_store
.read(cx)
.includes_user_rules(user_rules.prompt_id)
});
h_flex()
.gap_1p5()
.w_full()
.justify_between()
.child(
h_flex()
.gap_1p5()
.max_w_72()
.child(
Icon::new(RULES_ICON)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(Label::new(user_rules.title.clone()).truncate()),
)
.when(added, |el| {
el.child(
h_flex()
.gap_1()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Added").size(LabelSize::Small)),
)
})
}
pub(crate) fn search_rules(
query: String,
cancellation_flag: Arc<AtomicBool>,
prompt_store: &Entity<PromptStore>,
cx: &mut App,
) -> Task<Vec<RulesContextEntry>> {
let search_task = prompt_store.read(cx).search(query, cancellation_flag, cx);
cx.background_spawn(async move {
search_task
.await
.into_iter()
.flat_map(|metadata| {
// Default prompts are filtered out as they are automatically included.
if metadata.default {
None
} else {
match metadata.id {
PromptId::EditWorkflow => None,
PromptId::User { uuid } => Some(RulesContextEntry {
prompt_id: uuid,
title: metadata.title?,
}),
}
}
})
.collect::<Vec<_>>()
})
}

View File

@@ -10,6 +10,7 @@ use gpui::{
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
use project::{DocumentSymbol, Symbol};
use text::OffsetRangeExt;
use ui::{ListItem, prelude::*};
use util::ResultExt as _;
use workspace::Workspace;
@@ -171,7 +172,10 @@ impl PickerDelegate for SymbolContextPickerDelegate {
let mat = &self.matches[ix];
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
render_symbol_context_entry(ElementId::named_usize("symbol-ctx-picker", ix), mat),
render_symbol_context_entry(
ElementId::NamedInteger("symbol-ctx-picker".into(), ix),
mat,
),
))
}
}
@@ -224,16 +228,18 @@ pub(crate) fn add_symbol(
)
})?;
context_store.update(cx, move |context_store, cx| {
context_store.add_symbol(
buffer,
name.into(),
range,
enclosing_range,
remove_if_exists,
cx,
)
})
context_store
.update(cx, move |context_store, cx| {
context_store.add_symbol(
buffer,
name.into(),
range,
enclosing_range,
remove_if_exists,
cx,
)
})?
.await
})
}
@@ -347,13 +353,38 @@ fn compute_symbol_entries(
context_store: &ContextStore,
cx: &App,
) -> Vec<SymbolEntry> {
symbols
.into_iter()
.map(|SymbolMatch { symbol, .. }| SymbolEntry {
is_included: context_store.includes_symbol(&symbol, cx),
let mut symbol_entries = Vec::with_capacity(symbols.len());
for SymbolMatch { 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;
for included_symbol_id in symbols_for_path {
if included_symbol_id.name.as_ref() == symbol.name.as_str() {
if let Some(buffer) = context_store.buffer_for_symbol(included_symbol_id) {
let snapshot = buffer.read(cx).snapshot();
let included_symbol_range =
included_symbol_id.range.to_point_utf16(&snapshot);
if included_symbol_range.start == symbol.range.start.0
&& included_symbol_range.end == symbol.range.end.0
{
is_included = true;
break;
}
}
}
}
is_included
} else {
false
};
symbol_entries.push(SymbolEntry {
symbol,
is_included,
})
.collect::<Vec<_>>()
}
symbol_entries
}
pub fn render_symbol_context_entry(id: ElementId, entry: &SymbolEntry) -> Stateful<Div> {

View File

@@ -103,11 +103,11 @@ impl PickerDelegate for ThreadContextPickerDelegate {
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let Some(thread_store) = self.thread_store.upgrade() else {
let Some(threads) = self.thread_store.upgrade() else {
return Task::ready(());
};
let search_task = search_threads(query, Arc::new(AtomicBool::default()), thread_store, cx);
let search_task = search_threads(query, Arc::new(AtomicBool::default()), threads, cx);
cx.spawn_in(window, async move |this, cx| {
let matches = search_task.await;
this.update(cx, |this, cx| {
@@ -173,7 +173,7 @@ pub fn render_thread_context_entry(
cx: &mut App,
) -> Div {
let added = context_store.upgrade().map_or(false, |ctx_store| {
ctx_store.read(cx).includes_thread(&thread.id)
ctx_store.read(cx).includes_thread(&thread.id).is_some()
});
h_flex()
@@ -217,15 +217,15 @@ pub(crate) fn search_threads(
thread_store: Entity<ThreadStore>,
cx: &mut App,
) -> Task<Vec<ThreadMatch>> {
let threads = thread_store
.read(cx)
.reverse_chronological_threads()
.into_iter()
.map(|thread| ThreadContextEntry {
id: thread.id,
summary: thread.summary,
})
.collect::<Vec<_>>();
let threads = thread_store.update(cx, |this, _cx| {
this.threads()
.into_iter()
.map(|thread| ThreadContextEntry {
id: thread.id,
summary: thread.summary,
})
.collect::<Vec<_>>()
});
let executor = cx.background_executor().clone();
cx.background_spawn(async move {

File diff suppressed because it is too large Load Diff

View File

@@ -12,9 +12,9 @@ use itertools::Itertools;
use language::Buffer;
use project::ProjectItem;
use ui::{KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
use workspace::Workspace;
use workspace::{Workspace, notifications::NotifyResultExt};
use crate::context::{AgentContextHandle, ContextKind};
use crate::context::{ContextId, ContextKind};
use crate::context_picker::ContextPicker;
use crate::context_store::ContextStore;
use crate::thread::Thread;
@@ -32,7 +32,6 @@ pub struct ContextStrip {
focus_handle: FocusHandle,
suggest_context_kind: SuggestContextKind,
workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<ThreadStore>>,
_subscriptions: Vec<Subscription>,
focused_index: Option<usize>,
children_bounds: Option<Vec<Bounds<Pixels>>>,
@@ -74,33 +73,12 @@ impl ContextStrip {
focus_handle,
suggest_context_kind,
workspace,
thread_store,
_subscriptions: subscriptions,
focused_index: None,
children_bounds: None,
}
}
fn added_contexts(&self, cx: &App) -> Vec<AddedContext> {
if let Some(workspace) = self.workspace.upgrade() {
let project = workspace.read(cx).project().read(cx);
let prompt_store = self
.thread_store
.as_ref()
.and_then(|thread_store| thread_store.upgrade())
.and_then(|thread_store| thread_store.read(cx).prompt_store().as_ref());
self.context_store
.read(cx)
.context()
.flat_map(|context| {
AddedContext::new_pending(context.clone(), prompt_store, project, cx)
})
.collect::<Vec<_>>()
} else {
Vec::new()
}
}
fn suggested_context(&self, cx: &Context<Self>) -> Option<SuggestedContext> {
match self.suggest_context_kind {
SuggestContextKind::File => self.suggested_file(cx),
@@ -115,19 +93,22 @@ impl ContextStrip {
let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
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)?;
if self
.context_store
.read(cx)
.file_path_included(&project_path, cx)
.will_include_buffer(active_buffer.remote_id(), &project_path)
.is_some()
{
return None;
}
let file_name = active_buffer.file()?.file_name(cx);
let icon_path = FileIcons::get_icon(&Path::new(&file_name), cx);
Some(SuggestedContext::File {
name: file_name.to_string_lossy().into_owned().into(),
buffer: active_buffer_entity.downgrade(),
@@ -154,6 +135,7 @@ impl ContextStrip {
.context_store
.read(cx)
.includes_thread(active_thread.id())
.is_some()
{
return None;
}
@@ -290,12 +272,12 @@ impl ContextStrip {
best.map(|(index, _, _)| index)
}
fn open_context(&mut self, context: &AgentContextHandle, window: &mut Window, cx: &mut App) {
fn open_context(&mut self, id: ContextId, window: &mut Window, cx: &mut App) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
crate::active_thread::open_context(context, workspace, window, cx);
crate::active_thread::open_context(id, self.context_store.clone(), workspace, window, cx);
}
fn remove_focused_context(
@@ -305,17 +287,17 @@ impl ContextStrip {
cx: &mut Context<Self>,
) {
if let Some(index) = self.focused_index {
let added_contexts = self.added_contexts(cx);
let Some(context) = added_contexts.get(index) else {
return;
};
let mut is_empty = false;
self.context_store.update(cx, |this, cx| {
this.remove_context(&context.handle, cx);
if let Some(item) = this.context().get(index) {
this.remove_context(item.id(), cx);
}
is_empty = this.context().is_empty();
});
let is_now_empty = added_contexts.len() == 1;
if is_now_empty {
if is_empty {
cx.emit(ContextStripEvent::BlurredEmpty);
} else {
self.focused_index = Some(index.saturating_sub(1));
@@ -324,28 +306,49 @@ impl ContextStrip {
}
}
fn is_suggested_focused(&self, added_contexts: &Vec<AddedContext>) -> bool {
fn is_suggested_focused<T>(&self, context: &Vec<T>) -> bool {
// We only suggest one item after the actual context
self.focused_index == Some(added_contexts.len())
self.focused_index == Some(context.len())
}
fn accept_suggested_context(
&mut self,
_: &AcceptSuggestedContext,
_window: &mut Window,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(suggested) = self.suggested_context(cx) {
if self.is_suggested_focused(&self.added_contexts(cx)) {
self.add_suggested_context(&suggested, cx);
let context_store = self.context_store.read(cx);
if self.is_suggested_focused(context_store.context()) {
self.add_suggested_context(&suggested, window, cx);
}
}
}
fn add_suggested_context(&mut self, suggested: &SuggestedContext, cx: &mut Context<Self>) {
self.context_store.update(cx, |context_store, cx| {
context_store.add_suggested_context(&suggested, cx)
fn add_suggested_context(
&mut self,
suggested: &SuggestedContext,
window: &mut Window,
cx: &mut Context<Self>,
) {
let task = self.context_store.update(cx, |context_store, cx| {
context_store.accept_suggested_context(&suggested, cx)
});
cx.spawn_in(window, async move |this, cx| {
match task.await.notify_async_err(cx) {
None => {}
Some(()) => {
if let Some(this) = this.upgrade() {
this.update(cx, |_, cx| cx.notify())?;
}
}
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
cx.notify();
}
}
@@ -358,10 +361,17 @@ impl Focusable for ContextStrip {
impl Render for ContextStrip {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let context_store = self.context_store.read(cx);
let context = context_store.context();
let context_picker = self.context_picker.clone();
let focus_handle = self.focus_handle.clone();
let added_contexts = self.added_contexts(cx);
let suggested_context = self.suggested_context(cx);
let added_contexts = context
.iter()
.map(|c| AddedContext::new(c, cx))
.collect::<Vec<_>>();
let dupe_names = added_contexts
.iter()
.map(|c| c.name.clone())
@@ -370,14 +380,6 @@ impl Render for ContextStrip {
.filter(|(a, b)| a == b)
.map(|(a, _)| a)
.collect::<HashSet<SharedString>>();
let no_added_context = added_contexts.is_empty();
let suggested_context = self.suggested_context(cx).map(|suggested_context| {
(
suggested_context,
self.is_suggested_focused(&added_contexts),
)
});
h_flex()
.flex_wrap()
@@ -434,7 +436,7 @@ impl Render for ContextStrip {
})
.with_handle(self.context_picker_menu_handle.clone()),
)
.when(no_added_context && suggested_context.is_none(), {
.when(context.is_empty() && suggested_context.is_none(), {
|parent| {
parent.child(
h_flex()
@@ -464,17 +466,16 @@ impl Render for ContextStrip {
.enumerate()
.map(|(i, added_context)| {
let name = added_context.name.clone();
let context = added_context.handle.clone();
let id = added_context.id;
ContextPill::added(
added_context,
dupe_names.contains(&name),
self.focused_index == Some(i),
Some({
let context = context.clone();
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(&context, cx);
this.remove_context(id, cx);
});
cx.notify();
}))
@@ -483,7 +484,7 @@ impl Render for ContextStrip {
.on_click({
Rc::new(cx.listener(move |this, event: &ClickEvent, window, cx| {
if event.down.click_count > 1 {
this.open_context(&context, window, cx);
this.open_context(id, window, cx);
} else {
this.focused_index = Some(i);
}
@@ -492,22 +493,22 @@ impl Render for ContextStrip {
})
}),
)
.when_some(suggested_context, |el, (suggested, focused)| {
.when_some(suggested_context, |el, suggested| {
el.child(
ContextPill::suggested(
suggested.name().clone(),
suggested.icon_path(),
suggested.kind(),
focused,
self.is_suggested_focused(&context),
)
.on_click(Rc::new(cx.listener(
move |this, _event, _window, cx| {
this.add_suggested_context(&suggested, cx);
move |this, _event, window, cx| {
this.add_suggested_context(&suggested, window, cx);
},
))),
)
})
.when(!no_added_context, {
.when(!context.is_empty(), {
move |parent| {
parent.child(
IconButton::new("remove-all-context", IconName::Eraser)
@@ -533,7 +534,6 @@ impl Render for ContextStrip {
)
}
})
.into_any()
}
}

View File

@@ -1,27 +1,10 @@
use std::{collections::VecDeque, path::Path};
use anyhow::{Context as _, anyhow};
use assistant_context_editor::{AssistantContext, SavedContextMetadata};
use assistant_context_editor::SavedContextMetadata;
use chrono::{DateTime, Utc};
use futures::future::{TryFutureExt as _, join_all};
use gpui::{Entity, Task, prelude::*};
use serde::{Deserialize, Serialize};
use smol::future::FutureExt;
use std::time::Duration;
use ui::{App, SharedString};
use util::ResultExt as _;
use gpui::{Entity, prelude::*};
use crate::{
Thread,
thread::ThreadId,
thread_store::{SerializedThreadMetadata, ThreadStore},
};
use crate::thread_store::{SerializedThreadMetadata, ThreadStore};
const MAX_RECENTLY_OPENED_ENTRIES: usize = 6;
const NAVIGATION_HISTORY_PATH: &str = "agent-navigation-history.json";
const SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE: Duration = Duration::from_millis(50);
#[derive(Clone, Debug)]
#[derive(Debug)]
pub enum HistoryEntry {
Thread(SerializedThreadMetadata),
Context(SavedContextMetadata),
@@ -36,40 +19,16 @@ impl HistoryEntry {
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum RecentEntry {
Thread(Entity<Thread>),
Context(Entity<AssistantContext>),
}
impl RecentEntry {
pub(crate) fn summary(&self, cx: &App) -> SharedString {
match self {
RecentEntry::Thread(thread) => thread.read(cx).summary_or_default(),
RecentEntry::Context(context) => context.read(cx).summary_or_default(),
}
}
}
#[derive(Serialize, Deserialize)]
enum SerializedRecentEntry {
Thread(String),
Context(String),
}
pub struct HistoryStore {
thread_store: Entity<ThreadStore>,
context_store: Entity<assistant_context_editor::ContextStore>,
recently_opened_entries: VecDeque<RecentEntry>,
_subscriptions: Vec<gpui::Subscription>,
_save_recently_opened_entries_task: Task<()>,
}
impl HistoryStore {
pub fn new(
thread_store: Entity<ThreadStore>,
context_store: Entity<assistant_context_editor::ContextStore>,
initial_recent_entries: impl IntoIterator<Item = RecentEntry>,
cx: &mut Context<Self>,
) -> Self {
let subscriptions = vec![
@@ -77,61 +36,10 @@ impl HistoryStore {
cx.observe(&context_store, |_, _, cx| cx.notify()),
];
cx.spawn({
let thread_store = thread_store.downgrade();
let context_store = context_store.downgrade();
async move |this, cx| {
let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
let contents = cx
.background_spawn(async move { std::fs::read_to_string(path) })
.await
.context("reading persisted agent panel navigation history")?;
let entries = serde_json::from_str::<Vec<SerializedRecentEntry>>(&contents)
.context("deserializing persisted agent panel navigation history")?
.into_iter()
.take(MAX_RECENTLY_OPENED_ENTRIES)
.map(|serialized| match serialized {
SerializedRecentEntry::Thread(id) => thread_store
.update(cx, |thread_store, cx| {
thread_store
.open_thread(&ThreadId::from(id.as_str()), cx)
.map_ok(RecentEntry::Thread)
.boxed()
})
.unwrap_or_else(|_| async { Err(anyhow!("no thread store")) }.boxed()),
SerializedRecentEntry::Context(id) => context_store
.update(cx, |context_store, cx| {
context_store
.open_local_context(Path::new(&id).into(), cx)
.map_ok(RecentEntry::Context)
.boxed()
})
.unwrap_or_else(|_| async { Err(anyhow!("no context store")) }.boxed()),
});
let entries = join_all(entries)
.await
.into_iter()
.filter_map(|result| result.log_err())
.collect::<VecDeque<_>>();
this.update(cx, |this, _| {
this.recently_opened_entries.extend(entries);
this.recently_opened_entries
.truncate(MAX_RECENTLY_OPENED_ENTRIES);
})
.ok();
anyhow::Ok(())
}
})
.detach_and_log_err(cx);
Self {
thread_store,
context_store,
recently_opened_entries: initial_recent_entries.into_iter().collect(),
_subscriptions: subscriptions,
_save_recently_opened_entries_task: Task::ready(()),
}
}
@@ -143,10 +51,7 @@ impl HistoryStore {
return history_entries;
}
for thread in self
.thread_store
.update(cx, |this, _cx| this.reverse_chronological_threads())
{
for thread in self.thread_store.update(cx, |this, _cx| this.threads()) {
history_entries.push(HistoryEntry::Thread(thread));
}
@@ -161,57 +66,4 @@ impl HistoryStore {
pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
self.entries(cx).into_iter().take(limit).collect()
}
fn save_recently_opened_entries(&mut self, cx: &mut Context<Self>) {
let serialized_entries = self
.recently_opened_entries
.iter()
.filter_map(|entry| match entry {
RecentEntry::Context(context) => Some(SerializedRecentEntry::Context(
context.read(cx).path()?.to_str()?.to_owned(),
)),
RecentEntry::Thread(thread) => Some(SerializedRecentEntry::Thread(
thread.read(cx).id().to_string(),
)),
})
.collect::<Vec<_>>();
self._save_recently_opened_entries_task = cx.spawn(async move |_, cx| {
cx.background_executor()
.timer(SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE)
.await;
cx.background_spawn(async move {
let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
let content = serde_json::to_string(&serialized_entries)?;
std::fs::write(path, content)?;
anyhow::Ok(())
})
.await
.log_err();
});
}
pub fn push_recently_opened_entry(&mut self, entry: RecentEntry, cx: &mut Context<Self>) {
self.recently_opened_entries
.retain(|old_entry| old_entry != &entry);
self.recently_opened_entries.push_front(entry);
self.recently_opened_entries
.truncate(MAX_RECENTLY_OPENED_ENTRIES);
self.save_recently_opened_entries(cx);
}
pub fn remove_recently_opened_entry(&mut self, entry: &RecentEntry, cx: &mut Context<Self>) {
self.recently_opened_entries
.retain(|old_entry| old_entry != entry);
self.save_recently_opened_entries(cx);
}
pub fn recently_opened_entries(&self, _cx: &mut Context<Self>) -> VecDeque<RecentEntry> {
#[cfg(debug_assertions)]
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
return VecDeque::new();
}
self.recently_opened_entries.clone()
}
}

View File

@@ -24,7 +24,6 @@ use gpui::{
WeakEntity, Window, point,
};
use language::{Buffer, Point, Selection, TransactionId};
use language_model::ConfiguredModel;
use language_model::{LanguageModelRegistry, report_assistant_event};
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
@@ -32,7 +31,6 @@ use project::LspAction;
use project::Project;
use project::{CodeAction, ProjectTransaction};
use prompt_store::PromptBuilder;
use prompt_store::PromptStore;
use settings::{Settings, SettingsStore};
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
@@ -246,13 +244,9 @@ impl InlineAssistant {
.map_or(false, |model| model.provider.is_authenticated(cx))
};
let assistant_panel = workspace
let thread_store = workspace
.panel::<AssistantPanel>(cx)
.map(|assistant_panel| assistant_panel.read(cx));
let prompt_store = assistant_panel
.and_then(|assistant_panel| assistant_panel.prompt_store().as_ref().cloned());
let thread_store =
assistant_panel.map(|assistant_panel| assistant_panel.thread_store().downgrade());
.map(|assistant_panel| assistant_panel.read(cx).thread_store().downgrade());
let handle_assist =
|window: &mut Window, cx: &mut Context<Workspace>| match inline_assist_target {
@@ -262,7 +256,6 @@ impl InlineAssistant {
&active_editor,
cx.entity().downgrade(),
workspace.project().downgrade(),
prompt_store,
thread_store,
window,
cx,
@@ -275,7 +268,6 @@ impl InlineAssistant {
&active_terminal,
cx.entity().downgrade(),
workspace.project().downgrade(),
prompt_store,
thread_store,
window,
cx,
@@ -330,7 +322,6 @@ impl InlineAssistant {
editor: &Entity<Editor>,
workspace: WeakEntity<Workspace>,
project: WeakEntity<Project>,
prompt_store: Option<Entity<PromptStore>>,
thread_store: Option<WeakEntity<ThreadStore>>,
window: &mut Window,
cx: &mut App,
@@ -445,8 +436,6 @@ impl InlineAssistant {
range.clone(),
None,
context_store.clone(),
project.clone(),
prompt_store.clone(),
self.telemetry.clone(),
self.prompt_builder.clone(),
cx,
@@ -535,7 +524,6 @@ impl InlineAssistant {
initial_transaction_id: Option<TransactionId>,
focus: bool,
workspace: Entity<Workspace>,
prompt_store: Option<Entity<PromptStore>>,
thread_store: Option<WeakEntity<ThreadStore>>,
window: &mut Window,
cx: &mut App,
@@ -554,7 +542,7 @@ impl InlineAssistant {
}
let project = workspace.read(cx).project().downgrade();
let context_store = cx.new(|_cx| ContextStore::new(project.clone(), thread_store.clone()));
let context_store = cx.new(|_cx| ContextStore::new(project, thread_store.clone()));
let codegen = cx.new(|cx| {
BufferCodegen::new(
@@ -562,8 +550,6 @@ impl InlineAssistant {
range.clone(),
initial_transaction_id,
context_store.clone(),
project,
prompt_store,
self.telemetry.clone(),
self.prompt_builder.clone(),
cx,
@@ -1235,15 +1221,9 @@ impl InlineAssistant {
self.prompt_history.pop_front();
}
let Some(ConfiguredModel { model, .. }) =
LanguageModelRegistry::read_global(cx).inline_assistant_model()
else {
return;
};
assist
.codegen
.update(cx, |codegen, cx| codegen.start(model, user_prompt, cx))
.update(cx, |codegen, cx| codegen.start(user_prompt, cx))
.log_err();
}
@@ -1341,7 +1321,7 @@ impl InlineAssistant {
editor.highlight_rows::<InlineAssist>(
row_range,
cx.theme().status().info_background,
Default::default(),
false,
cx,
);
}
@@ -1406,7 +1386,7 @@ impl InlineAssistant {
editor.highlight_rows::<DeletedLines>(
Anchor::min()..Anchor::max(),
cx.theme().status().deleted_background,
Default::default(),
false,
cx,
);
editor
@@ -1802,7 +1782,6 @@ impl CodeActionProvider for AssistantCodeActionProvider {
let editor = self.editor.clone();
let workspace = self.workspace.clone();
let thread_store = self.thread_store.clone();
let prompt_store = PromptStore::global(cx);
window.spawn(cx, async move |cx| {
let workspace = workspace.upgrade().context("workspace was released")?;
let editor = editor.upgrade().context("editor was released")?;
@@ -1843,7 +1822,6 @@ impl CodeActionProvider for AssistantCodeActionProvider {
})?
.context("invalid range")?;
let prompt_store = prompt_store.await.ok();
cx.update_global(|assistant: &mut InlineAssistant, window, cx| {
let assist_id = assistant.suggest_assist(
&editor,
@@ -1852,7 +1830,6 @@ impl CodeActionProvider for AssistantCodeActionProvider {
None,
true,
workspace,
prompt_store,
thread_store,
window,
cx,

View File

@@ -13,7 +13,7 @@ use editor::{
Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, GutterDimensions, MultiBuffer,
actions::{MoveDown, MoveUp},
};
use feature_flags::{FeatureFlagAppExt as _, ZedProFeatureFlag};
use feature_flags::{FeatureFlagAppExt as _, ZedPro};
use fs::Fs;
use gpui::{
AnyElement, App, ClickEvent, Context, CursorStyle, Entity, EventEmitter, FocusHandle,
@@ -132,7 +132,7 @@ impl<T: 'static> Render for PromptEditor<T> {
let error_message = SharedString::from(error.to_string());
if error.error_code() == proto::ErrorCode::RateLimitExceeded
&& cx.has_flag::<ZedProFeatureFlag>()
&& cx.has_flag::<ZedPro>()
{
el.child(
v_flex()
@@ -931,7 +931,7 @@ impl PromptEditor<BufferCodegen> {
.update(cx, |editor, _| editor.set_read_only(false));
}
CodegenStatus::Error(error) => {
if cx.has_flag::<ZedProFeatureFlag>()
if cx.has_flag::<ZedPro>()
&& error.error_code() == proto::ErrorCode::RateLimitExceeded
&& !dismissed_rate_limit_notice()
{

View File

@@ -1,45 +1,40 @@
use std::collections::BTreeMap;
use std::sync::Arc;
use crate::assistant_model_selector::{AssistantModelSelector, ModelType};
use crate::context::{ContextLoadResult, load_context};
use crate::assistant_model_selector::ModelType;
use crate::context::{AssistantContext, format_context_as_string};
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
use crate::ui::AnimatedLabel;
use buffer_diff::BufferDiff;
use collections::HashSet;
use editor::actions::{MoveUp, Paste};
use editor::actions::MoveUp;
use editor::{
ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorEvent, EditorMode,
EditorStyle, MultiBuffer,
};
use feature_flags::{FeatureFlagAppExt, NewBillingFeatureFlag};
use file_icons::FileIcons;
use fs::Fs;
use futures::future::Shared;
use futures::{FutureExt as _, future};
use gpui::{
Animation, AnimationExt, App, ClipboardEntry, Entity, EventEmitter, Focusable, Subscription,
Task, TextStyle, WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
Animation, AnimationExt, App, Entity, EventEmitter, Focusable, Subscription, Task, TextStyle,
WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
};
use language::{Buffer, Language};
use language_model::{ConfiguredModel, LanguageModelRequestMessage, MessageContent};
use language_model::{ConfiguredModel, LanguageModelRegistry, LanguageModelRequestMessage};
use language_model_selector::ToggleModelSelector;
use multi_buffer;
use project::Project;
use prompt_store::PromptStore;
use settings::Settings;
use std::time::Duration;
use theme::ThemeSettings;
use ui::{Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
use util::ResultExt as _;
use workspace::Workspace;
use zed_llm_client::CompletionMode;
use crate::assistant_model_selector::AssistantModelSelector;
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider};
use crate::context_store::ContextStore;
use crate::context_store::{ContextStore, refresh_context_store_text};
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::profile_selector::ProfileSelector;
use crate::thread::{Thread, TokenUsageRatio};
use crate::thread::{RequestKind, Thread, TokenUsageRatio};
use crate::thread_store::ThreadStore;
use crate::{
AgentDiff, Chat, ChatMode, ExpandMessageEditor, NewThread, OpenAgentDiff, RemoveAllContext,
@@ -50,20 +45,19 @@ pub struct MessageEditor {
thread: Entity<Thread>,
incompatible_tools_state: Entity<IncompatibleToolsState>,
editor: Entity<Editor>,
#[allow(dead_code)]
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
context_store: Entity<ContextStore>,
prompt_store: Option<Entity<PromptStore>>,
context_strip: Entity<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
model_selector: Entity<AssistantModelSelector>,
last_loaded_context: Option<ContextLoadResult>,
load_context_task: Option<Shared<Task<()>>>,
profile_selector: Entity<ProfileSelector>,
edits_expanded: bool,
editor_is_expanded: bool,
waiting_for_summaries_to_send: bool,
last_estimated_token_count: Option<usize>,
update_token_count_task: Option<Task<()>>,
update_token_count_task: Option<Task<anyhow::Result<()>>>,
_subscriptions: Vec<Subscription>,
}
@@ -74,7 +68,6 @@ impl MessageEditor {
fs: Arc<dyn Fs>,
workspace: WeakEntity<Workspace>,
context_store: Entity<ContextStore>,
prompt_store: Option<Entity<PromptStore>>,
thread_store: WeakEntity<ThreadStore>,
thread: Entity<Thread>,
window: &mut Window,
@@ -142,26 +135,16 @@ impl MessageEditor {
let subscriptions = vec![
cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event),
cx.subscribe(&editor, |this, _, event, cx| match event {
EditorEvent::BufferEdited => this.handle_message_changed(cx),
EditorEvent::BufferEdited => {
this.message_or_context_changed(true, cx);
}
_ => {}
}),
cx.observe(&context_store, |this, _, cx| {
// When context changes, reload it for token counting.
let _ = this.reload_context(cx);
this.message_or_context_changed(false, cx);
}),
];
let model_selector = cx.new(|cx| {
AssistantModelSelector::new(
fs.clone(),
model_selector_menu_handle,
editor.focus_handle(cx),
ModelType::Default(thread.clone()),
window,
cx,
)
});
Self {
editor: editor.clone(),
project: thread.read(cx).project().clone(),
@@ -169,14 +152,21 @@ impl MessageEditor {
incompatible_tools_state: incompatible_tools.clone(),
workspace,
context_store,
prompt_store,
context_strip,
context_picker_menu_handle,
load_context_task: None,
last_loaded_context: None,
model_selector,
model_selector: cx.new(|cx| {
AssistantModelSelector::new(
fs.clone(),
model_selector_menu_handle,
editor.focus_handle(cx),
ModelType::Default,
window,
cx,
)
}),
edits_expanded: false,
editor_is_expanded: false,
waiting_for_summaries_to_send: false,
profile_selector: cx
.new(|cx| ProfileSelector::new(fs, thread_store, editor.focus_handle(cx), cx)),
last_estimated_token_count: None,
@@ -185,10 +175,6 @@ impl MessageEditor {
}
}
pub fn context_store(&self) -> &Entity<ContextStore> {
&self.context_store
}
fn toggle_chat_mode(&mut self, _: &ChatMode, _window: &mut Window, cx: &mut Context<Self>) {
cx.notify();
}
@@ -209,7 +195,6 @@ impl MessageEditor {
editor.set_mode(EditorMode::Full {
scale_ui_elements_with_buffer_font_size: false,
show_active_line_background: false,
sized_by_content: false,
})
} else {
editor.set_mode(EditorMode::AutoHeight {
@@ -228,7 +213,6 @@ impl MessageEditor {
) {
self.context_picker_menu_handle.toggle(window, cx);
}
pub fn remove_all_context(
&mut self,
_: &RemoveAllContext,
@@ -244,17 +228,13 @@ impl MessageEditor {
return;
}
self.thread.update(cx, |thread, cx| {
thread.cancel_editing(cx);
});
if self.thread.read(cx).is_generating() {
self.stop_current_and_send_new_message(window, cx);
return;
}
self.set_editor_is_expanded(false, cx);
self.send_to_model(window, cx);
self.send_to_model(RequestKind::Chat, window, cx);
cx.notify();
}
@@ -263,11 +243,20 @@ impl MessageEditor {
self.editor.read(cx).text(cx).trim().is_empty()
}
fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(ConfiguredModel { model, provider }) = self
.thread
.update(cx, |thread, cx| thread.get_or_init_configured_model(cx))
else {
fn is_model_selected(&self, cx: &App) -> bool {
LanguageModelRegistry::read_global(cx)
.default_model()
.is_some()
}
fn send_to_model(
&mut self,
request_kind: RequestKind,
window: &mut Window,
cx: &mut Context<Self>,
) {
let model_registry = LanguageModelRegistry::read_global(cx);
let Some(ConfiguredModel { model, provider }) = model_registry.default_model() else {
return;
};
@@ -285,26 +274,64 @@ impl MessageEditor {
self.last_estimated_token_count.take();
cx.emit(MessageEditorEvent::EstimatedTokenCount);
let refresh_task =
refresh_context_store_text(self.context_store.clone(), &HashSet::default(), cx);
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 context_task = self.reload_context(cx);
let window_handle = window.window_handle();
cx.spawn(async move |_this, cx| {
let (checkpoint, loaded_context) = future::join(checkpoint, context_task).await;
let loaded_context = loaded_context.unwrap_or_default();
cx.spawn(async move |this, cx| {
let checkpoint = checkpoint.await.ok();
refresh_task.await;
thread
.update(cx, |thread, cx| {
thread.insert_user_message(user_message, loaded_context, checkpoint.ok(), cx);
let context = context_store.read(cx).context().clone();
thread.insert_user_message(user_message, context, checkpoint, cx);
})
.log_err();
context_store
.update(cx, |context_store, cx| {
let excerpt_ids = context_store
.context()
.iter()
.filter(|ctx| matches!(ctx, AssistantContext::Excerpt(_)))
.map(|ctx| ctx.id())
.collect::<Vec<_>>();
for id in excerpt_ids {
context_store.remove_context(id, cx);
}
})
.log_err();
if let Some(wait_for_summaries) = context_store
.update(cx, |context_store, cx| context_store.wait_for_summaries(cx))
.log_err()
{
this.update(cx, |this, cx| {
this.waiting_for_summaries_to_send = true;
cx.notify();
})
.log_err();
wait_for_summaries.await;
this.update(cx, |this, cx| {
this.waiting_for_summaries_to_send = false;
cx.notify();
})
.log_err();
}
// Send to model after summaries are done
thread
.update(cx, |thread, cx| {
thread.advance_prompt_id();
thread.send_to_model(model, Some(window_handle), cx);
thread.send_to_model(model, request_kind, cx);
})
.log_err();
})
@@ -312,17 +339,13 @@ impl MessageEditor {
}
fn stop_current_and_send_new_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.thread.update(cx, |thread, cx| {
thread.cancel_editing(cx);
});
let cancelled = self.thread.update(cx, |thread, cx| {
thread.cancel_last_completion(Some(window.window_handle()), cx)
});
let cancelled = self
.thread
.update(cx, |thread, cx| thread.cancel_last_completion(cx));
if cancelled {
self.set_editor_is_expanded(false, cx);
self.send_to_model(window, cx);
self.send_to_model(RequestKind::Chat, window, cx);
}
}
@@ -352,38 +375,8 @@ impl MessageEditor {
}
}
fn paste(&mut self, _: &Paste, _: &mut Window, cx: &mut Context<Self>) {
let images = cx
.read_from_clipboard()
.map(|item| {
item.into_entries()
.filter_map(|entry| {
if let ClipboardEntry::Image(image) = entry {
Some(image)
} else {
None
}
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
if images.is_empty() {
return;
}
cx.stop_propagation();
self.context_store.update(cx, |store, cx| {
for image in images {
store.add_image_instance(Arc::new(image), cx);
}
});
}
fn handle_review_click(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.edits_expanded = true;
fn handle_review_click(&self, window: &mut Window, cx: &mut Context<Self>) {
AgentDiff::deploy(self.thread.clone(), self.workspace.clone(), window, cx).log_err();
cx.notify();
}
fn handle_file_click(
@@ -399,36 +392,6 @@ impl MessageEditor {
}
}
fn render_max_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
if !cx.has_flag::<NewBillingFeatureFlag>() {
return None;
}
let thread = self.thread.read(cx);
let model = thread.configured_model();
if !model?.model.supports_max_mode() {
return None;
}
let active_completion_mode = thread.completion_mode();
Some(
IconButton::new("max-mode", IconName::ZedMaxMode)
.icon_size(IconSize::Small)
.toggle_state(active_completion_mode == Some(CompletionMode::Max))
.on_click(cx.listener(move |this, _event, _window, cx| {
this.thread.update(cx, |thread, _cx| {
thread.set_completion_mode(match active_completion_mode {
Some(CompletionMode::Max) => Some(CompletionMode::Normal),
Some(CompletionMode::Normal) | None => Some(CompletionMode::Max),
});
});
}))
.tooltip(Tooltip::text("Toggle Max Mode"))
.into_any_element(),
)
}
fn render_editor(
&self,
font_size: Rems,
@@ -437,21 +400,24 @@ impl MessageEditor {
cx: &mut Context<Self>,
) -> Div {
let thread = self.thread.read(cx);
let model = thread.configured_model();
let editor_bg_color = cx.theme().colors().editor_background;
let is_generating = thread.is_generating();
let focus_handle = self.editor.focus_handle(cx);
let is_model_selected = model.is_some();
let is_model_selected = self.is_model_selected(cx);
let is_editor_empty = self.is_editor_empty(cx);
let model = LanguageModelRegistry::read_global(cx)
.default_model()
.map(|default| default.model.clone());
let incompatible_tools = model
.as_ref()
.map(|model| {
self.incompatible_tools_state.update(cx, |state, cx| {
state
.incompatible_tools(&model.model, cx)
.incompatible_tools(model, cx)
.iter()
.cloned()
.collect::<Vec<_>>()
@@ -484,7 +450,6 @@ impl MessageEditor {
.on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::toggle_chat_mode))
.on_action(cx.listener(Self::expand_message_editor))
.capture_action(cx.listener(Self::paste))
.gap_2()
.p_2()
.bg(editor_bg_color)
@@ -495,34 +460,32 @@ impl MessageEditor {
.items_start()
.justify_between()
.child(self.context_strip.clone())
.when(focus_handle.is_focused(window), |this| {
this.child(
IconButton::new("toggle-height", expand_icon)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
let expand_label = if is_editor_expanded {
"Minimize Message Editor".to_string()
} else {
"Expand Message Editor".to_string()
};
.child(
IconButton::new("toggle-height", expand_icon)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
let expand_label = if is_editor_expanded {
"Minimize Message Editor".to_string()
} else {
"Expand Message Editor".to_string()
};
Tooltip::for_action_in(
expand_label,
&ExpandMessageEditor,
&focus_handle,
window,
cx,
)
}
})
.on_click(cx.listener(|_, _, window, cx| {
window.dispatch_action(Box::new(ExpandMessageEditor), cx);
})),
)
}),
Tooltip::for_action_in(
expand_label,
&ExpandMessageEditor,
&focus_handle,
window,
cx,
)
}
})
.on_click(cx.listener(|_, _, window, cx| {
window.dispatch_action(Box::new(ExpandMessageEditor), cx);
})),
),
)
.child(
v_flex()
@@ -588,7 +551,6 @@ impl MessageEditor {
}),
)
})
.children(self.render_max_mode_toggle(cx))
.child(self.model_selector.clone())
.map({
let focus_handle = focus_handle.clone();
@@ -641,31 +603,31 @@ impl MessageEditor {
})
.when(!is_editor_empty, |parent| {
parent.child(
IconButton::new(
"send-message",
IconName::Send,
)
.icon_color(Color::Accent)
.style(ButtonStyle::Filled)
.disabled(!is_model_selected)
.on_click({
let focus_handle =
focus_handle.clone();
move |_event, window, cx| {
focus_handle.dispatch_action(
&Chat, window, cx,
);
}
})
.tooltip(move |window, cx| {
Tooltip::for_action(
"Stop and Send New Message",
&Chat,
window,
cx,
)
}),
IconButton::new("send-message", IconName::Send)
.icon_color(Color::Accent)
.style(ButtonStyle::Filled)
.disabled(
!is_model_selected
|| self
.waiting_for_summaries_to_send,
)
.on_click({
let focus_handle = focus_handle.clone();
move |_event, window, cx| {
focus_handle.dispatch_action(
&Chat, window, cx,
);
}
})
.tooltip(move |window, cx| {
Tooltip::for_action(
"Stop and Send New Message",
&Chat,
window,
cx,
)
}),
)
})
} else {
parent.child(
@@ -673,7 +635,10 @@ impl MessageEditor {
.icon_color(Color::Accent)
.style(ButtonStyle::Filled)
.disabled(
is_editor_empty || !is_model_selected,
is_editor_empty
|| !is_model_selected
|| self
.waiting_for_summaries_to_send,
)
.on_click({
let focus_handle = focus_handle.clone();
@@ -724,12 +689,9 @@ impl MessageEditor {
let border_color = cx.theme().colors().border;
let active_color = cx.theme().colors().element_selected;
let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
let is_edit_changes_expanded = self.edits_expanded;
let is_generating = self.thread.read(cx).is_generating();
v_flex()
.mt_1()
.mx_2()
.bg(bg_edit_files_disclosure)
.border_1()
@@ -764,44 +726,25 @@ impl MessageEditor {
cx.notify();
})),
)
.map(|this| {
if is_generating {
this.child(
AnimatedLabel::new(format!(
"Editing {} {}",
changed_buffers.len(),
if changed_buffers.len() == 1 {
"file"
} else {
"files"
}
))
.size(LabelSize::Small),
)
} else {
this.child(
Label::new("Edits")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
Label::new("").size(LabelSize::XSmall).color(Color::Muted),
)
.child(
Label::new(format!(
"{} {}",
changed_buffers.len(),
if changed_buffers.len() == 1 {
"file"
} else {
"files"
}
))
.size(LabelSize::Small)
.color(Color::Muted),
)
}
}),
.child(
Label::new("Edits")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(Label::new("").size(LabelSize::XSmall).color(Color::Muted))
.child(
Label::new(format!(
"{} {}",
changed_buffers.len(),
if changed_buffers.len() == 1 {
"file"
} else {
"files"
}
))
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.child(
Button::new("review", "Review Changes")
@@ -891,7 +834,7 @@ impl MessageEditor {
.justify_between()
.bg(cx.theme().colors().editor_background)
.hover(|style| style.bg(hover_color))
.when(index < changed_buffers.len() - 1, |parent| {
.when(index + 1 < changed_buffers.len(), |parent| {
parent.border_color(border_color).border_b_1()
})
.child(
@@ -907,9 +850,9 @@ impl MessageEditor {
.gap_0p5()
.children(name_label)
.children(parent_label),
), // TODO: Implement line diff
// .child(Label::new("+").color(Color::Created))
// .child(Label::new("-").color(Color::Deleted)),
) // TODO: show lines changed
.child(Label::new("+").color(Color::Created))
.child(Label::new("-").color(Color::Deleted)),
)
.child(
div().visible_on_hover("edited-code").child(
@@ -1037,49 +980,18 @@ impl MessageEditor {
self.update_token_count_task.is_some()
}
fn reload_context(&mut self, cx: &mut Context<Self>) -> Task<Option<ContextLoadResult>> {
let load_task = cx.spawn(async move |this, cx| {
let Ok(load_task) = this.update(cx, |this, cx| {
let new_context = this.context_store.read_with(cx, |context_store, cx| {
context_store.new_context_for_thread(this.thread.read(cx))
});
load_context(new_context, &this.project, &this.prompt_store, cx)
}) else {
return;
};
let result = load_task.await;
this.update(cx, |this, cx| {
this.last_loaded_context = Some(result);
this.load_context_task = None;
this.message_or_context_changed(false, cx);
})
.ok();
});
// Replace existing load task, if any, causing it to be cancelled.
let load_task = load_task.shared();
self.load_context_task = Some(load_task.clone());
cx.spawn(async move |this, cx| {
load_task.await;
this.read_with(cx, |this, _cx| this.last_loaded_context.clone())
.ok()
.flatten()
})
}
fn handle_message_changed(&mut self, cx: &mut Context<Self>) {
self.message_or_context_changed(true, cx);
}
fn message_or_context_changed(&mut self, debounce: bool, cx: &mut Context<Self>) {
cx.emit(MessageEditorEvent::Changed);
self.update_token_count_task.take();
let Some(model) = self.thread.read(cx).configured_model() else {
let Some(default_model) = LanguageModelRegistry::read_global(cx).default_model() else {
self.last_estimated_token_count.take();
return;
};
let context_store = self.context_store.clone();
let editor = self.editor.clone();
let thread = self.thread.clone();
self.update_token_count_task = Some(cx.spawn(async move |this, cx| {
if debounce {
@@ -1088,64 +1000,44 @@ impl MessageEditor {
.await;
}
let token_count = if let Some(task) = this
.update(cx, |this, cx| {
let loaded_context = this
.last_loaded_context
.as_ref()
.map(|context_load_result| &context_load_result.loaded_context);
let message_text = editor.read(cx).text(cx);
let token_count = if let Some(task) = cx.update(|cx| {
let context = context_store.read(cx).context().iter();
let new_context = thread.read(cx).filter_new_context(context);
let context_text =
format_context_as_string(new_context, cx).unwrap_or(String::new());
let message_text = editor.read(cx).text(cx);
if message_text.is_empty()
&& loaded_context.map_or(true, |loaded_context| loaded_context.is_empty())
{
return None;
}
let content = context_text + &message_text;
let mut request_message = LanguageModelRequestMessage {
if content.is_empty() {
return None;
}
let request = language_model::LanguageModelRequest {
thread_id: None,
prompt_id: None,
messages: vec![LanguageModelRequestMessage {
role: language_model::Role::User,
content: Vec::new(),
content: vec![content.into()],
cache: false,
};
}],
tools: vec![],
stop: vec![],
temperature: None,
};
if let Some(loaded_context) = loaded_context {
loaded_context.add_to_request_message(&mut request_message);
}
if !message_text.is_empty() {
request_message
.content
.push(MessageContent::Text(message_text));
}
let request = language_model::LanguageModelRequest {
thread_id: None,
prompt_id: None,
mode: None,
messages: vec![request_message],
tools: vec![],
stop: vec![],
temperature: None,
};
Some(model.model.count_tokens(request, cx))
})
.ok()
.flatten()
{
task.await.log_err()
Some(default_model.model.count_tokens(request, cx))
})? {
task.await?
} else {
Some(0)
0
};
this.update(cx, |this, cx| {
if let Some(token_count) = token_count {
this.last_estimated_token_count = Some(token_count);
cx.emit(MessageEditorEvent::EstimatedTokenCount);
}
this.last_estimated_token_count = Some(token_count);
cx.emit(MessageEditorEvent::EstimatedTokenCount);
this.update_token_count_task.take();
})
.ok();
}));
}
}
@@ -1166,11 +1058,8 @@ impl Focusable for MessageEditor {
impl Render for MessageEditor {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let thread = self.thread.read(cx);
let token_usage_ratio = thread
.total_token_usage()
.map_or(TokenUsageRatio::Normal, |total_token_usage| {
total_token_usage.ratio()
});
let total_token_usage = thread.total_token_usage(cx);
let token_usage_ratio = total_token_usage.ratio();
let action_log = self.thread.read(cx).action_log();
let changed_buffers = action_log.read(cx).changed_buffers(cx);
@@ -1180,6 +1069,41 @@ impl Render for MessageEditor {
v_flex()
.size_full()
.when(self.waiting_for_summaries_to_send, |parent| {
parent.child(
h_flex().py_3().w_full().justify_center().child(
h_flex()
.flex_none()
.px_2()
.py_2()
.bg(cx.theme().colors().editor_background)
.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("Summarizing context…")
.size(LabelSize::XSmall)
.color(Color::Muted),
),
),
)
})
.when(changed_buffers.len() > 0, |parent| {
parent.child(self.render_changed_buffers(&changed_buffers, window, cx))
})

View File

@@ -68,41 +68,30 @@ impl ProfileSelector {
menu = menu.header("Profiles");
for (profile_id, profile) in self.profiles.clone() {
let documentation = match profile.name.to_lowercase().as_str() {
"write" => Some("Get help to write anything."),
"ask" => Some("Chat about your codebase."),
"manual" => Some("Chat about anything; no tools."),
_ => None,
};
menu = menu.toggleable_entry(
profile.name.clone(),
profile_id == settings.default_profile,
icon_position,
None,
{
let fs = self.fs.clone();
let thread_store = self.thread_store.clone();
move |_window, cx| {
update_settings_file::<AssistantSettings>(fs.clone(), cx, {
let profile_id = profile_id.clone();
move |settings, _cx| {
settings.set_profile(profile_id.clone());
}
});
let entry = ContextMenuEntry::new(profile.name.clone())
.toggleable(icon_position, profile_id == settings.default_profile);
let entry = if let Some(doc_text) = documentation {
entry.documentation_aside(move |_| Label::new(doc_text).into_any_element())
} else {
entry
};
menu = menu.item(entry.handler({
let fs = self.fs.clone();
let thread_store = self.thread_store.clone();
let profile_id = profile_id.clone();
move |_window, cx| {
update_settings_file::<AssistantSettings>(fs.clone(), cx, {
let profile_id = profile_id.clone();
move |settings, _cx| {
settings.set_profile(profile_id.clone());
}
});
thread_store
.update(cx, |this, cx| {
this.load_profile_by_id(profile_id.clone(), cx);
})
.log_err();
}
}));
thread_store
.update(cx, |this, cx| {
this.load_profile_by_id(profile_id.clone(), cx);
})
.log_err();
}
},
);
}
menu = menu.separator();
@@ -152,7 +141,6 @@ impl Render for ProfileSelector {
let this = cx.entity().clone();
let focus_handle = self.focus_handle.clone();
PopoverMenu::new("profile-selector")
.menu(move |window, cx| {
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
@@ -195,7 +183,7 @@ impl Render for ProfileSelector {
)
.tooltip(Tooltip::text("The current model does not support tools."))
})
.anchor(gpui::Corner::BottomRight)
.anchor(gpui::Corner::BottomLeft)
.with_handle(self.menu_handle.clone())
}
}

View File

@@ -32,7 +32,7 @@ impl TerminalCodegen {
}
}
pub fn start(&mut self, prompt_task: Task<LanguageModelRequest>, cx: &mut Context<Self>) {
pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut Context<Self>) {
let Some(ConfiguredModel { model, .. }) =
LanguageModelRegistry::read_global(cx).inline_assistant_model()
else {
@@ -45,7 +45,6 @@ impl TerminalCodegen {
self.status = CodegenStatus::Pending;
self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
self.generation = cx.spawn(async move |this, cx| {
let prompt = prompt_task.await;
let model_telemetry_id = model.telemetry_id();
let model_provider_id = model.provider_id();
let response = model.stream_completion_text(prompt, &cx).await;

View File

@@ -1,4 +1,4 @@
use crate::context::load_context;
use crate::context::attach_context_to_message;
use crate::context_store::ContextStore;
use crate::inline_prompt_editor::{
CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId,
@@ -10,14 +10,14 @@ use client::telemetry::Telemetry;
use collections::{HashMap, VecDeque};
use editor::{MultiBuffer, actions::SelectAll};
use fs::Fs;
use gpui::{App, Entity, Focusable, Global, Subscription, Task, UpdateGlobal, WeakEntity};
use gpui::{App, Entity, Focusable, Global, Subscription, UpdateGlobal, WeakEntity};
use language::Buffer;
use language_model::{
ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
Role, report_assistant_event,
};
use project::Project;
use prompt_store::{PromptBuilder, PromptStore};
use prompt_store::PromptBuilder;
use std::sync::Arc;
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
use terminal_view::TerminalView;
@@ -69,7 +69,6 @@ impl TerminalInlineAssistant {
terminal_view: &Entity<TerminalView>,
workspace: WeakEntity<Workspace>,
project: WeakEntity<Project>,
prompt_store: Option<Entity<PromptStore>>,
thread_store: Option<WeakEntity<ThreadStore>>,
window: &mut Window,
cx: &mut App,
@@ -110,7 +109,6 @@ impl TerminalInlineAssistant {
prompt_editor,
workspace.clone(),
context_store,
prompt_store,
window,
cx,
);
@@ -198,11 +196,11 @@ impl TerminalInlineAssistant {
.log_err();
let codegen = assist.codegen.clone();
let Some(request_task) = self.request_for_inline_assist(assist_id, cx).log_err() else {
let Some(request) = self.request_for_inline_assist(assist_id, cx).log_err() else {
return;
};
codegen.update(cx, |codegen, cx| codegen.start(request_task, cx));
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
}
fn stop_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut App) {
@@ -219,7 +217,7 @@ impl TerminalInlineAssistant {
&self,
assist_id: TerminalInlineAssistId,
cx: &mut App,
) -> Result<Task<LanguageModelRequest>> {
) -> Result<LanguageModelRequest> {
let assist = self.assists.get(&assist_id).context("invalid assist")?;
let shell = std::env::var("SHELL").ok();
@@ -248,41 +246,28 @@ impl TerminalInlineAssistant {
&latest_output,
)?;
let contexts = assist
.context_store
.read(cx)
.context()
.cloned()
.collect::<Vec<_>>();
let context_load_task = assist.workspace.update(cx, |workspace, cx| {
let project = workspace.project();
load_context(contexts, project, &assist.prompt_store, cx)
})?;
let mut request_message = LanguageModelRequestMessage {
role: Role::User,
content: vec![],
cache: false,
};
Ok(cx.background_spawn(async move {
let mut request_message = LanguageModelRequestMessage {
role: Role::User,
content: vec![],
cache: false,
};
attach_context_to_message(
&mut request_message,
assist.context_store.read(cx).context().iter(),
cx,
);
context_load_task
.await
.loaded_context
.add_to_request_message(&mut request_message);
request_message.content.push(prompt.into());
request_message.content.push(prompt.into());
LanguageModelRequest {
thread_id: None,
prompt_id: None,
mode: None,
messages: vec![request_message],
tools: Vec::new(),
stop: Vec::new(),
temperature: None,
}
}))
Ok(LanguageModelRequest {
thread_id: None,
prompt_id: None,
messages: vec![request_message],
tools: Vec::new(),
stop: Vec::new(),
temperature: None,
})
}
fn finish_assist(
@@ -395,7 +380,6 @@ struct TerminalInlineAssist {
codegen: Entity<TerminalCodegen>,
workspace: WeakEntity<Workspace>,
context_store: Entity<ContextStore>,
prompt_store: Option<Entity<PromptStore>>,
_subscriptions: Vec<Subscription>,
}
@@ -406,7 +390,6 @@ impl TerminalInlineAssist {
prompt_editor: Entity<PromptEditor<TerminalCodegen>>,
workspace: WeakEntity<Workspace>,
context_store: Entity<ContextStore>,
prompt_store: Option<Entity<PromptStore>>,
window: &mut Window,
cx: &mut App,
) -> Self {
@@ -417,7 +400,6 @@ impl TerminalInlineAssist {
codegen: codegen.clone(),
workspace: workspace.clone(),
context_store,
prompt_store,
_subscriptions: vec![
window.subscribe(&prompt_editor, cx, |prompt_editor, event, window, cx| {
TerminalInlineAssistant::update_global(cx, |this, cx| {

File diff suppressed because it is too large Load Diff

View File

@@ -270,9 +270,9 @@ impl ThreadHistory {
fn confirm(&mut self, _: &menu::Confirm, 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, move |this, cx| {
this.open_thread_by_id(&thread.id, window, cx)
}),
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| {
this.open_saved_prompt_editor(context.path.clone(), window, cx)
@@ -525,8 +525,7 @@ impl RenderOnce for PastThread {
move |_event, window, cx| {
assistant_panel
.update(cx, |this, cx| {
this.open_thread_by_id(&id, window, cx)
.detach_and_log_err(cx);
this.open_thread(&id, window, cx).detach_and_log_err(cx);
})
.ok();
}

View File

@@ -11,6 +11,7 @@ use chrono::{DateTime, Utc};
use collections::HashMap;
use context_server::manager::ContextServerManager;
use context_server::{ContextServerFactoryRegistry, ContextServerTool};
use fs::Fs;
use futures::channel::{mpsc, oneshot};
use futures::future::{self, BoxFuture, Shared};
use futures::{FutureExt as _, StreamExt as _};
@@ -21,10 +22,10 @@ use gpui::{
use heed::Database;
use heed::types::SerdeBincode;
use language_model::{LanguageModelToolUseId, Role, TokenUsage};
use project::{Project, ProjectItem, ProjectPath, Worktree};
use project::{Project, Worktree};
use prompt_store::{
ProjectContext, PromptBuilder, PromptId, PromptStore, PromptsUpdatedEvent, RulesFileContext,
UserRulesContext, WorktreeContext,
DefaultUserRulesContext, ProjectContext, PromptBuilder, PromptId, PromptStore,
PromptsUpdatedEvent, RulesFileContext, WorktreeContext,
};
use serde::{Deserialize, Serialize};
use settings::{Settings as _, SettingsStore};
@@ -61,7 +62,6 @@ pub struct ThreadStore {
project: Entity<Project>,
tools: Entity<ToolWorkingSet>,
prompt_builder: Arc<PromptBuilder>,
prompt_store: Option<Entity<PromptStore>>,
context_server_manager: Entity<ContextServerManager>,
context_server_tool_ids: HashMap<Arc<str>, Vec<ToolId>>,
threads: Vec<SerializedThreadMetadata>,
@@ -81,11 +81,12 @@ impl ThreadStore {
pub fn load(
project: Entity<Project>,
tools: Entity<ToolWorkingSet>,
prompt_store: Option<Entity<PromptStore>>,
prompt_builder: Arc<PromptBuilder>,
cx: &mut App,
) -> Task<Result<Entity<Self>>> {
let prompt_store = PromptStore::global(cx);
cx.spawn(async move |cx| {
let prompt_store = prompt_store.await.ok();
let (thread_store, ready_rx) = cx.update(|cx| {
let mut option_ready_rx = None;
let thread_store = cx.new(|cx| {
@@ -134,7 +135,6 @@ impl ThreadStore {
let (ready_tx, ready_rx) = oneshot::channel();
let mut ready_tx = Some(ready_tx);
let reload_system_prompt_task = cx.spawn({
let prompt_store = prompt_store.clone();
async move |thread_store, cx| {
loop {
let Some(reload_task) = thread_store
@@ -158,7 +158,6 @@ impl ThreadStore {
project,
tools,
prompt_builder,
prompt_store,
context_server_manager,
context_server_tool_ids: HashMap::default(),
threads: Vec::new(),
@@ -206,15 +205,15 @@ impl ThreadStore {
prompt_store: Option<Entity<PromptStore>>,
cx: &mut Context<Self>,
) -> Task<()> {
let worktrees = self
.project
.read(cx)
let project = self.project.read(cx);
let worktree_tasks = project
.visible_worktrees(cx)
.collect::<Vec<_>>();
let worktree_tasks = worktrees
.into_iter()
.map(|worktree| {
Self::load_worktree_info_for_system_prompt(worktree, self.project.clone(), cx)
Self::load_worktree_info_for_system_prompt(
project.fs().clone(),
worktree.read(cx),
cx,
)
})
.collect::<Vec<_>>();
let default_user_rules_task = match prompt_store {
@@ -246,7 +245,7 @@ impl ThreadStore {
let default_user_rules = default_user_rules
.into_iter()
.flat_map(|(contents, prompt_metadata)| match contents {
Ok(contents) => Some(UserRulesContext {
Ok(contents) => Some(DefaultUserRulesContext {
uuid: match prompt_metadata.id {
PromptId::User { uuid } => uuid,
PromptId::EditWorkflow => return None,
@@ -275,13 +274,13 @@ impl ThreadStore {
}
fn load_worktree_info_for_system_prompt(
worktree: Entity<Worktree>,
project: Entity<Project>,
cx: &mut App,
fs: Arc<dyn Fs>,
worktree: &Worktree,
cx: &App,
) -> Task<(WorktreeContext, Option<RulesLoadingError>)> {
let root_name = worktree.read(cx).root_name().into();
let root_name = worktree.root_name().into();
let rules_task = Self::load_worktree_rules_file(worktree, project, cx);
let rules_task = Self::load_worktree_rules_file(fs, worktree, cx);
let Some(rules_task) = rules_task else {
return Task::ready((
WorktreeContext {
@@ -311,44 +310,33 @@ impl ThreadStore {
}
fn load_worktree_rules_file(
worktree: Entity<Worktree>,
project: Entity<Project>,
cx: &mut App,
fs: Arc<dyn Fs>,
worktree: &Worktree,
cx: &App,
) -> Option<Task<Result<RulesFileContext>>> {
let worktree_ref = worktree.read(cx);
let worktree_id = worktree_ref.id();
let selected_rules_file = RULES_FILE_NAMES
.into_iter()
.filter_map(|name| {
worktree_ref
worktree
.entry_for_path(name)
.filter(|entry| entry.is_file())
.map(|entry| entry.path.clone())
.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| {
let project_path = ProjectPath {
worktree_id,
path: path_in_worktree.clone(),
};
let buffer_task =
project.update(cx, |project, cx| project.open_buffer(project_path, cx));
let rope_task = cx.spawn(async move |cx| {
buffer_task.await?.read_with(cx, |buffer, cx| {
let project_entry_id = buffer.entry_id(cx).context("buffer has no file")?;
anyhow::Ok((project_entry_id, buffer.as_rope().clone()))
})?
});
// Build a string from the rope on a background thread.
selected_rules_file.map(|(path_in_worktree, abs_path)| {
let fs = fs.clone();
cx.background_spawn(async move {
let (project_entry_id, rope) = rope_task.await?;
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(RulesFileContext {
path_in_worktree,
text: rope.to_string().trim().to_string(),
project_entry_id: project_entry_id.to_usize(),
abs_path: abs_path.into(),
text: text.trim().to_string(),
})
})
})
@@ -358,10 +346,6 @@ impl ThreadStore {
self.context_server_manager.clone()
}
pub fn prompt_store(&self) -> &Option<Entity<PromptStore>> {
&self.prompt_store
}
pub fn tools(&self) -> Entity<ToolWorkingSet> {
self.tools.clone()
}
@@ -371,12 +355,16 @@ impl ThreadStore {
self.threads.len()
}
pub fn reverse_chronological_threads(&self) -> Vec<SerializedThreadMetadata> {
pub fn threads(&self) -> Vec<SerializedThreadMetadata> {
let mut threads = self.threads.iter().cloned().collect::<Vec<_>>();
threads.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.updated_at));
threads
}
pub fn recent_threads(&self, limit: usize) -> Vec<SerializedThreadMetadata> {
self.threads().into_iter().take(limit).collect()
}
pub fn create_thread(&mut self, cx: &mut Context<Self>) -> Entity<Thread> {
cx.new(|cx| {
Thread::new(
@@ -504,22 +492,6 @@ impl ThreadStore {
);
});
}
// Enable all the tools from all context servers, but disable the ones that are explicitly disabled
for (context_server_id, preset) in &profile.context_servers {
self.tools.update(cx, |tools, cx| {
tools.disable(
ToolSource::ContextServer {
id: context_server_id.clone().into(),
},
&preset
.tools
.iter()
.filter_map(|(tool, enabled)| (!enabled).then(|| tool.clone()))
.collect::<Vec<_>>(),
cx,
)
})
}
} else {
for (context_server_id, preset) in &profile.context_servers {
self.tools.update(cx, |tools, cx| {
@@ -640,28 +612,15 @@ pub struct SerializedThread {
pub detailed_summary_state: DetailedSummaryState,
#[serde(default)]
pub exceeded_window_error: Option<ExceededWindowError>,
#[serde(default)]
pub model: Option<SerializedLanguageModel>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct SerializedLanguageModel {
pub provider: String,
pub model: String,
}
impl SerializedThread {
pub const VERSION: &'static str = "0.2.0";
pub const VERSION: &'static str = "0.1.0";
pub fn from_json(json: &[u8]) -> Result<Self> {
let saved_thread_json = serde_json::from_slice::<serde_json::Value>(json)?;
match saved_thread_json.get("version") {
Some(serde_json::Value::String(version)) => match version.as_str() {
SerializedThreadV0_1_0::VERSION => {
let saved_thread =
serde_json::from_value::<SerializedThreadV0_1_0>(saved_thread_json)?;
Ok(saved_thread.upgrade())
}
SerializedThread::VERSION => Ok(serde_json::from_value::<SerializedThread>(
saved_thread_json,
)?),
@@ -683,38 +642,6 @@ impl SerializedThread {
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct SerializedThreadV0_1_0(
// The structure did not change, so we are reusing the latest SerializedThread.
// When making the next version, make sure this points to SerializedThreadV0_2_0
SerializedThread,
);
impl SerializedThreadV0_1_0 {
pub const VERSION: &'static str = "0.1.0";
pub fn upgrade(self) -> SerializedThread {
debug_assert_eq!(SerializedThread::VERSION, "0.2.0");
let mut messages: Vec<SerializedMessage> = Vec::with_capacity(self.0.messages.len());
for message in self.0.messages {
if message.role == Role::User && !message.tool_results.is_empty() {
if let Some(last_message) = messages.last_mut() {
debug_assert!(last_message.role == Role::Assistant);
last_message.tool_results = message.tool_results;
continue;
}
}
messages.push(message);
}
SerializedThread { messages, ..self.0 }
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SerializedMessage {
pub id: MessageId,
@@ -733,18 +660,9 @@ pub struct SerializedMessage {
#[serde(tag = "type")]
pub enum SerializedMessageSegment {
#[serde(rename = "text")]
Text {
text: String,
},
Text { text: String },
#[serde(rename = "thinking")]
Thinking {
text: String,
#[serde(skip_serializing_if = "Option::is_none")]
signature: Option<String>,
},
RedactedThinking {
data: Vec<u8>,
},
Thinking { text: String },
}
#[derive(Debug, Serialize, Deserialize)]
@@ -782,7 +700,6 @@ impl LegacySerializedThread {
request_token_usage: Vec::new(),
detailed_summary_state: DetailedSummaryState::default(),
exceeded_window_error: None,
model: None,
}
}
}

View File

@@ -7,13 +7,13 @@ use futures::FutureExt as _;
use futures::future::Shared;
use gpui::{App, Entity, SharedString, Task};
use language_model::{
ConfiguredModel, LanguageModel, LanguageModelRequestMessage, LanguageModelToolResult,
LanguageModelRegistry, LanguageModelRequestMessage, LanguageModelToolResult,
LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role,
};
use ui::IconName;
use util::truncate_lines_to_byte_limit;
use crate::thread::{MessageId, PromptId, ThreadId};
use crate::thread::MessageId;
use crate::thread_store::SerializedMessage;
#[derive(Debug)]
@@ -27,13 +27,15 @@ pub struct ToolUse {
pub needs_confirmation: bool,
}
pub const USING_TOOL_MARKER: &str = "<using_tool>";
pub struct ToolUseState {
tools: Entity<ToolWorkingSet>,
tool_uses_by_assistant_message: HashMap<MessageId, Vec<LanguageModelToolUse>>,
tool_uses_by_user_message: HashMap<MessageId, Vec<LanguageModelToolUseId>>,
tool_results: HashMap<LanguageModelToolUseId, LanguageModelToolResult>,
pending_tool_uses_by_id: HashMap<LanguageModelToolUseId, PendingToolUse>,
tool_result_cards: HashMap<LanguageModelToolUseId, AnyToolCard>,
tool_use_metadata_by_id: HashMap<LanguageModelToolUseId, ToolUseMetadata>,
}
impl ToolUseState {
@@ -41,10 +43,10 @@ impl ToolUseState {
Self {
tools,
tool_uses_by_assistant_message: HashMap::default(),
tool_uses_by_user_message: HashMap::default(),
tool_results: HashMap::default(),
pending_tool_uses_by_id: HashMap::default(),
tool_result_cards: HashMap::default(),
tool_use_metadata_by_id: HashMap::default(),
}
}
@@ -54,6 +56,7 @@ impl ToolUseState {
pub fn from_serialized_messages(
tools: Entity<ToolWorkingSet>,
messages: &[SerializedMessage],
mut filter_by_tool_name: impl FnMut(&str) -> bool,
) -> Self {
let mut this = Self::new(tools);
let mut tool_names_by_id = HashMap::default();
@@ -65,12 +68,11 @@ impl ToolUseState {
let tool_uses = message
.tool_uses
.iter()
.filter(|tool_use| (filter_by_tool_name)(tool_use.name.as_ref()))
.map(|tool_use| LanguageModelToolUse {
id: tool_use.id.clone(),
name: tool_use.name.clone().into(),
raw_input: tool_use.input.to_string(),
input: tool_use.input.clone(),
is_input_complete: true,
})
.collect::<Vec<_>>();
@@ -82,6 +84,14 @@ impl ToolUseState {
this.tool_uses_by_assistant_message
.insert(message.id, tool_uses);
}
}
Role::User => {
if !message.tool_results.is_empty() {
let tool_uses_by_user_message = this
.tool_uses_by_user_message
.entry(message.id)
.or_default();
for tool_result in &message.tool_results {
let tool_use_id = tool_result.tool_use_id.clone();
@@ -90,6 +100,11 @@ impl ToolUseState {
continue;
};
if !(filter_by_tool_name)(tool_use.as_ref()) {
continue;
}
tool_uses_by_user_message.push(tool_use_id.clone());
this.tool_results.insert(
tool_use_id.clone(),
LanguageModelToolResult {
@@ -102,7 +117,7 @@ impl ToolUseState {
}
}
}
Role::System | Role::User => {}
Role::System => {}
}
}
@@ -159,9 +174,6 @@ impl ToolUseState {
PendingToolUseStatus::Error(ref err) => {
ToolUseStatus::Error(err.clone().into())
}
PendingToolUseStatus::InputStillStreaming => {
ToolUseStatus::InputStillStreaming
}
}
} else {
ToolUseStatus::Pending
@@ -178,12 +190,7 @@ impl ToolUseState {
tool_uses.push(ToolUse {
id: tool_use.id.clone(),
name: tool_use.name.clone().into(),
ui_text: self.tool_ui_label(
&tool_use.name,
&tool_use.input,
tool_use.is_input_complete,
cx,
),
ui_text: self.tool_ui_label(&tool_use.name, &tool_use.input, cx),
input: tool_use.input.clone(),
status,
icon,
@@ -198,40 +205,29 @@ impl ToolUseState {
&self,
tool_name: &str,
input: &serde_json::Value,
is_input_complete: bool,
cx: &App,
) -> SharedString {
if let Some(tool) = self.tools.read(cx).tool(tool_name, cx) {
if is_input_complete {
tool.ui_text(input).into()
} else {
tool.still_streaming_ui_text(input).into()
}
tool.ui_text(input).into()
} else {
format!("Unknown tool {tool_name:?}").into()
}
}
pub fn tool_results_for_message(
&self,
assistant_message_id: MessageId,
) -> Vec<&LanguageModelToolResult> {
let Some(tool_uses) = self
.tool_uses_by_assistant_message
.get(&assistant_message_id)
else {
return Vec::new();
};
pub fn tool_results_for_message(&self, message_id: MessageId) -> Vec<&LanguageModelToolResult> {
let empty = Vec::new();
tool_uses
self.tool_uses_by_user_message
.get(&message_id)
.unwrap_or(&empty)
.iter()
.filter_map(|tool_use| self.tool_results.get(&tool_use.id))
.filter_map(|tool_use_id| self.tool_results.get(&tool_use_id))
.collect()
}
pub fn message_has_tool_results(&self, assistant_message_id: MessageId) -> bool {
self.tool_uses_by_assistant_message
.get(&assistant_message_id)
pub fn message_has_tool_results(&self, message_id: MessageId) -> bool {
self.tool_uses_by_user_message
.get(&message_id)
.map_or(false, |results| !results.is_empty())
}
@@ -258,44 +254,20 @@ impl ToolUseState {
&mut self,
assistant_message_id: MessageId,
tool_use: LanguageModelToolUse,
metadata: ToolUseMetadata,
cx: &App,
) -> Arc<str> {
let tool_uses = self
.tool_uses_by_assistant_message
) {
self.tool_uses_by_assistant_message
.entry(assistant_message_id)
.or_default();
.or_default()
.push(tool_use.clone());
let mut existing_tool_use_found = false;
for existing_tool_use in tool_uses.iter_mut() {
if existing_tool_use.id == tool_use.id {
*existing_tool_use = tool_use.clone();
existing_tool_use_found = true;
}
}
if !existing_tool_use_found {
tool_uses.push(tool_use.clone());
}
let status = if tool_use.is_input_complete {
self.tool_use_metadata_by_id
.insert(tool_use.id.clone(), metadata);
PendingToolUseStatus::Idle
} else {
PendingToolUseStatus::InputStillStreaming
};
let ui_text: Arc<str> = self
.tool_ui_label(
&tool_use.name,
&tool_use.input,
tool_use.is_input_complete,
cx,
)
.into();
// The tool use is being requested by the Assistant, so we want to
// attach the tool results to the next user message.
let next_user_message_id = MessageId(assistant_message_id.0 + 1);
self.tool_uses_by_user_message
.entry(next_user_message_id)
.or_default()
.push(tool_use.id.clone());
self.pending_tool_uses_by_id.insert(
tool_use.id.clone(),
@@ -303,13 +275,13 @@ impl ToolUseState {
assistant_message_id,
id: tool_use.id,
name: tool_use.name.clone(),
ui_text: ui_text.clone(),
ui_text: self
.tool_ui_label(&tool_use.name, &tool_use.input, cx)
.into(),
input: tool_use.input,
status,
status: PendingToolUseStatus::Idle,
},
);
ui_text
}
pub fn run_pending_tool(
@@ -353,30 +325,19 @@ impl ToolUseState {
tool_use_id: LanguageModelToolUseId,
tool_name: Arc<str>,
output: Result<String>,
configured_model: Option<&ConfiguredModel>,
cx: &App,
) -> Option<PendingToolUse> {
let metadata = self.tool_use_metadata_by_id.remove(&tool_use_id);
telemetry::event!(
"Agent Tool Finished",
model = metadata
.as_ref()
.map(|metadata| metadata.model.telemetry_id()),
model_provider = metadata
.as_ref()
.map(|metadata| metadata.model.provider_id().to_string()),
thread_id = metadata.as_ref().map(|metadata| metadata.thread_id.clone()),
prompt_id = metadata.as_ref().map(|metadata| metadata.prompt_id.clone()),
tool_name,
success = output.is_ok()
);
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 = configured_model
let tool_output_limit = model_registry
.default_model()
.map(|model| model.model.max_token_count() * BYTES_PER_TOKEN_ESTIMATE)
.unwrap_or(usize::MAX);
@@ -429,8 +390,28 @@ impl ToolUseState {
request_message: &mut LanguageModelRequestMessage,
) {
if let Some(tool_uses) = self.tool_uses_by_assistant_message.get(&message_id) {
let mut found_tool_use = false;
for tool_use in tool_uses {
if self.tool_results.contains_key(&tool_use.id) {
if !found_tool_use {
// The API fails if a message contains a tool use without any (non-whitespace) text around it
match request_message.content.last_mut() {
Some(MessageContent::Text(txt)) => {
if txt.is_empty() {
txt.push_str(USING_TOOL_MARKER);
}
}
None | Some(_) => {
request_message
.content
.push(MessageContent::Text(USING_TOOL_MARKER.into()));
}
};
}
found_tool_use = true;
// Do not send tool uses until they are completed
request_message
.content
@@ -445,49 +426,31 @@ impl ToolUseState {
}
}
pub fn has_tool_results(&self, assistant_message_id: MessageId) -> bool {
self.tool_uses_by_assistant_message
.contains_key(&assistant_message_id)
}
pub fn tool_results_message(
pub fn attach_tool_results(
&self,
assistant_message_id: MessageId,
) -> Option<LanguageModelRequestMessage> {
let tool_uses = self
.tool_uses_by_assistant_message
.get(&assistant_message_id)?;
if tool_uses.is_empty() {
return None;
}
let mut request_message = LanguageModelRequestMessage {
role: Role::User,
content: vec![],
cache: false,
};
for tool_use in tool_uses {
if let Some(tool_result) = self.tool_results.get(&tool_use.id) {
request_message
.content
.push(MessageContent::ToolResult(LanguageModelToolResult {
tool_use_id: tool_use.id.clone(),
tool_name: tool_result.tool_name.clone(),
is_error: tool_result.is_error,
content: if tool_result.content.is_empty() {
// Surprisingly, the API fails if we return an empty string here.
// It thinks we are sending a tool use without a tool result.
"<Tool returned an empty string>".into()
} else {
tool_result.content.clone()
message_id: MessageId,
request_message: &mut LanguageModelRequestMessage,
) {
if let Some(tool_uses) = self.tool_uses_by_user_message.get(&message_id) {
for tool_use_id in tool_uses {
if let Some(tool_result) = self.tool_results.get(tool_use_id) {
request_message.content.push(MessageContent::ToolResult(
LanguageModelToolResult {
tool_use_id: tool_use_id.clone(),
tool_name: tool_result.tool_name.clone(),
is_error: tool_result.is_error,
content: if tool_result.content.is_empty() {
// Surprisingly, the API fails if we return an empty string here.
// It thinks we are sending a tool use without a tool result.
"<Tool returned an empty string>".into()
} else {
tool_result.content.clone()
},
},
}));
));
}
}
}
Some(request_message)
}
}
@@ -514,7 +477,6 @@ pub struct Confirmation {
#[derive(Debug, Clone)]
pub enum PendingToolUseStatus {
InputStillStreaming,
Idle,
NeedsConfirmation(Arc<Confirmation>),
Running { _task: Shared<Task<()>> },
@@ -534,10 +496,3 @@ impl PendingToolUseStatus {
matches!(self, PendingToolUseStatus::NeedsConfirmation { .. })
}
}
#[derive(Clone)]
pub struct ToolUseMetadata {
pub model: Arc<dyn LanguageModel>,
pub thread_id: ThreadId,
pub prompt_id: PromptId,
}

View File

@@ -1,9 +1,7 @@
mod agent_notification;
mod animated_label;
mod context_pill;
mod usage_banner;
pub use agent_notification::*;
pub use animated_label::*;
pub use context_pill::*;
pub use usage_banner::*;

View File

@@ -12,7 +12,6 @@ pub struct AgentNotification {
title: SharedString,
caption: SharedString,
icon: IconName,
project_name: Option<SharedString>,
}
impl AgentNotification {
@@ -20,13 +19,11 @@ impl AgentNotification {
title: impl Into<SharedString>,
caption: impl Into<SharedString>,
icon: IconName,
project_name: Option<impl Into<SharedString>>,
) -> Self {
Self {
title: title.into(),
caption: caption.into(),
icon,
project_name: project_name.map(|name| name.into()),
}
}
@@ -133,34 +130,11 @@ impl Render for AgentNotification {
.child(gradient_overflow()),
)
.child(
h_flex()
div()
.relative()
.gap_1p5()
.text_size(px(12.))
.text_color(cx.theme().colors().text_muted)
.truncate()
.when_some(
self.project_name.clone(),
|description, project_name| {
description.child(
h_flex()
.gap_1p5()
.child(
div()
.max_w_16()
.truncate()
.child(project_name),
)
.child(
div().size(px(3.)).rounded_full().bg(cx
.theme()
.colors()
.text
.opacity(0.5)),
),
)
},
)
.child(self.caption.clone())
.child(gradient_overflow()),
),

View File

@@ -1,121 +0,0 @@
use gpui::{Animation, AnimationExt, FontWeight, pulsating_between};
use std::time::Duration;
use ui::prelude::*;
#[derive(IntoElement)]
pub struct AnimatedLabel {
base: Label,
text: SharedString,
}
impl AnimatedLabel {
pub fn new(text: impl Into<SharedString>) -> Self {
let text = text.into();
AnimatedLabel {
base: Label::new(text.clone()),
text,
}
}
}
impl LabelCommon for AnimatedLabel {
fn size(mut self, size: LabelSize) -> Self {
self.base = self.base.size(size);
self
}
fn weight(mut self, weight: FontWeight) -> Self {
self.base = self.base.weight(weight);
self
}
fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self {
self.base = self.base.line_height_style(line_height_style);
self
}
fn color(mut self, color: Color) -> Self {
self.base = self.base.color(color);
self
}
fn strikethrough(mut self) -> Self {
self.base = self.base.strikethrough();
self
}
fn italic(mut self) -> Self {
self.base = self.base.italic();
self
}
fn alpha(mut self, alpha: f32) -> Self {
self.base = self.base.alpha(alpha);
self
}
fn underline(mut self) -> Self {
self.base = self.base.underline();
self
}
fn truncate(mut self) -> Self {
self.base = self.base.truncate();
self
}
fn single_line(mut self) -> Self {
self.base = self.base.single_line();
self
}
fn buffer_font(mut self, cx: &App) -> Self {
self.base = self.base.buffer_font(cx);
self
}
fn inline_code(mut self, cx: &App) -> Self {
self.base = self.base.inline_code(cx);
self
}
}
impl RenderOnce for AnimatedLabel {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let text = self.text.clone();
self.base
.color(Color::Muted)
.with_animations(
"animated-label",
vec![
Animation::new(Duration::from_secs(1)),
Animation::new(Duration::from_secs(1)).repeat(),
],
move |mut label, animation_ix, delta| {
match animation_ix {
0 => {
let chars_to_show = (delta * text.len() as f32).ceil() as usize;
let text = SharedString::from(text[0..chars_to_show].to_string());
label.set_text(text);
}
1 => match delta {
d if d < 0.25 => label.set_text(text.clone()),
d if d < 0.5 => label.set_text(format!("{}.", text)),
d if d < 0.75 => label.set_text(format!("{}..", text)),
_ => label.set_text(format!("{}...", text)),
},
_ => {}
}
label
},
)
.with_animation(
"pulsating-label",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.6, 1.)),
|label, delta| label.map_element(|label| label.alpha(delta)),
)
}
}

View File

@@ -1,23 +1,11 @@
use std::{ops::Range, path::Path, rc::Rc, sync::Arc, time::Duration};
use std::{rc::Rc, time::Duration};
use file_icons::FileIcons;
use futures::FutureExt as _;
use gpui::{
Animation, AnimationExt as _, AnyView, ClickEvent, Entity, Image, MouseButton, Task,
pulsating_between,
};
use language_model::LanguageModelImage;
use project::Project;
use prompt_store::PromptStore;
use rope::Point;
use ui::{IconButtonShape, Tooltip, prelude::*, tooltip_container};
use gpui::ClickEvent;
use gpui::{Animation, AnimationExt as _, pulsating_between};
use ui::{IconButtonShape, Tooltip, prelude::*};
use crate::context::{
AgentContext, AgentContextHandle, ContextId, ContextKind, DirectoryContext,
DirectoryContextHandle, FetchedUrlContext, FileContext, FileContextHandle, ImageContext,
ImageStatus, RulesContext, RulesContextHandle, SelectionContext, SelectionContextHandle,
SymbolContext, SymbolContextHandle, ThreadContext, ThreadContextHandle,
};
use crate::context::{AssistantContext, ContextId, ContextKind};
#[derive(IntoElement)]
pub enum ContextPill {
@@ -82,7 +70,9 @@ impl ContextPill {
pub fn id(&self) -> ElementId {
match self {
Self::Added { context, .. } => context.handle.element_id("context-pill".into()),
Self::Added { context, .. } => {
ElementId::NamedInteger("context-pill".into(), context.id.0)
}
Self::Suggested { .. } => "suggested-context-pill".into(),
}
}
@@ -130,81 +120,43 @@ impl RenderOnce for ContextPill {
on_remove,
focused,
on_click,
} => {
let status_is_error = matches!(context.status, ContextStatus::Error { .. });
base_pill
.pr(if on_remove.is_some() { px(2.) } else { px(4.) })
.map(|pill| {
if status_is_error {
pill.bg(cx.theme().status().error_background)
.border_color(cx.theme().status().error_border)
} else if *focused {
pill.bg(color.element_background)
.border_color(color.border_focused)
} else {
pill.bg(color.element_background)
.border_color(color.border.opacity(0.5))
}
})
.child(
h_flex()
.id("context-data")
.gap_1()
.child(
div().max_w_64().child(
Label::new(context.name.clone())
.size(LabelSize::Small)
.truncate(),
),
)
.when_some(context.parent.as_ref(), |element, parent_name| {
if *dupe_name {
element.child(
Label::new(parent_name.clone())
.size(LabelSize::XSmall)
.color(Color::Muted),
)
} else {
element
}
})
.when_some(context.tooltip.as_ref(), |element, tooltip| {
element.tooltip(Tooltip::text(tooltip.clone()))
})
.map(|element| match &context.status {
ContextStatus::Ready => element
.when_some(
context.render_hover.as_ref(),
|element, render_hover| {
let render_hover = render_hover.clone();
element.hoverable_tooltip(move |window, cx| {
render_hover(window, cx)
})
},
)
.into_any(),
ContextStatus::Loading { message } => element
.tooltip(ui::Tooltip::text(message.clone()))
.with_animation(
"pulsating-ctx-pill",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 0.8)),
|label, delta| label.opacity(delta),
)
.into_any_element(),
ContextStatus::Error { message } => element
.tooltip(ui::Tooltip::text(message.clone()))
.into_any_element(),
}),
)
.when_some(on_remove.as_ref(), |element, on_remove| {
element.child(
IconButton::new(
context.handle.element_id("remove".into()),
IconName::Close,
)
} => base_pill
.bg(color.element_background)
.border_color(if *focused {
color.border_focused
} else {
color.border.opacity(0.5)
})
.pr(if on_remove.is_some() { px(2.) } else { px(4.) })
.child(
h_flex()
.id("context-data")
.gap_1()
.child(
div().max_w_64().child(
Label::new(context.name.clone())
.size(LabelSize::Small)
.truncate(),
),
)
.when_some(context.parent.as_ref(), |element, parent_name| {
if *dupe_name {
element.child(
Label::new(parent_name.clone())
.size(LabelSize::XSmall)
.color(Color::Muted),
)
} else {
element
}
})
.when_some(context.tooltip.as_ref(), |element, tooltip| {
element.tooltip(Tooltip::text(tooltip.clone()))
}),
)
.when_some(on_remove.as_ref(), |element, on_remove| {
element.child(
IconButton::new(("remove", context.id.0), IconName::Close)
.shape(IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.tooltip(Tooltip::text("Remove Context"))
@@ -212,16 +164,30 @@ impl RenderOnce for ContextPill {
let on_remove = on_remove.clone();
move |event, window, cx| on_remove(event, window, cx)
}),
)
})
.when_some(on_click.as_ref(), |element, on_click| {
let on_click = on_click.clone();
)
})
.when_some(on_click.as_ref(), |element, on_click| {
let on_click = on_click.clone();
element
.cursor_pointer()
.on_click(move |event, window, cx| on_click(event, window, cx))
})
.map(|element| {
if context.summarizing {
element
.cursor_pointer()
.on_click(move |event, window, cx| on_click(event, window, cx))
})
.into_any_element()
}
.tooltip(ui::Tooltip::text("Summarizing..."))
.with_animation(
"pulsating-ctx-pill",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 0.8)),
|label, delta| label.opacity(delta),
)
.into_any_element()
} else {
element.into_any()
}
}),
ContextPill::Suggested {
name,
icon_path: _,
@@ -232,15 +198,15 @@ impl RenderOnce for ContextPill {
.cursor_pointer()
.pr_1()
.border_dashed()
.map(|pill| {
if *focused {
pill.border_color(color.border_focused)
.bg(color.element_background.opacity(0.5))
} else {
pill.border_color(color.border)
}
.border_color(if *focused {
color.border_focused
} else {
color.border
})
.hover(|style| style.bg(color.element_hover.opacity(0.5)))
.when(*focused, |this| {
this.bg(color.element_background.opacity(0.5))
})
.child(
div().max_w_64().child(
Label::new(name.clone())
@@ -261,513 +227,133 @@ impl RenderOnce for ContextPill {
}
}
pub enum ContextStatus {
Ready,
Loading { message: SharedString },
Error { message: SharedString },
}
#[derive(RegisterComponent)]
pub struct AddedContext {
pub handle: AgentContextHandle,
pub id: ContextId,
pub kind: ContextKind,
pub name: SharedString,
pub parent: Option<SharedString>,
pub tooltip: Option<SharedString>,
pub icon_path: Option<SharedString>,
pub status: ContextStatus,
pub render_hover: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
pub summarizing: bool,
}
impl AddedContext {
/// Creates an `AddedContext` by retrieving relevant details of `AgentContext`. This returns a
/// `None` if `DirectoryContext` or `RulesContext` no longer exist.
///
/// TODO: `None` cases are unremovable from `ContextStore` and so are a very minor memory leak.
pub fn new_pending(
handle: AgentContextHandle,
prompt_store: Option<&Entity<PromptStore>>,
project: &Project,
cx: &App,
) -> Option<AddedContext> {
match handle {
AgentContextHandle::File(handle) => Self::pending_file(handle, cx),
AgentContextHandle::Directory(handle) => Self::pending_directory(handle, project, cx),
AgentContextHandle::Symbol(handle) => Self::pending_symbol(handle, cx),
AgentContextHandle::Selection(handle) => Self::pending_selection(handle, cx),
AgentContextHandle::FetchedUrl(handle) => Some(Self::fetched_url(handle)),
AgentContextHandle::Thread(handle) => Some(Self::pending_thread(handle, cx)),
AgentContextHandle::Rules(handle) => Self::pending_rules(handle, prompt_store, cx),
AgentContextHandle::Image(handle) => Some(Self::image(handle)),
}
}
pub fn new_attached(context: &AgentContext, cx: &App) -> AddedContext {
pub fn new(context: &AssistantContext, cx: &App) -> AddedContext {
match context {
AgentContext::File(context) => Self::attached_file(context, cx),
AgentContext::Directory(context) => Self::attached_directory(context),
AgentContext::Symbol(context) => Self::attached_symbol(context, cx),
AgentContext::Selection(context) => Self::attached_selection(context, cx),
AgentContext::FetchedUrl(context) => Self::fetched_url(context.clone()),
AgentContext::Thread(context) => Self::attached_thread(context),
AgentContext::Rules(context) => Self::attached_rules(context),
AgentContext::Image(context) => Self::image(context.clone()),
}
}
fn pending_file(handle: FileContextHandle, cx: &App) -> Option<AddedContext> {
let full_path = handle.buffer.read(cx).file()?.full_path(cx);
Some(Self::file(handle, &full_path, cx))
}
fn attached_file(context: &FileContext, cx: &App) -> AddedContext {
Self::file(context.handle.clone(), &context.full_path, cx)
}
fn file(handle: FileContextHandle, full_path: &Path, cx: &App) -> AddedContext {
let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
let name = full_path
.file_name()
.map(|n| n.to_string_lossy().into_owned().into())
.unwrap_or_else(|| full_path_string.clone());
let parent = full_path
.parent()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().into_owned().into());
AddedContext {
kind: ContextKind::File,
name,
parent,
tooltip: Some(full_path_string),
icon_path: FileIcons::get_icon(&full_path, cx),
status: ContextStatus::Ready,
render_hover: None,
handle: AgentContextHandle::File(handle),
}
}
fn pending_directory(
handle: DirectoryContextHandle,
project: &Project,
cx: &App,
) -> Option<AddedContext> {
let worktree = project.worktree_for_entry(handle.entry_id, cx)?.read(cx);
let entry = worktree.entry_for_id(handle.entry_id)?;
let full_path = worktree.full_path(&entry.path);
Some(Self::directory(handle, &full_path))
}
fn attached_directory(context: &DirectoryContext) -> AddedContext {
Self::directory(context.handle.clone(), &context.full_path)
}
fn directory(handle: DirectoryContextHandle, full_path: &Path) -> AddedContext {
let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
let name = full_path
.file_name()
.map(|n| n.to_string_lossy().into_owned().into())
.unwrap_or_else(|| full_path_string.clone());
let parent = full_path
.parent()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().into_owned().into());
AddedContext {
kind: ContextKind::Directory,
name,
parent,
tooltip: Some(full_path_string),
icon_path: None,
status: ContextStatus::Ready,
render_hover: None,
handle: AgentContextHandle::Directory(handle),
}
}
fn pending_symbol(handle: SymbolContextHandle, cx: &App) -> Option<AddedContext> {
let excerpt =
ContextFileExcerpt::new(&handle.full_path(cx)?, handle.enclosing_line_range(cx), cx);
Some(AddedContext {
kind: ContextKind::Symbol,
name: handle.symbol.clone(),
parent: Some(excerpt.file_name_and_range.clone()),
tooltip: None,
icon_path: None,
status: ContextStatus::Ready,
render_hover: {
let handle = handle.clone();
Some(Rc::new(move |_, cx| {
excerpt.hover_view(handle.text(cx), cx).into()
}))
},
handle: AgentContextHandle::Symbol(handle),
})
}
fn attached_symbol(context: &SymbolContext, cx: &App) -> AddedContext {
let excerpt = ContextFileExcerpt::new(&context.full_path, context.line_range.clone(), cx);
AddedContext {
kind: ContextKind::Symbol,
name: context.handle.symbol.clone(),
parent: Some(excerpt.file_name_and_range.clone()),
tooltip: None,
icon_path: None,
status: ContextStatus::Ready,
render_hover: {
let text = context.text.clone();
Some(Rc::new(move |_, cx| {
excerpt.hover_view(text.clone(), cx).into()
}))
},
handle: AgentContextHandle::Symbol(context.handle.clone()),
}
}
fn pending_selection(handle: SelectionContextHandle, cx: &App) -> Option<AddedContext> {
let excerpt = ContextFileExcerpt::new(&handle.full_path(cx)?, handle.line_range(cx), cx);
Some(AddedContext {
kind: ContextKind::Selection,
name: excerpt.file_name_and_range.clone(),
parent: excerpt.parent_name.clone(),
tooltip: None,
icon_path: excerpt.icon_path.clone(),
status: ContextStatus::Ready,
render_hover: {
let handle = handle.clone();
Some(Rc::new(move |_, cx| {
excerpt.hover_view(handle.text(cx), cx).into()
}))
},
handle: AgentContextHandle::Selection(handle),
})
}
fn attached_selection(context: &SelectionContext, cx: &App) -> AddedContext {
let excerpt = ContextFileExcerpt::new(&context.full_path, context.line_range.clone(), cx);
AddedContext {
kind: ContextKind::Selection,
name: excerpt.file_name_and_range.clone(),
parent: excerpt.parent_name.clone(),
tooltip: None,
icon_path: excerpt.icon_path.clone(),
status: ContextStatus::Ready,
render_hover: {
let text = context.text.clone();
Some(Rc::new(move |_, cx| {
excerpt.hover_view(text.clone(), cx).into()
}))
},
handle: AgentContextHandle::Selection(context.handle.clone()),
}
}
fn fetched_url(context: FetchedUrlContext) -> AddedContext {
AddedContext {
kind: ContextKind::FetchedUrl,
name: context.url.clone(),
parent: None,
tooltip: None,
icon_path: None,
status: ContextStatus::Ready,
render_hover: None,
handle: AgentContextHandle::FetchedUrl(context),
}
}
fn pending_thread(handle: ThreadContextHandle, cx: &App) -> AddedContext {
AddedContext {
kind: ContextKind::Thread,
name: handle.title(cx),
parent: None,
tooltip: None,
icon_path: None,
status: if handle.thread.read(cx).is_generating_detailed_summary() {
ContextStatus::Loading {
message: "Summarizing…".into(),
AssistantContext::File(file_context) => {
let full_path = file_context.context_buffer.file.full_path(cx);
let full_path_string: SharedString =
full_path.to_string_lossy().into_owned().into();
let name = full_path
.file_name()
.map(|n| n.to_string_lossy().into_owned().into())
.unwrap_or_else(|| full_path_string.clone());
let parent = full_path
.parent()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().into_owned().into());
AddedContext {
id: file_context.id,
kind: ContextKind::File,
name,
parent,
tooltip: Some(full_path_string),
icon_path: FileIcons::get_icon(&full_path, cx),
summarizing: false,
}
} else {
ContextStatus::Ready
},
render_hover: {
let thread = handle.thread.clone();
Some(Rc::new(move |_, cx| {
let text = thread.read(cx).latest_detailed_summary_or_text();
ContextPillHover::new_text(text.clone(), cx).into()
}))
},
handle: AgentContextHandle::Thread(handle),
}
}
}
fn attached_thread(context: &ThreadContext) -> AddedContext {
AddedContext {
kind: ContextKind::Thread,
name: context.title.clone(),
parent: None,
tooltip: None,
icon_path: None,
status: ContextStatus::Ready,
render_hover: {
let text = context.text.clone();
Some(Rc::new(move |_, cx| {
ContextPillHover::new_text(text.clone(), cx).into()
}))
},
handle: AgentContextHandle::Thread(context.handle.clone()),
}
}
fn pending_rules(
handle: RulesContextHandle,
prompt_store: Option<&Entity<PromptStore>>,
cx: &App,
) -> Option<AddedContext> {
let title = prompt_store
.as_ref()?
.read(cx)
.metadata(handle.prompt_id.into())?
.title
.unwrap_or_else(|| "Unnamed Rule".into());
Some(AddedContext {
kind: ContextKind::Rules,
name: title.clone(),
parent: None,
tooltip: None,
icon_path: None,
status: ContextStatus::Ready,
render_hover: None,
handle: AgentContextHandle::Rules(handle),
})
}
fn attached_rules(context: &RulesContext) -> AddedContext {
let title = context
.title
.clone()
.unwrap_or_else(|| "Unnamed Rule".into());
AddedContext {
kind: ContextKind::Rules,
name: title,
parent: None,
tooltip: None,
icon_path: None,
status: ContextStatus::Ready,
render_hover: {
let text = context.text.clone();
Some(Rc::new(move |_, cx| {
ContextPillHover::new_text(text.clone(), cx).into()
}))
},
handle: AgentContextHandle::Rules(context.handle.clone()),
}
}
fn image(context: ImageContext) -> AddedContext {
AddedContext {
kind: ContextKind::Image,
name: "Image".into(),
parent: None,
tooltip: None,
icon_path: None,
status: match context.status() {
ImageStatus::Loading => ContextStatus::Loading {
message: "Loading…".into(),
},
ImageStatus::Error => ContextStatus::Error {
message: "Failed to load image".into(),
},
ImageStatus::Ready => ContextStatus::Ready,
},
render_hover: Some(Rc::new({
let image = context.original_image.clone();
move |_, cx| {
let image = image.clone();
ContextPillHover::new(cx, move |_, _| {
gpui::img(image.clone())
.max_w_96()
.max_h_96()
.into_any_element()
})
.into()
AssistantContext::Directory(directory_context) => {
let full_path = directory_context
.worktree
.read(cx)
.full_path(&directory_context.path);
let full_path_string: SharedString =
full_path.to_string_lossy().into_owned().into();
let name = full_path
.file_name()
.map(|n| n.to_string_lossy().into_owned().into())
.unwrap_or_else(|| full_path_string.clone());
let parent = full_path
.parent()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().into_owned().into());
AddedContext {
id: directory_context.id,
kind: ContextKind::Directory,
name,
parent,
tooltip: Some(full_path_string),
icon_path: None,
summarizing: false,
}
})),
handle: AgentContextHandle::Image(context),
}
AssistantContext::Symbol(symbol_context) => AddedContext {
id: symbol_context.id,
kind: ContextKind::Symbol,
name: symbol_context.context_symbol.id.name.clone(),
parent: None,
tooltip: None,
icon_path: None,
summarizing: false,
},
AssistantContext::Excerpt(excerpt_context) => {
let full_path = excerpt_context.context_buffer.file.full_path(cx);
let mut full_path_string = full_path.to_string_lossy().into_owned();
let mut name = full_path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| full_path_string.clone());
let line_range_text = format!(
" ({}-{})",
excerpt_context.line_range.start.row + 1,
excerpt_context.line_range.end.row + 1
);
full_path_string.push_str(&line_range_text);
name.push_str(&line_range_text);
let parent = full_path
.parent()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().into_owned().into());
AddedContext {
id: excerpt_context.id,
kind: ContextKind::File, // Use File icon for excerpts
name: name.into(),
parent,
tooltip: Some(full_path_string.into()),
icon_path: FileIcons::get_icon(&full_path, cx),
summarizing: false,
}
}
AssistantContext::FetchedUrl(fetched_url_context) => AddedContext {
id: fetched_url_context.id,
kind: ContextKind::FetchedUrl,
name: fetched_url_context.url.clone(),
parent: None,
tooltip: None,
icon_path: None,
summarizing: false,
},
AssistantContext::Thread(thread_context) => AddedContext {
id: thread_context.id,
kind: ContextKind::Thread,
name: thread_context.summary(cx),
parent: None,
tooltip: None,
icon_path: None,
summarizing: thread_context
.thread
.read(cx)
.is_generating_detailed_summary(),
},
}
}
}
#[derive(Debug, Clone)]
struct ContextFileExcerpt {
pub file_name_and_range: SharedString,
pub full_path_and_range: SharedString,
pub parent_name: Option<SharedString>,
pub icon_path: Option<SharedString>,
}
impl ContextFileExcerpt {
pub fn new(full_path: &Path, line_range: Range<Point>, cx: &App) -> Self {
let full_path_string = full_path.to_string_lossy().into_owned();
let file_name = full_path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| full_path_string.clone());
let line_range_text = format!(" ({}-{})", line_range.start.row + 1, line_range.end.row + 1);
let mut full_path_and_range = full_path_string;
full_path_and_range.push_str(&line_range_text);
let mut file_name_and_range = file_name;
file_name_and_range.push_str(&line_range_text);
let parent_name = full_path
.parent()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().into_owned().into());
let icon_path = FileIcons::get_icon(&full_path, cx);
ContextFileExcerpt {
file_name_and_range: file_name_and_range.into(),
full_path_and_range: full_path_and_range.into(),
parent_name,
icon_path,
}
}
fn hover_view(&self, text: SharedString, cx: &mut App) -> Entity<ContextPillHover> {
let icon_path = self.icon_path.clone();
let full_path_and_range = self.full_path_and_range.clone();
ContextPillHover::new(cx, move |_, cx| {
v_flex()
.child(
h_flex()
.gap_0p5()
.w_full()
.max_w_full()
.border_b_1()
.border_color(cx.theme().colors().border.opacity(0.6))
.children(
icon_path
.clone()
.map(Icon::from_path)
.map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)),
)
.child(
// TODO: make this truncate on the left.
Label::new(full_path_and_range.clone())
.size(LabelSize::Small)
.ml_1(),
),
)
.child(
div()
.id("context-pill-hover-contents")
.overflow_scroll()
.max_w_128()
.max_h_96()
.child(Label::new(text.clone()).buffer_font(cx)),
)
.into_any_element()
})
}
}
struct ContextPillHover {
render_hover: Box<dyn Fn(&mut Window, &mut App) -> AnyElement>,
}
impl ContextPillHover {
fn new(
cx: &mut App,
render_hover: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
) -> Entity<Self> {
cx.new(|_| Self {
render_hover: Box::new(render_hover),
})
}
fn new_text(content: SharedString, cx: &mut App) -> Entity<Self> {
Self::new(cx, move |_, _| {
div()
.id("context-pill-hover-contents")
.overflow_scroll()
.max_w_128()
.max_h_96()
.child(content.clone())
.into_any_element()
})
}
}
impl Render for ContextPillHover {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
tooltip_container(window, cx, move |this, window, cx| {
this.occlude()
.on_mouse_move(|_, _, cx| cx.stop_propagation())
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
.child((self.render_hover)(window, cx))
})
}
}
impl Component for AddedContext {
fn scope() -> ComponentScope {
ComponentScope::Agent
}
fn sort_name() -> &'static str {
"AddedContext"
}
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let mut next_context_id = ContextId::zero();
let image_ready = (
"Ready",
AddedContext::image(ImageContext {
context_id: next_context_id.post_inc(),
project_path: None,
original_image: Arc::new(Image::empty()),
image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
}),
);
let image_loading = (
"Loading",
AddedContext::image(ImageContext {
context_id: next_context_id.post_inc(),
project_path: None,
original_image: Arc::new(Image::empty()),
image_task: cx
.background_spawn(async move {
smol::Timer::after(Duration::from_secs(60 * 5)).await;
Some(LanguageModelImage::empty())
})
.shared(),
}),
);
let image_error = (
"Error",
AddedContext::image(ImageContext {
context_id: next_context_id.post_inc(),
project_path: None,
original_image: Arc::new(Image::empty()),
image_task: Task::ready(None).shared(),
}),
);
Some(
v_flex()
.gap_6()
.children(
vec![image_ready, image_loading, image_error]
.into_iter()
.map(|(text, context)| {
single_example(
text,
ContextPill::added(context, false, false, None).into_any_element(),
)
}),
)
.into_any(),
)
}
}

View File

@@ -1,30 +1,31 @@
use client::zed_urls;
use language_model::RequestUsage;
use ui::{Banner, ProgressBar, Severity, prelude::*};
use zed_llm_client::{Plan, UsageLimit};
#[derive(IntoElement, RegisterComponent)]
pub struct UsageBanner {
plan: Plan,
usage: RequestUsage,
requests: i32,
}
impl UsageBanner {
pub fn new(plan: Plan, usage: RequestUsage) -> Self {
Self { plan, usage }
pub fn new(plan: Plan, requests: i32) -> Self {
Self { plan, requests }
}
}
impl RenderOnce for UsageBanner {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let used_percentage = match self.usage.limit {
UsageLimit::Limited(limit) => Some((self.usage.amount as f32 / limit as f32) * 100.),
let request_limit = self.plan.model_requests_limit();
let used_percentage = match request_limit {
UsageLimit::Limited(limit) => Some((self.requests as f32 / limit as f32) * 100.),
UsageLimit::Unlimited => None,
};
let (severity, message) = match self.usage.limit {
let (severity, message) = match request_limit {
UsageLimit::Limited(limit) => {
if self.usage.amount >= limit {
if self.requests >= limit {
let message = match self.plan {
Plan::ZedPro => "Monthly request limit reached",
Plan::ZedProTrial => "Trial request limit reached",
@@ -32,7 +33,7 @@ impl RenderOnce for UsageBanner {
};
(Severity::Error, message)
} else if (self.usage.amount as f32 / limit as f32) >= 0.9 {
} else if (self.requests as f32 / limit as f32) >= 0.9 {
(Severity::Warning, "Approaching request limit")
} else {
let message = match self.plan {
@@ -80,11 +81,11 @@ impl RenderOnce for UsageBanner {
.child(ProgressBar::new("usage", percent, 100., cx))
}))
.child(
Label::new(match self.usage.limit {
Label::new(match request_limit {
UsageLimit::Limited(limit) => {
format!("{} / {limit}", self.usage.amount)
format!("{} / {limit}", self.requests)
}
UsageLimit::Unlimited => format!("{} / ∞", self.usage.amount),
UsageLimit::Unlimited => format!("{} / ∞", self.requests),
})
.size(LabelSize::Small)
.color(Color::Muted),
@@ -103,131 +104,74 @@ impl Component for UsageBanner {
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
let trial_limit = Plan::ZedProTrial.model_requests_limit();
let trial_examples = vec![
single_example(
"Zed Pro Trial - New User",
div()
.size_full()
.child(UsageBanner::new(
Plan::ZedProTrial,
RequestUsage {
limit: trial_limit,
amount: 10,
},
))
.child(UsageBanner::new(Plan::ZedProTrial, 10))
.into_any_element(),
),
single_example(
"Zed Pro Trial - Approaching Limit",
div()
.size_full()
.child(UsageBanner::new(
Plan::ZedProTrial,
RequestUsage {
limit: trial_limit,
amount: 135,
},
))
.child(UsageBanner::new(Plan::ZedProTrial, 135))
.into_any_element(),
),
single_example(
"Zed Pro Trial - Request Limit Reached",
div()
.size_full()
.child(UsageBanner::new(
Plan::ZedProTrial,
RequestUsage {
limit: trial_limit,
amount: 150,
},
))
.child(UsageBanner::new(Plan::ZedProTrial, 150))
.into_any_element(),
),
];
let free_limit = Plan::Free.model_requests_limit();
let free_examples = vec![
single_example(
"Free - Normal Usage",
div()
.size_full()
.child(UsageBanner::new(
Plan::Free,
RequestUsage {
limit: free_limit,
amount: 25,
},
))
.child(UsageBanner::new(Plan::Free, 25))
.into_any_element(),
),
single_example(
"Free - Approaching Limit",
div()
.size_full()
.child(UsageBanner::new(
Plan::Free,
RequestUsage {
limit: free_limit,
amount: 45,
},
))
.child(UsageBanner::new(Plan::Free, 45))
.into_any_element(),
),
single_example(
"Free - Request Limit Reached",
div()
.size_full()
.child(UsageBanner::new(
Plan::Free,
RequestUsage {
limit: free_limit,
amount: 50,
},
))
.child(UsageBanner::new(Plan::Free, 50))
.into_any_element(),
),
];
let zed_pro_limit = Plan::ZedPro.model_requests_limit();
let zed_pro_examples = vec![
single_example(
"Zed Pro - Normal Usage",
div()
.size_full()
.child(UsageBanner::new(
Plan::ZedPro,
RequestUsage {
limit: zed_pro_limit,
amount: 250,
},
))
.child(UsageBanner::new(Plan::ZedPro, 250))
.into_any_element(),
),
single_example(
"Zed Pro - Approaching Limit",
div()
.size_full()
.child(UsageBanner::new(
Plan::ZedPro,
RequestUsage {
limit: zed_pro_limit,
amount: 450,
},
))
.child(UsageBanner::new(Plan::ZedPro, 450))
.into_any_element(),
),
single_example(
"Zed Pro - Request Limit Reached",
div()
.size_full()
.child(UsageBanner::new(
Plan::ZedPro,
RequestUsage {
limit: zed_pro_limit,
amount: 500,
},
))
.child(UsageBanner::new(Plan::ZedPro, 500))
.into_any_element(),
),
];

View File

@@ -1,3 +1,5 @@
mod supported_countries;
use std::str::FromStr;
use anyhow::{Context as _, Result, anyhow};
@@ -9,6 +11,8 @@ use serde::{Deserialize, Serialize};
use strum::{EnumIter, EnumString};
use thiserror::Error;
pub use supported_countries::*;
pub const ANTHROPIC_API_URL: &str = "https://api.anthropic.com";
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
@@ -70,10 +74,6 @@ pub enum Model {
}
impl Model {
pub fn default_fast() -> Self {
Self::Claude3_5Haiku
}
pub fn from_id(id: &str) -> Result<Self> {
if id.starts_with("claude-3-5-sonnet") {
Ok(Self::Claude3_5Sonnet)
@@ -507,15 +507,6 @@ pub enum RequestContent {
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
#[serde(rename = "thinking")]
Thinking {
thinking: String,
signature: String,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
#[serde(rename = "redacted_thinking")]
RedactedThinking { data: String },
#[serde(rename = "image")]
Image {
source: ImageSource,

View File

@@ -0,0 +1,225 @@
use std::collections::HashSet;
use std::sync::LazyLock;
/// Returns whether the given country code is supported by Anthropic.
///
/// <https://www.anthropic.com/supported-countries>
pub fn is_supported_country(country_code: &str) -> bool {
SUPPORTED_COUNTRIES.contains(&country_code)
}
/// The list of country codes supported by Anthropic.
///
/// https://www.anthropic.com/supported-countries
static SUPPORTED_COUNTRIES: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
vec![
"AL", // Albania
"DZ", // Algeria
"AS", // American Samoa (US)
"AD", // Andorra
"AO", // Angola
"AI", // Anguilla (UK)
"AG", // Antigua and Barbuda
"AR", // Argentina
"AM", // Armenia
"AU", // Australia
"AT", // Austria
"AZ", // Azerbaijan
"BS", // Bahamas
"BH", // Bahrain
"BD", // Bangladesh
"BB", // Barbados
"BE", // Belgium
"BZ", // Belize
"BJ", // Benin
"BM", // Bermuda (UK)
"BT", // Bhutan
"BO", // Bolivia
"BA", // Bosnia and Herzegovina
"BW", // Botswana
"BR", // Brazil
"IO", // British Indian Ocean Territory (UK)
"BN", // Brunei
"BG", // Bulgaria
"BF", // Burkina Faso
"BI", // Burundi
"CV", // Cabo Verde
"KH", // Cambodia
"CM", // Cameroon
"CA", // Canada
"KY", // Cayman Islands (UK)
"TD", // Chad
"CL", // Chile
"CX", // Christmas Island (AU)
"CC", // Cocos (Keeling) Islands (AU)
"CO", // Colombia
"KM", // Comoros
"CG", // Congo (Brazzaville)
"CK", // Cook Islands (NZ)
"CR", // Costa Rica
"CI", // Côte d'Ivoire
"HR", // Croatia
"CY", // Cyprus
"CZ", // Czechia (Czech Republic)
"DK", // Denmark
"DJ", // Djibouti
"DM", // Dominica
"DO", // Dominican Republic
"EC", // Ecuador
"EG", // Egypt
"SV", // El Salvador
"GQ", // Equatorial Guinea
"EE", // Estonia
"SZ", // Eswatini
"FK", // Falkland Islands (UK)
"FJ", // Fiji
"FI", // Finland
"FR", // France
"GF", // French Guiana (FR)
"PF", // French Polynesia (FR)
"TF", // French Southern Territories
"GA", // Gabon
"GM", // Gambia
"GE", // Georgia
"DE", // Germany
"GH", // Ghana
"GI", // Gibraltar (UK)
"GR", // Greece
"GD", // Grenada
"GT", // Guatemala
"GU", // Guam (US)
"GN", // Guinea
"GW", // Guinea-Bissau
"GY", // Guyana
"HT", // Haiti
"HM", // Heard Island and McDonald Islands (AU)
"HN", // Honduras
"HU", // Hungary
"IS", // Iceland
"IN", // India
"ID", // Indonesia
"IQ", // Iraq
"IE", // Ireland
"IL", // Israel
"IT", // Italy
"JM", // Jamaica
"JP", // Japan
"JO", // Jordan
"KZ", // Kazakhstan
"KE", // Kenya
"KI", // Kiribati
"KW", // Kuwait
"KG", // Kyrgyzstan
"LA", // Laos
"LV", // Latvia
"LB", // Lebanon
"LS", // Lesotho
"LR", // Liberia
"LI", // Liechtenstein
"LT", // Lithuania
"LU", // Luxembourg
"MG", // Madagascar
"MW", // Malawi
"MY", // Malaysia
"MV", // Maldives
"MT", // Malta
"MH", // Marshall Islands
"MR", // Mauritania
"MU", // Mauritius
"MX", // Mexico
"FM", // Micronesia
"MD", // Moldova
"MC", // Monaco
"MN", // Mongolia
"MS", // Montserrat (UK)
"ME", // Montenegro
"MA", // Morocco
"MZ", // Mozambique
"NA", // Namibia
"NR", // Nauru
"NP", // Nepal
"NL", // Netherlands
"NZ", // New Zealand
"NE", // Niger
"NG", // Nigeria
"NF", // Norfolk Island (AU)
"MK", // North Macedonia
"MI", // Northern Mariana Islands (UK)
"NO", // Norway
"NU", // Niue (NZ)
"OM", // Oman
"PK", // Pakistan
"PW", // Palau
"PS", // Palestine
"PA", // Panama
"PG", // Papua New Guinea
"PY", // Paraguay
"PE", // Peru
"PH", // Philippines
"PN", // Pitcairn (UK)
"PL", // Poland
"PT", // Portugal
"PR", // Puerto Rico (US)
"QA", // Qatar
"RO", // Romania
"RW", // Rwanda
"BL", // Saint Barthélemy (FR)
"KN", // Saint Kitts and Nevis
"LC", // Saint Lucia
"MF", // Saint Martin (FR)
"PM", // Saint Pierre and Miquelon (FR)
"VC", // Saint Vincent and the Grenadines
"WS", // Samoa
"SM", // San Marino
"ST", // São Tomé and Príncipe
"SA", // Saudi Arabia
"SN", // Senegal
"RS", // Serbia
"SC", // Seychelles
"SH", // Saint Helena, Ascension and Tristan da Cunha (UK)
"SL", // Sierra Leone
"SG", // Singapore
"SK", // Slovakia
"SI", // Slovenia
"SB", // Solomon Islands
"ZA", // South Africa
"KR", // South Korea
"ES", // Spain
"LK", // Sri Lanka
"SR", // Suriname
"SE", // Sweden
"CH", // Switzerland
"TW", // Taiwan
"TJ", // Tajikistan
"TZ", // Tanzania
"TH", // Thailand
"TL", // Timor-Leste
"TG", // Togo
"TK", // Tokelau (NZ)
"TO", // Tonga
"TT", // Trinidad and Tobago
"TN", // Tunisia
"TR", // Türkiye (Turkey)
"TM", // Turkmenistan
"TC", // Turks and Caicos Islands (UK)
"TV", // Tuvalu
"UG", // Uganda
"UA", // Ukraine (except Crimea, Donetsk, and Luhansk regions)
"AE", // United Arab Emirates
"GB", // United Kingdom
"UM", // United States Minor Outlying Islands (US)
"US", // United States of America
"UY", // Uruguay
"UZ", // Uzbekistan
"VU", // Vanuatu
"VA", // Vatican City
"VN", // Vietnam
"VI", // Virgin Islands (US)
"VG", // Virgin Islands (UK)
"WF", // Wallis and Futuna (FR)
"ZM", // Zambia
"ZW", // Zimbabwe
]
.into_iter()
.collect()
});

View File

@@ -15,7 +15,6 @@ path = "src/askpass.rs"
anyhow.workspace = true
futures.workspace = true
gpui.workspace = true
shlex.workspace = true
smol.workspace = true
tempfile.workspace = true
util.workspace = true

View File

@@ -72,7 +72,8 @@ impl AskPassSession {
let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>();
let listener =
UnixListener::bind(&askpass_socket).context("failed to create askpass socket")?;
let zed_path = get_shell_safe_zed_path()?;
let zed_path = std::env::current_exe()
.context("Failed to figure out current executable path for use in askpass")?;
let (askpass_kill_master_tx, askpass_kill_master_rx) = oneshot::channel::<()>();
let mut kill_tx = Some(askpass_kill_master_tx);
@@ -114,7 +115,7 @@ impl AskPassSession {
// Create an askpass script that communicates back to this process.
let askpass_script = format!(
"{shebang}\n{print_args} | {zed_exe} --askpass={askpass_socket} 2> /dev/null \n",
zed_exe = zed_path,
zed_exe = zed_path.display(),
askpass_socket = askpass_socket.display(),
print_args = "printf '%s\\0' \"$@\"",
shebang = "#!/bin/sh",
@@ -160,32 +161,6 @@ impl AskPassSession {
}
}
#[cfg(unix)]
fn get_shell_safe_zed_path() -> anyhow::Result<String> {
let zed_path = std::env::current_exe()
.context("Failed to figure out current executable path for use in askpass")?
.to_string_lossy()
.to_string();
// sanity check on unix systems that the path exists and is executable
// todo(windows): implement this check for windows (or just use `is-executable` crate)
use std::os::unix::fs::MetadataExt;
let metadata = std::fs::metadata(&zed_path)
.context("Failed to check metadata of Zed executable path for use in askpass")?;
let is_executable = metadata.is_file() && metadata.mode() & 0o111 != 0;
anyhow::ensure!(
is_executable,
"Failed to verify Zed executable path for use in askpass"
);
// As of writing, this can only be fail if the path contains a null byte, which shouldn't be possible
// but shlex has annotated the error as #[non_exhaustive] so we can't make it a compile error if other
// errors are introduced in the future :(
let zed_path_escaped = shlex::try_quote(&zed_path)
.context("Failed to shell-escape Zed executable path for use in askpass")?;
return Ok(zed_path_escaped.to_string());
}
/// The main function for when Zed is running in netcat mode for use in askpass.
/// Called from both the remote server binary and the zed binary in their respective main functions.
#[cfg(unix)]

View File

@@ -49,7 +49,7 @@ menu.workspace = true
multi_buffer.workspace = true
parking_lot.workspace = true
project.workspace = true
rules_library.workspace = true
prompt_library.workspace = true
prompt_store.workspace = true
proto.workspace = true
rope.workspace = true

View File

@@ -8,7 +8,7 @@ mod terminal_inline_assistant;
use std::sync::Arc;
use assistant_settings::{AssistantSettings, LanguageModelSelection};
use assistant_settings::AssistantSettings;
use assistant_slash_command::SlashCommandRegistry;
use client::Client;
use command_palette_hooks::CommandPaletteFilter;
@@ -101,7 +101,7 @@ pub fn init(
SlashCommandSettings::register(cx);
assistant_context_editor::init(client.clone(), cx);
rules_library::init(cx);
prompt_library::init(cx);
init_language_model_settings(cx);
assistant_slash_command::init(cx);
assistant_tool::init(cx);
@@ -161,38 +161,71 @@ fn init_language_model_settings(cx: &mut App) {
fn update_active_language_model_from_settings(cx: &mut App) {
let settings = AssistantSettings::get_global(cx);
// Default model - used as fallback
let active_model_provider_name =
LanguageModelProviderId::from(settings.default_model.provider.clone());
let active_model_id = LanguageModelId::from(settings.default_model.model.clone());
fn to_selected_model(selection: &LanguageModelSelection) -> language_model::SelectedModel {
language_model::SelectedModel {
provider: LanguageModelProviderId::from(selection.provider.clone()),
model: LanguageModelId::from(selection.model.clone()),
}
}
let default = to_selected_model(&settings.default_model);
let inline_assistant = settings
// Inline assistant model
let inline_assistant_model = settings
.inline_assistant_model
.as_ref()
.map(to_selected_model);
let commit_message = settings
.unwrap_or(&settings.default_model);
let inline_assistant_provider_name =
LanguageModelProviderId::from(inline_assistant_model.provider.clone());
let inline_assistant_model_id = LanguageModelId::from(inline_assistant_model.model.clone());
// Commit message model
let commit_message_model = settings
.commit_message_model
.as_ref()
.map(to_selected_model);
let thread_summary = settings
.unwrap_or(&settings.default_model);
let commit_message_provider_name =
LanguageModelProviderId::from(commit_message_model.provider.clone());
let commit_message_model_id = LanguageModelId::from(commit_message_model.model.clone());
// Thread summary model
let thread_summary_model = settings
.thread_summary_model
.as_ref()
.map(to_selected_model);
.unwrap_or(&settings.default_model);
let thread_summary_provider_name =
LanguageModelProviderId::from(thread_summary_model.provider.clone());
let thread_summary_model_id = LanguageModelId::from(thread_summary_model.model.clone());
let inline_alternatives = settings
.inline_alternatives
.iter()
.map(to_selected_model)
.map(|alternative| {
(
LanguageModelProviderId::from(alternative.provider.clone()),
LanguageModelId::from(alternative.model.clone()),
)
})
.collect::<Vec<_>>();
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
registry.select_default_model(Some(&default), cx);
registry.select_inline_assistant_model(inline_assistant.as_ref(), cx);
registry.select_commit_message_model(commit_message.as_ref(), cx);
registry.select_thread_summary_model(thread_summary.as_ref(), cx);
// Set the default model
registry.select_default_model(&active_model_provider_name, &active_model_id, cx);
// Set the specific models
registry.select_inline_assistant_model(
&inline_assistant_provider_name,
&inline_assistant_model_id,
cx,
);
registry.select_commit_message_model(
&commit_message_provider_name,
&commit_message_model_id,
cx,
);
registry.select_thread_summary_model(
&thread_summary_provider_name,
&thread_summary_model_id,
cx,
);
// Set the alternatives
registry.select_inline_alternative_models(inline_alternatives, cx);
});
}

View File

@@ -193,7 +193,7 @@ impl Focusable for ConfigurationView {
impl Item for ConfigurationView {
type Event = ConfigurationViewEvent;
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
"Configuration".into()
fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
Some("Configuration".into())
}
}

View File

@@ -23,17 +23,17 @@ use gpui::{
use language::LanguageRegistry;
use language_model::{
AuthenticateError, ConfiguredModel, LanguageModelProviderId, LanguageModelRegistry,
ZED_CLOUD_PROVIDER_ID,
};
use project::Project;
use prompt_store::{PromptBuilder, UserPromptId};
use rules_library::{RulesLibrary, open_rules_library};
use prompt_library::{PromptLibrary, open_prompt_library};
use prompt_store::{PromptBuilder, PromptId};
use search::{BufferSearchBar, buffer_search::DivRegistrar};
use settings::{Settings, update_settings_file};
use smol::stream::StreamExt;
use std::ops::Range;
use std::path::Path;
use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
use ui::{ContextMenu, PopoverMenu, Tooltip, prelude::*};
@@ -44,7 +44,7 @@ use workspace::{
dock::{DockPosition, Panel, PanelEvent},
pane,
};
use zed_actions::assistant::{InlineAssist, OpenRulesLibrary, ShowConfiguration, ToggleFocus};
use zed_actions::assistant::{InlineAssist, OpenPromptLibrary, ShowConfiguration, ToggleFocus};
pub fn init(cx: &mut App) {
workspace::FollowableViewRegistry::register::<ContextEditor>(cx);
@@ -58,11 +58,11 @@ pub fn init(cx: &mut App) {
.register_action(AssistantPanel::show_configuration)
.register_action(AssistantPanel::create_new_context)
.register_action(AssistantPanel::restart_context_servers)
.register_action(|workspace, action: &OpenRulesLibrary, window, cx| {
.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_rules_library(action, window, cx)
panel.deploy_prompt_library(&OpenPromptLibrary::default(), window, cx)
});
}
});
@@ -273,8 +273,8 @@ impl AssistantPanel {
.action("New Chat", Box::new(NewChat))
.action("History", Box::new(DeployHistory))
.action(
"Rules Library",
Box::new(OpenRulesLibrary::default()),
"Prompt Library",
Box::new(OpenPromptLibrary::default()),
)
.action("Configure", Box::new(ShowConfiguration))
.action(zoom_label, Box::new(ToggleZoom))
@@ -477,7 +477,7 @@ impl AssistantPanel {
{
return;
}
context.set_custom_summary(new_summary, cx)
context.custom_summary(new_summary, cx)
});
});
}
@@ -489,8 +489,8 @@ impl AssistantPanel {
// If we're signed out and don't have a provider configured, or we're signed-out AND Zed.dev is
// the provider, we want to show a nudge to sign in.
let show_zed_ai_notice =
client_status.is_signed_out() && model.map_or(true, |model| model.is_provided_by_zed());
let show_zed_ai_notice = client_status.is_signed_out()
&& model.map_or(true, |model| model.provider.id().0 == ZED_CLOUD_PROVIDER_ID);
self.show_zed_ai_notice = show_zed_ai_notice;
cx.notify();
@@ -1044,13 +1044,13 @@ impl AssistantPanel {
}
}
fn deploy_rules_library(
fn deploy_prompt_library(
&mut self,
action: &OpenRulesLibrary,
action: &OpenPromptLibrary,
_window: &mut Window,
cx: &mut Context<Self>,
) {
open_rules_library(
open_prompt_library(
self.languages.clone(),
Box::new(PromptLibraryInlineAssist),
Arc::new(|| {
@@ -1060,9 +1060,7 @@ impl AssistantPanel {
None,
))
}),
action
.prompt_to_select
.map(|uuid| UserPromptId(uuid).into()),
action.prompt_to_focus.map(|uuid| PromptId::User { uuid }),
cx,
)
.detach_and_log_err(cx);
@@ -1081,7 +1079,7 @@ impl AssistantPanel {
pub fn open_saved_context(
&mut self,
path: Arc<Path>,
path: PathBuf,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
@@ -1236,7 +1234,7 @@ impl Render for AssistantPanel {
this.show_configuration_tab(window, cx)
}))
.on_action(cx.listener(AssistantPanel::deploy_history))
.on_action(cx.listener(AssistantPanel::deploy_rules_library))
.on_action(cx.listener(AssistantPanel::deploy_prompt_library))
.child(registrar.size_full().child(self.pane.clone()))
.into_any_element()
}
@@ -1351,13 +1349,13 @@ impl Focusable for AssistantPanel {
struct PromptLibraryInlineAssist;
impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist {
fn assist(
&self,
prompt_editor: &Entity<Editor>,
initial_prompt: Option<String>,
window: &mut Window,
cx: &mut Context<RulesLibrary>,
cx: &mut Context<PromptLibrary>,
) {
InlineAssistant::update_global(cx, |assistant, cx| {
assistant.assist(&prompt_editor, None, None, initial_prompt, window, cx)
@@ -1392,7 +1390,7 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
fn open_saved_context(
&self,
workspace: &mut Workspace,
path: Arc<Path>,
path: PathBuf,
window: &mut Window,
cx: &mut Context<Workspace>,
) -> Task<Result<()>> {

View File

@@ -18,11 +18,11 @@ use editor::{
},
};
use feature_flags::{
Assistant2FeatureFlag, FeatureFlagAppExt as _, FeatureFlagViewExt as _, ZedProFeatureFlag,
Assistant2FeatureFlag, FeatureFlagAppExt as _, FeatureFlagViewExt as _, ZedPro,
};
use fs::Fs;
use futures::{
SinkExt, Stream, StreamExt, TryStreamExt as _,
SinkExt, Stream, StreamExt,
channel::mpsc,
future::{BoxFuture, LocalBoxFuture},
join,
@@ -1226,7 +1226,7 @@ impl InlineAssistant {
editor.highlight_rows::<InlineAssist>(
row_range,
cx.theme().status().info_background,
Default::default(),
false,
cx,
);
}
@@ -1291,7 +1291,7 @@ impl InlineAssistant {
editor.highlight_rows::<DeletedLines>(
Anchor::min()..Anchor::max(),
cx.theme().status().deleted_background,
Default::default(),
false,
cx,
);
editor
@@ -1652,7 +1652,7 @@ impl Render for PromptEditor {
let error_message = SharedString::from(error.to_string());
if error.error_code() == proto::ErrorCode::RateLimitExceeded
&& cx.has_flag::<ZedProFeatureFlag>()
&& cx.has_flag::<ZedPro>()
{
el.child(
v_flex()
@@ -1759,7 +1759,6 @@ impl PromptEditor {
language_model_selector: cx.new(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
|cx| LanguageModelRegistry::read_global(cx).default_model(),
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
@@ -1966,7 +1965,7 @@ impl PromptEditor {
.update(cx, |editor, _| editor.set_read_only(false));
}
CodegenStatus::Error(error) => {
if cx.has_flag::<ZedProFeatureFlag>()
if cx.has_flag::<ZedPro>()
&& error.error_code() == proto::ErrorCode::RateLimitExceeded
&& !dismissed_rate_limit_notice()
{
@@ -2981,7 +2980,6 @@ impl CodegenAlternative {
Ok(LanguageModelRequest {
thread_id: None,
prompt_id: None,
mode: None,
messages,
tools: Vec::new(),
stop: Vec::new(),
@@ -3024,7 +3022,7 @@ impl CodegenAlternative {
}
}
let http_client = cx.http_client();
let http_client = cx.http_client().clone();
let telemetry = self.telemetry.clone();
let language_name = {
let multibuffer = self.buffer.read(cx);
@@ -3057,8 +3055,7 @@ impl CodegenAlternative {
let mut response_latency = None;
let request_start = Instant::now();
let diff = async {
let chunks =
StripInvalidSpans::new(stream?.stream.map_err(|e| e.into()));
let chunks = StripInvalidSpans::new(stream?.stream);
futures::pin_mut!(chunks);
let mut diff = StreamingDiff::new(selected_text.to_string());
let mut line_diff = LineDiff::default();

View File

@@ -44,6 +44,4 @@ impl Settings for SlashCommandSettings {
.chain(sources.server),
)
}
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}

View File

@@ -294,7 +294,6 @@ impl TerminalInlineAssistant {
Ok(LanguageModelRequest {
thread_id: None,
prompt_id: None,
mode: None,
messages,
tools: Vec::new(),
stop: Vec::new(),
@@ -749,7 +748,6 @@ impl PromptEditor {
language_model_selector: cx.new(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
|cx| LanguageModelRegistry::read_global(cx).default_model(),
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),

View File

@@ -35,7 +35,7 @@ use std::{
fmt::Debug,
iter, mem,
ops::Range,
path::Path,
path::{Path, PathBuf},
str::FromStr as _,
sync::Arc,
time::{Duration, Instant},
@@ -46,7 +46,7 @@ use ui::IconName;
use util::{ResultExt, TryFutureExt, post_inc};
use uuid::Uuid;
#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct ContextId(String);
impl ContextId {
@@ -459,7 +459,6 @@ pub enum ContextEvent {
ShowMaxMonthlySpendReachedError,
MessagesEdited,
SummaryChanged,
SummaryGenerated,
StreamedCompletion,
StartedThoughtProcess(Range<language::Anchor>),
EndedThoughtProcess(language::Anchor),
@@ -483,7 +482,7 @@ pub enum ContextEvent {
#[derive(Clone, Default, Debug)]
pub struct ContextSummary {
pub text: String,
pub done: bool,
done: bool,
timestamp: clock::Lamport,
}
@@ -641,14 +640,14 @@ pub struct AssistantContext {
contents: Vec<Content>,
messages_metadata: HashMap<MessageId, MessageMetadata>,
summary: Option<ContextSummary>,
summary_task: Task<Option<()>>,
pending_summary: Task<Option<()>>,
completion_count: usize,
pending_completions: Vec<PendingCompletion>,
token_count: Option<usize>,
pending_token_count: Task<Option<()>>,
pending_save: Task<Result<()>>,
pending_cache_warming_task: Task<Option<()>>,
path: Option<Arc<Path>>,
path: Option<PathBuf>,
_subscriptions: Vec<Subscription>,
telemetry: Option<Arc<Telemetry>>,
language_registry: Arc<LanguageRegistry>,
@@ -742,7 +741,7 @@ impl AssistantContext {
thought_process_output_sections: Vec::new(),
edits_since_last_parse: edits_since_last_slash_command_parse,
summary: None,
summary_task: Task::ready(None),
pending_summary: Task::ready(None),
completion_count: Default::default(),
pending_completions: Default::default(),
token_count: None,
@@ -839,7 +838,7 @@ impl AssistantContext {
pub fn deserialize(
saved_context: SavedContext,
path: Arc<Path>,
path: PathBuf,
language_registry: Arc<LanguageRegistry>,
prompt_builder: Arc<PromptBuilder>,
slash_commands: Arc<SlashCommandWorkingSet>,
@@ -952,7 +951,7 @@ impl AssistantContext {
fn flush_ops(&mut self, cx: &mut Context<AssistantContext>) {
let mut changed_messages = HashSet::default();
let mut summary_generated = false;
let mut summary_changed = false;
self.pending_ops.sort_unstable_by_key(|op| op.timestamp());
for op in mem::take(&mut self.pending_ops) {
@@ -994,7 +993,7 @@ impl AssistantContext {
.map_or(true, |summary| new_summary.timestamp > summary.timestamp)
{
self.summary = Some(new_summary);
summary_generated = true;
summary_changed = true;
}
}
ContextOperation::SlashCommandStarted {
@@ -1073,9 +1072,8 @@ impl AssistantContext {
cx.notify();
}
if summary_generated {
if summary_changed {
cx.emit(ContextEvent::SummaryChanged);
cx.emit(ContextEvent::SummaryGenerated);
cx.notify();
}
}
@@ -1147,8 +1145,8 @@ impl AssistantContext {
self.prompt_builder.clone()
}
pub fn path(&self) -> Option<&Arc<Path>> {
self.path.as_ref()
pub fn path(&self) -> Option<&Path> {
self.path.as_deref()
}
pub fn summary(&self) -> Option<&ContextSummary> {
@@ -2375,7 +2373,7 @@ impl AssistantContext {
LanguageModelCompletionEvent::Stop(reason) => {
stop_reason = reason;
}
LanguageModelCompletionEvent::Thinking { text: chunk, .. } => {
LanguageModelCompletionEvent::Thinking(chunk) => {
if thought_process_stack.is_empty() {
let start =
buffer.anchor_before(message_old_end_offset);
@@ -2559,7 +2557,6 @@ impl AssistantContext {
let mut completion_request = LanguageModelRequest {
thread_id: None,
prompt_id: None,
mode: None,
messages: Vec::new(),
tools: Vec::new(),
stop: Vec::new(),
@@ -2614,9 +2611,7 @@ impl AssistantContext {
.map(MessageContent::Text),
);
if !request_message.contents_empty() {
completion_request.messages.push(request_message);
}
completion_request.messages.push(request_message);
}
if let RequestType::SuggestEdits = request_type {
@@ -2950,7 +2945,7 @@ impl AssistantContext {
self.message_anchors.insert(insertion_ix, new_anchor);
}
pub fn summarize(&mut self, mut replace_old: bool, cx: &mut Context<Self>) {
pub fn summarize(&mut self, replace_old: bool, cx: &mut Context<Self>) {
let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
return;
};
@@ -2970,18 +2965,7 @@ impl AssistantContext {
cache: false,
});
// If there is no summary, it is set with `done: false` so that "Loading Summary…" can
// be displayed.
if self.summary.is_none() {
self.summary = Some(ContextSummary {
text: "".to_string(),
done: false,
timestamp: clock::Lamport::default(),
});
replace_old = true;
}
self.summary_task = cx.spawn(async move |this, cx| {
self.pending_summary = cx.spawn(async move |this, cx| {
async move {
let stream = model.model.stream_completion_text(request, &cx);
let mut messages = stream.await?;
@@ -3006,7 +2990,6 @@ impl AssistantContext {
};
this.push_op(operation, cx);
cx.emit(ContextEvent::SummaryChanged);
cx.emit(ContextEvent::SummaryGenerated);
})?;
// Stop if the LLM generated multiple lines.
@@ -3027,7 +3010,6 @@ impl AssistantContext {
};
this.push_op(operation, cx);
cx.emit(ContextEvent::SummaryChanged);
cx.emit(ContextEvent::SummaryGenerated);
}
})?;
@@ -3181,7 +3163,7 @@ impl AssistantContext {
fs.atomic_write(new_path.clone(), serde_json::to_string(&context).unwrap())
.await?;
if let Some(old_path) = old_path {
if new_path.as_path() != old_path.as_ref() {
if new_path != old_path {
fs.remove_file(
&old_path,
RemoveOptions {
@@ -3193,14 +3175,14 @@ impl AssistantContext {
}
}
this.update(cx, |this, _| this.path = Some(new_path.into()))?;
this.update(cx, |this, _| this.path = Some(new_path))?;
}
Ok(())
});
}
pub fn set_custom_summary(&mut self, custom_summary: String, cx: &mut Context<Self>) {
pub fn custom_summary(&mut self, custom_summary: String, cx: &mut Context<Self>) {
let timestamp = self.next_timestamp();
let summary = self.summary.get_or_insert(ContextSummary::default());
summary.timestamp = timestamp;
@@ -3208,15 +3190,6 @@ impl AssistantContext {
summary.text = custom_summary;
cx.emit(ContextEvent::SummaryChanged);
}
pub const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Text Thread");
pub fn summary_or_default(&self) -> SharedString {
self.summary
.as_ref()
.map(|summary| summary.text.clone().into())
.unwrap_or(Self::DEFAULT_SUMMARY)
}
}
fn trimmed_text_in_range(buffer: &BufferSnapshot, range: Range<text::Anchor>) -> String {
@@ -3589,6 +3562,6 @@ impl SavedContextV0_1_0 {
#[derive(Debug, Clone)]
pub struct SavedContextMetadata {
pub title: String,
pub path: Arc<Path>,
pub path: PathBuf,
pub mtime: chrono::DateTime<chrono::Local>,
}

View File

@@ -959,7 +959,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
let deserialized_context = cx.new(|cx| {
AssistantContext::deserialize(
serialized_context,
Path::new("").into(),
Default::default(),
registry.clone(),
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
@@ -1120,7 +1120,7 @@ async fn test_serialization(cx: &mut TestAppContext) {
let deserialized_context = cx.new(|cx| {
AssistantContext::deserialize(
serialized_context,
Path::new("").into(),
Default::default(),
registry.clone(),
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),

View File

@@ -48,14 +48,7 @@ use project::{Project, Worktree};
use rope::Point;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore, update_settings_file};
use std::{
any::TypeId,
cmp,
ops::Range,
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use std::{any::TypeId, borrow::Cow, cmp, ops::Range, path::PathBuf, sync::Arc, time::Duration};
use text::SelectionGoal;
use ui::{
ButtonLike, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle, TintColor, Tooltip,
@@ -146,7 +139,7 @@ pub trait AssistantPanelDelegate {
fn open_saved_context(
&self,
workspace: &mut Workspace,
path: Arc<Path>,
path: PathBuf,
window: &mut Window,
cx: &mut Context<Workspace>,
) -> Task<Result<()>>;
@@ -298,7 +291,6 @@ impl ContextEditor {
dragged_file_worktrees: Vec::new(),
language_model_selector: cx.new(|cx| {
LanguageModelSelector::new(
|cx| LanguageModelRegistry::read_global(cx).default_model(),
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
@@ -625,7 +617,6 @@ impl ContextEditor {
context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
});
}
ContextEvent::SummaryGenerated => {}
ContextEvent::StartedThoughtProcess(range) => {
let creases = self.insert_thought_process_output_sections(
[(
@@ -2097,7 +2088,7 @@ impl ContextEditor {
continue;
};
let image_id = image.id();
let image_task = LanguageModelImage::from_image(Arc::new(image), cx).shared();
let image_task = LanguageModelImage::from_image(image, cx).shared();
for image_position in image_positions.iter() {
context.insert_content(
@@ -2187,8 +2178,13 @@ impl ContextEditor {
});
}
pub fn title(&self, cx: &App) -> SharedString {
self.context.read(cx).summary_or_default()
pub fn title(&self, cx: &App) -> Cow<str> {
self.context
.read(cx)
.summary()
.map(|summary| summary.text.clone())
.map(Cow::Owned)
.unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE))
}
fn render_patch_block(
@@ -3163,8 +3159,8 @@ impl Focusable for ContextEditor {
impl Item for ContextEditor {
type Event = editor::EditorEvent;
fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
util::truncate_and_trailoff(&self.title(cx), MAX_TAB_TITLE_LEN).into()
fn tab_content_text(&self, _window: &Window, cx: &App) -> Option<SharedString> {
Some(util::truncate_and_trailoff(&self.title(cx), MAX_TAB_TITLE_LEN).into())
}
fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
@@ -3771,7 +3767,7 @@ pub fn make_lsp_adapter_delegate(
let Some(worktree) = project.worktrees(cx).next() else {
return Ok(None::<Arc<dyn LspAdapterDelegate>>);
};
let http_client = project.client().http_client();
let http_client = project.client().http_client().clone();
project.lsp_store().update(cx, |_, cx| {
Ok(Some(LocalLspAdapterDelegate::new(
project.languages().clone(),

View File

@@ -108,8 +108,8 @@ impl EventEmitter<()> for ContextHistory {}
impl Item for ContextHistory {
type Event = ();
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
"History".into()
fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
Some("History".into())
}
}

View File

@@ -20,7 +20,14 @@ use prompt_store::PromptBuilder;
use regex::Regex;
use rpc::AnyProtoClient;
use std::sync::LazyLock;
use std::{cmp::Reverse, ffi::OsStr, mem, path::Path, sync::Arc, time::Duration};
use std::{
cmp::Reverse,
ffi::OsStr,
mem,
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use util::{ResultExt, TryFutureExt};
pub(crate) fn init(client: &AnyProtoClient) {
@@ -423,7 +430,7 @@ impl ContextStore {
pub fn open_local_context(
&mut self,
path: Arc<Path>,
path: PathBuf,
cx: &Context<Self>,
) -> Task<Result<Entity<AssistantContext>>> {
if let Some(existing_context) = self.loaded_context_for_path(&path, cx) {
@@ -471,7 +478,7 @@ impl ContextStore {
pub fn delete_local_context(
&mut self,
path: Arc<Path>,
path: PathBuf,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let fs = self.fs.clone();
@@ -494,7 +501,7 @@ impl ContextStore {
!= Some(&path)
});
this.contexts_metadata
.retain(|context| context.path.as_ref() != path.as_ref());
.retain(|context| context.path != path);
})?;
Ok(())
@@ -504,7 +511,7 @@ impl ContextStore {
fn loaded_context_for_path(&self, path: &Path, cx: &App) -> Option<Entity<AssistantContext>> {
self.contexts.iter().find_map(|context| {
let context = context.upgrade()?;
if context.read(cx).path().map(Arc::as_ref) == Some(path) {
if context.read(cx).path() == Some(path) {
Some(context)
} else {
None
@@ -787,7 +794,7 @@ impl ContextStore {
{
contexts.push(SavedContextMetadata {
title: title.to_string(),
path: path.into(),
path,
mtime: metadata.mtime.timestamp_for_user().into(),
});
}

View File

@@ -112,27 +112,13 @@ impl AssistantSettings {
}
/// Assistant panel settings
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
pub struct AssistantSettingsContent {
#[serde(flatten)]
pub inner: Option<AssistantSettingsContentInner>,
}
#[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum AssistantSettingsContentInner {
pub enum AssistantSettingsContent {
Versioned(Box<VersionedAssistantSettingsContent>),
Legacy(LegacyAssistantSettingsContent),
}
impl AssistantSettingsContentInner {
fn for_v2(content: AssistantSettingsContentV2) -> Self {
AssistantSettingsContentInner::Versioned(Box::new(VersionedAssistantSettingsContent::V2(
content,
)))
}
}
impl JsonSchema for AssistantSettingsContent {
fn schema_name() -> String {
VersionedAssistantSettingsContent::schema_name()
@@ -147,21 +133,26 @@ impl JsonSchema for AssistantSettingsContent {
}
}
impl Default for AssistantSettingsContent {
fn default() -> Self {
Self::Versioned(Box::new(VersionedAssistantSettingsContent::default()))
}
}
impl AssistantSettingsContent {
pub fn is_version_outdated(&self) -> bool {
match &self.inner {
Some(AssistantSettingsContentInner::Versioned(settings)) => match **settings {
match self {
AssistantSettingsContent::Versioned(settings) => match **settings {
VersionedAssistantSettingsContent::V1(_) => true,
VersionedAssistantSettingsContent::V2(_) => false,
},
Some(AssistantSettingsContentInner::Legacy(_)) => true,
None => false,
AssistantSettingsContent::Legacy(_) => true,
}
}
fn upgrade(&self) -> AssistantSettingsContentV2 {
match &self.inner {
Some(AssistantSettingsContentInner::Versioned(settings)) => match **settings {
match self {
AssistantSettingsContent::Versioned(settings) => match **settings {
VersionedAssistantSettingsContent::V1(ref settings) => AssistantSettingsContentV2 {
enabled: settings.enabled,
button: settings.button,
@@ -221,7 +212,7 @@ impl AssistantSettingsContent {
},
VersionedAssistantSettingsContent::V2(ref settings) => settings.clone(),
},
Some(AssistantSettingsContentInner::Legacy(settings)) => AssistantSettingsContentV2 {
AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV2 {
enabled: None,
button: settings.button,
dock: settings.dock,
@@ -246,13 +237,12 @@ impl AssistantSettingsContent {
always_allow_tool_actions: None,
notify_when_agent_waiting: None,
},
None => AssistantSettingsContentV2::default(),
}
}
pub fn set_dock(&mut self, dock: AssistantDockPosition) {
match &mut self.inner {
Some(AssistantSettingsContentInner::Versioned(settings)) => match **settings {
match self {
AssistantSettingsContent::Versioned(settings) => match **settings {
VersionedAssistantSettingsContent::V1(ref mut settings) => {
settings.dock = Some(dock);
}
@@ -260,17 +250,9 @@ impl AssistantSettingsContent {
settings.dock = Some(dock);
}
},
Some(AssistantSettingsContentInner::Legacy(settings)) => {
AssistantSettingsContent::Legacy(settings) => {
settings.dock = Some(dock);
}
None => {
self.inner = Some(AssistantSettingsContentInner::for_v2(
AssistantSettingsContentV2 {
dock: Some(dock),
..Default::default()
},
))
}
}
}
@@ -278,8 +260,8 @@ impl AssistantSettingsContent {
let model = language_model.id().0.to_string();
let provider = language_model.provider_id().0.to_string();
match &mut self.inner {
Some(AssistantSettingsContentInner::Versioned(settings)) => match **settings {
match self {
AssistantSettingsContent::Versioned(settings) => match **settings {
VersionedAssistantSettingsContent::V1(ref mut settings) => {
match provider.as_ref() {
"zed.dev" => {
@@ -355,80 +337,56 @@ impl AssistantSettingsContent {
settings.default_model = Some(LanguageModelSelection { provider, model });
}
},
Some(AssistantSettingsContentInner::Legacy(settings)) => {
AssistantSettingsContent::Legacy(settings) => {
if let Ok(model) = OpenAiModel::from_id(&language_model.id().0) {
settings.default_open_ai_model = Some(model);
}
}
None => {
self.inner = Some(AssistantSettingsContentInner::for_v2(
AssistantSettingsContentV2 {
default_model: Some(LanguageModelSelection { provider, model }),
..Default::default()
},
));
}
}
}
pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
self.v2_setting(|setting| {
setting.inline_assistant_model = Some(LanguageModelSelection { provider, model });
Ok(())
})
.ok();
if let AssistantSettingsContent::Versioned(boxed) = self {
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
settings.inline_assistant_model = Some(LanguageModelSelection { provider, model });
}
}
}
pub fn set_commit_message_model(&mut self, provider: String, model: String) {
self.v2_setting(|setting| {
setting.commit_message_model = Some(LanguageModelSelection { provider, model });
Ok(())
})
.ok();
}
pub fn v2_setting(
&mut self,
f: impl FnOnce(&mut AssistantSettingsContentV2) -> anyhow::Result<()>,
) -> anyhow::Result<()> {
match self.inner.get_or_insert_with(|| {
AssistantSettingsContentInner::for_v2(AssistantSettingsContentV2 {
..Default::default()
})
}) {
AssistantSettingsContentInner::Versioned(boxed) => {
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
f(settings)
} else {
Ok(())
}
if let AssistantSettingsContent::Versioned(boxed) = self {
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
settings.commit_message_model = Some(LanguageModelSelection { provider, model });
}
_ => Ok(()),
}
}
pub fn set_thread_summary_model(&mut self, provider: String, model: String) {
self.v2_setting(|setting| {
setting.thread_summary_model = Some(LanguageModelSelection { provider, model });
Ok(())
})
.ok();
if let AssistantSettingsContent::Versioned(boxed) = self {
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
settings.thread_summary_model = Some(LanguageModelSelection { provider, model });
}
}
}
pub fn set_always_allow_tool_actions(&mut self, allow: bool) {
self.v2_setting(|setting| {
setting.always_allow_tool_actions = Some(allow);
Ok(())
})
.ok();
let AssistantSettingsContent::Versioned(boxed) = self else {
return;
};
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
settings.always_allow_tool_actions = Some(allow);
}
}
pub fn set_profile(&mut self, profile_id: AgentProfileId) {
self.v2_setting(|setting| {
setting.default_profile = Some(profile_id);
Ok(())
})
.ok();
let AssistantSettingsContent::Versioned(boxed) = self else {
return;
};
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
settings.default_profile = Some(profile_id);
}
}
pub fn create_profile(
@@ -436,7 +394,11 @@ impl AssistantSettingsContent {
profile_id: AgentProfileId,
profile: AgentProfile,
) -> Result<()> {
self.v2_setting(|settings| {
let AssistantSettingsContent::Versioned(boxed) = self else {
return Ok(());
};
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
let profiles = settings.profiles.get_or_insert_default();
if profiles.contains_key(&profile_id) {
bail!("profile with ID '{profile_id}' already exists");
@@ -462,9 +424,9 @@ impl AssistantSettingsContent {
.collect(),
},
);
}
Ok(())
})
Ok(())
}
}
@@ -499,7 +461,7 @@ impl Default for VersionedAssistantSettingsContent {
}
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)]
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
pub struct AssistantSettingsContentV2 {
/// Whether the Assistant is enabled.
///
@@ -746,39 +708,6 @@ impl Settings for AssistantSettings {
Ok(settings)
}
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
if let Some(b) = vscode
.read_value("chat.agent.enabled")
.and_then(|b| b.as_bool())
{
match &mut current.inner {
Some(AssistantSettingsContentInner::Versioned(versioned)) => {
match versioned.as_mut() {
VersionedAssistantSettingsContent::V1(setting) => {
setting.enabled = Some(b);
setting.button = Some(b);
}
VersionedAssistantSettingsContent::V2(setting) => {
setting.enabled = Some(b);
setting.button = Some(b);
}
}
}
Some(AssistantSettingsContentInner::Legacy(setting)) => setting.button = Some(b),
None => {
current.inner = Some(AssistantSettingsContentInner::for_v2(
AssistantSettingsContentV2 {
enabled: Some(b),
button: Some(b),
..Default::default()
},
));
}
}
}
}
}
fn merge<T>(target: &mut T, value: Option<T>) {
@@ -822,30 +751,28 @@ mod tests {
settings::SettingsStore::global(cx).update_settings_file::<AssistantSettings>(
fs.clone(),
|settings, _| {
*settings = AssistantSettingsContent {
inner: Some(AssistantSettingsContentInner::for_v2(
AssistantSettingsContentV2 {
default_model: Some(LanguageModelSelection {
provider: "test-provider".into(),
model: "gpt-99".into(),
}),
inline_assistant_model: None,
commit_message_model: None,
thread_summary_model: None,
inline_alternatives: None,
enabled: None,
button: None,
dock: None,
default_width: None,
default_height: None,
enable_experimental_live_diffs: None,
default_profile: None,
profiles: None,
always_allow_tool_actions: None,
notify_when_agent_waiting: None,
},
)),
}
*settings = AssistantSettingsContent::Versioned(Box::new(
VersionedAssistantSettingsContent::V2(AssistantSettingsContentV2 {
default_model: Some(LanguageModelSelection {
provider: "test-provider".into(),
model: "gpt-99".into(),
}),
inline_assistant_model: None,
commit_message_model: None,
thread_summary_model: None,
inline_alternatives: None,
enabled: None,
button: None,
dock: None,
default_width: None,
default_height: None,
enable_experimental_live_diffs: None,
default_profile: None,
profiles: None,
always_allow_tool_actions: None,
notify_when_agent_waiting: None,
}),
))
},
);
});

View File

@@ -83,7 +83,7 @@ impl DocsSlashCommand {
.upgrade()
.ok_or_else(|| anyhow!("workspace was dropped"))?;
let project = workspace.read(cx).project().clone();
anyhow::Ok(project.read(cx).client().http_client())
anyhow::Ok(project.read(cx).client().http_client().clone())
});
if let Some(http_client) = http_client.log_err() {

View File

@@ -44,10 +44,9 @@ impl SlashCommand for PromptSlashCommand {
let store = PromptStore::global(cx);
let query = arguments.to_owned().join(" ");
cx.spawn(async move |cx| {
let cancellation_flag = Arc::new(AtomicBool::default());
let prompts: Vec<PromptMetadata> = store
.await?
.read_with(cx, |store, cx| store.search(query, cancellation_flag, cx))?
.read_with(cx, |store, cx| store.search(query, cx))?
.await;
Ok(prompts
.into_iter()

View File

@@ -28,7 +28,6 @@ serde.workspace = true
serde_json.workspace = true
text.workspace = true
util.workspace = true
workspace.workspace = true
workspace-hack.workspace = true
[dev-dependencies]

View File

@@ -39,9 +39,10 @@ impl ActionLog {
self.edited_since_project_diagnostics_check
}
fn track_buffer_internal(
fn track_buffer(
&mut self,
buffer: Entity<Buffer>,
created: bool,
cx: &mut Context<Self>,
) -> &mut TrackedBuffer {
let tracked_buffer = self
@@ -58,11 +59,7 @@ impl ActionLog {
let base_text;
let status;
let unreviewed_changes;
if buffer
.read(cx)
.file()
.map_or(true, |file| !file.disk_state().exists())
{
if created {
base_text = Rope::default();
status = TrackedBufferStatus::Created;
unreviewed_changes = Patch::new(vec![Edit {
@@ -149,7 +146,7 @@ impl ActionLog {
// resurrected externally, we want to clear the changes we
// were tracking and reset the buffer's state.
self.tracked_buffers.remove(&buffer);
self.track_buffer_internal(buffer, cx);
self.track_buffer(buffer, false, cx);
}
cx.notify();
}
@@ -263,15 +260,26 @@ impl ActionLog {
}
/// Track a buffer as read, so we can notify the model about user edits.
pub fn track_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
self.track_buffer_internal(buffer, cx);
pub fn buffer_read(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
self.track_buffer(buffer, false, cx);
}
/// Track a buffer that was added as context, so we can notify the model about user edits.
pub fn buffer_added_as_context(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
self.track_buffer(buffer, false, cx);
}
/// Track a buffer as read, so we can notify the model about user edits.
pub fn will_create_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
self.track_buffer(buffer.clone(), true, cx);
self.buffer_edited(buffer, cx)
}
/// Mark a buffer as edited, so we can refresh it in the context
pub fn buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
self.edited_since_project_diagnostics_check = true;
let tracked_buffer = self.track_buffer_internal(buffer.clone(), cx);
let tracked_buffer = self.track_buffer(buffer.clone(), false, cx);
if let TrackedBufferStatus::Deleted = tracked_buffer.status {
tracked_buffer.status = TrackedBufferStatus::Modified;
}
@@ -279,7 +287,7 @@ impl ActionLog {
}
pub fn will_delete_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
let tracked_buffer = self.track_buffer_internal(buffer.clone(), cx);
let tracked_buffer = self.track_buffer(buffer.clone(), false, cx);
match tracked_buffer.status {
TrackedBufferStatus::Created => {
self.tracked_buffers.remove(&buffer);
@@ -389,7 +397,7 @@ impl ActionLog {
// Clear all tracked changes for this buffer and start over as if we just read it.
self.tracked_buffers.remove(&buffer);
self.track_buffer_internal(buffer.clone(), cx);
self.track_buffer(buffer.clone(), false, cx);
cx.notify();
save
}
@@ -687,20 +695,12 @@ mod tests {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
.await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let project = Project::test(fs.clone(), [], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let file_path = project
.read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
.unwrap();
let buffer = project
.update(cx, |project, cx| project.open_buffer(file_path, cx))
.await
.unwrap();
let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
cx.update(|cx| {
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
buffer.update(cx, |buffer, cx| {
buffer
.edit([(Point::new(1, 1)..Point::new(1, 2), "E")], None, cx)
@@ -765,23 +765,12 @@ mod tests {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/dir"),
json!({"file": "abc\ndef\nghi\njkl\nmno\npqr"}),
)
.await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let project = Project::test(fs.clone(), [], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let file_path = project
.read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
.unwrap();
let buffer = project
.update(cx, |project, cx| project.open_buffer(file_path, cx))
.await
.unwrap();
let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno\npqr", cx));
cx.update(|cx| {
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
buffer.update(cx, |buffer, cx| {
buffer
.edit([(Point::new(1, 0)..Point::new(2, 0), "")], None, cx)
@@ -850,20 +839,12 @@ mod tests {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
.await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let project = Project::test(fs.clone(), [], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let file_path = project
.read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
.unwrap();
let buffer = project
.update(cx, |project, cx| project.open_buffer(file_path, cx))
.await
.unwrap();
let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
cx.update(|cx| {
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
buffer.update(cx, |buffer, cx| {
buffer
.edit([(Point::new(1, 2)..Point::new(2, 3), "F\nGHI")], None, cx)
@@ -947,21 +928,25 @@ mod tests {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/dir"), json!({})).await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/dir"), json!({})).await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let file_path = project
.read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
.unwrap();
// Simulate file2 being recreated by a tool.
let buffer = project
.update(cx, |project, cx| project.open_buffer(file_path, cx))
.await
.unwrap();
cx.update(|cx| {
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
buffer.update(cx, |buffer, cx| buffer.set_text("lorem", cx));
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
action_log.update(cx, |log, cx| log.will_create_buffer(buffer.clone(), cx));
});
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
@@ -1082,9 +1067,8 @@ mod tests {
.update(cx, |project, cx| project.open_buffer(file2_path, cx))
.await
.unwrap();
action_log.update(cx, |log, cx| log.track_buffer(buffer2.clone(), cx));
buffer2.update(cx, |buffer, cx| buffer.set_text("IPSUM", cx));
action_log.update(cx, |log, cx| log.buffer_edited(buffer2.clone(), cx));
action_log.update(cx, |log, cx| log.will_create_buffer(buffer2.clone(), cx));
project
.update(cx, |project, cx| project.save_buffer(buffer2.clone(), cx))
.await
@@ -1129,7 +1113,7 @@ mod tests {
.unwrap();
cx.update(|cx| {
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
buffer.update(cx, |buffer, cx| {
buffer
.edit([(Point::new(1, 1)..Point::new(1, 2), "E\nXYZ")], None, cx)
@@ -1264,7 +1248,7 @@ mod tests {
.unwrap();
cx.update(|cx| {
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
buffer.update(cx, |buffer, cx| {
buffer
.edit([(Point::new(1, 1)..Point::new(1, 2), "E\nXYZ")], None, cx)
@@ -1397,9 +1381,8 @@ mod tests {
.await
.unwrap();
cx.update(|cx| {
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
buffer.update(cx, |buffer, cx| buffer.set_text("content", cx));
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
action_log.update(cx, |log, cx| log.will_create_buffer(buffer.clone(), cx));
});
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
@@ -1455,7 +1438,7 @@ mod tests {
.await
.unwrap();
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
for _ in 0..operations {
match rng.gen_range(0..100) {
@@ -1507,7 +1490,7 @@ mod tests {
log::info!("quiescing...");
cx.run_until_parked();
action_log.update(cx, |log, cx| {
let tracked_buffer = log.track_buffer_internal(buffer.clone(), cx);
let tracked_buffer = log.track_buffer(buffer.clone(), false, cx);
let mut old_text = tracked_buffer.base_text.clone();
let new_text = buffer.read(cx).as_rope();
for edit in tracked_buffer.unreviewed_changes.edits() {

View File

@@ -10,16 +10,14 @@ use std::sync::Arc;
use anyhow::Result;
use gpui::AnyElement;
use gpui::AnyWindowHandle;
use gpui::Context;
use gpui::IntoElement;
use gpui::Window;
use gpui::{App, Entity, SharedString, Task, WeakEntity};
use gpui::{App, Entity, SharedString, Task};
use icons::IconName;
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
use project::Project;
use workspace::Workspace;
pub use crate::action_log::*;
pub use crate::tool_registry::*;
@@ -32,7 +30,6 @@ pub fn init(cx: &mut App) {
#[derive(Debug, Clone)]
pub enum ToolUseStatus {
InputStillStreaming,
NeedsConfirmation,
Pending,
Running,
@@ -44,20 +41,12 @@ impl ToolUseStatus {
pub fn text(&self) -> SharedString {
match self {
ToolUseStatus::NeedsConfirmation => "".into(),
ToolUseStatus::InputStillStreaming => "".into(),
ToolUseStatus::Pending => "".into(),
ToolUseStatus::Running => "".into(),
ToolUseStatus::Finished(out) => out.clone(),
ToolUseStatus::Error(out) => out.clone(),
}
}
pub fn error(&self) -> Option<SharedString> {
match self {
ToolUseStatus::Error(out) => Some(out.clone()),
_ => None,
}
}
}
/// The result of running a tool, containing both the asynchronous output
@@ -74,7 +63,6 @@ pub trait ToolCard: 'static + Sized {
&mut self,
status: &ToolUseStatus,
window: &mut Window,
workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> impl IntoElement;
}
@@ -86,7 +74,6 @@ pub struct AnyToolCard {
entity: gpui::AnyEntity,
status: &ToolUseStatus,
window: &mut Window,
workspace: WeakEntity<Workspace>,
cx: &mut App,
) -> AnyElement,
}
@@ -97,14 +84,11 @@ impl<T: ToolCard> From<Entity<T>> for AnyToolCard {
entity: gpui::AnyEntity,
status: &ToolUseStatus,
window: &mut Window,
workspace: WeakEntity<Workspace>,
cx: &mut App,
) -> AnyElement {
let entity = entity.downcast::<T>().unwrap();
entity.update(cx, |entity, cx| {
entity
.render(status, window, workspace, cx)
.into_any_element()
entity.render(status, window, cx).into_any_element()
})
}
@@ -116,14 +100,8 @@ impl<T: ToolCard> From<Entity<T>> for AnyToolCard {
}
impl AnyToolCard {
pub fn render(
&self,
status: &ToolUseStatus,
window: &mut Window,
workspace: WeakEntity<Workspace>,
cx: &mut App,
) -> AnyElement {
(self.render)(self.entity.clone(), status, window, workspace, cx)
pub fn render(&self, status: &ToolUseStatus, window: &mut Window, cx: &mut App) -> AnyElement {
(self.render)(self.entity.clone(), status, window, cx)
}
}
@@ -170,12 +148,6 @@ pub trait Tool: 'static + Send + Sync {
/// Returns markdown to be displayed in the UI for this tool.
fn ui_text(&self, input: &serde_json::Value) -> String;
/// Returns markdown to be displayed in the UI for this tool, while the input JSON is still streaming
/// (so information may be missing).
fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String {
self.ui_text(input)
}
/// Runs the tool with the provided input.
fn run(
self: Arc<Self>,
@@ -183,7 +155,6 @@ pub trait Tool: 'static + Send + Sync {
messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult;
}

View File

@@ -10,11 +10,6 @@ pub fn adapt_schema_to_format(
json: &mut Value,
format: LanguageModelToolSchemaFormat,
) -> Result<()> {
if let Value::Object(obj) = json {
obj.remove("$schema");
obj.remove("title");
}
match format {
LanguageModelToolSchemaFormat::JsonSchema => Ok(()),
LanguageModelToolSchemaFormat::JsonSchemaSubset => adapt_to_json_schema_subset(json),
@@ -35,12 +30,7 @@ fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> {
}
}
const KEYS_TO_REMOVE: [&str; 4] = [
"format",
"additionalProperties",
"exclusiveMinimum",
"exclusiveMaximum",
];
const KEYS_TO_REMOVE: [&str; 2] = ["format", "$schema"];
for key in KEYS_TO_REMOVE {
obj.remove(key);
}
@@ -55,7 +45,7 @@ fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> {
}
// If a type is not specified for an input parameter, add a default type
if matches!(obj.get("description"), Some(Value::String(_)))
if obj.contains_key("description")
&& !obj.contains_key("type")
&& !(obj.contains_key("anyOf")
|| obj.contains_key("oneOf")
@@ -127,37 +117,14 @@ mod tests {
"type": "string"
})
);
// Ensure that we do not add a type if it is an object
let mut json = json!({
"description": {
"value": "abc",
"type": "string"
}
});
adapt_to_json_schema_subset(&mut json).unwrap();
assert_eq!(
json,
json!({
"description": {
"value": "abc",
"type": "string"
}
})
);
}
#[test]
fn test_transform_removes_unsupported_keys() {
fn test_transform_removes_format() {
let mut json = json!({
"description": "A test field",
"type": "integer",
"format": "uint32",
"exclusiveMinimum": 0,
"exclusiveMaximum": 100,
"additionalProperties": false
"format": "uint32"
});
adapt_to_json_schema_subset(&mut json).unwrap();

View File

@@ -14,11 +14,10 @@ path = "src/assistant_tools.rs"
[dependencies]
anyhow.workspace = true
assistant_tool.workspace = true
buffer_diff.workspace = true
chrono.workspace = true
collections.workspace = true
component.workspace = true
editor.workspace = true
feature_flags.workspace = true
futures.workspace = true
gpui.workspace = true
html_to_markdown.workspace = true
@@ -34,19 +33,14 @@ regex.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
task.workspace = true
terminal.workspace = true
terminal_view.workspace = true
ui.workspace = true
util.workspace = true
web_search.workspace = true
workspace-hack.workspace = true
workspace.workspace = true
worktree.workspace = true
zed_llm_client.workspace = true
[dev-dependencies]
client = { workspace = true, features = ["test-support"] }
clock = { workspace = true, features = ["test-support"] }
collections = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
@@ -54,7 +48,6 @@ project = { workspace = true, features = ["test-support"] }
rand.workspace = true
pretty_assertions.workspace = true
settings = { workspace = true, features = ["test-support"] }
task = { workspace = true, features = ["test-support"]}
tree-sitter-rust.workspace = true
workspace = { workspace = true, features = ["test-support"] }
unindent.workspace = true

View File

@@ -9,29 +9,28 @@ mod delete_path_tool;
mod diagnostics_tool;
mod edit_file_tool;
mod fetch_tool;
mod find_path_tool;
mod grep_tool;
mod list_directory_tool;
mod move_path_tool;
mod now_tool;
mod open_tool;
mod path_search_tool;
mod read_file_tool;
mod regex_search_tool;
mod rename_tool;
mod replace;
mod schema;
mod symbol_info_tool;
mod terminal_tool;
mod thinking_tool;
mod ui;
mod web_search_tool;
use std::sync::Arc;
use assistant_tool::ToolRegistry;
use copy_path_tool::CopyPathTool;
use feature_flags::FeatureFlagAppExt;
use gpui::App;
use http_client::HttpClientWithUrl;
use language_model::LanguageModelRegistry;
use move_path_tool::MovePathTool;
use web_search_tool::WebSearchTool;
@@ -45,22 +44,17 @@ use crate::delete_path_tool::DeletePathTool;
use crate::diagnostics_tool::DiagnosticsTool;
use crate::edit_file_tool::EditFileTool;
use crate::fetch_tool::FetchTool;
use crate::find_path_tool::FindPathTool;
use crate::grep_tool::GrepTool;
use crate::list_directory_tool::ListDirectoryTool;
use crate::now_tool::NowTool;
use crate::open_tool::OpenTool;
use crate::path_search_tool::PathSearchTool;
use crate::read_file_tool::ReadFileTool;
use crate::regex_search_tool::RegexSearchTool;
use crate::rename_tool::RenameTool;
use crate::symbol_info_tool::SymbolInfoTool;
use crate::terminal_tool::TerminalTool;
use crate::thinking_tool::ThinkingTool;
pub use create_file_tool::CreateFileToolInput;
pub use edit_file_tool::EditFileToolInput;
pub use find_path_tool::FindPathToolInput;
pub use read_file_tool::ReadFileToolInput;
pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
assistant_tool::init(cx);
@@ -81,79 +75,41 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
registry.register_tool(OpenTool);
registry.register_tool(CodeSymbolsTool);
registry.register_tool(ContentsTool);
registry.register_tool(FindPathTool);
registry.register_tool(PathSearchTool);
registry.register_tool(ReadFileTool);
registry.register_tool(GrepTool);
registry.register_tool(RegexSearchTool);
registry.register_tool(RenameTool);
registry.register_tool(ThinkingTool);
registry.register_tool(FetchTool::new(http_client));
cx.subscribe(
&LanguageModelRegistry::global(cx),
move |registry, event, cx| match event {
language_model::Event::DefaultModelChanged => {
let using_zed_provider = registry
.read(cx)
.default_model()
.map_or(false, |default| default.is_provided_by_zed());
if using_zed_provider {
ToolRegistry::global(cx).register_tool(WebSearchTool);
} else {
ToolRegistry::global(cx).unregister_tool(WebSearchTool);
}
cx.observe_flag::<feature_flags::ZedProWebSearchTool, _>({
move |is_enabled, cx| {
if is_enabled {
ToolRegistry::global(cx).register_tool(WebSearchTool);
} else {
ToolRegistry::global(cx).unregister_tool(WebSearchTool);
}
_ => {}
},
)
}
})
.detach();
}
#[cfg(test)]
mod tests {
use super::*;
use client::Client;
use clock::FakeSystemClock;
use http_client::FakeHttpClient;
use schemars::JsonSchema;
use serde::Serialize;
#[test]
fn test_json_schema() {
#[derive(Serialize, JsonSchema)]
struct GetWeatherTool {
location: String,
}
let schema = schema::json_schema_for::<GetWeatherTool>(
language_model::LanguageModelToolSchemaFormat::JsonSchema,
)
.unwrap();
assert_eq!(
schema,
serde_json::json!({
"type": "object",
"properties": {
"location": {
"type": "string"
}
},
"required": ["location"],
})
);
}
use super::*;
#[gpui::test]
fn test_builtin_tool_schema_compatibility(cx: &mut App) {
settings::init(cx);
let client = Client::new(
Arc::new(FakeSystemClock::new()),
FakeHttpClient::with_200_response(),
crate::init(
Arc::new(http_client::HttpClientWithUrl::new(
FakeHttpClient::with_200_response(),
"https://zed.dev",
None,
)),
cx,
);
language_model::init(client.clone(), cx);
crate::init(client.http_client(), cx);
for tool in ToolRegistry::global(cx).tools() {
let actual_schema = tool

View File

@@ -2,7 +2,7 @@ use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult, ToolWorkingSet};
use futures::future::join_all;
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use gpui::{App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
@@ -43,7 +43,7 @@ pub struct BatchToolInput {
/// }
/// },
/// {
/// "name": "grep",
/// "name": "regex_search",
/// "input": {
/// "regex": "fn run\\("
/// }
@@ -91,13 +91,13 @@ pub struct BatchToolInput {
/// {
/// "invocations": [
/// {
/// "name": "grep",
/// "name": "regex_search",
/// "input": {
/// "regex": "impl Database"
/// }
/// },
/// {
/// "name": "find_path",
/// "name": "path_search",
/// "input": {
/// "glob": "**/*test*.rs"
/// }
@@ -218,7 +218,6 @@ impl Tool for BatchTool {
messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<BatchToolInput>(input) {
@@ -259,9 +258,7 @@ impl Tool for BatchTool {
let action_log = action_log.clone();
let messages = messages.clone();
let tool_result = cx
.update(|cx| {
tool.run(invocation.input, &messages, project, action_log, window, 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(tool_result.output);

View File

@@ -1,6 +1,6 @@
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{AnyWindowHandle, App, Entity, Task};
use gpui::{App, Entity, Task};
use language::{self, Anchor, Buffer, ToPointUtf16};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::{self, LspAction, Project};
@@ -140,7 +140,6 @@ impl Tool for CodeActionTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<CodeActionToolInput>(input) {
@@ -160,7 +159,7 @@ impl Tool for CodeActionTool {
};
action_log.update(cx, |action_log, cx| {
action_log.track_buffer(buffer.clone(), cx);
action_log.buffer_read(buffer.clone(), cx);
})?;
let range = {

View File

@@ -6,7 +6,7 @@ use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use collections::IndexMap;
use gpui::{AnyWindowHandle, App, AsyncApp, Entity, Task};
use gpui::{App, AsyncApp, Entity, Task};
use language::{OutlineItem, ParseStatus, Point};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::{Project, Symbol};
@@ -14,7 +14,7 @@ use regex::{Regex, RegexBuilder};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use ui::IconName;
use util::markdown::MarkdownInlineCode;
use util::markdown::MarkdownString;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CodeSymbolsInput {
@@ -102,7 +102,7 @@ impl Tool for CodeSymbolsTool {
match &input.path {
Some(path) => {
let path = MarkdownInlineCode(path);
let path = MarkdownString::inline_code(path);
if page > 1 {
format!("List page {page} of code symbols for {path}")
} else {
@@ -128,7 +128,6 @@ impl Tool for CodeSymbolsTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<CodeSymbolsInput>(input) {
@@ -175,7 +174,7 @@ pub async fn file_outline(
};
action_log.update(cx, |action_log, cx| {
action_log.track_buffer(buffer.clone(), cx);
action_log.buffer_read(buffer.clone(), cx);
})?;
// Wait until the buffer has been fully parsed, so that we can read its outline.

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use crate::{code_symbols_tool::file_outline, schema::json_schema_for};
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{AnyWindowHandle, App, Entity, Task};
use gpui::{App, Entity, Task};
use itertools::Itertools;
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -11,7 +11,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{fmt::Write, path::Path};
use ui::IconName;
use util::markdown::MarkdownInlineCode;
use util::markdown::MarkdownString;
/// If the model requests to read a file whose size exceeds this, then
/// the tool will return the file's symbol outline instead of its contents,
@@ -82,7 +82,7 @@ impl Tool for ContentsTool {
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<ContentsToolInput>(input.clone()) {
Ok(input) => {
let path = MarkdownInlineCode(&input.path);
let path = MarkdownString::inline_code(&input.path);
match (input.start, input.end) {
(Some(start), None) => format!("Read {path} (from line {start})"),
@@ -102,7 +102,6 @@ impl Tool for ContentsTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<ContentsToolInput>(input) {
@@ -210,7 +209,7 @@ impl Tool for ContentsTool {
})?;
action_log.update(cx, |log, cx| {
log.track_buffer(buffer, cx);
log.buffer_read(buffer, cx);
})?;
Ok(result)
@@ -222,7 +221,7 @@ impl Tool for ContentsTool {
let result = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
action_log.update(cx, |log, cx| {
log.track_buffer(buffer, cx);
log.buffer_read(buffer, cx);
})?;
Ok(result)

View File

@@ -1,7 +1,6 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::AnyWindowHandle;
use gpui::{App, AppContext, Entity, Task};
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
@@ -10,7 +9,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use ui::IconName;
use util::markdown::MarkdownInlineCode;
use util::markdown::MarkdownString;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CopyPathToolInput {
@@ -63,8 +62,8 @@ impl Tool for CopyPathTool {
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<CopyPathToolInput>(input.clone()) {
Ok(input) => {
let src = MarkdownInlineCode(&input.source_path);
let dest = MarkdownInlineCode(&input.destination_path);
let src = MarkdownString::inline_code(&input.source_path);
let dest = MarkdownString::inline_code(&input.destination_path);
format!("Copy {src} to {dest}")
}
Err(_) => "Copy path".to_string(),
@@ -77,7 +76,6 @@ impl Tool for CopyPathTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<CopyPathToolInput>(input) {

View File

@@ -1,7 +1,6 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::AnyWindowHandle;
use gpui::{App, Entity, Task};
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
@@ -10,7 +9,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use ui::IconName;
use util::markdown::MarkdownInlineCode;
use util::markdown::MarkdownString;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CreateDirectoryToolInput {
@@ -53,7 +52,10 @@ impl Tool for CreateDirectoryTool {
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<CreateDirectoryToolInput>(input.clone()) {
Ok(input) => {
format!("Create directory {}", MarkdownInlineCode(&input.path))
format!(
"Create directory {}",
MarkdownString::inline_code(&input.path)
)
}
Err(_) => "Create directory".to_string(),
}
@@ -65,7 +67,6 @@ impl Tool for CreateDirectoryTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<CreateDirectoryToolInput>(input) {

View File

@@ -1,7 +1,6 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::AnyWindowHandle;
use gpui::{App, Entity, Task};
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
@@ -10,7 +9,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use ui::IconName;
use util::markdown::MarkdownInlineCode;
use util::markdown::MarkdownString;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CreateFileToolInput {
@@ -24,9 +23,6 @@ pub struct CreateFileToolInput {
///
/// You can create a new file by providing a path of "directory1/new_file.txt"
/// </example>
///
/// Make sure to include this field before the `contents` field in the input object
/// so that we can display it immediately.
pub path: String,
/// The text contents of the file to create.
@@ -37,18 +33,8 @@ pub struct CreateFileToolInput {
pub contents: String,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct PartialInput {
#[serde(default)]
path: String,
#[serde(default)]
contents: String,
}
pub struct CreateFileTool;
const DEFAULT_UI_TEXT: &str = "Create file";
impl Tool for CreateFileTool {
fn name(&self) -> String {
"create_file".into()
@@ -73,17 +59,10 @@ impl Tool for CreateFileTool {
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<CreateFileToolInput>(input.clone()) {
Ok(input) => {
let path = MarkdownInlineCode(&input.path);
let path = MarkdownString::inline_code(&input.path);
format!("Create file {path}")
}
Err(_) => DEFAULT_UI_TEXT.to_string(),
}
}
fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<PartialInput>(input.clone()).ok() {
Some(input) if !input.path.is_empty() => input.path,
_ => DEFAULT_UI_TEXT.to_string(),
Err(_) => "Create file".to_string(),
}
}
@@ -93,7 +72,6 @@ impl Tool for CreateFileTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<CreateFileToolInput>(input) {
@@ -117,12 +95,9 @@ impl Tool for CreateFileTool {
.await
.map_err(|err| anyhow!("Unable to open buffer for {destination_path}: {err}"))?;
cx.update(|cx| {
action_log.update(cx, |action_log, cx| {
action_log.track_buffer(buffer.clone(), cx)
});
buffer.update(cx, |buffer, cx| buffer.set_text(contents, cx));
action_log.update(cx, |action_log, cx| {
action_log.buffer_edited(buffer.clone(), cx)
action_log.will_create_buffer(buffer.clone(), cx)
});
})?;
@@ -136,60 +111,3 @@ impl Tool for CreateFileTool {
.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn still_streaming_ui_text_with_path() {
let tool = CreateFileTool;
let input = json!({
"path": "src/main.rs",
"contents": "fn main() {\n println!(\"Hello, world!\");\n}"
});
assert_eq!(tool.still_streaming_ui_text(&input), "src/main.rs");
}
#[test]
fn still_streaming_ui_text_without_path() {
let tool = CreateFileTool;
let input = json!({
"path": "",
"contents": "fn main() {\n println!(\"Hello, world!\");\n}"
});
assert_eq!(tool.still_streaming_ui_text(&input), DEFAULT_UI_TEXT);
}
#[test]
fn still_streaming_ui_text_with_null() {
let tool = CreateFileTool;
let input = serde_json::Value::Null;
assert_eq!(tool.still_streaming_ui_text(&input), DEFAULT_UI_TEXT);
}
#[test]
fn ui_text_with_valid_input() {
let tool = CreateFileTool;
let input = json!({
"path": "src/main.rs",
"contents": "fn main() {\n println!(\"Hello, world!\");\n}"
});
assert_eq!(tool.ui_text(&input), "Create file `src/main.rs`");
}
#[test]
fn ui_text_with_invalid_input() {
let tool = CreateFileTool;
let input = json!({
"invalid": "field"
});
assert_eq!(tool.ui_text(&input), DEFAULT_UI_TEXT);
}
}

View File

@@ -2,7 +2,7 @@ use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use futures::{SinkExt, StreamExt, channel::mpsc};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use gpui::{App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::{Project, ProjectPath};
use schemars::JsonSchema;
@@ -62,7 +62,6 @@ impl Tool for DeletePathTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let path_str = match serde_json::from_value::<DeletePathToolInput>(input) {

View File

@@ -1,7 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{AnyWindowHandle, App, Entity, Task};
use gpui::{App, Entity, Task};
use language::{DiagnosticSeverity, OffsetRangeExt};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -9,7 +9,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{fmt::Write, path::Path, sync::Arc};
use ui::IconName;
use util::markdown::MarkdownInlineCode;
use util::markdown::MarkdownString;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct DiagnosticsToolInput {
@@ -66,11 +66,11 @@ impl Tool for DiagnosticsTool {
if let Some(path) = serde_json::from_value::<DiagnosticsToolInput>(input.clone())
.ok()
.and_then(|input| match input.path {
Some(path) if !path.is_empty() => Some(path),
Some(path) if !path.is_empty() => Some(MarkdownString::inline_code(&path)),
_ => None,
})
{
format!("Check diagnostics for {}", MarkdownInlineCode(&path))
format!("Check diagnostics for {path}")
} else {
"Check project diagnostics".to_string()
}
@@ -82,7 +82,6 @@ impl Tool for DiagnosticsTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
match serde_json::from_value::<DiagnosticsToolInput>(input)

View File

@@ -1,43 +1,18 @@
use crate::{
replace::{replace_exact, replace_with_flexible_indent},
schema::json_schema_for,
};
use crate::{replace::replace_with_flexible_indent, schema::json_schema_for};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolUseStatus};
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use editor::{Editor, EditorMode, MultiBuffer, PathKey};
use gpui::{
AnyWindowHandle, App, AppContext, AsyncApp, Context, Entity, EntityId, Task, WeakEntity,
};
use language::{
Anchor, Buffer, Capability, LanguageRegistry, LineEnding, OffsetRangeExt, Rope, TextBuffer,
language_settings::SoftWrap,
};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, AppContext, AsyncApp, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use ui::{Disclosure, Tooltip, Window, prelude::*};
use util::ResultExt;
use workspace::Workspace;
use std::{path::PathBuf, sync::Arc};
use ui::IconName;
pub struct EditFileTool;
use crate::replace::replace_exact;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct EditFileToolInput {
/// A user-friendly markdown description of what's being replaced. This will be shown in the UI.
///
/// <example>Fix API endpoint URLs</example>
/// <example>Update copyright year in `page_footer`</example>
///
/// Make sure to include this field before all the others in the input object
/// so that we can display it immediately.
pub display_description: String,
/// The full path of the file to modify in the project.
///
/// WARNING: When specifying which file path need changing, you MUST
@@ -59,6 +34,12 @@ pub struct EditFileToolInput {
/// </example>
pub path: PathBuf,
/// A user-friendly markdown description of what's being replaced. This will be shown in the UI.
///
/// <example>Fix API endpoint URLs</example>
/// <example>Update copyright year in `page_footer`</example>
pub display_description: String,
/// The text to replace.
pub old_string: String,
@@ -66,19 +47,7 @@ pub struct EditFileToolInput {
pub new_string: String,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct PartialInput {
#[serde(default)]
path: String,
#[serde(default)]
display_description: String,
#[serde(default)]
old_string: String,
#[serde(default)]
new_string: String,
}
const DEFAULT_UI_TEXT: &str = "Editing file";
pub struct EditFileTool;
impl Tool for EditFileTool {
fn name(&self) -> String {
@@ -104,33 +73,16 @@ impl Tool for EditFileTool {
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<EditFileToolInput>(input.clone()) {
Ok(input) => input.display_description,
Err(_) => "Editing file".to_string(),
Err(_) => "Edit file".to_string(),
}
}
fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String {
if let Some(input) = serde_json::from_value::<PartialInput>(input.clone()).ok() {
let description = input.display_description.trim();
if !description.is_empty() {
return description.to_string();
}
let path = input.path.trim();
if !path.is_empty() {
return path.to_string();
}
}
DEFAULT_UI_TEXT.to_string()
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<EditFileToolInput>(input) {
@@ -138,18 +90,7 @@ impl Tool for EditFileTool {
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let card = window.and_then(|window| {
window
.update(cx, |_, window, cx| {
cx.new(|cx| {
EditFileToolCard::new(input.path.clone(), project.clone(), window, cx)
})
})
.ok()
});
let card_clone = card.clone();
let task = cx.spawn(async move |cx: &mut AsyncApp| {
cx.spawn(async move |cx: &mut AsyncApp| {
let project_path = project.read_with(cx, |project, cx| {
project
.find_project_path(&input.path, cx)
@@ -157,38 +98,26 @@ impl Tool for EditFileTool {
})??;
let buffer = project
.update(cx, |project, cx| {
project.open_buffer(project_path.clone(), cx)
})?
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
.await?;
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
if input.old_string.is_empty() {
return Err(anyhow!(
"`old_string` can't be empty, use another tool if you want to create a file."
));
return Err(anyhow!("`old_string` cannot be empty. Use a different tool if you want to create a file."));
}
if input.old_string == input.new_string {
return Err(anyhow!(
"The `old_string` and `new_string` are identical, so no changes would be made."
));
return Err(anyhow!("The `old_string` and `new_string` are identical, so no changes would be made."));
}
let result = cx
.background_spawn(async move {
// Try to match exactly
let diff = replace_exact(&input.old_string, &input.new_string, &snapshot)
.await
// If that fails, try being flexible about indentation
.or_else(|| {
replace_with_flexible_indent(
&input.old_string,
&input.new_string,
&snapshot,
)
})?;
.await
// If that fails, try being flexible about indentation
.or_else(|| replace_with_flexible_indent(&input.old_string, &input.new_string, &snapshot))?;
if diff.edits.is_empty() {
return None;
@@ -218,545 +147,37 @@ impl Tool for EditFileTool {
}
})?;
return Err(err);
return Err(err)
};
let snapshot = cx.update(|cx| {
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
action_log.update(cx, |log, cx| {
log.buffer_read(buffer.clone(), cx)
});
let snapshot = buffer.update(cx, |buffer, cx| {
buffer.finalize_last_transaction();
buffer.apply_diff(diff, cx);
buffer.finalize_last_transaction();
buffer.snapshot()
});
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
action_log.update(cx, |log, cx| {
log.buffer_edited(buffer.clone(), cx)
});
snapshot
})?;
project
.update(cx, |project, cx| project.save_buffer(buffer, cx))?
.await?;
project.update( cx, |project, cx| {
project.save_buffer(buffer, cx)
})?.await?;
let new_text = snapshot.text();
let diff_str = cx
.background_spawn({
let old_text = old_text.clone();
let new_text = new_text.clone();
async move { language::unified_diff(&old_text, &new_text) }
})
.await;
let diff_str = cx.background_spawn(async move {
let new_text = snapshot.text();
language::unified_diff(&old_text, &new_text)
}).await;
if let Some(card) = card_clone {
card.update(cx, |card, cx| {
card.set_diff(project_path.path.clone(), old_text, new_text, cx);
})
.log_err();
}
Ok(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))
ToolResult {
output: task,
card: card.map(AnyToolCard::from),
}
}
}
pub struct EditFileToolCard {
path: PathBuf,
editor: Entity<Editor>,
multibuffer: Entity<MultiBuffer>,
project: Entity<Project>,
diff_task: Option<Task<Result<()>>>,
preview_expanded: bool,
error_expanded: bool,
full_height_expanded: bool,
total_lines: Option<u32>,
editor_unique_id: EntityId,
}
impl EditFileToolCard {
fn new(path: PathBuf, project: Entity<Project>, window: &mut Window, cx: &mut App) -> Self {
let multibuffer = cx.new(|_| MultiBuffer::without_headers(Capability::ReadOnly));
let editor = cx.new(|cx| {
let mut editor = Editor::new(
EditorMode::Full {
scale_ui_elements_with_buffer_font_size: false,
show_active_line_background: false,
sized_by_content: true,
},
multibuffer.clone(),
Some(project.clone()),
window,
cx,
);
editor.set_show_gutter(false, cx);
editor.disable_inline_diagnostics();
editor.disable_expand_excerpt_buttons(cx);
editor.set_soft_wrap_mode(SoftWrap::None, cx);
editor.scroll_manager.set_forbid_vertical_scroll(true);
editor.set_show_scrollbars(false, cx);
editor.set_read_only(true);
editor.set_show_breakpoints(false, cx);
editor.set_show_code_actions(false, cx);
editor.set_show_git_diff_gutter(false, cx);
editor.set_expand_all_diff_hunks(cx);
editor
});
Self {
editor_unique_id: editor.entity_id(),
path,
project,
editor,
multibuffer,
diff_task: None,
preview_expanded: true,
error_expanded: false,
full_height_expanded: false,
total_lines: None,
}
}
fn set_diff(
&mut self,
path: Arc<Path>,
old_text: String,
new_text: String,
cx: &mut Context<Self>,
) {
let language_registry = self.project.read(cx).languages().clone();
self.diff_task = Some(cx.spawn(async move |this, cx| {
let buffer = build_buffer(new_text, path.clone(), &language_registry, cx).await?;
let buffer_diff = build_buffer_diff(old_text, &buffer, &language_registry, cx).await?;
this.update(cx, |this, cx| {
this.total_lines = this.multibuffer.update(cx, |multibuffer, cx| {
let snapshot = buffer.read(cx).snapshot();
let diff = buffer_diff.read(cx);
let diff_hunk_ranges = diff
.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx)
.map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
.collect::<Vec<_>>();
let (_, is_newly_added) = multibuffer.set_excerpts_for_path(
PathKey::for_buffer(&buffer, cx),
buffer,
diff_hunk_ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
cx,
);
debug_assert!(is_newly_added);
multibuffer.add_diff(buffer_diff, cx);
let end = multibuffer.len(cx);
Some(multibuffer.snapshot(cx).offset_to_point(end).row + 1)
});
cx.notify();
})
}));
}
}
impl ToolCard for EditFileToolCard {
fn render(
&mut self,
status: &ToolUseStatus,
window: &mut Window,
workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> impl IntoElement {
let (failed, error_message) = match status {
ToolUseStatus::Error(err) => (true, Some(err.to_string())),
_ => (false, None),
};
let path_label_button = h_flex()
.id(("edit-tool-path-label-button", self.editor_unique_id))
.w_full()
.max_w_full()
.px_1()
.gap_0p5()
.cursor_pointer()
.rounded_sm()
.opacity(0.8)
.hover(|label| {
label
.opacity(1.)
.bg(cx.theme().colors().element_hover.opacity(0.5))
})
.tooltip(Tooltip::text("Jump to File"))
.child(
h_flex()
.child(
Icon::new(IconName::Pencil)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(
div()
.text_size(rems(0.8125))
.child(self.path.display().to_string())
.ml_1p5()
.mr_0p5(),
)
.child(
Icon::new(IconName::ArrowUpRight)
.size(IconSize::XSmall)
.color(Color::Ignored),
),
)
.on_click({
let path = self.path.clone();
let workspace = workspace.clone();
move |_, window, cx| {
workspace
.update(cx, {
|workspace, cx| {
let Some(project_path) =
workspace.project().read(cx).find_project_path(&path, cx)
else {
return;
};
let open_task =
workspace.open_path(project_path, None, true, window, cx);
window
.spawn(cx, async move |cx| {
let item = open_task.await?;
if let Some(active_editor) = item.downcast::<Editor>() {
active_editor
.update_in(cx, |editor, window, cx| {
editor.go_to_singleton_buffer_point(
language::Point::new(0, 0),
window,
cx,
);
})
.log_err();
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
})
.ok();
}
})
.into_any_element();
let codeblock_header_bg = cx
.theme()
.colors()
.element_background
.blend(cx.theme().colors().editor_foreground.opacity(0.025));
let codeblock_header = h_flex()
.flex_none()
.p_1()
.gap_1()
.justify_between()
.rounded_t_md()
.when(!failed, |header| header.bg(codeblock_header_bg))
.child(path_label_button)
.map(|container| {
if failed {
container.child(
h_flex()
.gap_1()
.child(
Icon::new(IconName::Close)
.size(IconSize::Small)
.color(Color::Error),
)
.child(
Disclosure::new(
("edit-file-error-disclosure", self.editor_unique_id),
self.error_expanded,
)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.on_click(cx.listener(
move |this, _event, _window, _cx| {
this.error_expanded = !this.error_expanded;
},
)),
),
)
} else {
container.child(
Disclosure::new(
("edit-file-disclosure", self.editor_unique_id),
self.preview_expanded,
)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.on_click(cx.listener(
move |this, _event, _window, _cx| {
this.preview_expanded = !this.preview_expanded;
},
)),
)
}
});
let (editor, editor_line_height) = self.editor.update(cx, |editor, cx| {
let line_height = editor
.style()
.map(|style| style.text.line_height_in_pixels(window.rem_size()))
.unwrap_or_default();
let element = editor.render(window, cx);
(element.into_any_element(), line_height)
});
let (full_height_icon, full_height_tooltip_label) = if self.full_height_expanded {
(IconName::ChevronUp, "Collapse Code Block")
} else {
(IconName::ChevronDown, "Expand Code Block")
};
let gradient_overlay = div()
.absolute()
.bottom_0()
.left_0()
.w_full()
.h_2_5()
.rounded_b_lg()
.bg(gpui::linear_gradient(
0.,
gpui::linear_color_stop(cx.theme().colors().editor_background, 0.),
gpui::linear_color_stop(cx.theme().colors().editor_background.opacity(0.), 1.),
));
let border_color = cx.theme().colors().border.opacity(0.6);
const DEFAULT_COLLAPSED_LINES: u32 = 10;
let is_collapsible = self.total_lines.unwrap_or(0) > DEFAULT_COLLAPSED_LINES;
v_flex()
.mb_2()
.border_1()
.when(failed, |card| card.border_dashed())
.border_color(border_color)
.rounded_lg()
.overflow_hidden()
.child(codeblock_header)
.when(failed && self.error_expanded, |card| {
card.child(
v_flex()
.p_2()
.gap_1()
.border_t_1()
.border_dashed()
.border_color(border_color)
.bg(cx.theme().colors().editor_background)
.rounded_b_md()
.child(
Label::new("Error")
.size(LabelSize::XSmall)
.color(Color::Error),
)
.child(
div()
.rounded_md()
.text_ui_sm(cx)
.bg(cx.theme().colors().editor_background)
.children(
error_message
.map(|error| div().child(error).into_any_element()),
),
),
)
})
.when(!failed && self.preview_expanded, |card| {
card.child(
v_flex()
.relative()
.map(|editor_container| {
if self.full_height_expanded {
editor_container.h_full()
} else {
editor_container
.h(DEFAULT_COLLAPSED_LINES as f32 * editor_line_height)
}
})
.overflow_hidden()
.border_t_1()
.border_color(border_color)
.bg(cx.theme().colors().editor_background)
.child(div().pl_1().child(editor))
.when(
!self.full_height_expanded && is_collapsible,
|editor_container| editor_container.child(gradient_overlay),
),
)
.when(is_collapsible, |editor_container| {
editor_container.child(
h_flex()
.id(("expand-button", self.editor_unique_id))
.flex_none()
.cursor_pointer()
.h_5()
.justify_center()
.rounded_b_md()
.border_t_1()
.border_color(border_color)
.bg(cx.theme().colors().editor_background)
.hover(|style| style.bg(cx.theme().colors().element_hover.opacity(0.1)))
.child(
Icon::new(full_height_icon)
.size(IconSize::Small)
.color(Color::Muted),
)
.tooltip(Tooltip::text(full_height_tooltip_label))
.on_click(cx.listener(move |this, _event, _window, _cx| {
this.full_height_expanded = !this.full_height_expanded;
})),
)
})
})
}
}
async fn build_buffer(
mut text: String,
path: Arc<Path>,
language_registry: &Arc<language::LanguageRegistry>,
cx: &mut AsyncApp,
) -> Result<Entity<Buffer>> {
let line_ending = LineEnding::detect(&text);
LineEnding::normalize(&mut text);
let text = Rope::from(text);
let language = cx
.update(|_cx| language_registry.language_for_file_path(&path))?
.await
.ok();
let buffer = cx.new(|cx| {
let buffer = TextBuffer::new_normalized(
0,
cx.entity_id().as_non_zero_u64().into(),
line_ending,
text,
);
let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite);
buffer.set_language(language, cx);
buffer
})?;
Ok(buffer)
}
async fn build_buffer_diff(
mut old_text: String,
buffer: &Entity<Buffer>,
language_registry: &Arc<LanguageRegistry>,
cx: &mut AsyncApp,
) -> Result<Entity<BufferDiff>> {
LineEnding::normalize(&mut old_text);
let buffer = cx.update(|cx| buffer.read(cx).snapshot())?;
let base_buffer = cx
.update(|cx| {
Buffer::build_snapshot(
old_text.clone().into(),
buffer.language().cloned(),
Some(language_registry.clone()),
cx,
)
})?
.await;
let diff_snapshot = cx
.update(|cx| {
BufferDiffSnapshot::new_with_base_buffer(
buffer.text.clone(),
Some(old_text.into()),
base_buffer,
cx,
)
})?
.await;
cx.new(|cx| {
let mut diff = BufferDiff::new(&buffer.text, cx);
diff.set_snapshot(diff_snapshot, &buffer.text, cx);
diff
})
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn still_streaming_ui_text_with_path() {
let input = json!({
"path": "src/main.rs",
"display_description": "",
"old_string": "old code",
"new_string": "new code"
});
assert_eq!(EditFileTool.still_streaming_ui_text(&input), "src/main.rs");
}
#[test]
fn still_streaming_ui_text_with_description() {
let input = json!({
"path": "",
"display_description": "Fix error handling",
"old_string": "old code",
"new_string": "new code"
});
assert_eq!(
EditFileTool.still_streaming_ui_text(&input),
"Fix error handling",
);
}
#[test]
fn still_streaming_ui_text_with_path_and_description() {
let input = json!({
"path": "src/main.rs",
"display_description": "Fix error handling",
"old_string": "old code",
"new_string": "new code"
});
assert_eq!(
EditFileTool.still_streaming_ui_text(&input),
"Fix error handling",
);
}
#[test]
fn still_streaming_ui_text_no_path_or_description() {
let input = json!({
"path": "",
"display_description": "",
"old_string": "old code",
"new_string": "new code"
});
assert_eq!(
EditFileTool.still_streaming_ui_text(&input),
DEFAULT_UI_TEXT,
);
}
#[test]
fn still_streaming_ui_text_with_null() {
let input = serde_json::Value::Null;
assert_eq!(
EditFileTool.still_streaming_ui_text(&input),
DEFAULT_UI_TEXT,
);
}).into()
}
}

View File

@@ -6,7 +6,7 @@ use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow, bail};
use assistant_tool::{ActionLog, Tool, ToolResult};
use futures::AsyncReadExt as _;
use gpui::{AnyWindowHandle, App, AppContext as _, Entity, Task};
use gpui::{App, AppContext as _, Entity, Task};
use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
use http_client::{AsyncBody, HttpClientWithUrl};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -14,7 +14,7 @@ use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use ui::IconName;
use util::markdown::MarkdownEscaped;
use util::markdown::MarkdownString;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
enum ContentType {
@@ -134,7 +134,7 @@ impl Tool for FetchTool {
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<FetchToolInput>(input.clone()) {
Ok(input) => format!("Fetch {}", MarkdownEscaped(&input.url)),
Ok(input) => format!("Fetch {}", MarkdownString::escape(&input.url)),
Err(_) => "Fetch URL".to_string(),
}
}
@@ -145,7 +145,6 @@ impl Tool for FetchTool {
_messages: &[LanguageModelRequestMessage],
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<FetchToolInput>(input) {

View File

@@ -1,421 +0,0 @@
use crate::{schema::json_schema_for, ui::ToolCallCardHeader};
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
use editor::Editor;
use futures::channel::oneshot::{self, Receiver};
use gpui::{
AnyWindowHandle, App, AppContext, Context, Entity, IntoElement, Task, WeakEntity, Window,
};
use language;
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt::Write;
use std::{cmp, path::PathBuf, sync::Arc};
use ui::{Disclosure, Tooltip, prelude::*};
use util::{ResultExt, paths::PathMatcher};
use workspace::Workspace;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct FindPathToolInput {
/// The glob to match against every path in the project.
///
/// <example>
/// If the project has the following root directories:
///
/// - directory1/a/something.txt
/// - directory2/a/things.txt
/// - directory3/a/other.txt
///
/// You can get back the first two paths by providing a glob of "*thing*.txt"
/// </example>
pub glob: String,
/// Optional starting position for paginated results (0-based).
/// When not provided, starts from the beginning.
#[serde(default)]
pub offset: usize,
}
const RESULTS_PER_PAGE: usize = 50;
pub struct FindPathTool;
impl Tool for FindPathTool {
fn name(&self) -> String {
"find_path".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
}
fn description(&self) -> String {
include_str!("./find_path_tool/description.md").into()
}
fn icon(&self) -> IconName {
IconName::SearchCode
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<FindPathToolInput>(format)
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<FindPathToolInput>(input.clone()) {
Ok(input) => format!("Find paths matching “`{}`”", input.glob),
Err(_) => "Search paths".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let (offset, glob) = match serde_json::from_value::<FindPathToolInput>(input) {
Ok(input) => (input.offset, input.glob),
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let (sender, receiver) = oneshot::channel();
let card = cx.new(|cx| FindPathToolCard::new(glob.clone(), receiver, cx));
let search_paths_task = search_paths(&glob, project, cx);
let task = cx.background_spawn(async move {
let matches = search_paths_task.await?;
let paginated_matches: &[PathBuf] = &matches[cmp::min(offset, matches.len())
..cmp::min(offset + RESULTS_PER_PAGE, matches.len())];
sender.send(paginated_matches.to_vec()).log_err();
if matches.is_empty() {
Ok("No matches found".to_string())
} else {
let mut message = format!("Found {} total matches.", matches.len());
if matches.len() > RESULTS_PER_PAGE {
write!(
&mut message,
"\nShowing results {}-{} (provide 'offset' parameter for more results):",
offset + 1,
offset + paginated_matches.len()
)
.unwrap();
}
for mat in matches.into_iter().skip(offset).take(RESULTS_PER_PAGE) {
write!(&mut message, "\n{}", mat.display()).unwrap();
}
Ok(message)
}
});
ToolResult {
output: task,
card: Some(card.into()),
}
}
}
fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Result<Vec<PathBuf>>> {
let path_matcher = match PathMatcher::new([
// Sometimes models try to search for "". In this case, return all paths in the project.
if glob.is_empty() { "*" } else { glob },
]) {
Ok(matcher) => matcher,
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))),
};
let snapshots: Vec<_> = project
.read(cx)
.worktrees(cx)
.map(|worktree| worktree.read(cx).snapshot())
.collect();
cx.background_spawn(async move {
Ok(snapshots
.iter()
.flat_map(|snapshot| {
let root_name = PathBuf::from(snapshot.root_name());
snapshot
.entries(false, 0)
.map(move |entry| root_name.join(&entry.path))
.filter(|path| path_matcher.is_match(&path))
})
.collect())
})
}
struct FindPathToolCard {
paths: Vec<PathBuf>,
expanded: bool,
glob: String,
_receiver_task: Option<Task<Result<()>>>,
}
impl FindPathToolCard {
fn new(glob: String, receiver: Receiver<Vec<PathBuf>>, cx: &mut Context<Self>) -> Self {
let _receiver_task = cx.spawn(async move |this, cx| {
let paths = receiver.await?;
this.update(cx, |this, _cx| {
this.paths = paths;
})
.log_err();
Ok(())
});
Self {
paths: Vec::new(),
expanded: false,
glob,
_receiver_task: Some(_receiver_task),
}
}
}
impl ToolCard for FindPathToolCard {
fn render(
&mut self,
_status: &ToolUseStatus,
_window: &mut Window,
workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> impl IntoElement {
let matches_label: SharedString = if self.paths.len() == 0 {
"No matches".into()
} else if self.paths.len() == 1 {
"1 match".into()
} else {
format!("{} matches", self.paths.len()).into()
};
let glob_label = self.glob.to_string();
let content = if !self.paths.is_empty() && self.expanded {
Some(
v_flex()
.relative()
.ml_1p5()
.px_1p5()
.gap_0p5()
.border_l_1()
.border_color(cx.theme().colors().border_variant)
.children(self.paths.iter().enumerate().map(|(index, path)| {
let path_clone = path.clone();
let workspace_clone = workspace.clone();
let button_label = path.to_string_lossy().to_string();
Button::new(("path", index), button_label)
.icon(IconName::ArrowUpRight)
.icon_size(IconSize::XSmall)
.icon_position(IconPosition::End)
.label_size(LabelSize::Small)
.color(Color::Muted)
.tooltip(Tooltip::text("Jump to File"))
.on_click(move |_, window, cx| {
workspace_clone
.update(cx, |workspace, cx| {
let path = PathBuf::from(&path_clone);
let Some(project_path) = workspace
.project()
.read(cx)
.find_project_path(&path, cx)
else {
return;
};
let open_task = workspace.open_path(
project_path,
None,
true,
window,
cx,
);
window
.spawn(cx, async move |cx| {
let item = open_task.await?;
if let Some(active_editor) =
item.downcast::<Editor>()
{
active_editor
.update_in(cx, |editor, window, cx| {
editor.go_to_singleton_buffer_point(
language::Point::new(0, 0),
window,
cx,
);
})
.log_err();
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
})
.ok();
})
}))
.into_any(),
)
} else {
None
};
v_flex()
.mb_2()
.gap_1()
.child(
ToolCallCardHeader::new(IconName::SearchCode, matches_label)
.with_code_path(glob_label)
.disclosure_slot(
Disclosure::new("path-search-disclosure", self.expanded)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.disabled(self.paths.is_empty())
.on_click(cx.listener(move |this, _, _, _cx| {
this.expanded = !this.expanded;
})),
),
)
.children(content)
}
}
impl Component for FindPathTool {
fn scope() -> ComponentScope {
ComponentScope::Agent
}
fn sort_name() -> &'static str {
"FindPathTool"
}
fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let successful_card = cx.new(|_| FindPathToolCard {
paths: vec![
PathBuf::from("src/main.rs"),
PathBuf::from("src/lib.rs"),
PathBuf::from("tests/test.rs"),
],
expanded: true,
glob: "*.rs".to_string(),
_receiver_task: None,
});
let empty_card = cx.new(|_| FindPathToolCard {
paths: Vec::new(),
expanded: false,
glob: "*.nonexistent".to_string(),
_receiver_task: None,
});
Some(
v_flex()
.gap_6()
.children(vec![example_group(vec![
single_example(
"With Paths",
div()
.size_full()
.child(successful_card.update(cx, |tool, cx| {
tool.render(
&ToolUseStatus::Finished("".into()),
window,
WeakEntity::new_invalid(),
cx,
)
.into_any_element()
}))
.into_any_element(),
),
single_example(
"No Paths",
div()
.size_full()
.child(empty_card.update(cx, |tool, cx| {
tool.render(
&ToolUseStatus::Finished("".into()),
window,
WeakEntity::new_invalid(),
cx,
)
.into_any_element()
}))
.into_any_element(),
),
])])
.into_any_element(),
)
}
}
#[cfg(test)]
mod test {
use super::*;
use gpui::TestAppContext;
use project::{FakeFs, Project};
use settings::SettingsStore;
use util::path;
#[gpui::test]
async fn test_find_path_tool(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
serde_json::json!({
"apple": {
"banana": {
"carrot": "1",
},
"bandana": {
"carbonara": "2",
},
"endive": "3"
}
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let matches = cx
.update(|cx| search_paths("root/**/car*", project.clone(), cx))
.await
.unwrap();
assert_eq!(
matches,
&[
PathBuf::from("root/apple/banana/carrot"),
PathBuf::from("root/apple/bandana/carbonara")
]
);
let matches = cx
.update(|cx| search_paths("**/car*", project.clone(), cx))
.await
.unwrap();
assert_eq!(
matches,
&[
PathBuf::from("root/apple/banana/carrot"),
PathBuf::from("root/apple/bandana/carbonara")
]
);
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
});
}
}

View File

@@ -0,0 +1,268 @@
use crate::{replace::replace_with_flexible_indent, schema::json_schema_for};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, AppContext, AsyncApp, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{path::PathBuf, sync::Arc};
use ui::IconName;
use crate::replace::replace_exact;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct FindReplaceFileToolInput {
/// The path of the file to modify.
///
/// WARNING: When specifying which file path need changing, you MUST
/// start each path with one of the project's root directories.
///
/// The following examples assume we have two root directories in the project:
/// - backend
/// - frontend
///
/// <example>
/// `backend/src/main.rs`
///
/// Notice how the file path starts with root-1. Without that, the path
/// would be ambiguous and the call would fail!
/// </example>
///
/// <example>
/// `frontend/db.js`
/// </example>
pub path: PathBuf,
/// A user-friendly markdown description of what's being replaced. This will be shown in the UI.
///
/// <example>Fix API endpoint URLs</example>
/// <example>Update copyright year in `page_footer`</example>
pub display_description: String,
/// The unique string to find in the file. This string cannot be empty;
/// if the string is empty, the tool call will fail. Remember, do not use this tool
/// to create new files from scratch, or to overwrite existing files! Use a different
/// approach if you want to do that.
///
/// If this string appears more than once in the file, this tool call will fail,
/// so it is absolutely critical that you verify ahead of time that the string
/// is unique. You can search within the file to verify this.
///
/// To make the string more likely to be unique, include a minimum of 3 lines of context
/// before the string you actually want to find, as well as a minimum of 3 lines of
/// context after the string you want to find. (These lines of context should appear
/// in the `replace` string as well.) If 3 lines of context is 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 3 lines before and 3 lines 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. This string must be exactly as
/// it appears in the file, because this tool will do a literal find/replace, and if
/// 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:
///
/// ```ignore
/// fn check_user_permissions(user_id: &str) -> Result<bool> {
/// // Check if user exists first
/// let user = database.find_user(user_id)?;
///
/// // This is the part we want to modify
/// if user.role == "admin" {
/// return Ok(true);
/// }
///
/// // Check other permissions
/// check_custom_permissions(user_id)
/// }
/// ```
///
/// Your find string should include at least 3 lines of context before and after the part
/// you want to change:
///
/// ```ignore
/// fn check_user_permissions(user_id: &str) -> Result<bool> {
/// // Check if user exists first
/// let user = database.find_user(user_id)?;
///
/// // This is the part we want to modify
/// if user.role == "admin" {
/// return Ok(true);
/// }
///
/// // Check other permissions
/// check_custom_permissions(user_id)
/// }
/// ```
///
/// And your replace string might look like:
///
/// ```ignore
/// fn check_user_permissions(user_id: &str) -> Result<bool> {
/// // Check if user exists first
/// let user = database.find_user(user_id)?;
///
/// // This is the part we want to modify
/// if user.role == "admin" || user.role == "superuser" {
/// return Ok(true);
/// }
///
/// // Check other permissions
/// check_custom_permissions(user_id)
/// }
/// ```
/// </example>
pub find: String,
/// The string to replace the one unique occurrence of the find string with.
pub replace: String,
}
pub struct FindReplaceFileTool;
impl Tool for FindReplaceFileTool {
fn name(&self) -> String {
"find_replace_file".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
}
fn description(&self) -> String {
include_str!("find_replace_tool/description.md").to_string()
}
fn icon(&self) -> IconName {
IconName::Pencil
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<FindReplaceFileToolInput>(format)
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<FindReplaceFileToolInput>(input.clone()) {
Ok(input) => input.display_description,
Err(_) => "Edit file".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<FindReplaceFileToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
cx.spawn(async move |cx: &mut AsyncApp| {
let project_path = project.read_with(cx, |project, cx| {
project
.find_project_path(&input.path, cx)
.context("Path not found in project")
})??;
let buffer = project
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
.await?;
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
if input.find.is_empty() {
return Err(anyhow!("`find` string cannot be empty. Use a different tool if you want to create a file."));
}
if input.find == input.replace {
return Err(anyhow!("The `find` and `replace` strings are identical, so no changes would be made."));
}
let result = cx
.background_spawn(async move {
// Try to match exactly
let diff = replace_exact(&input.find, &input.replace, &snapshot)
.await
// If that fails, try being flexible about indentation
.or_else(|| replace_with_flexible_indent(&input.find, &input.replace, &snapshot))?;
if diff.edits.is_empty() {
return None;
}
let old_text = snapshot.text();
Some((old_text, diff))
})
.await;
let Some((old_text, diff)) = result else {
let err = buffer.read_with(cx, |buffer, _cx| {
let file_exists = buffer
.file()
.map_or(false, |file| file.disk_state().exists());
if !file_exists {
anyhow!("{} does not exist", input.path.display())
} else if buffer.is_empty() {
anyhow!(
"{} is empty, so the provided `find` string wasn't found.",
input.path.display()
)
} else {
anyhow!("Failed to match the provided `find` string")
}
})?;
return Err(err)
};
let snapshot = cx.update(|cx| {
action_log.update(cx, |log, cx| {
log.buffer_read(buffer.clone(), cx)
});
let snapshot = buffer.update(cx, |buffer, cx| {
buffer.finalize_last_transaction();
buffer.apply_diff(diff, cx);
buffer.finalize_last_transaction();
buffer.snapshot()
});
action_log.update(cx, |log, cx| {
log.buffer_edited(buffer.clone(), cx)
});
snapshot
})?;
project.update( cx, |project, cx| {
project.save_buffer(buffer, cx)
})?.await?;
let diff_str = cx.background_spawn(async move {
let new_text = snapshot.text();
language::unified_diff(&old_text, &new_text)
}).await;
Ok(format!("Edited {}:\n\n```diff\n{}\n```", input.path.display(), diff_str))
}).into()
}
}

View File

@@ -1,784 +0,0 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use futures::StreamExt;
use gpui::{AnyWindowHandle, App, Entity, Task};
use language::{OffsetRangeExt, ParseStatus, Point};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::{
Project,
search::{SearchQuery, SearchResult},
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{cmp, fmt::Write, sync::Arc};
use ui::IconName;
use util::RangeExt;
use util::markdown::MarkdownInlineCode;
use util::paths::PathMatcher;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct GrepToolInput {
/// A regex pattern to search for in the entire project. Note that the regex
/// will be parsed by the Rust `regex` crate.
///
/// Do NOT specify a path here! This will only be matched against the code **content**.
pub regex: String,
/// A glob pattern for the paths of files to include in the search.
/// Supports standard glob patterns like "**/*.rs" or "src/**/*.ts".
/// If omitted, all files in the project will be searched.
pub include_pattern: Option<String>,
/// Optional starting position for paginated results (0-based).
/// When not provided, starts from the beginning.
#[serde(default)]
pub offset: u32,
/// Whether the regex is case-sensitive. Defaults to false (case-insensitive).
#[serde(default)]
pub case_sensitive: bool,
}
impl GrepToolInput {
/// Which page of search results this is.
pub fn page(&self) -> u32 {
1 + (self.offset / RESULTS_PER_PAGE)
}
}
const RESULTS_PER_PAGE: u32 = 20;
pub struct GrepTool;
impl Tool for GrepTool {
fn name(&self) -> String {
"grep".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
}
fn description(&self) -> String {
include_str!("./grep_tool/description.md").into()
}
fn icon(&self) -> IconName {
IconName::Regex
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<GrepToolInput>(format)
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<GrepToolInput>(input.clone()) {
Ok(input) => {
let page = input.page();
let regex_str = MarkdownInlineCode(&input.regex);
let case_info = if input.case_sensitive {
" (case-sensitive)"
} else {
""
};
if page > 1 {
format!("Get page {page} of search results for regex {regex_str}{case_info}")
} else {
format!("Search files for regex {regex_str}{case_info}")
}
}
Err(_) => "Search with regex".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
const CONTEXT_LINES: u32 = 2;
const MAX_ANCESTOR_LINES: u32 = 10;
let input = match serde_json::from_value::<GrepToolInput>(input) {
Ok(input) => input,
Err(error) => {
return Task::ready(Err(anyhow!("Failed to parse input: {}", error))).into();
}
};
let include_matcher = match PathMatcher::new(
input
.include_pattern
.as_ref()
.into_iter()
.collect::<Vec<_>>(),
) {
Ok(matcher) => matcher,
Err(error) => {
return Task::ready(Err(anyhow!("invalid include glob pattern: {}", error))).into();
}
};
let query = match SearchQuery::regex(
&input.regex,
false,
input.case_sensitive,
false,
false,
include_matcher,
PathMatcher::default(), // For now, keep it simple and don't enable an exclude pattern.
true, // Always match file include pattern against *full project paths* that start with a project root.
None,
) {
Ok(query) => query,
Err(error) => return Task::ready(Err(error)).into(),
};
let results = project.update(cx, |project, cx| project.search(query, cx));
cx.spawn(async move |cx| {
futures::pin_mut!(results);
let mut output = String::new();
let mut skips_remaining = input.offset;
let mut matches_found = 0;
let mut has_more_matches = false;
'outer: while let Some(SearchResult::Buffer { buffer, ranges }) = results.next().await {
if ranges.is_empty() {
continue;
}
let (Some(path), mut parse_status) = buffer.read_with(cx, |buffer, cx| {
(buffer.file().map(|file| file.full_path(cx)), buffer.parse_status())
})? else {
continue;
};
while *parse_status.borrow() != ParseStatus::Idle {
parse_status.changed().await?;
}
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let mut ranges = ranges
.into_iter()
.map(|range| {
let matched = range.to_point(&snapshot);
let matched_end_line_len = snapshot.line_len(matched.end.row);
let full_lines = Point::new(matched.start.row, 0)..Point::new(matched.end.row, matched_end_line_len);
let symbols = snapshot.symbols_containing(matched.start, None);
if let Some(ancestor_node) = snapshot.syntax_ancestor(full_lines.clone()) {
let full_ancestor_range = ancestor_node.byte_range().to_point(&snapshot);
let end_row = full_ancestor_range.end.row.min(full_ancestor_range.start.row + MAX_ANCESTOR_LINES);
let end_col = snapshot.line_len(end_row);
let capped_ancestor_range = Point::new(full_ancestor_range.start.row, 0)..Point::new(end_row, end_col);
if capped_ancestor_range.contains_inclusive(&full_lines) {
return (capped_ancestor_range, Some(full_ancestor_range), symbols)
}
}
let mut matched = matched;
matched.start.column = 0;
matched.start.row =
matched.start.row.saturating_sub(CONTEXT_LINES);
matched.end.row = cmp::min(
snapshot.max_point().row,
matched.end.row + CONTEXT_LINES,
);
matched.end.column = snapshot.line_len(matched.end.row);
(matched, None, symbols)
})
.peekable();
let mut file_header_written = false;
while let Some((mut range, ancestor_range, parent_symbols)) = ranges.next(){
if skips_remaining > 0 {
skips_remaining -= 1;
continue;
}
// We'd already found a full page of matches, and we just found one more.
if matches_found >= RESULTS_PER_PAGE {
has_more_matches = true;
break 'outer;
}
while let Some((next_range, _, _)) = ranges.peek() {
if range.end.row >= next_range.start.row {
range.end = next_range.end;
ranges.next();
} else {
break;
}
}
if !file_header_written {
writeln!(output, "\n## Matches in {}", path.display())?;
file_header_written = true;
}
let end_row = range.end.row;
output.push_str("\n### ");
if let Some(parent_symbols) = &parent_symbols {
for symbol in parent_symbols {
write!(output, "{} ", symbol.text)?;
}
}
if range.start.row == end_row {
writeln!(output, "L{}", range.start.row + 1)?;
} else {
writeln!(output, "L{}-{}", range.start.row + 1, end_row + 1)?;
}
output.push_str("```\n");
output.extend(snapshot.text_for_range(range));
output.push_str("\n```\n");
if let Some(ancestor_range) = ancestor_range {
if end_row < ancestor_range.end.row {
let remaining_lines = ancestor_range.end.row - end_row;
writeln!(output, "\n{} lines remaining in ancestor node. Read the file to see all.", remaining_lines)?;
}
}
matches_found += 1;
}
}
if matches_found == 0 {
Ok("No matches found".to_string())
} else if has_more_matches {
Ok(format!(
"Showing matches {}-{} (there were more matches found; use offset: {} to see next page):\n{output}",
input.offset + 1,
input.offset + matches_found,
input.offset + RESULTS_PER_PAGE,
))
} else {
Ok(format!("Found {matches_found} matches:\n{output}"))
}
}).into()
}
}
#[cfg(test)]
mod tests {
use super::*;
use assistant_tool::Tool;
use gpui::{AppContext, TestAppContext};
use language::{Language, LanguageConfig, LanguageMatcher};
use project::{FakeFs, Project};
use settings::SettingsStore;
use unindent::Unindent;
use util::path;
#[gpui::test]
async fn test_grep_tool_with_include_pattern(cx: &mut TestAppContext) {
init_test(cx);
cx.executor().allow_parking();
let fs = FakeFs::new(cx.executor().clone());
fs.insert_tree(
"/root",
serde_json::json!({
"src": {
"main.rs": "fn main() {\n println!(\"Hello, world!\");\n}",
"utils": {
"helper.rs": "fn helper() {\n println!(\"I'm a helper!\");\n}",
},
},
"tests": {
"test_main.rs": "fn test_main() {\n assert!(true);\n}",
}
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
// Test with include pattern for Rust files inside the root of the project
let input = serde_json::to_value(GrepToolInput {
regex: "println".to_string(),
include_pattern: Some("root/**/*.rs".to_string()),
offset: 0,
case_sensitive: false,
})
.unwrap();
let result = run_grep_tool(input, project.clone(), cx).await;
assert!(result.contains("main.rs"), "Should find matches in main.rs");
assert!(
result.contains("helper.rs"),
"Should find matches in helper.rs"
);
assert!(
!result.contains("test_main.rs"),
"Should not include test_main.rs even though it's a .rs file (because it doesn't have the pattern)"
);
// Test with include pattern for src directory only
let input = serde_json::to_value(GrepToolInput {
regex: "fn".to_string(),
include_pattern: Some("root/**/src/**".to_string()),
offset: 0,
case_sensitive: false,
})
.unwrap();
let result = run_grep_tool(input, project.clone(), cx).await;
assert!(
result.contains("main.rs"),
"Should find matches in src/main.rs"
);
assert!(
result.contains("helper.rs"),
"Should find matches in src/utils/helper.rs"
);
assert!(
!result.contains("test_main.rs"),
"Should not include test_main.rs as it's not in src directory"
);
// Test with empty include pattern (should default to all files)
let input = serde_json::to_value(GrepToolInput {
regex: "fn".to_string(),
include_pattern: None,
offset: 0,
case_sensitive: false,
})
.unwrap();
let result = run_grep_tool(input, project.clone(), cx).await;
assert!(result.contains("main.rs"), "Should find matches in main.rs");
assert!(
result.contains("helper.rs"),
"Should find matches in helper.rs"
);
assert!(
result.contains("test_main.rs"),
"Should include test_main.rs"
);
}
#[gpui::test]
async fn test_grep_tool_with_case_sensitivity(cx: &mut TestAppContext) {
init_test(cx);
cx.executor().allow_parking();
let fs = FakeFs::new(cx.executor().clone());
fs.insert_tree(
"/root",
serde_json::json!({
"case_test.txt": "This file has UPPERCASE and lowercase text.\nUPPERCASE patterns should match only with case_sensitive: true",
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
// Test case-insensitive search (default)
let input = serde_json::to_value(GrepToolInput {
regex: "uppercase".to_string(),
include_pattern: Some("**/*.txt".to_string()),
offset: 0,
case_sensitive: false,
})
.unwrap();
let result = run_grep_tool(input, project.clone(), cx).await;
assert!(
result.contains("UPPERCASE"),
"Case-insensitive search should match uppercase"
);
// Test case-sensitive search
let input = serde_json::to_value(GrepToolInput {
regex: "uppercase".to_string(),
include_pattern: Some("**/*.txt".to_string()),
offset: 0,
case_sensitive: true,
})
.unwrap();
let result = run_grep_tool(input, project.clone(), cx).await;
assert!(
!result.contains("UPPERCASE"),
"Case-sensitive search should not match uppercase"
);
// Test case-sensitive search
let input = serde_json::to_value(GrepToolInput {
regex: "LOWERCASE".to_string(),
include_pattern: Some("**/*.txt".to_string()),
offset: 0,
case_sensitive: true,
})
.unwrap();
let result = run_grep_tool(input, project.clone(), cx).await;
assert!(
!result.contains("lowercase"),
"Case-sensitive search should match lowercase"
);
// Test case-sensitive search for lowercase pattern
let input = serde_json::to_value(GrepToolInput {
regex: "lowercase".to_string(),
include_pattern: Some("**/*.txt".to_string()),
offset: 0,
case_sensitive: true,
})
.unwrap();
let result = run_grep_tool(input, project.clone(), cx).await;
assert!(
result.contains("lowercase"),
"Case-sensitive search should match lowercase text"
);
}
/// Helper function to set up a syntax test environment
async fn setup_syntax_test(cx: &mut TestAppContext) -> Entity<Project> {
use unindent::Unindent;
init_test(cx);
cx.executor().allow_parking();
let fs = FakeFs::new(cx.executor().clone());
// Create test file with syntax structures
fs.insert_tree(
"/root",
serde_json::json!({
"test_syntax.rs": r#"
fn top_level_function() {
println!("This is at the top level");
}
mod feature_module {
pub mod nested_module {
pub fn nested_function(
first_arg: String,
second_arg: i32,
) {
println!("Function in nested module");
println!("{first_arg}");
println!("{second_arg}");
}
}
}
struct MyStruct {
field1: String,
field2: i32,
}
impl MyStruct {
fn method_with_block() {
let condition = true;
if condition {
println!("Inside if block");
}
}
fn long_function() {
println!("Line 1");
println!("Line 2");
println!("Line 3");
println!("Line 4");
println!("Line 5");
println!("Line 6");
println!("Line 7");
println!("Line 8");
println!("Line 9");
println!("Line 10");
println!("Line 11");
println!("Line 12");
}
}
trait Processor {
fn process(&self, input: &str) -> String;
}
impl Processor for MyStruct {
fn process(&self, input: &str) -> String {
format!("Processed: {}", input)
}
}
"#.unindent().trim(),
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
project.update(cx, |project, _cx| {
project.languages().add(rust_lang().into())
});
project
}
#[gpui::test]
async fn test_grep_top_level_function(cx: &mut TestAppContext) {
let project = setup_syntax_test(cx).await;
// Test: Line at the top level of the file
let input = serde_json::to_value(GrepToolInput {
regex: "This is at the top level".to_string(),
include_pattern: Some("**/*.rs".to_string()),
offset: 0,
case_sensitive: false,
})
.unwrap();
let result = run_grep_tool(input, project.clone(), cx).await;
let expected = r#"
Found 1 matches:
## Matches in root/test_syntax.rs
### fn top_level_function L1-3
```
fn top_level_function() {
println!("This is at the top level");
}
```
"#
.unindent();
assert_eq!(result, expected);
}
#[gpui::test]
async fn test_grep_function_body(cx: &mut TestAppContext) {
let project = setup_syntax_test(cx).await;
// Test: Line inside a function body
let input = serde_json::to_value(GrepToolInput {
regex: "Function in nested module".to_string(),
include_pattern: Some("**/*.rs".to_string()),
offset: 0,
case_sensitive: false,
})
.unwrap();
let result = run_grep_tool(input, project.clone(), cx).await;
let expected = r#"
Found 1 matches:
## Matches in root/test_syntax.rs
### mod feature_module pub mod nested_module pub fn nested_function L10-14
```
) {
println!("Function in nested module");
println!("{first_arg}");
println!("{second_arg}");
}
```
"#
.unindent();
assert_eq!(result, expected);
}
#[gpui::test]
async fn test_grep_function_args_and_body(cx: &mut TestAppContext) {
let project = setup_syntax_test(cx).await;
// Test: Line with a function argument
let input = serde_json::to_value(GrepToolInput {
regex: "second_arg".to_string(),
include_pattern: Some("**/*.rs".to_string()),
offset: 0,
case_sensitive: false,
})
.unwrap();
let result = run_grep_tool(input, project.clone(), cx).await;
let expected = r#"
Found 1 matches:
## Matches in root/test_syntax.rs
### mod feature_module pub mod nested_module pub fn nested_function L7-14
```
pub fn nested_function(
first_arg: String,
second_arg: i32,
) {
println!("Function in nested module");
println!("{first_arg}");
println!("{second_arg}");
}
```
"#
.unindent();
assert_eq!(result, expected);
}
#[gpui::test]
async fn test_grep_if_block(cx: &mut TestAppContext) {
use unindent::Unindent;
let project = setup_syntax_test(cx).await;
// Test: Line inside an if block
let input = serde_json::to_value(GrepToolInput {
regex: "Inside if block".to_string(),
include_pattern: Some("**/*.rs".to_string()),
offset: 0,
case_sensitive: false,
})
.unwrap();
let result = run_grep_tool(input, project.clone(), cx).await;
let expected = r#"
Found 1 matches:
## Matches in root/test_syntax.rs
### impl MyStruct fn method_with_block L26-28
```
if condition {
println!("Inside if block");
}
```
"#
.unindent();
assert_eq!(result, expected);
}
#[gpui::test]
async fn test_grep_long_function_top(cx: &mut TestAppContext) {
use unindent::Unindent;
let project = setup_syntax_test(cx).await;
// Test: Line in the middle of a long function - should show message about remaining lines
let input = serde_json::to_value(GrepToolInput {
regex: "Line 5".to_string(),
include_pattern: Some("**/*.rs".to_string()),
offset: 0,
case_sensitive: false,
})
.unwrap();
let result = run_grep_tool(input, project.clone(), cx).await;
let expected = r#"
Found 1 matches:
## Matches in root/test_syntax.rs
### impl MyStruct fn long_function L31-41
```
fn long_function() {
println!("Line 1");
println!("Line 2");
println!("Line 3");
println!("Line 4");
println!("Line 5");
println!("Line 6");
println!("Line 7");
println!("Line 8");
println!("Line 9");
println!("Line 10");
```
3 lines remaining in ancestor node. Read the file to see all.
"#
.unindent();
assert_eq!(result, expected);
}
#[gpui::test]
async fn test_grep_long_function_bottom(cx: &mut TestAppContext) {
use unindent::Unindent;
let project = setup_syntax_test(cx).await;
// Test: Line in the long function
let input = serde_json::to_value(GrepToolInput {
regex: "Line 12".to_string(),
include_pattern: Some("**/*.rs".to_string()),
offset: 0,
case_sensitive: false,
})
.unwrap();
let result = run_grep_tool(input, project.clone(), cx).await;
let expected = r#"
Found 1 matches:
## Matches in root/test_syntax.rs
### impl MyStruct fn long_function L41-45
```
println!("Line 10");
println!("Line 11");
println!("Line 12");
}
}
```
"#
.unindent();
assert_eq!(result, expected);
}
async fn run_grep_tool(
input: serde_json::Value,
project: Entity<Project>,
cx: &mut TestAppContext,
) -> String {
let tool = Arc::new(GrepTool);
let action_log = cx.new(|_cx| ActionLog::new(project.clone()));
let task = cx.update(|cx| tool.run(input, &[], project, action_log, None, cx));
match task.output.await {
Ok(result) => {
if cfg!(windows) {
result.replace("root\\", "root/")
} else {
result
}
}
Err(e) => panic!("Failed to run grep tool: {}", e),
}
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
});
}
fn rust_lang() -> Language {
Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_rust::LANGUAGE.into()),
)
.with_outline_query(include_str!("../../languages/src/rust/outline.scm"))
.unwrap()
}
}

View File

@@ -1,8 +0,0 @@
Searches the contents of files in the project with a regular expression
- Prefer this tool to path search when searching for symbols in the project, because you won't need to guess what path it's in.
- Supports full regex syntax (eg. "log.*Error", "function\\s+\\w+", etc.)
- Pass an `include_pattern` if you know how to narrow your search on the files system
- Never use this tool to search for paths. Only search file contents with this tool.
- Use this tool when you need to find files containing specific patterns
- Results are paginated with 20 matches per page. Use the optional 'offset' parameter to request subsequent pages.

View File

@@ -1,14 +1,14 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{AnyWindowHandle, App, Entity, Task};
use gpui::{App, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{fmt::Write, path::Path, sync::Arc};
use ui::IconName;
use util::markdown::MarkdownInlineCode;
use util::markdown::MarkdownString;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ListDirectoryToolInput {
@@ -63,7 +63,7 @@ impl Tool for ListDirectoryTool {
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<ListDirectoryToolInput>(input.clone()) {
Ok(input) => {
let path = MarkdownInlineCode(&input.path);
let path = MarkdownString::inline_code(&input.path);
format!("List the {path} directory's contents")
}
Err(_) => "List directory".to_string(),
@@ -76,7 +76,6 @@ impl Tool for ListDirectoryTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<ListDirectoryToolInput>(input) {

View File

@@ -1 +1 @@
Lists files and directories in a given path. Prefer the `grep` or `find_path` tools when searching the codebase.
Lists files and directories in a given path. Prefer the `regex_search` or `path_search` tools when searching the codebase.

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