Compare commits
6 Commits
folding
...
linux/keys
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce9edd7980 | ||
|
|
d2235ae5cd | ||
|
|
aeb2b39e00 | ||
|
|
ed286ed1aa | ||
|
|
aa0f53df5c | ||
|
|
da715d3ab1 |
@@ -1,8 +1,8 @@
|
||||
name: Bug Report (AI Related)
|
||||
name: Bug Report (Agent Panel)
|
||||
description: Zed Agent Panel Bugs
|
||||
type: "Bug"
|
||||
labels: ["ai"]
|
||||
title: "AI: <a short description of the AI Related bug>"
|
||||
labels: ["agent", "ai"]
|
||||
title: "Agent Panel: <a short description of the Agent Panel bug>"
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
@@ -14,6 +14,7 @@ body:
|
||||
|
||||
### Description
|
||||
<!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
|
||||
<!-- Please include the LLM provider and model name you are using -->
|
||||
Steps to trigger the problem:
|
||||
1.
|
||||
2.
|
||||
@@ -21,13 +22,6 @@ body:
|
||||
|
||||
Actual Behavior:
|
||||
Expected Behavior:
|
||||
|
||||
### Model Provider Details
|
||||
- Provider: (Anthropic via ZedPro, Anthropic via API key, Copilot Chat, Mistral, OpenAI, etc)
|
||||
- Model Name:
|
||||
- Mode: (Agent Panel, Inline Assistant, Terminal Assistant or Text Threads)
|
||||
- MCP Servers in-use:
|
||||
- Other Details:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
51
.github/workflows/ci.yml
vendored
51
.github/workflows/ci.yml
vendored
@@ -524,6 +524,7 @@ jobs:
|
||||
APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
|
||||
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
@@ -610,6 +611,7 @@ jobs:
|
||||
needs: [linux_tests]
|
||||
env:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
@@ -667,6 +669,7 @@ jobs:
|
||||
needs: [linux_tests]
|
||||
env:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
@@ -714,13 +717,49 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
nix-build:
|
||||
name: Build with Nix
|
||||
uses: ./.github/workflows/nix.yml
|
||||
timeout-minutes: 60
|
||||
name: Nix Build
|
||||
continue-on-error: true
|
||||
if: github.repository_owner == 'zed-industries' && contains(github.event.pull_request.labels.*.name, 'run-nix')
|
||||
with:
|
||||
flake-output: debug
|
||||
# excludes the final package to only cache dependencies
|
||||
cachix-filter: "-zed-editor-[0-9.]*-nightly"
|
||||
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
|
||||
runs-on: ${{ matrix.system.runner }}
|
||||
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
|
||||
- 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 }}"
|
||||
skipPush: true
|
||||
- run: nix build .#debug
|
||||
- name: Limit /nix/store to 50GB
|
||||
run: "[ $(du -sm /nix/store | cut -f1) -gt 50000 ] && nix-collect-garbage -d"
|
||||
|
||||
auto-release-preview:
|
||||
name: Auto release preview
|
||||
|
||||
66
.github/workflows/nix.yml
vendored
66
.github/workflows/nix.yml
vendored
@@ -1,66 +0,0 @@
|
||||
name: "Nix build"
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
flake-output:
|
||||
type: string
|
||||
default: "default"
|
||||
cachix-filter:
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
jobs:
|
||||
nix-build:
|
||||
timeout-minutes: 60
|
||||
name: (${{ matrix.system.os }}) Nix Build
|
||||
continue-on-error: true # TODO: remove when we want this to start blocking CI
|
||||
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 }}
|
||||
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@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
|
||||
if: ${{ matrix.system.install_nix }}
|
||||
with:
|
||||
github_access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
with:
|
||||
name: zed
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
pushFilter: "${{ inputs.cachix-filter }}"
|
||||
cachixArgs: '-v'
|
||||
|
||||
- run: nix build .#${{ inputs.flake-output }} -L --accept-flake-config
|
||||
|
||||
- name: Limit /nix/store to 50GB on macs
|
||||
if: ${{ ! matrix.system.install_nix }}
|
||||
run: |
|
||||
[ $(du -sm /nix/store | cut -f1) -gt 50000 ] && nix-collect-garbage -d || :
|
||||
8
.github/workflows/release_nightly.yml
vendored
8
.github/workflows/release_nightly.yml
vendored
@@ -68,6 +68,7 @@ jobs:
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
@@ -103,6 +104,7 @@ jobs:
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
@@ -142,6 +144,7 @@ jobs:
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
@@ -167,11 +170,6 @@ jobs:
|
||||
- name: Upload Zed Nightly
|
||||
run: script/upload-nightly linux-targz
|
||||
|
||||
bundle-nix:
|
||||
name: Build and cache Nix package
|
||||
needs: tests
|
||||
uses: ./.github/workflows/nix.yml
|
||||
|
||||
update-nightly-tag:
|
||||
name: Update nightly tag
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
|
||||
6
.rules
6
.rules
@@ -5,12 +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 silently discard errors with `let _ =` on fallible operations. Always handle errors appropriately:
|
||||
- Propagate errors with `?` when the calling function should handle them
|
||||
- Use `.log_err()` or similar when you need to ignore errors but want visibility
|
||||
- Use explicit error handling with `match` or `if let Err(...)` when you need custom logic
|
||||
- Example: avoid `let _ = client.request(...).await?;` - use `client.request(...).await?;` instead
|
||||
* When implementing async operations that may fail, ensure errors propagate to the UI layer so users get meaningful feedback.
|
||||
* Never create files with `mod.rs` paths - prefer `src/some_module.rs` instead of `src/some_module/mod.rs`.
|
||||
|
||||
# GPUI
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
{
|
||||
"label": "Debug Zed (CodeLLDB)",
|
||||
"adapter": "CodeLLDB",
|
||||
"build": { "label": "Build Zed", "command": "cargo", "args": ["build"] }
|
||||
"program": "target/debug/zed",
|
||||
"request": "launch"
|
||||
},
|
||||
{
|
||||
"label": "Debug Zed (GDB)",
|
||||
"adapter": "GDB",
|
||||
"build": { "label": "Build Zed", "command": "cargo", "args": ["build"] }
|
||||
"program": "target/debug/zed",
|
||||
"request": "launch",
|
||||
"initialize_args": {
|
||||
"stopAtBeginningOfMainSubprogram": true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
376
Cargo.lock
generated
376
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
24
Cargo.toml
24
Cargo.toml
@@ -3,11 +3,11 @@ resolver = "2"
|
||||
members = [
|
||||
"crates/activity_indicator",
|
||||
"crates/agent",
|
||||
"crates/agent_settings",
|
||||
"crates/anthropic",
|
||||
"crates/askpass",
|
||||
"crates/assets",
|
||||
"crates/assistant_context_editor",
|
||||
"crates/assistant_settings",
|
||||
"crates/assistant_slash_command",
|
||||
"crates/assistant_slash_commands",
|
||||
"crates/assistant_tool",
|
||||
@@ -73,7 +73,6 @@ members = [
|
||||
"crates/indexed_docs",
|
||||
"crates/inline_completion",
|
||||
"crates/inline_completion_button",
|
||||
"crates/inspector_ui",
|
||||
"crates/install_cli",
|
||||
"crates/jj",
|
||||
"crates/jj_ui",
|
||||
@@ -81,6 +80,7 @@ members = [
|
||||
"crates/language",
|
||||
"crates/language_extension",
|
||||
"crates/language_model",
|
||||
"crates/language_model_selector",
|
||||
"crates/language_models",
|
||||
"crates/language_selector",
|
||||
"crates/language_tools",
|
||||
@@ -211,12 +211,12 @@ edition = "2024"
|
||||
|
||||
activity_indicator = { path = "crates/activity_indicator" }
|
||||
agent = { path = "crates/agent" }
|
||||
agent_settings = { path = "crates/agent_settings" }
|
||||
ai = { path = "crates/ai" }
|
||||
anthropic = { path = "crates/anthropic" }
|
||||
askpass = { path = "crates/askpass" }
|
||||
assets = { path = "crates/assets" }
|
||||
assistant_context_editor = { path = "crates/assistant_context_editor" }
|
||||
assistant_settings = { path = "crates/assistant_settings" }
|
||||
assistant_slash_command = { path = "crates/assistant_slash_command" }
|
||||
assistant_slash_commands = { path = "crates/assistant_slash_commands" }
|
||||
assistant_tool = { path = "crates/assistant_tool" }
|
||||
@@ -280,7 +280,6 @@ image_viewer = { path = "crates/image_viewer" }
|
||||
indexed_docs = { path = "crates/indexed_docs" }
|
||||
inline_completion = { path = "crates/inline_completion" }
|
||||
inline_completion_button = { path = "crates/inline_completion_button" }
|
||||
inspector_ui = { path = "crates/inspector_ui" }
|
||||
install_cli = { path = "crates/install_cli" }
|
||||
jj = { path = "crates/jj" }
|
||||
jj_ui = { path = "crates/jj_ui" }
|
||||
@@ -288,6 +287,7 @@ journal = { path = "crates/journal" }
|
||||
language = { path = "crates/language" }
|
||||
language_extension = { path = "crates/language_extension" }
|
||||
language_model = { path = "crates/language_model" }
|
||||
language_model_selector = { path = "crates/language_model_selector" }
|
||||
language_models = { path = "crates/language_models" }
|
||||
language_selector = { path = "crates/language_selector" }
|
||||
language_tools = { path = "crates/language_tools" }
|
||||
@@ -432,7 +432,7 @@ core-foundation-sys = "0.8.6"
|
||||
core-video = { version = "0.4.3", features = ["metal"] }
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
ctor = "0.4.0"
|
||||
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "68516de327fa1be15214133a0a2e52a12982ce75" }
|
||||
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "be69a016ba710191b9fdded28c8b042af4b617f7" }
|
||||
dashmap = "6.0"
|
||||
derive_more = "0.99.17"
|
||||
dirs = "4.0"
|
||||
@@ -449,7 +449,6 @@ futures-batch = "0.6.1"
|
||||
futures-lite = "1.13"
|
||||
git2 = { version = "0.20.1", default-features = false }
|
||||
globset = "0.4"
|
||||
hashbrown = "0.15.3"
|
||||
handlebars = "4.3"
|
||||
heck = "0.5"
|
||||
heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
|
||||
@@ -465,7 +464,6 @@ indoc = "2"
|
||||
inventory = "0.3.19"
|
||||
itertools = "0.14.0"
|
||||
jj-lib = { git = "https://github.com/jj-vcs/jj", rev = "e18eb8e05efaa153fad5ef46576af145bba1807f" }
|
||||
json_dotpath = "1.1"
|
||||
jsonschema = "0.30.0"
|
||||
jsonwebtoken = "9.3"
|
||||
jupyter-protocol = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||
@@ -478,7 +476,6 @@ lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "c9c189
|
||||
markup5ever_rcdom = "0.3.0"
|
||||
metal = "0.29"
|
||||
mlua = { version = "0.10", features = ["lua54", "vendored", "async", "send"] }
|
||||
moka = { version = "0.12.10", features = ["sync"] }
|
||||
naga = { version = "25.0", features = ["wgsl-in"] }
|
||||
nanoid = "0.4"
|
||||
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||
@@ -553,7 +550,7 @@ streaming-iterator = "0.1"
|
||||
strsim = "0.11"
|
||||
strum = { version = "0.27.0", features = ["derive"] }
|
||||
subtle = "2.5.0"
|
||||
syn = { version = "2.0.101", features = ["full", "extra-traits"] }
|
||||
syn = { version = "1.0.72", features = ["full", "extra-traits"] }
|
||||
sys-locale = "0.3.1"
|
||||
sysinfo = "0.31.0"
|
||||
take-until = "0.2.0"
|
||||
@@ -572,7 +569,7 @@ tokio = { version = "1" }
|
||||
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"] }
|
||||
toml = "0.8"
|
||||
tower-http = "0.4.4"
|
||||
tree-sitter = { version = "0.25.5", features = ["wasm"] }
|
||||
tree-sitter = { version = "0.25.3", features = ["wasm"] }
|
||||
tree-sitter-bash = "0.23"
|
||||
tree-sitter-c = "0.23"
|
||||
tree-sitter-cpp = "0.23"
|
||||
@@ -602,7 +599,8 @@ unindent = "0.2.0"
|
||||
url = "2.2"
|
||||
urlencoding = "2.1.2"
|
||||
uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
|
||||
walkdir = "2.5"
|
||||
walkdir = "2.3"
|
||||
wasi-preview1-component-adapter-provider = "29"
|
||||
wasm-encoder = "0.221"
|
||||
wasmparser = "0.221"
|
||||
wasmtime = { version = "29", default-features = false, features = [
|
||||
@@ -611,13 +609,13 @@ wasmtime = { version = "29", default-features = false, features = [
|
||||
"runtime",
|
||||
"cranelift",
|
||||
"component-model",
|
||||
"incremental-cache",
|
||||
"parallel-compilation",
|
||||
] }
|
||||
wasmtime-wasi = "29"
|
||||
which = "6.0.0"
|
||||
wit-component = "0.221"
|
||||
workspace-hack = "0.1.0"
|
||||
zed_llm_client = "0.8.4"
|
||||
zed_llm_client = "0.8.1"
|
||||
zstd = "0.11"
|
||||
|
||||
[workspace.dependencies.async-stripe]
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.3 1.75L3 7.35H5.8L4.7 12.25L11 6.65H8.2L9.3 1.75Z" stroke="black" stroke-width="1.25" stroke-linejoin="round"/>
|
||||
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.76019 3.50003H6.50231C6.71012 3.50003 6.89761 3.62971 6.95698 3.82346C7.04292 4.01876 6.98823 4.23906 6.83199 4.37656L2.83214 7.87643C2.65558 8.02954 2.39731 8.04204 2.20857 7.90455C2.01967 7.76705 1.95092 7.51706 2.04295 7.30301L3.24462 4.49999H1.48844C1.29423 4.49999 1.10767 4.37031 1.0344 4.17657C0.961132 3.98126 1.01643 3.76096 1.17323 3.62346L5.17261 0.123753C5.34917 -0.0299914 5.60697 -0.0417097 5.79603 0.0954726C5.98508 0.232749 6.05383 0.482177 5.96165 0.69695L4.76013 3.49981L4.76019 3.50003Z" fill="white"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 227 B After Width: | Height: | Size: 633 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.76019 3.50003H6.50231C6.71012 3.50003 6.89761 3.62971 6.95698 3.82346C7.04292 4.01876 6.98823 4.23906 6.83199 4.37656L2.83214 7.87643C2.65558 8.02954 2.39731 8.04204 2.20857 7.90455C2.01967 7.76705 1.95092 7.51706 2.04295 7.30301L3.24462 4.49999H1.48844C1.29423 4.49999 1.10767 4.37031 1.0344 4.17657C0.961132 3.98126 1.01643 3.76096 1.17323 3.62346L5.17261 0.123753C5.34917 -0.0299914 5.60697 -0.0417097 5.79603 0.0954726C5.98508 0.232749 6.05383 0.482177 5.96165 0.69695L4.76013 3.49981L4.76019 3.50003Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 633 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.99207 8.14741C5.37246 8.14741 5.73726 7.9963 6.00623 7.72733C6.27521 7.45836 6.42631 7.09355 6.42631 6.71317C6.42631 5.92147 6.13946 5.56578 5.85262 4.99208C5.23761 3.76265 5.72411 2.66631 7.00001 1.5499C7.28686 2.98414 8.1474 4.36101 9.2948 5.27893C10.4422 6.19684 11.0159 7.28687 11.0159 8.43426C11.0159 8.96163 10.912 9.48384 10.7102 9.97107C10.5084 10.4583 10.2126 10.901 9.83967 11.2739C9.46676 11.6468 9.02405 11.9426 8.53682 12.1444C8.04959 12.3463 7.52738 12.4501 7.00001 12.4501C6.47264 12.4501 5.95043 12.3463 5.4632 12.1444C4.97597 11.9426 4.53326 11.6468 4.16035 11.2739C3.78745 10.901 3.49164 10.4583 3.28982 9.97107C3.088 9.48384 2.98413 8.96163 2.98413 8.43426C2.98413 7.77279 3.23254 7.1182 3.55783 6.71317C3.55783 7.09355 3.70894 7.45836 3.97791 7.72733C4.24688 7.9963 4.61169 8.14741 4.99207 8.14741Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1018 B |
@@ -1,13 +0,0 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2595_5640)">
|
||||
<path d="M4.99207 8.14741C5.37246 8.14741 5.73726 7.9963 6.00623 7.72733C6.27521 7.45836 6.42631 7.09355 6.42631 6.71317C6.42631 5.92147 6.13946 5.56578 5.85262 4.99208C5.23761 3.76265 5.72411 2.66631 7.00001 1.5499C7.28686 2.98414 8.1474 4.36101 9.2948 5.27893C10.4422 6.19684 11.0159 7.28687 11.0159 8.43426C11.0159 8.96163 10.912 9.48384 10.7102 9.97107C10.5084 10.4583 10.2126 10.901 9.83967 11.2739C9.46676 11.6468 9.02405 11.9426 8.53682 12.1444C8.04959 12.3463 7.52738 12.4501 7.00001 12.4501C6.47264 12.4501 5.95043 12.3463 5.4632 12.1444C4.97597 11.9426 4.53326 11.6468 4.16035 11.2739C3.78745 10.901 3.49164 10.4583 3.28982 9.97107C3.088 9.48384 2.98413 8.96163 2.98413 8.43426C2.98413 7.77279 3.23254 7.1182 3.55783 6.71317C3.55783 7.09355 3.70894 7.45836 3.97791 7.72733C4.24688 7.9963 4.61169 8.14741 4.99207 8.14741Z" fill="black" fill-opacity="0.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2 4C2.55228 4 3 3.55228 3 3C3 2.44772 2.55228 2 2 2C1.44772 2 1 2.44772 1 3C1 3.55228 1.44772 4 2 4Z" fill="black"/>
|
||||
<path d="M10 2C10.5523 2 11 1.55228 11 1C11 0.44772 10.5523 0 10 0C9.44772 0 9 0.44772 9 1C9 1.55228 9.44772 2 10 2Z" fill="black"/>
|
||||
<path d="M13 5C13.5522 5 14 4.55228 14 4C14 3.44772 13.5522 3 13 3C12.4478 3 12 3.44772 12 4C12 4.55228 12.4478 5 13 5Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2595_5640">
|
||||
<rect width="14" height="14" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
14
assets/icons/zed_max_mode.svg
Normal file
14
assets/icons/zed_max_mode.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 687 B |
@@ -31,10 +31,8 @@
|
||||
"ctrl-,": "zed::OpenSettings",
|
||||
"ctrl-q": "zed::Quit",
|
||||
"f4": "debugger::Start",
|
||||
"alt-f4": "debugger::RerunLastSession",
|
||||
"f5": "debugger::Continue",
|
||||
"shift-f5": "debugger::Stop",
|
||||
"ctrl-shift-f5": "debugger::Restart",
|
||||
"f6": "debugger::Pause",
|
||||
"f7": "debugger::StepOver",
|
||||
"cmd-f11": "debugger::StepInto",
|
||||
@@ -127,7 +125,9 @@
|
||||
"shift-f10": "editor::OpenContextMenu",
|
||||
"ctrl-shift-e": "editor::ToggleEditPrediction",
|
||||
"f9": "editor::ToggleBreakpoint",
|
||||
"shift-f9": "editor::EditLogBreakpoint"
|
||||
"shift-f9": "editor::EditLogBreakpoint",
|
||||
"ctrl-shift-backspace": "editor::GoToPreviousChange",
|
||||
"ctrl-shift-alt-backspace": "editor::GoToNextChange"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -146,8 +146,6 @@
|
||||
"ctrl->": "assistant::QuoteSelection",
|
||||
"ctrl-<": "assistant::InsertIntoEditor",
|
||||
"ctrl-alt-e": "editor::SelectEnclosingSymbol",
|
||||
"ctrl-shift-backspace": "editor::GoToPreviousChange",
|
||||
"ctrl-shift-alt-backspace": "editor::GoToNextChange",
|
||||
"alt-enter": "editor::OpenSelectionsInMultibuffer"
|
||||
}
|
||||
},
|
||||
@@ -244,14 +242,11 @@
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"ctrl-alt-/": "agent::ToggleModelSelector",
|
||||
"ctrl-shift-a": "agent::ToggleContextPicker",
|
||||
"ctrl-shift-j": "agent::ToggleNavigationMenu",
|
||||
"ctrl-shift-o": "agent::ToggleNavigationMenu",
|
||||
"ctrl-shift-i": "agent::ToggleOptionsMenu",
|
||||
"shift-alt-escape": "agent::ExpandMessageEditor",
|
||||
"ctrl-alt-e": "agent::RemoveAllContext",
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||
"ctrl-shift-enter": "agent::ContinueThread",
|
||||
"alt-enter": "agent::ContinueWithBurnMode",
|
||||
"ctrl-alt-b": "agent::ToggleBurnMode"
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -278,7 +273,6 @@
|
||||
"context": "MessageEditor > Editor",
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"ctrl-enter": "agent::ChatWithFollow",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff"
|
||||
}
|
||||
@@ -564,7 +558,6 @@
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||
"ctrl-shift-b": "outline_panel::ToggleFocus",
|
||||
"ctrl-shift-g": "git_panel::ToggleFocus",
|
||||
"ctrl-shift-d": "debug_panel::ToggleFocus",
|
||||
"ctrl-?": "agent::ToggleFocus",
|
||||
"alt-save": "workspace::SaveAll",
|
||||
"ctrl-alt-s": "workspace::SaveAll",
|
||||
@@ -602,6 +595,7 @@
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-shift-d": "editor::DuplicateLineDown",
|
||||
"ctrl-shift-j": "editor::JoinLines",
|
||||
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
|
||||
"ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",
|
||||
@@ -680,8 +674,7 @@
|
||||
{
|
||||
"bindings": {
|
||||
"ctrl-alt-shift-f": "workspace::FollowNextCollaborator",
|
||||
// Only available in debug builds: opens an element inspector for development.
|
||||
"ctrl-alt-i": "dev::ToggleInspector"
|
||||
"ctrl-alt-i": "zed::DebugElements"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -869,37 +862,11 @@
|
||||
"alt-l": "git::GenerateCommitMessage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "DebugPanel",
|
||||
"bindings": {
|
||||
"ctrl-t": "debugger::ToggleThreadPicker",
|
||||
"ctrl-i": "debugger::ToggleSessionPicker"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "VariableList",
|
||||
"bindings": {
|
||||
"left": "variable_list::CollapseSelectedEntry",
|
||||
"right": "variable_list::ExpandSelectedEntry",
|
||||
"enter": "variable_list::EditVariable",
|
||||
"ctrl-c": "variable_list::CopyVariableValue",
|
||||
"ctrl-alt-c": "variable_list::CopyVariableName"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BreakpointList",
|
||||
"bindings": {
|
||||
"space": "debugger::ToggleEnableBreakpoint",
|
||||
"backspace": "debugger::UnsetBreakpoint"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "CollabPanel && not_editing",
|
||||
"bindings": {
|
||||
"ctrl-backspace": "collab_panel::Remove",
|
||||
"space": "menu::Confirm",
|
||||
"ctrl-up": "collab_panel::MoveChannelUp",
|
||||
"ctrl-down": "collab_panel::MoveChannelDown"
|
||||
"space": "menu::Confirm"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1021,12 +988,5 @@
|
||||
"bindings": {
|
||||
"enter": "menu::Confirm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "RunModal",
|
||||
"bindings": {
|
||||
"ctrl-tab": "pane::ActivateNextItem",
|
||||
"ctrl-shift-tab": "pane::ActivatePreviousItem"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
[
|
||||
// Moved before Standard macOS bindings so that `cmd-w` is not the last binding for
|
||||
// `workspace::CloseWindow` and displayed/intercepted by macOS
|
||||
{
|
||||
"context": "PromptLibrary",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "rules_library::NewRule",
|
||||
"cmd-shift-s": "rules_library::ToggleDefaultRule",
|
||||
"cmd-w": "workspace::CloseWindow"
|
||||
}
|
||||
},
|
||||
// Standard macOS bindings
|
||||
{
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"f4": "debugger::Start",
|
||||
"alt-f4": "debugger::RerunLastSession",
|
||||
"f5": "debugger::Continue",
|
||||
"shift-f5": "debugger::Stop",
|
||||
"shift-cmd-f5": "debugger::Restart",
|
||||
"f6": "debugger::Pause",
|
||||
"f7": "debugger::StepOver",
|
||||
"f11": "debugger::StepInto",
|
||||
@@ -279,14 +288,11 @@
|
||||
"cmd-i": "agent::ToggleProfileSelector",
|
||||
"cmd-alt-/": "agent::ToggleModelSelector",
|
||||
"cmd-shift-a": "agent::ToggleContextPicker",
|
||||
"cmd-shift-j": "agent::ToggleNavigationMenu",
|
||||
"cmd-shift-o": "agent::ToggleNavigationMenu",
|
||||
"cmd-shift-i": "agent::ToggleOptionsMenu",
|
||||
"shift-alt-escape": "agent::ExpandMessageEditor",
|
||||
"cmd-alt-e": "agent::RemoveAllContext",
|
||||
"cmd-shift-e": "project_panel::ToggleFocus",
|
||||
"cmd-shift-enter": "agent::ContinueThread",
|
||||
"alt-enter": "agent::ContinueWithBurnMode",
|
||||
"cmd-alt-b": "agent::ToggleBurnMode"
|
||||
"cmd-shift-e": "project_panel::ToggleFocus"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -315,7 +321,6 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"cmd-enter": "agent::ChatWithFollow",
|
||||
"cmd-i": "agent::ToggleProfileSelector",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff"
|
||||
}
|
||||
@@ -363,18 +368,15 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ThreadHistory > Editor",
|
||||
"context": "ThreadHistory",
|
||||
"bindings": {
|
||||
"shift-backspace": "agent::RemoveSelectedThread"
|
||||
"ctrl--": "pane::GoBack"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "PromptLibrary",
|
||||
"use_key_equivalents": true,
|
||||
"context": "ThreadHistory > Editor",
|
||||
"bindings": {
|
||||
"cmd-n": "rules_library::NewRule",
|
||||
"cmd-shift-s": "rules_library::ToggleDefaultRule",
|
||||
"cmd-w": "workspace::CloseWindow"
|
||||
"shift-backspace": "agent::RemoveSelectedThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -546,7 +548,9 @@
|
||||
"cmd-\\": "pane::SplitRight",
|
||||
"cmd-k v": "markdown::OpenPreviewToTheSide",
|
||||
"cmd-shift-v": "markdown::OpenPreview",
|
||||
"ctrl-cmd-c": "editor::DisplayCursorNames"
|
||||
"ctrl-cmd-c": "editor::DisplayCursorNames",
|
||||
"cmd-shift-backspace": "editor::GoToPreviousChange",
|
||||
"cmd-shift-alt-backspace": "editor::GoToNextChange"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -554,9 +558,7 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-shift-o": "outline::Toggle",
|
||||
"ctrl-g": "go_to_line::Toggle",
|
||||
"cmd-shift-backspace": "editor::GoToPreviousChange",
|
||||
"cmd-shift-alt-backspace": "editor::GoToNextChange"
|
||||
"ctrl-g": "go_to_line::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -622,7 +624,6 @@
|
||||
"cmd-shift-e": "project_panel::ToggleFocus",
|
||||
"cmd-shift-b": "outline_panel::ToggleFocus",
|
||||
"ctrl-shift-g": "git_panel::ToggleFocus",
|
||||
"cmd-shift-d": "debug_panel::ToggleFocus",
|
||||
"cmd-?": "agent::ToggleFocus",
|
||||
"cmd-alt-s": "workspace::SaveAll",
|
||||
"cmd-k m": "language_selector::Toggle",
|
||||
@@ -740,8 +741,7 @@
|
||||
"ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
|
||||
// TODO: Move this to a dock open action
|
||||
"cmd-shift-c": "collab_panel::ToggleFocus",
|
||||
// Only available in debug builds: opens an element inspector for development.
|
||||
"cmd-alt-i": "dev::ToggleInspector"
|
||||
"cmd-alt-i": "zed::DebugElements"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -844,10 +844,7 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"left": "variable_list::CollapseSelectedEntry",
|
||||
"right": "variable_list::ExpandSelectedEntry",
|
||||
"enter": "variable_list::EditVariable",
|
||||
"cmd-c": "variable_list::CopyVariableValue",
|
||||
"cmd-alt-c": "variable_list::CopyVariableName"
|
||||
"right": "variable_list::ExpandSelectedEntry"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -932,28 +929,12 @@
|
||||
"alt-tab": "git::GenerateCommitMessage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "DebugPanel",
|
||||
"bindings": {
|
||||
"cmd-t": "debugger::ToggleThreadPicker",
|
||||
"cmd-i": "debugger::ToggleSessionPicker"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BreakpointList",
|
||||
"bindings": {
|
||||
"space": "debugger::ToggleEnableBreakpoint",
|
||||
"backspace": "debugger::UnsetBreakpoint"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "CollabPanel && not_editing",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-backspace": "collab_panel::Remove",
|
||||
"space": "menu::Confirm",
|
||||
"cmd-up": "collab_panel::MoveChannelUp",
|
||||
"cmd-down": "collab_panel::MoveChannelDown"
|
||||
"space": "menu::Confirm"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1111,13 +1092,5 @@
|
||||
"bindings": {
|
||||
"enter": "menu::Confirm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "RunModal",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-tab": "pane::ActivateNextItem",
|
||||
"ctrl-shift-tab": "pane::ActivatePreviousItem"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
[
|
||||
// Cursor for MacOS. See: https://docs.cursor.com/kbd
|
||||
{
|
||||
"context": "Workspace",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-i": "agent::ToggleFocus",
|
||||
"ctrl-shift-i": "agent::ToggleFocus",
|
||||
"ctrl-l": "agent::ToggleFocus",
|
||||
"ctrl-shift-l": "agent::ToggleFocus",
|
||||
"ctrl-alt-b": "agent::ToggleFocus",
|
||||
"ctrl-shift-j": "agent::OpenConfiguration"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-i": "agent::ToggleFocus",
|
||||
"ctrl-shift-i": "agent::ToggleFocus",
|
||||
"ctrl-shift-l": "assistant::QuoteSelection", // In cursor uses "Ask" mode
|
||||
"ctrl-l": "assistant::QuoteSelection", // In cursor uses "Agent" mode
|
||||
"ctrl-k": "assistant::InlineAssist",
|
||||
"ctrl-shift-k": "assistant::InsertIntoEditor"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "InlineAssistEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-backspace": "editor::Cancel"
|
||||
// "alt-enter": // Quick Question
|
||||
// "ctrl-shift-enter": // Full File Context
|
||||
// "ctrl-shift-k": // Toggle input focus (editor <> inline assist)
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel || ContextEditor || (MessageEditor > Editor)",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-i": "workspace::ToggleRightDock",
|
||||
"ctrl-shift-i": "workspace::ToggleRightDock",
|
||||
"ctrl-l": "workspace::ToggleRightDock",
|
||||
"ctrl-shift-l": "workspace::ToggleRightDock",
|
||||
"ctrl-alt-b": "workspace::ToggleRightDock",
|
||||
"ctrl-w": "workspace::ToggleRightDock", // technically should close chat
|
||||
"ctrl-.": "agent::ToggleProfileSelector",
|
||||
"ctrl-/": "agent::ToggleModelSelector",
|
||||
"ctrl-shift-backspace": "editor::Cancel",
|
||||
"ctrl-r": "agent::NewThread",
|
||||
"ctrl-shift-v": "editor::Paste",
|
||||
"ctrl-shift-k": "assistant::InsertIntoEditor"
|
||||
// "escape": "agent::ToggleFocus"
|
||||
///// Enable when Zed supports multiple thread tabs
|
||||
// "ctrl-t": // new thread tab
|
||||
// "ctrl-[": // next thread tab
|
||||
// "ctrl-]": // next thread tab
|
||||
///// Enable if Zed adds support for keyboard navigation of thread elements
|
||||
// "tab": // cycle to next message
|
||||
// "shift-tab": // cycle to previous message
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && editor_agent_diff",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-enter": "agent::KeepAll",
|
||||
"ctrl-backspace": "agent::RejectAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full && edit_prediction",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-right": "editor::AcceptPartialEditPrediction"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Terminal",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-k": "assistant::InlineAssist"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,85 +0,0 @@
|
||||
[
|
||||
// Cursor for MacOS. See: https://docs.cursor.com/kbd
|
||||
{
|
||||
"context": "Workspace",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-i": "agent::ToggleFocus",
|
||||
"cmd-shift-i": "agent::ToggleFocus",
|
||||
"cmd-l": "agent::ToggleFocus",
|
||||
"cmd-shift-l": "agent::ToggleFocus",
|
||||
"cmd-alt-b": "agent::ToggleFocus",
|
||||
"cmd-shift-j": "agent::OpenConfiguration"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-i": "agent::ToggleFocus",
|
||||
"cmd-shift-i": "agent::ToggleFocus",
|
||||
"cmd-shift-l": "assistant::QuoteSelection", // In cursor uses "Ask" mode
|
||||
"cmd-l": "assistant::QuoteSelection", // In cursor uses "Agent" mode
|
||||
"cmd-k": "assistant::InlineAssist",
|
||||
"cmd-shift-k": "assistant::InsertIntoEditor"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "InlineAssistEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-shift-backspace": "editor::Cancel"
|
||||
// "alt-enter": // Quick Question
|
||||
// "cmd-shift-enter": // Full File Context
|
||||
// "cmd-shift-k": // Toggle input focus (editor <> inline assist)
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel || ContextEditor || (MessageEditor > Editor)",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-i": "workspace::ToggleRightDock",
|
||||
"cmd-shift-i": "workspace::ToggleRightDock",
|
||||
"cmd-l": "workspace::ToggleRightDock",
|
||||
"cmd-shift-l": "workspace::ToggleRightDock",
|
||||
"cmd-alt-b": "workspace::ToggleRightDock",
|
||||
"cmd-w": "workspace::ToggleRightDock", // technically should close chat
|
||||
"cmd-.": "agent::ToggleProfileSelector",
|
||||
"cmd-/": "agent::ToggleModelSelector",
|
||||
"cmd-shift-backspace": "editor::Cancel",
|
||||
"cmd-r": "agent::NewThread",
|
||||
"cmd-shift-v": "editor::Paste",
|
||||
"cmd-shift-k": "assistant::InsertIntoEditor"
|
||||
// "escape": "agent::ToggleFocus"
|
||||
///// Enable when Zed supports multiple thread tabs
|
||||
// "cmd-t": // new thread tab
|
||||
// "cmd-[": // next thread tab
|
||||
// "cmd-]": // next thread tab
|
||||
///// Enable if Zed adds support for keyboard navigation of thread elements
|
||||
// "tab": // cycle to next message
|
||||
// "shift-tab": // cycle to previous message
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && editor_agent_diff",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-enter": "agent::KeepAll",
|
||||
"cmd-backspace": "agent::RejectAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full && edit_prediction",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-right": "editor::AcceptPartialEditPrediction"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Terminal",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-k": "assistant::InlineAssist"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -846,5 +846,13 @@
|
||||
// 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -213,8 +213,6 @@
|
||||
// Whether to show the signature help after completion or a bracket pair inserted.
|
||||
// If `auto_signature_help` is enabled, this setting will be treated as enabled also.
|
||||
"show_signature_help_after_edits": false,
|
||||
// Whether to show code action button at start of buffer line.
|
||||
"inline_code_actions": true,
|
||||
// What to do when go to definition yields no results.
|
||||
//
|
||||
// 1. Do nothing: `none`
|
||||
@@ -232,11 +230,11 @@
|
||||
// Possible values:
|
||||
// - "off" — no diagnostics are allowed
|
||||
// - "error"
|
||||
// - "warning"
|
||||
// - "warning" (default)
|
||||
// - "info"
|
||||
// - "hint"
|
||||
// - null — allow all diagnostics (default)
|
||||
"diagnostics_max_severity": null,
|
||||
// - null — allow all diagnostics
|
||||
"diagnostics_max_severity": "warning",
|
||||
// Whether to show wrap guides (vertical rulers) in the editor.
|
||||
// Setting this to true will show a guide at the 'preferred_line_length' value
|
||||
// if 'soft_wrap' is set to 'preferred_line_length', and will show any
|
||||
@@ -324,9 +322,7 @@
|
||||
// Whether to show the Selections menu in the editor toolbar.
|
||||
"selections_menu": true,
|
||||
// Whether to show agent review buttons in the editor toolbar.
|
||||
"agent_review": true,
|
||||
// Whether to show code action buttons in the editor toolbar.
|
||||
"code_actions": false
|
||||
"agent_review": true
|
||||
},
|
||||
// Titlebar related settings
|
||||
"title_bar": {
|
||||
@@ -475,10 +471,6 @@
|
||||
// Scroll sensitivity multiplier. This multiplier is applied
|
||||
// to both the horizontal and vertical delta values while scrolling.
|
||||
"scroll_sensitivity": 1.0,
|
||||
// Scroll sensitivity multiplier for fast scrolling. This multiplier is applied
|
||||
// to both the horizontal and vertical delta values while scrolling. Fast scrolling
|
||||
// happens when a user holds the alt or option key while scrolling.
|
||||
"fast_scroll_sensitivity": 4.0,
|
||||
"relative_line_numbers": false,
|
||||
// If 'search_wrap' is disabled, search result do not wrap around the end of the file.
|
||||
"search_wrap": true,
|
||||
@@ -714,7 +706,7 @@
|
||||
"version": "2",
|
||||
// Whether the agent is enabled.
|
||||
"enabled": true,
|
||||
/// What completion mode to start new threads in, if available. Can be 'normal' or 'burn'.
|
||||
/// What completion mode to start new threads in, if available. Can be 'normal' or 'max'.
|
||||
"preferred_completion_mode": "normal",
|
||||
// Whether to show the agent panel button in the status bar.
|
||||
"button": true,
|
||||
@@ -729,14 +721,14 @@
|
||||
// The provider to use.
|
||||
"provider": "zed.dev",
|
||||
// The model to use.
|
||||
"model": "claude-sonnet-4"
|
||||
"model": "claude-3-7-sonnet-latest"
|
||||
},
|
||||
// The model to use when applying edits from the agent.
|
||||
"editor_model": {
|
||||
// The provider to use.
|
||||
"provider": "zed.dev",
|
||||
// The model to use.
|
||||
"model": "claude-sonnet-4"
|
||||
"model": "claude-3-7-sonnet-latest"
|
||||
},
|
||||
// Additional parameters for language model requests. When making a request to a model, parameters will be taken
|
||||
// from the last entry in this list that matches the model's provider and name. In each entry, both provider
|
||||
@@ -756,7 +748,7 @@
|
||||
// To set parameters for a specific provider and model:
|
||||
// {
|
||||
// "provider": "zed.dev",
|
||||
// "model": "claude-sonnet-4",
|
||||
// "model": "claude-3-7-sonnet-latest",
|
||||
// "temperature": 1.0
|
||||
// }
|
||||
],
|
||||
@@ -822,12 +814,7 @@
|
||||
// "primary_screen" - Show the notification only on your primary screen (default)
|
||||
// "all_screens" - Show these notifications on all screens
|
||||
// "never" - Never show these notifications
|
||||
"notify_when_agent_waiting": "primary_screen",
|
||||
// Whether to play a sound when the agent has either completed
|
||||
// its response, or needs user input.
|
||||
|
||||
// Default: false
|
||||
"play_sound_when_agent_done": false
|
||||
"notify_when_agent_waiting": "primary_screen"
|
||||
},
|
||||
// The settings for slash commands.
|
||||
"slash_commands": {
|
||||
@@ -959,17 +946,7 @@
|
||||
// "skip_focus_for_active_in_search": false
|
||||
//
|
||||
// Default: true
|
||||
"skip_focus_for_active_in_search": true,
|
||||
// Whether to show the git status in the file finder.
|
||||
"git_status": true,
|
||||
// Whether to use gitignored files when searching.
|
||||
// Only the file Zed had indexed will be used, not necessary all the gitignored files.
|
||||
//
|
||||
// Can accept 3 values:
|
||||
// * `true`: Use all gitignored files
|
||||
// * `false`: Use only the files Zed had indexed
|
||||
// * `null`: Be smart and search for ignored when called from a gitignored worktree
|
||||
"include_ignored": null
|
||||
"skip_focus_for_active_in_search": true
|
||||
},
|
||||
// Whether or not to remove any trailing whitespace from lines of a buffer
|
||||
// before saving it.
|
||||
@@ -1314,17 +1291,7 @@
|
||||
// Settings related to running tasks.
|
||||
"tasks": {
|
||||
"variables": {},
|
||||
"enabled": true,
|
||||
// Use LSP tasks over Zed language extension ones.
|
||||
// If no LSP tasks are returned due to error/timeout or regular execution,
|
||||
// Zed language extension tasks will be used instead.
|
||||
//
|
||||
// Other Zed tasks will still be shown:
|
||||
// * Zed task from either of the task config file
|
||||
// * Zed task from history (e.g. one-off task was spawned before)
|
||||
//
|
||||
// Default: true
|
||||
"prefer_lsp": true
|
||||
"enabled": true
|
||||
},
|
||||
// An object whose keys are language names, and whose values
|
||||
// are arrays of filenames or extensions of files that should
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
[
|
||||
{
|
||||
"label": "Debug active PHP file",
|
||||
"adapter": "PHP",
|
||||
"adapter": "php",
|
||||
"program": "$ZED_FILE",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT"
|
||||
},
|
||||
{
|
||||
"label": "Debug active Python file",
|
||||
"adapter": "Debugpy",
|
||||
"adapter": "python",
|
||||
"program": "$ZED_FILE",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT"
|
||||
},
|
||||
{
|
||||
"label": "Debug active JavaScript file",
|
||||
"adapter": "JavaScript",
|
||||
"adapter": "javascript",
|
||||
"program": "$ZED_FILE",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT"
|
||||
},
|
||||
{
|
||||
"label": "JavaScript debug terminal",
|
||||
"adapter": "JavaScript",
|
||||
"adapter": "javascript",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT",
|
||||
"console": "integratedTerminal"
|
||||
"initialize_args": {
|
||||
"console": "integratedTerminal"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Binary file not shown.
@@ -24,9 +24,8 @@ project.workspace = true
|
||||
smallvec.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
release_channel.workspace = true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage, VersionCheckType};
|
||||
use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage};
|
||||
use editor::Editor;
|
||||
use extension_host::ExtensionStore;
|
||||
use futures::StreamExt;
|
||||
@@ -60,7 +60,6 @@ struct Content {
|
||||
message: String,
|
||||
on_click:
|
||||
Option<Arc<dyn Fn(&mut ActivityIndicator, &mut Window, &mut Context<ActivityIndicator>)>>,
|
||||
tooltip_message: Option<String>,
|
||||
}
|
||||
|
||||
impl ActivityIndicator {
|
||||
@@ -263,7 +262,6 @@ impl ActivityIndicator {
|
||||
});
|
||||
window.dispatch_action(Box::new(workspace::OpenLog), cx);
|
||||
})),
|
||||
tooltip_message: None,
|
||||
});
|
||||
}
|
||||
// Show any language server has pending activity.
|
||||
@@ -307,32 +305,6 @@ impl ActivityIndicator {
|
||||
),
|
||||
message,
|
||||
on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)),
|
||||
tooltip_message: None,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(session) = self
|
||||
.project
|
||||
.read(cx)
|
||||
.dap_store()
|
||||
.read(cx)
|
||||
.sessions()
|
||||
.find(|s| !s.read(cx).is_started())
|
||||
{
|
||||
return Some(Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: format!("Debug: {}", session.read(cx).adapter()),
|
||||
tooltip_message: Some(session.read(cx).label().to_string()),
|
||||
on_click: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -360,7 +332,6 @@ impl ActivityIndicator {
|
||||
),
|
||||
message: job_info.message.into(),
|
||||
on_click: None,
|
||||
tooltip_message: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -403,7 +374,6 @@ impl ActivityIndicator {
|
||||
.retain(|status| !downloading.contains(&status.name));
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
})),
|
||||
tooltip_message: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -432,7 +402,6 @@ impl ActivityIndicator {
|
||||
.retain(|status| !checking_for_update.contains(&status.name));
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
})),
|
||||
tooltip_message: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -459,7 +428,6 @@ impl ActivityIndicator {
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.show_error_message(&Default::default(), window, cx)
|
||||
})),
|
||||
tooltip_message: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -478,7 +446,6 @@ impl ActivityIndicator {
|
||||
});
|
||||
window.dispatch_action(Box::new(workspace::OpenLog), cx);
|
||||
})),
|
||||
tooltip_message: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -495,9 +462,8 @@ impl ActivityIndicator {
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
})),
|
||||
tooltip_message: None,
|
||||
}),
|
||||
AutoUpdateStatus::Downloading { version } => Some(Content {
|
||||
AutoUpdateStatus::Downloading => Some(Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::Download)
|
||||
.size(IconSize::Small)
|
||||
@@ -507,9 +473,8 @@ impl ActivityIndicator {
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
})),
|
||||
tooltip_message: Some(Self::version_tooltip_message(&version)),
|
||||
}),
|
||||
AutoUpdateStatus::Installing { version } => Some(Content {
|
||||
AutoUpdateStatus::Installing => Some(Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::Download)
|
||||
.size(IconSize::Small)
|
||||
@@ -519,12 +484,8 @@ impl ActivityIndicator {
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
})),
|
||||
tooltip_message: Some(Self::version_tooltip_message(&version)),
|
||||
}),
|
||||
AutoUpdateStatus::Updated {
|
||||
binary_path,
|
||||
version,
|
||||
} => Some(Content {
|
||||
AutoUpdateStatus::Updated { binary_path, .. } => Some(Content {
|
||||
icon: None,
|
||||
message: "Click to restart and update Zed".to_string(),
|
||||
on_click: Some(Arc::new({
|
||||
@@ -533,7 +494,6 @@ impl ActivityIndicator {
|
||||
};
|
||||
move |_, _, cx| workspace::reload(&reload, cx)
|
||||
})),
|
||||
tooltip_message: Some(Self::version_tooltip_message(&version)),
|
||||
}),
|
||||
AutoUpdateStatus::Errored => Some(Content {
|
||||
icon: Some(
|
||||
@@ -545,7 +505,6 @@ impl ActivityIndicator {
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
})),
|
||||
tooltip_message: None,
|
||||
}),
|
||||
AutoUpdateStatus::Idle => None,
|
||||
};
|
||||
@@ -565,7 +524,6 @@ impl ActivityIndicator {
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
})),
|
||||
tooltip_message: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -573,17 +531,6 @@ impl ActivityIndicator {
|
||||
None
|
||||
}
|
||||
|
||||
fn version_tooltip_message(version: &VersionCheckType) -> String {
|
||||
format!("Version: {}", {
|
||||
match version {
|
||||
auto_update::VersionCheckType::Sha(sha) => format!("{}…", sha.short()),
|
||||
auto_update::VersionCheckType::Semantic(semantic_version) => {
|
||||
semantic_version.to_string()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn toggle_language_server_work_context_menu(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
@@ -628,14 +575,7 @@ impl Render for ActivityIndicator {
|
||||
)
|
||||
.tooltip(Tooltip::text(content.message))
|
||||
} else {
|
||||
button
|
||||
.child(Label::new(content.message).size(LabelSize::Small))
|
||||
.when_some(
|
||||
content.tooltip_message,
|
||||
|this, tooltip_message| {
|
||||
this.tooltip(Tooltip::text(tooltip_message))
|
||||
},
|
||||
)
|
||||
button.child(Label::new(content.message).size(LabelSize::Small))
|
||||
}
|
||||
})
|
||||
.when_some(content.on_click, |this, handler| {
|
||||
@@ -715,26 +655,3 @@ impl StatusItemView for ActivityIndicator {
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::SemanticVersion;
|
||||
use release_channel::AppCommitSha;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_version_tooltip_message() {
|
||||
let message = ActivityIndicator::version_tooltip_message(&VersionCheckType::Semantic(
|
||||
SemanticVersion::new(1, 0, 0),
|
||||
));
|
||||
|
||||
assert_eq!(message, "Version: 1.0.0");
|
||||
|
||||
let message = ActivityIndicator::version_tooltip_message(&VersionCheckType::Sha(
|
||||
AppCommitSha::new("14d9a4189f058d8736339b06ff2340101eaea5af".to_string()),
|
||||
));
|
||||
|
||||
assert_eq!(message, "Version: 14d9a41…");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,14 +19,13 @@ test-support = [
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
agent_settings.workspace = true
|
||||
anyhow.workspace = true
|
||||
assistant_context_editor.workspace = true
|
||||
assistant_settings.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
assistant_slash_commands.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
async-watch.workspace = true
|
||||
audio.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
@@ -53,6 +52,7 @@ itertools.workspace = true
|
||||
jsonschema.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
language_model_selector.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
markdown.workspace = true
|
||||
@@ -77,6 +77,7 @@ serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
settings.workspace = true
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
streaming_diff.workspace = true
|
||||
telemetry.workspace = true
|
||||
|
||||
@@ -13,10 +13,9 @@ use crate::tool_use::{PendingToolUseStatus, ToolUse};
|
||||
use crate::ui::{
|
||||
AddedContext, AgentNotification, AgentNotificationEvent, AnimatedLabel, ContextPill,
|
||||
};
|
||||
use agent_settings::{AgentSettings, NotifyWhenAgentWaiting};
|
||||
use anyhow::Context as _;
|
||||
use assistant_settings::{AssistantSettings, NotifyWhenAgentWaiting};
|
||||
use assistant_tool::ToolUseStatus;
|
||||
use audio::{Audio, Sound};
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::actions::{MoveUp, Paste};
|
||||
use editor::scroll::Autoscroll;
|
||||
@@ -53,9 +52,8 @@ use ui::{
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use util::markdown::MarkdownCodeBlock;
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use workspace::Workspace;
|
||||
use zed_actions::assistant::OpenRulesLibrary;
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
pub struct ActiveThread {
|
||||
context_store: Entity<ContextStore>,
|
||||
@@ -973,22 +971,7 @@ impl ActiveThread {
|
||||
ThreadEvent::ShowError(error) => {
|
||||
self.last_error = Some(error.clone());
|
||||
}
|
||||
ThreadEvent::NewRequest => {
|
||||
cx.notify();
|
||||
}
|
||||
ThreadEvent::CompletionCanceled => {
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.project().update(cx, |project, cx| {
|
||||
project.set_agent_location(None, cx);
|
||||
})
|
||||
});
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
if workspace.is_being_followed(CollaboratorId::Agent) {
|
||||
workspace.unfollow(CollaboratorId::Agent, window, cx);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
ThreadEvent::NewRequest | ThreadEvent::CompletionCanceled => {
|
||||
cx.notify();
|
||||
}
|
||||
ThreadEvent::StreamedCompletion
|
||||
@@ -998,10 +981,9 @@ impl ActiveThread {
|
||||
}
|
||||
ThreadEvent::Stopped(reason) => match reason {
|
||||
Ok(StopReason::EndTurn | StopReason::MaxTokens) => {
|
||||
let used_tools = self.thread.read(cx).used_tools_since_last_user_message();
|
||||
self.play_notification_sound(cx);
|
||||
let thread = self.thread.read(cx);
|
||||
self.show_notification(
|
||||
if used_tools {
|
||||
if thread.used_tools_since_last_user_message() {
|
||||
"Finished running tools"
|
||||
} else {
|
||||
"New message"
|
||||
@@ -1014,7 +996,6 @@ impl ActiveThread {
|
||||
_ => {}
|
||||
},
|
||||
ThreadEvent::ToolConfirmationNeeded => {
|
||||
self.play_notification_sound(cx);
|
||||
self.show_notification("Waiting for tool confirmation", IconName::Info, window, cx);
|
||||
}
|
||||
ThreadEvent::StreamedAssistantText(message_id, text) => {
|
||||
@@ -1037,6 +1018,7 @@ impl ActiveThread {
|
||||
self.push_message(message_id, &message_segments, window, cx);
|
||||
}
|
||||
|
||||
self.scroll_to_bottom(cx);
|
||||
self.save_thread(cx);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -1151,13 +1133,6 @@ impl ActiveThread {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn play_notification_sound(&self, cx: &mut App) {
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
if settings.play_sound_when_agent_done {
|
||||
Audio::play_sound(Sound::AgentDone, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn show_notification(
|
||||
&mut self,
|
||||
caption: impl Into<SharedString>,
|
||||
@@ -1171,7 +1146,7 @@ impl ActiveThread {
|
||||
|
||||
let title = self.thread.read(cx).summary().unwrap_or("Agent Panel");
|
||||
|
||||
match AgentSettings::get_global(cx).notify_when_agent_waiting {
|
||||
match AssistantSettings::get_global(cx).notify_when_agent_waiting {
|
||||
NotifyWhenAgentWaiting::PrimaryScreen => {
|
||||
if let Some(primary) = cx.primary_display() {
|
||||
self.pop_up(icon, caption.into(), title.clone(), window, primary, cx);
|
||||
@@ -1437,13 +1412,12 @@ impl ActiveThread {
|
||||
let request = language_model::LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
intent: None,
|
||||
mode: None,
|
||||
messages: vec![request_message],
|
||||
tools: vec![],
|
||||
tool_choice: None,
|
||||
stop: vec![],
|
||||
temperature: AgentSettings::temperature_for_model(
|
||||
temperature: AssistantSettings::temperature_for_model(
|
||||
&configured_model.model,
|
||||
cx,
|
||||
),
|
||||
@@ -1493,7 +1467,7 @@ impl ActiveThread {
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.context_store.update(cx, |store, cx| store.clear(cx));
|
||||
self.context_store.update(cx, |store, _cx| store.clear());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -1535,22 +1509,9 @@ impl ActiveThread {
|
||||
});
|
||||
}
|
||||
|
||||
fn cancel_editing_message(
|
||||
&mut self,
|
||||
_: &menu::Cancel,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
fn cancel_editing_message(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.editing_message.take();
|
||||
cx.notify();
|
||||
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
panel.focus_handle(cx).focus(window);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm_editing_message(
|
||||
@@ -1612,12 +1573,7 @@ impl ActiveThread {
|
||||
|
||||
this.thread.update(cx, |thread, cx| {
|
||||
thread.advance_prompt_id();
|
||||
thread.send_to_model(
|
||||
model.model,
|
||||
CompletionIntent::UserPrompt,
|
||||
Some(window.window_handle()),
|
||||
cx,
|
||||
);
|
||||
thread.send_to_model(model.model, Some(window.window_handle()), cx);
|
||||
});
|
||||
this._load_edited_message_context_task = None;
|
||||
cx.notify();
|
||||
@@ -1798,11 +1754,6 @@ impl ActiveThread {
|
||||
let Some(message) = self.thread.read(cx).message(message_id) else {
|
||||
return Empty.into_any();
|
||||
};
|
||||
|
||||
if message.is_hidden {
|
||||
return Empty.into_any();
|
||||
}
|
||||
|
||||
let message_creases = message.creases.clone();
|
||||
|
||||
let Some(rendered_message) = self.rendered_messages_by_id.get(&message_id) else {
|
||||
@@ -1838,7 +1789,6 @@ impl ActiveThread {
|
||||
|
||||
let colors = cx.theme().colors();
|
||||
let editor_bg_color = colors.editor_background;
|
||||
let panel_bg = colors.panel_background;
|
||||
|
||||
let open_as_markdown = IconButton::new(("open-as-markdown", ix), IconName::DocumentText)
|
||||
.icon_size(IconSize::XSmall)
|
||||
@@ -1859,6 +1809,7 @@ impl ActiveThread {
|
||||
const RESPONSE_PADDING_X: Pixels = px(19.);
|
||||
|
||||
let show_feedback = thread.is_turn_end(ix);
|
||||
|
||||
let feedback_container = h_flex()
|
||||
.group("feedback_container")
|
||||
.mt_1()
|
||||
@@ -1923,7 +1874,7 @@ impl ActiveThread {
|
||||
.child(open_as_markdown),
|
||||
)
|
||||
.into_any_element(),
|
||||
None if AgentSettings::get_global(cx).enable_feedback =>
|
||||
None if AssistantSettings::get_global(cx).enable_feedback =>
|
||||
feedback_container
|
||||
.child(
|
||||
div().visible_on_hover("feedback_container").child(
|
||||
@@ -2031,89 +1982,65 @@ impl ActiveThread {
|
||||
.border_1()
|
||||
.border_color(colors.border)
|
||||
.hover(|hover| hover.border_color(colors.text_accent.opacity(0.5)))
|
||||
.cursor_pointer()
|
||||
.child(
|
||||
v_flex()
|
||||
h_flex()
|
||||
.p_2p5()
|
||||
.gap_1()
|
||||
.items_end()
|
||||
.children(message_content)
|
||||
.when_some(editing_message_state, |this, state| {
|
||||
let focus_handle = state.editor.focus_handle(cx).clone();
|
||||
|
||||
this.child(
|
||||
this.w_full().justify_between().child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.justify_between()
|
||||
.flex_wrap()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
div()
|
||||
.opacity(0.8)
|
||||
.child(
|
||||
Icon::new(IconName::Warning)
|
||||
.size(IconSize::Indicator)
|
||||
.color(Color::Warning)
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Editing will restart the thread from this point.")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
),
|
||||
IconButton::new(
|
||||
"cancel-edit-message",
|
||||
IconName::Close,
|
||||
)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_color(Color::Error)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Cancel Edit",
|
||||
&menu::Cancel,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(Self::handle_cancel_click)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
IconButton::new(
|
||||
"cancel-edit-message",
|
||||
IconName::Close,
|
||||
IconButton::new(
|
||||
"confirm-edit-message",
|
||||
IconName::Return,
|
||||
)
|
||||
.disabled(state.editor.read(cx).is_empty(cx))
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Regenerate",
|
||||
&menu::Confirm,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_color(Color::Error)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Cancel Edit",
|
||||
&menu::Cancel,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(Self::handle_cancel_click)),
|
||||
)
|
||||
.child(
|
||||
IconButton::new(
|
||||
"confirm-edit-message",
|
||||
IconName::Return,
|
||||
)
|
||||
.disabled(state.editor.read(cx).is_empty(cx))
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Regenerate",
|
||||
&menu::Confirm,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(
|
||||
cx.listener(Self::handle_regenerate_click),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(
|
||||
cx.listener(Self::handle_regenerate_click),
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
@@ -2155,14 +2082,16 @@ impl ActiveThread {
|
||||
message_id > *editing_message_id
|
||||
});
|
||||
|
||||
let panel_background = cx.theme().colors().panel_background;
|
||||
|
||||
let backdrop = div()
|
||||
.id(("backdrop", ix))
|
||||
.size_full()
|
||||
.id("backdrop")
|
||||
.stop_mouse_events_except_scroll()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.bg(panel_bg)
|
||||
.size_full()
|
||||
.bg(panel_background)
|
||||
.opacity(0.8)
|
||||
.block_mouse_except_scroll()
|
||||
.on_click(cx.listener(Self::handle_cancel_click));
|
||||
|
||||
v_flex()
|
||||
@@ -3125,7 +3054,7 @@ impl ActiveThread {
|
||||
.on_click(cx.listener(
|
||||
move |this, event, window, cx| {
|
||||
if let Some(fs) = fs.clone() {
|
||||
update_settings_file::<AgentSettings>(
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
|settings, _| {
|
||||
@@ -3664,164 +3593,3 @@ fn open_editor_at_position(
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use assistant_tool::{ToolRegistry, ToolWorkingSet};
|
||||
use editor::EditorSettings;
|
||||
use fs::FakeFs;
|
||||
use gpui::{AppContext, TestAppContext, VisualTestContext};
|
||||
use language_model::{LanguageModel, fake_provider::FakeLanguageModel};
|
||||
use project::Project;
|
||||
use prompt_store::PromptBuilder;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use util::path;
|
||||
use workspace::CollaboratorId;
|
||||
|
||||
use crate::{ContextLoadResult, thread_store};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_agent_is_unfollowed_after_cancelling_completion(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(
|
||||
cx,
|
||||
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let (cx, _active_thread, workspace, thread, model) =
|
||||
setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Insert user message without any context (empty context vector)
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.insert_user_message(
|
||||
"What is the best way to learn Rust?",
|
||||
ContextLoadResult::default(),
|
||||
None,
|
||||
vec![],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// Stream response to user message
|
||||
thread.update(cx, |thread, cx| {
|
||||
let request =
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx);
|
||||
thread.stream_completion(request, model, cx.active_window(), cx)
|
||||
});
|
||||
// Follow the agent
|
||||
cx.update(|window, cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.follow(CollaboratorId::Agent, window, cx);
|
||||
})
|
||||
});
|
||||
assert!(cx.read(|cx| workspace.read(cx).is_being_followed(CollaboratorId::Agent)));
|
||||
|
||||
// Cancel the current completion
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.cancel_last_completion(cx.active_window(), cx)
|
||||
});
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
// No longer following the agent
|
||||
assert!(!cx.read(|cx| workspace.read(cx).is_being_followed(CollaboratorId::Agent)));
|
||||
}
|
||||
|
||||
fn init_test_settings(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
AgentSettings::register(cx);
|
||||
prompt_store::init(cx);
|
||||
thread_store::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
language_model::init_settings(cx);
|
||||
ThemeSettings::register(cx);
|
||||
EditorSettings::register(cx);
|
||||
ToolRegistry::default_global(cx);
|
||||
});
|
||||
}
|
||||
|
||||
// Helper to create a test project with test files
|
||||
async fn create_test_project(
|
||||
cx: &mut TestAppContext,
|
||||
files: serde_json::Value,
|
||||
) -> Entity<Project> {
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/test"), files).await;
|
||||
Project::test(fs, [path!("/test").as_ref()], cx).await
|
||||
}
|
||||
|
||||
async fn setup_test_environment(
|
||||
cx: &mut TestAppContext,
|
||||
project: Entity<Project>,
|
||||
) -> (
|
||||
&mut VisualTestContext,
|
||||
Entity<ActiveThread>,
|
||||
Entity<Workspace>,
|
||||
Entity<Thread>,
|
||||
Arc<dyn LanguageModel>,
|
||||
) {
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
|
||||
let thread_store = cx
|
||||
.update(|_, cx| {
|
||||
ThreadStore::load(
|
||||
project.clone(),
|
||||
cx.new(|_| ToolWorkingSet::default()),
|
||||
None,
|
||||
Arc::new(PromptBuilder::new(None).unwrap()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let text_thread_store = cx
|
||||
.update(|_, cx| {
|
||||
TextThreadStore::new(
|
||||
project.clone(),
|
||||
Arc::new(PromptBuilder::new(None).unwrap()),
|
||||
Default::default(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
|
||||
let context_store =
|
||||
cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade())));
|
||||
|
||||
let model = FakeLanguageModel::default();
|
||||
let model: Arc<dyn LanguageModel> = Arc::new(model);
|
||||
|
||||
let language_registry = LanguageRegistry::new(cx.executor());
|
||||
let language_registry = Arc::new(language_registry);
|
||||
|
||||
let active_thread = cx.update(|window, cx| {
|
||||
cx.new(|cx| {
|
||||
ActiveThread::new(
|
||||
thread.clone(),
|
||||
thread_store.clone(),
|
||||
text_thread_store,
|
||||
context_store.clone(),
|
||||
language_registry.clone(),
|
||||
workspace.downgrade(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
(cx, active_thread, workspace, thread, model)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ mod ui;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection};
|
||||
use assistant_settings::{AgentProfileId, AssistantSettings, LanguageModelSelection};
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::Client;
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
@@ -69,7 +69,6 @@ actions!(
|
||||
AddContextServer,
|
||||
RemoveSelectedThread,
|
||||
Chat,
|
||||
ChatWithFollow,
|
||||
CycleNextInlineAssist,
|
||||
CyclePreviousInlineAssist,
|
||||
FocusUp,
|
||||
@@ -87,9 +86,6 @@ actions!(
|
||||
Follow,
|
||||
ResetTrialUpsell,
|
||||
ResetTrialEndUpsell,
|
||||
ContinueThread,
|
||||
ContinueWithBurnMode,
|
||||
ToggleBurnMode,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -121,19 +117,14 @@ pub fn init(
|
||||
client: Arc<Client>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
is_eval: bool,
|
||||
cx: &mut App,
|
||||
) {
|
||||
AgentSettings::register(cx);
|
||||
AssistantSettings::register(cx);
|
||||
SlashCommandSettings::register(cx);
|
||||
|
||||
assistant_context_editor::init(client.clone(), cx);
|
||||
rules_library::init(cx);
|
||||
if !is_eval {
|
||||
// Initializing the language model from the user settings messes with the eval, so we only initialize them when
|
||||
// we're not running inside of the eval.
|
||||
init_language_model_settings(cx);
|
||||
}
|
||||
init_language_model_settings(cx);
|
||||
assistant_slash_command::init(cx);
|
||||
thread_store::init(cx);
|
||||
agent_panel::init(cx);
|
||||
@@ -177,7 +168,7 @@ fn init_language_model_settings(cx: &mut App) {
|
||||
}
|
||||
|
||||
fn update_active_language_model_from_settings(cx: &mut App) {
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
|
||||
fn to_selected_model(selection: &LanguageModelSelection) -> language_model::SelectedModel {
|
||||
language_model::SelectedModel {
|
||||
@@ -226,6 +217,7 @@ fn register_slash_commands(cx: &mut App) {
|
||||
slash_command_registry.register_command(assistant_slash_commands::PromptSlashCommand, true);
|
||||
slash_command_registry.register_command(assistant_slash_commands::SelectionCommand, true);
|
||||
slash_command_registry.register_command(assistant_slash_commands::DefaultSlashCommand, false);
|
||||
slash_command_registry.register_command(assistant_slash_commands::TerminalSlashCommand, true);
|
||||
slash_command_registry.register_command(assistant_slash_commands::NowSlashCommand, false);
|
||||
slash_command_registry
|
||||
.register_command(assistant_slash_commands::DiagnosticsSlashCommand, true);
|
||||
|
||||
@@ -5,7 +5,7 @@ mod tool_picker;
|
||||
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use agent_settings::AgentSettings;
|
||||
use assistant_settings::AssistantSettings;
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use collections::HashMap;
|
||||
use context_server::ContextServerId;
|
||||
@@ -249,7 +249,7 @@ impl AgentConfiguration {
|
||||
}
|
||||
|
||||
fn render_command_permission(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let always_allow_tool_actions = AgentSettings::get_global(cx).always_allow_tool_actions;
|
||||
let always_allow_tool_actions = AssistantSettings::get_global(cx).always_allow_tool_actions;
|
||||
|
||||
h_flex()
|
||||
.gap_4()
|
||||
@@ -277,7 +277,7 @@ impl AgentConfiguration {
|
||||
let fs = self.fs.clone();
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file::<AgentSettings>(
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| {
|
||||
@@ -290,7 +290,7 @@ impl AgentConfiguration {
|
||||
}
|
||||
|
||||
fn render_single_file_review(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let single_file_review = AgentSettings::get_global(cx).single_file_review;
|
||||
let single_file_review = AssistantSettings::get_global(cx).single_file_review;
|
||||
|
||||
h_flex()
|
||||
.gap_4()
|
||||
@@ -315,7 +315,7 @@ impl AgentConfiguration {
|
||||
let fs = self.fs.clone();
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file::<AgentSettings>(
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| {
|
||||
@@ -327,44 +327,6 @@ impl AgentConfiguration {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_sound_notification(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let play_sound_when_agent_done = AgentSettings::get_global(cx).play_sound_when_agent_done;
|
||||
|
||||
h_flex()
|
||||
.gap_4()
|
||||
.justify_between()
|
||||
.flex_wrap()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.max_w_5_6()
|
||||
.child(Label::new("Play sound when finished generating"))
|
||||
.child(
|
||||
Label::new(
|
||||
"Hear a notification sound when the agent is done generating changes or needs your input.",
|
||||
)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Switch::new("play-sound-notification-switch", play_sound_when_agent_done.into())
|
||||
.color(SwitchColor::Accent)
|
||||
.on_click({
|
||||
let fs = self.fs.clone();
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file::<AgentSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| {
|
||||
settings.set_play_sound_when_agent_done(allow);
|
||||
},
|
||||
);
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_general_settings_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
@@ -375,7 +337,6 @@ impl AgentConfiguration {
|
||||
.child(Headline::new("General Settings"))
|
||||
.child(self.render_command_permission(cx))
|
||||
.child(self.render_single_file_review(cx))
|
||||
.child(self.render_sound_notification(cx))
|
||||
}
|
||||
|
||||
fn render_context_servers_section(
|
||||
|
||||
@@ -2,7 +2,7 @@ mod profile_modal_header;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent_settings::{AgentProfile, AgentProfileId, AgentSettings, builtin_profiles};
|
||||
use assistant_settings::{AgentProfile, AgentProfileId, AssistantSettings, builtin_profiles};
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use convert_case::{Case, Casing as _};
|
||||
use editor::Editor;
|
||||
@@ -42,7 +42,7 @@ enum Mode {
|
||||
|
||||
impl Mode {
|
||||
pub fn choose_profile(_window: &mut Window, cx: &mut Context<ManageProfilesModal>) -> Self {
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
|
||||
let mut builtin_profiles = Vec::new();
|
||||
let mut custom_profiles = Vec::new();
|
||||
@@ -196,7 +196,7 @@ impl ManageProfilesModal {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
let Some(profile) = settings.profiles.get(&profile_id).cloned() else {
|
||||
return;
|
||||
};
|
||||
@@ -234,7 +234,7 @@ impl ManageProfilesModal {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
let Some(profile) = settings.profiles.get(&profile_id).cloned() else {
|
||||
return;
|
||||
};
|
||||
@@ -270,7 +270,7 @@ impl ManageProfilesModal {
|
||||
match &self.mode {
|
||||
Mode::ChooseProfile { .. } => {}
|
||||
Mode::NewProfile(mode) => {
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
|
||||
let base_profile = mode
|
||||
.base_profile_id
|
||||
@@ -332,7 +332,7 @@ impl ManageProfilesModal {
|
||||
profile: AgentProfile,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
update_settings_file::<AgentSettings>(self.fs.clone(), cx, {
|
||||
update_settings_file::<AssistantSettings>(self.fs.clone(), cx, {
|
||||
move |settings, _cx| {
|
||||
settings.create_profile(profile_id, profile).log_err();
|
||||
}
|
||||
@@ -485,7 +485,7 @@ impl ManageProfilesModal {
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
|
||||
let base_profile_name = mode.base_profile_id.as_ref().map(|base_profile_id| {
|
||||
settings
|
||||
@@ -518,7 +518,7 @@ impl ManageProfilesModal {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
|
||||
let profile_id = &settings.default_profile;
|
||||
let profile_name = settings
|
||||
@@ -712,7 +712,7 @@ impl ManageProfilesModal {
|
||||
|
||||
impl Render for ManageProfilesModal {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
|
||||
let go_back_item = div()
|
||||
.id("cancel-item")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
use agent_settings::{
|
||||
AgentProfile, AgentProfileContent, AgentProfileId, AgentSettings, AgentSettingsContent,
|
||||
use assistant_settings::{
|
||||
AgentProfile, AgentProfileContent, AgentProfileId, AssistantSettings, AssistantSettingsContent,
|
||||
ContextServerPresetContent,
|
||||
};
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
@@ -259,7 +259,7 @@ impl PickerDelegate for ToolPickerDelegate {
|
||||
is_enabled
|
||||
};
|
||||
|
||||
let active_profile_id = &AgentSettings::get_global(cx).default_profile;
|
||||
let active_profile_id = &AssistantSettings::get_global(cx).default_profile;
|
||||
if active_profile_id == &self.profile_id {
|
||||
self.thread_store
|
||||
.update(cx, |this, cx| {
|
||||
@@ -268,12 +268,12 @@ impl PickerDelegate for ToolPickerDelegate {
|
||||
.log_err();
|
||||
}
|
||||
|
||||
update_settings_file::<AgentSettings>(self.fs.clone(), cx, {
|
||||
update_settings_file::<AssistantSettings>(self.fs.clone(), cx, {
|
||||
let profile_id = self.profile_id.clone();
|
||||
let default_profile = self.profile.clone();
|
||||
let server_id = server_id.clone();
|
||||
let tool_name = tool_name.clone();
|
||||
move |settings: &mut AgentSettingsContent, _cx| {
|
||||
move |settings: &mut AssistantSettingsContent, _cx| {
|
||||
settings
|
||||
.v2_setting(|v2_settings| {
|
||||
let profiles = v2_settings.profiles.get_or_insert_default();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll, Thread, ThreadEvent};
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::Result;
|
||||
use assistant_settings::AssistantSettings;
|
||||
use buffer_diff::DiffHunkStatus;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
@@ -699,7 +699,7 @@ fn render_diff_hunk_controls(
|
||||
.rounded_b_md()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.gap_1()
|
||||
.block_mouse_except_scroll()
|
||||
.occlude()
|
||||
.shadow_md()
|
||||
.children(vec![
|
||||
Button::new(("reject", row as u64), "Reject")
|
||||
@@ -1253,9 +1253,9 @@ impl AgentDiff {
|
||||
|
||||
let settings_subscription = cx.observe_global_in::<SettingsStore>(window, {
|
||||
let workspace = workspace.clone();
|
||||
let mut was_active = AgentSettings::get_global(cx).single_file_review;
|
||||
let mut was_active = AssistantSettings::get_global(cx).single_file_review;
|
||||
move |this, window, cx| {
|
||||
let is_active = AgentSettings::get_global(cx).single_file_review;
|
||||
let is_active = AssistantSettings::get_global(cx).single_file_review;
|
||||
if was_active != is_active {
|
||||
was_active = is_active;
|
||||
this.update_reviewing_editors(&workspace, window, cx);
|
||||
@@ -1348,7 +1348,6 @@ impl AgentDiff {
|
||||
ThreadEvent::NewRequest
|
||||
| ThreadEvent::Stopped(Ok(StopReason::EndTurn))
|
||||
| ThreadEvent::Stopped(Ok(StopReason::MaxTokens))
|
||||
| ThreadEvent::Stopped(Ok(StopReason::Refusal))
|
||||
| ThreadEvent::Stopped(Err(_))
|
||||
| ThreadEvent::ShowError(_)
|
||||
| ThreadEvent::CompletionCanceled => {
|
||||
@@ -1461,7 +1460,7 @@ impl AgentDiff {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if !AgentSettings::get_global(cx).single_file_review {
|
||||
if !AssistantSettings::get_global(cx).single_file_review {
|
||||
for (editor, _) in self.reviewing_editors.drain() {
|
||||
editor
|
||||
.update(cx, |editor, cx| editor.end_temporary_diff_override(cx))
|
||||
@@ -1736,7 +1735,7 @@ impl editor::Addon for EditorAgentDiffAddon {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{Keep, ThreadStore, thread_store};
|
||||
use agent_settings::AgentSettings;
|
||||
use assistant_settings::AssistantSettings;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use editor::EditorSettings;
|
||||
use gpui::{TestAppContext, UpdateGlobal, VisualTestContext};
|
||||
@@ -1755,7 +1754,7 @@ mod tests {
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
AgentSettings::register(cx);
|
||||
AssistantSettings::register(cx);
|
||||
prompt_store::init(cx);
|
||||
thread_store::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
@@ -1911,7 +1910,7 @@ mod tests {
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
AgentSettings::register(cx);
|
||||
AssistantSettings::register(cx);
|
||||
prompt_store::init(cx);
|
||||
thread_store::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use agent_settings::AgentSettings;
|
||||
use assistant_settings::AssistantSettings;
|
||||
use fs::Fs;
|
||||
use gpui::{Entity, FocusHandle, SharedString};
|
||||
use picker::popover_menu::PickerPopoverMenu;
|
||||
|
||||
use crate::Thread;
|
||||
use assistant_context_editor::language_model_selector::{
|
||||
LanguageModelSelector, ToggleModelSelector, language_model_selector,
|
||||
};
|
||||
use language_model::{ConfiguredModel, LanguageModelRegistry};
|
||||
use language_model_selector::{
|
||||
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
|
||||
};
|
||||
use settings::update_settings_file;
|
||||
use std::sync::Arc;
|
||||
use ui::{PopoverMenuHandle, Tooltip, prelude::*};
|
||||
@@ -36,7 +35,7 @@ impl AgentModelSelector {
|
||||
Self {
|
||||
selector: cx.new(move |cx| {
|
||||
let fs = fs.clone();
|
||||
language_model_selector(
|
||||
LanguageModelSelector::new(
|
||||
{
|
||||
let model_type = model_type.clone();
|
||||
move |cx| match &model_type {
|
||||
@@ -64,7 +63,7 @@ impl AgentModelSelector {
|
||||
);
|
||||
}
|
||||
});
|
||||
update_settings_file::<AgentSettings>(
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _cx| {
|
||||
@@ -73,7 +72,7 @@ impl AgentModelSelector {
|
||||
);
|
||||
}
|
||||
ModelType::InlineAssistant => {
|
||||
update_settings_file::<AgentSettings>(
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _cx| {
|
||||
@@ -101,14 +100,15 @@ impl AgentModelSelector {
|
||||
}
|
||||
|
||||
impl Render for AgentModelSelector {
|
||||
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 focus_handle = self.focus_handle.clone();
|
||||
|
||||
let model = self.selector.read(cx).delegate.active_model(cx);
|
||||
let model = self.selector.read(cx).active_model(cx);
|
||||
let model_name = model
|
||||
.map(|model| model.model.name().0)
|
||||
.unwrap_or_else(|| SharedString::from("No model selected"));
|
||||
PickerPopoverMenu::new(
|
||||
|
||||
LanguageModelSelectorPopoverMenu::new(
|
||||
self.selector.clone(),
|
||||
Button::new("active-model", model_name)
|
||||
.label_size(LabelSize::Small)
|
||||
@@ -127,9 +127,7 @@ impl Render for AgentModelSelector {
|
||||
)
|
||||
},
|
||||
gpui::Corner::BottomRight,
|
||||
cx,
|
||||
)
|
||||
.with_handle(self.menu_handle.clone())
|
||||
.render(window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
use std::ops::Range;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
||||
use markdown::Markdown;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use agent_settings::{AgentDockPosition, AgentSettings, CompletionMode, DefaultView};
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_context_editor::{
|
||||
AgentPanelDelegate, AssistantContext, ConfigurationError, ContextEditor, ContextEvent,
|
||||
ContextSummary, SlashCommandCompletionProvider, humanize_token_count,
|
||||
make_lsp_adapter_delegate, render_remaining_tokens,
|
||||
};
|
||||
use assistant_settings::{AssistantDockPosition, AssistantSettings};
|
||||
use assistant_slash_command::SlashCommandWorkingSet;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
|
||||
use assistant_context_editor::language_model_selector::ToggleModelSelector;
|
||||
use client::{UserStore, zed_urls};
|
||||
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
|
||||
use fs::Fs;
|
||||
@@ -31,6 +30,7 @@ use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
LanguageModelProviderTosView, LanguageModelRegistry, RequestUsage, ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
use project::{Project, ProjectPath, Worktree};
|
||||
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
|
||||
use proto::Plan;
|
||||
@@ -41,8 +41,8 @@ use theme::ThemeSettings;
|
||||
use time::UtcOffset;
|
||||
use ui::utils::WithRemSize;
|
||||
use ui::{
|
||||
Banner, CheckboxWithLabel, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu,
|
||||
PopoverMenuHandle, ProgressBar, Tab, Tooltip, Vector, VectorName, prelude::*,
|
||||
Banner, CheckboxWithLabel, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle,
|
||||
ProgressBar, Tab, Tooltip, Vector, VectorName, prelude::*,
|
||||
};
|
||||
use util::{ResultExt as _, maybe};
|
||||
use workspace::dock::{DockPosition, Panel, PanelEvent};
|
||||
@@ -52,7 +52,7 @@ use workspace::{
|
||||
use zed_actions::agent::{OpenConfiguration, OpenOnboardingModal, ResetOnboarding};
|
||||
use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus};
|
||||
use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize};
|
||||
use zed_llm_client::{CompletionIntent, UsageLimit};
|
||||
use zed_llm_client::UsageLimit;
|
||||
|
||||
use crate::active_thread::{self, ActiveThread, ActiveThreadEvent};
|
||||
use crate::agent_configuration::{AgentConfiguration, AssistantConfigurationEvent};
|
||||
@@ -64,11 +64,10 @@ use crate::thread_history::{HistoryEntryElement, ThreadHistory};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::ui::AgentOnboardingModal;
|
||||
use crate::{
|
||||
AddContextServer, AgentDiffPane, ContextStore, ContinueThread, ContinueWithBurnMode,
|
||||
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
|
||||
NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell,
|
||||
ResetTrialUpsell, TextThreadStore, ThreadEvent, ToggleBurnMode, ToggleContextPicker,
|
||||
ToggleNavigationMenu, ToggleOptionsMenu,
|
||||
AddContextServer, AgentDiffPane, ContextStore, DeleteRecentlyOpenThread, ExpandMessageEditor,
|
||||
Follow, InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
|
||||
OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, TextThreadStore, ThreadEvent,
|
||||
ToggleContextPicker, ToggleNavigationMenu, ToggleOptionsMenu,
|
||||
};
|
||||
|
||||
const AGENT_PANEL_KEY: &str = "agent_panel";
|
||||
@@ -158,7 +157,7 @@ pub fn init(cx: &mut App) {
|
||||
window.refresh();
|
||||
})
|
||||
.register_action(|_workspace, _: &ResetTrialUpsell, _window, cx| {
|
||||
Upsell::set_dismissed(false, cx);
|
||||
TrialUpsell::set_dismissed(false, cx);
|
||||
})
|
||||
.register_action(|_workspace, _: &ResetTrialEndUpsell, _window, cx| {
|
||||
TrialEndUpsell::set_dismissed(false, cx);
|
||||
@@ -174,7 +173,7 @@ enum ActiveView {
|
||||
thread: WeakEntity<Thread>,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
},
|
||||
TextThread {
|
||||
PromptEditor {
|
||||
context_editor: Entity<ContextEditor>,
|
||||
title_editor: Entity<Editor>,
|
||||
buffer_search_bar: Entity<BufferSearchBar>,
|
||||
@@ -194,7 +193,7 @@ impl ActiveView {
|
||||
pub fn which_font_size_used(&self) -> WhichFontSize {
|
||||
match self {
|
||||
ActiveView::Thread { .. } | ActiveView::History => WhichFontSize::AgentFont,
|
||||
ActiveView::TextThread { .. } => WhichFontSize::BufferFont,
|
||||
ActiveView::PromptEditor { .. } => WhichFontSize::BufferFont,
|
||||
ActiveView::Configuration => WhichFontSize::None,
|
||||
}
|
||||
}
|
||||
@@ -333,7 +332,7 @@ impl ActiveView {
|
||||
buffer_search_bar.set_active_pane_item(Some(&context_editor), window, cx)
|
||||
});
|
||||
|
||||
Self::TextThread {
|
||||
Self::PromptEditor {
|
||||
context_editor,
|
||||
title_editor: editor,
|
||||
buffer_search_bar,
|
||||
@@ -371,7 +370,8 @@ pub struct AgentPanel {
|
||||
height: Option<Pixels>,
|
||||
zoomed: bool,
|
||||
pending_serialization: Option<Task<Result<()>>>,
|
||||
hide_upsell: bool,
|
||||
hide_trial_upsell: bool,
|
||||
_trial_markdown: Entity<Markdown>,
|
||||
}
|
||||
|
||||
impl AgentPanel {
|
||||
@@ -524,30 +524,7 @@ impl AgentPanel {
|
||||
|
||||
cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
|
||||
|
||||
let panel_type = AgentSettings::get_global(cx).default_view;
|
||||
let active_view = match panel_type {
|
||||
DefaultView::Thread => ActiveView::thread(thread.clone(), window, cx),
|
||||
DefaultView::TextThread => {
|
||||
let context =
|
||||
context_store.update(cx, |context_store, cx| context_store.create(cx));
|
||||
let lsp_adapter_delegate = make_lsp_adapter_delegate(&project.clone(), cx).unwrap();
|
||||
let context_editor = cx.new(|cx| {
|
||||
let mut editor = ContextEditor::for_context(
|
||||
context,
|
||||
fs.clone(),
|
||||
workspace.clone(),
|
||||
project.clone(),
|
||||
lsp_adapter_delegate,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.insert_default_prompt(window, cx);
|
||||
editor
|
||||
});
|
||||
ActiveView::prompt_editor(context_editor, language_registry.clone(), window, cx)
|
||||
}
|
||||
};
|
||||
|
||||
let active_view = ActiveView::thread(thread.clone(), window, cx);
|
||||
let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
|
||||
if let ThreadEvent::MessageAdded(_) = &event {
|
||||
// needed to leave empty state
|
||||
@@ -699,6 +676,15 @@ impl AgentPanel {
|
||||
},
|
||||
);
|
||||
|
||||
let trial_markdown = cx.new(|cx| {
|
||||
Markdown::new(
|
||||
include_str!("trial_markdown.md").into(),
|
||||
Some(language_registry.clone()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
Self {
|
||||
active_view,
|
||||
workspace,
|
||||
@@ -735,7 +721,8 @@ impl AgentPanel {
|
||||
height: None,
|
||||
zoomed: false,
|
||||
pending_serialization: None,
|
||||
hide_upsell: false,
|
||||
hide_trial_upsell: false,
|
||||
_trial_markdown: trial_markdown,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -917,8 +904,8 @@ impl AgentPanel {
|
||||
open_rules_library(
|
||||
self.language_registry.clone(),
|
||||
Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
|
||||
Rc::new(|| {
|
||||
Rc::new(SlashCommandCompletionProvider::new(
|
||||
Arc::new(|| {
|
||||
Box::new(SlashCommandCompletionProvider::new(
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
None,
|
||||
None,
|
||||
@@ -1084,23 +1071,9 @@ impl AgentPanel {
|
||||
pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
|
||||
match self.active_view {
|
||||
ActiveView::Configuration | ActiveView::History => {
|
||||
if let Some(previous_view) = self.previous_view.take() {
|
||||
self.active_view = previous_view;
|
||||
|
||||
match &self.active_view {
|
||||
ActiveView::Thread { .. } => {
|
||||
self.message_editor.focus_handle(cx).focus(window);
|
||||
}
|
||||
ActiveView::TextThread { context_editor, .. } => {
|
||||
context_editor.focus_handle(cx).focus(window);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
self.active_view =
|
||||
ActiveView::thread(self.thread.read(cx).thread().clone(), window, cx);
|
||||
self.message_editor.focus_handle(cx).focus(window);
|
||||
}
|
||||
self.active_view =
|
||||
ActiveView::thread(self.thread.read(cx).thread().clone(), window, cx);
|
||||
self.message_editor.focus_handle(cx).focus(window);
|
||||
cx.notify();
|
||||
}
|
||||
_ => {}
|
||||
@@ -1265,7 +1238,7 @@ impl AgentPanel {
|
||||
.map_or(true, |model| model.provider.id() != provider.id())
|
||||
{
|
||||
if let Some(model) = provider.default_model(cx) {
|
||||
update_settings_file::<AgentSettings>(
|
||||
update_settings_file::<AssistantSettings>(
|
||||
self.fs.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model),
|
||||
@@ -1298,52 +1271,9 @@ impl AgentPanel {
|
||||
matches!(self.active_view, ActiveView::Thread { .. })
|
||||
}
|
||||
|
||||
fn continue_conversation(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let thread_state = self.thread.read(cx).thread().read(cx);
|
||||
if !thread_state.tool_use_limit_reached() {
|
||||
return;
|
||||
}
|
||||
|
||||
let model = thread_state.configured_model().map(|cm| cm.model.clone());
|
||||
if let Some(model) = model {
|
||||
self.thread.update(cx, |active_thread, cx| {
|
||||
active_thread.thread().update(cx, |thread, cx| {
|
||||
thread.insert_invisible_continue_message(cx);
|
||||
thread.advance_prompt_id();
|
||||
thread.send_to_model(
|
||||
model,
|
||||
CompletionIntent::UserPrompt,
|
||||
Some(window.window_handle()),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
log::warn!("No configured model available for continuation");
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_burn_mode(
|
||||
&mut self,
|
||||
_: &ToggleBurnMode,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.thread.update(cx, |active_thread, cx| {
|
||||
active_thread.thread().update(cx, |thread, _cx| {
|
||||
let current_mode = thread.completion_mode();
|
||||
|
||||
thread.set_completion_mode(match current_mode {
|
||||
CompletionMode::Burn => CompletionMode::Normal,
|
||||
CompletionMode::Normal => CompletionMode::Burn,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn active_context_editor(&self) -> Option<Entity<ContextEditor>> {
|
||||
match &self.active_view {
|
||||
ActiveView::TextThread { context_editor, .. } => Some(context_editor.clone()),
|
||||
ActiveView::PromptEditor { context_editor, .. } => Some(context_editor.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -1366,12 +1296,6 @@ impl AgentPanel {
|
||||
let current_is_history = matches!(self.active_view, ActiveView::History);
|
||||
let new_is_history = matches!(new_view, ActiveView::History);
|
||||
|
||||
let current_is_config = matches!(self.active_view, ActiveView::Configuration);
|
||||
let new_is_config = matches!(new_view, ActiveView::Configuration);
|
||||
|
||||
let current_is_special = current_is_history || current_is_config;
|
||||
let new_is_special = new_is_history || new_is_config;
|
||||
|
||||
match &self.active_view {
|
||||
ActiveView::Thread { thread, .. } => {
|
||||
if let Some(thread) = thread.upgrade() {
|
||||
@@ -1383,7 +1307,7 @@ impl AgentPanel {
|
||||
}
|
||||
}
|
||||
}
|
||||
ActiveView::TextThread { context_editor, .. } => {
|
||||
ActiveView::PromptEditor { context_editor, .. } => {
|
||||
let context = context_editor.read(cx).context();
|
||||
// When switching away from an unsaved text thread, delete its entry.
|
||||
if context.read(cx).path().is_none() {
|
||||
@@ -1403,7 +1327,7 @@ impl AgentPanel {
|
||||
store.push_recently_opened_entry(RecentEntry::Thread(id, thread), cx);
|
||||
}
|
||||
}),
|
||||
ActiveView::TextThread { context_editor, .. } => {
|
||||
ActiveView::PromptEditor { context_editor, .. } => {
|
||||
self.history_store.update(cx, |store, cx| {
|
||||
let context = context_editor.read(cx).context().clone();
|
||||
store.push_recently_opened_entry(RecentEntry::Context(context), cx)
|
||||
@@ -1412,12 +1336,12 @@ impl AgentPanel {
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if current_is_special && !new_is_special {
|
||||
if current_is_history && !new_is_history {
|
||||
self.active_view = new_view;
|
||||
} else if !current_is_special && new_is_special {
|
||||
} else if !current_is_history && new_is_history {
|
||||
self.previous_view = Some(std::mem::replace(&mut self.active_view, new_view));
|
||||
} else {
|
||||
if !new_is_special {
|
||||
if !new_is_history {
|
||||
self.previous_view = None;
|
||||
}
|
||||
self.active_view = new_view;
|
||||
@@ -1432,7 +1356,7 @@ impl Focusable for AgentPanel {
|
||||
match &self.active_view {
|
||||
ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
|
||||
ActiveView::History => self.history.focus_handle(cx),
|
||||
ActiveView::TextThread { context_editor, .. } => context_editor.focus_handle(cx),
|
||||
ActiveView::PromptEditor { context_editor, .. } => context_editor.focus_handle(cx),
|
||||
ActiveView::Configuration => {
|
||||
if let Some(configuration) = self.configuration.as_ref() {
|
||||
configuration.focus_handle(cx)
|
||||
@@ -1445,10 +1369,10 @@ impl Focusable for AgentPanel {
|
||||
}
|
||||
|
||||
fn agent_panel_dock_position(cx: &App) -> DockPosition {
|
||||
match AgentSettings::get_global(cx).dock {
|
||||
AgentDockPosition::Left => DockPosition::Left,
|
||||
AgentDockPosition::Bottom => DockPosition::Bottom,
|
||||
AgentDockPosition::Right => DockPosition::Right,
|
||||
match AssistantSettings::get_global(cx).dock {
|
||||
AssistantDockPosition::Left => DockPosition::Left,
|
||||
AssistantDockPosition::Bottom => DockPosition::Bottom,
|
||||
AssistantDockPosition::Right => DockPosition::Right,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1468,18 +1392,22 @@ impl Panel for AgentPanel {
|
||||
}
|
||||
|
||||
fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
|
||||
settings::update_settings_file::<AgentSettings>(self.fs.clone(), cx, move |settings, _| {
|
||||
let dock = match position {
|
||||
DockPosition::Left => AgentDockPosition::Left,
|
||||
DockPosition::Bottom => AgentDockPosition::Bottom,
|
||||
DockPosition::Right => AgentDockPosition::Right,
|
||||
};
|
||||
settings.set_dock(dock);
|
||||
});
|
||||
settings::update_settings_file::<AssistantSettings>(
|
||||
self.fs.clone(),
|
||||
cx,
|
||||
move |settings, _| {
|
||||
let dock = match position {
|
||||
DockPosition::Left => AssistantDockPosition::Left,
|
||||
DockPosition::Bottom => AssistantDockPosition::Bottom,
|
||||
DockPosition::Right => AssistantDockPosition::Right,
|
||||
};
|
||||
settings.set_dock(dock);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn size(&self, window: &Window, cx: &App) -> Pixels {
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
match self.position(window, cx) {
|
||||
DockPosition::Left | DockPosition::Right => {
|
||||
self.width.unwrap_or(settings.default_width)
|
||||
@@ -1504,7 +1432,8 @@ impl Panel for AgentPanel {
|
||||
}
|
||||
|
||||
fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
|
||||
(self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAssistant)
|
||||
(self.enabled(cx) && AssistantSettings::get_global(cx).button)
|
||||
.then_some(IconName::ZedAssistant)
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
|
||||
@@ -1520,7 +1449,7 @@ impl Panel for AgentPanel {
|
||||
}
|
||||
|
||||
fn enabled(&self, cx: &App) -> bool {
|
||||
AgentSettings::get_global(cx).enabled
|
||||
AssistantSettings::get_global(cx).enabled
|
||||
}
|
||||
|
||||
fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
|
||||
@@ -1584,7 +1513,7 @@ impl AgentPanel {
|
||||
.into_any_element(),
|
||||
}
|
||||
}
|
||||
ActiveView::TextThread {
|
||||
ActiveView::PromptEditor {
|
||||
title_editor,
|
||||
context_editor,
|
||||
..
|
||||
@@ -1676,7 +1605,7 @@ impl AgentPanel {
|
||||
|
||||
let show_token_count = match &self.active_view {
|
||||
ActiveView::Thread { .. } => !is_empty || !editor_empty,
|
||||
ActiveView::TextThread { .. } => true,
|
||||
ActiveView::PromptEditor { .. } => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
@@ -1992,7 +1921,7 @@ impl AgentPanel {
|
||||
|
||||
Some(token_count)
|
||||
}
|
||||
ActiveView::TextThread { context_editor, .. } => {
|
||||
ActiveView::PromptEditor { context_editor, .. } => {
|
||||
let element = render_remaining_tokens(context_editor, cx)?;
|
||||
|
||||
Some(element.into_any_element())
|
||||
@@ -2017,7 +1946,7 @@ impl AgentPanel {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.hide_upsell || Upsell::dismissed() {
|
||||
if self.hide_trial_upsell || TrialUpsell::dismissed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2047,7 +1976,7 @@ impl AgentPanel {
|
||||
true
|
||||
}
|
||||
|
||||
fn render_upsell(
|
||||
fn render_trial_upsell(
|
||||
&self,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -2056,14 +1985,6 @@ impl AgentPanel {
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.user_store.read(cx).account_too_young() {
|
||||
Some(self.render_young_account_upsell(cx).into_any_element())
|
||||
} else {
|
||||
Some(self.render_trial_upsell(cx).into_any_element())
|
||||
}
|
||||
}
|
||||
|
||||
fn render_young_account_upsell(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let checkbox = CheckboxWithLabel::new(
|
||||
"dont-show-again",
|
||||
Label::new("Don't show again").color(Color::Muted),
|
||||
@@ -2071,70 +1992,7 @@ impl AgentPanel {
|
||||
move |toggle_state, _window, cx| {
|
||||
let toggle_state_bool = toggle_state.selected();
|
||||
|
||||
Upsell::set_dismissed(toggle_state_bool, cx);
|
||||
},
|
||||
);
|
||||
|
||||
let contents = div()
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
|
||||
.child(
|
||||
Label::new("Your GitHub account was created less than 30 days ago, so we can't offer you a free trial.")
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
Label::new(
|
||||
"Use your own API keys, upgrade to Zed Pro or send an email to billing-support@zed.dev.",
|
||||
)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.px_neg_1()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.child(h_flex().items_center().gap_1().child(checkbox))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("dismiss-button", "Not Now")
|
||||
.style(ButtonStyle::Transparent)
|
||||
.color(Color::Muted)
|
||||
.on_click({
|
||||
let agent_panel = cx.entity();
|
||||
move |_, _, cx| {
|
||||
agent_panel.update(cx, |this, cx| {
|
||||
this.hide_upsell = true;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("cta-button", "Upgrade to Zed Pro")
|
||||
.style(ButtonStyle::Transparent)
|
||||
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
self.render_upsell_container(cx, contents)
|
||||
}
|
||||
|
||||
fn render_trial_upsell(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let checkbox = CheckboxWithLabel::new(
|
||||
"dont-show-again",
|
||||
Label::new("Don't show again").color(Color::Muted),
|
||||
ToggleState::Unselected,
|
||||
move |toggle_state, _window, cx| {
|
||||
let toggle_state_bool = toggle_state.selected();
|
||||
|
||||
Upsell::set_dismissed(toggle_state_bool, cx);
|
||||
TrialUpsell::set_dismissed(toggle_state_bool, cx);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -2172,7 +2030,7 @@ impl AgentPanel {
|
||||
let agent_panel = cx.entity();
|
||||
move |_, _, cx| {
|
||||
agent_panel.update(cx, |this, cx| {
|
||||
this.hide_upsell = true;
|
||||
this.hide_trial_upsell = true;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
@@ -2186,7 +2044,7 @@ impl AgentPanel {
|
||||
),
|
||||
);
|
||||
|
||||
self.render_upsell_container(cx, contents)
|
||||
Some(self.render_upsell_container(cx, contents))
|
||||
}
|
||||
|
||||
fn render_trial_end_upsell(
|
||||
@@ -2638,11 +2496,7 @@ impl AgentPanel {
|
||||
})
|
||||
}
|
||||
|
||||
fn render_tool_use_limit_reached(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<AnyElement> {
|
||||
fn render_tool_use_limit_reached(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
let tool_use_limit_reached = self
|
||||
.thread
|
||||
.read(cx)
|
||||
@@ -2661,59 +2515,17 @@ impl AgentPanel {
|
||||
.configured_model()?
|
||||
.model;
|
||||
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
let max_mode_upsell = if model.supports_max_mode() {
|
||||
" Enable max mode for unlimited tool use."
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let banner = Banner::new()
|
||||
.severity(ui::Severity::Info)
|
||||
.child(Label::new("Consecutive tool use limit reached.").size(LabelSize::Small))
|
||||
.action_slot(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new("continue-conversation", "Continue")
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.label_size(LabelSize::Small)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&ContinueThread,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(10.))),
|
||||
)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.continue_conversation(window, cx);
|
||||
})),
|
||||
)
|
||||
.when(model.supports_max_mode(), |this| {
|
||||
this.child(
|
||||
Button::new("continue-burn-mode", "Continue with Burn Mode")
|
||||
.style(ButtonStyle::Filled)
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.label_size(LabelSize::Small)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&ContinueWithBurnMode,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(10.))),
|
||||
)
|
||||
.tooltip(Tooltip::text("Enable Burn Mode for unlimited tool use."))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.thread.update(cx, |active_thread, cx| {
|
||||
active_thread.thread().update(cx, |thread, _cx| {
|
||||
thread.set_completion_mode(CompletionMode::Burn);
|
||||
});
|
||||
});
|
||||
this.continue_conversation(window, cx);
|
||||
})),
|
||||
)
|
||||
}),
|
||||
);
|
||||
.child(h_flex().child(Label::new(format!(
|
||||
"Consecutive tool use limit reached.{max_mode_upsell}"
|
||||
))));
|
||||
|
||||
Some(div().px_2().pb_2().child(banner).into_any_element())
|
||||
}
|
||||
@@ -2910,7 +2722,7 @@ impl AgentPanel {
|
||||
) -> Div {
|
||||
let mut registrar = buffer_search::DivRegistrar::new(
|
||||
|this, _, _cx| match &this.active_view {
|
||||
ActiveView::TextThread {
|
||||
ActiveView::PromptEditor {
|
||||
buffer_search_bar, ..
|
||||
} => Some(buffer_search_bar.clone()),
|
||||
_ => None,
|
||||
@@ -3028,7 +2840,7 @@ impl AgentPanel {
|
||||
.detach();
|
||||
});
|
||||
}
|
||||
ActiveView::TextThread { context_editor, .. } => {
|
||||
ActiveView::PromptEditor { context_editor, .. } => {
|
||||
context_editor.update(cx, |context_editor, cx| {
|
||||
ContextEditor::insert_dragged_files(
|
||||
context_editor,
|
||||
@@ -3055,7 +2867,7 @@ impl AgentPanel {
|
||||
fn key_context(&self) -> KeyContext {
|
||||
let mut key_context = KeyContext::new_with_defaults();
|
||||
key_context.add("AgentPanel");
|
||||
if matches!(self.active_view, ActiveView::TextThread { .. }) {
|
||||
if matches!(self.active_view, ActiveView::PromptEditor { .. }) {
|
||||
key_context.add("prompt_editor");
|
||||
}
|
||||
key_context
|
||||
@@ -3068,9 +2880,9 @@ impl Render for AgentPanel {
|
||||
// non-obvious implications to the layout of children.
|
||||
//
|
||||
// If you need to change it, please confirm:
|
||||
// - The message editor expands (cmd-option-esc) correctly
|
||||
// - The message editor expands (⌘esc) correctly
|
||||
// - When expanded, the buttons at the bottom of the panel are displayed correctly
|
||||
// - Font size works as expected and can be changed with cmd-+/cmd-
|
||||
// - Font size works as expected and can be changed with ⌘+/⌘-
|
||||
// - Scrolling in all views works as expected
|
||||
// - Files can be dropped into the panel
|
||||
let content = v_flex()
|
||||
@@ -3097,31 +2909,19 @@ impl Render for AgentPanel {
|
||||
.on_action(cx.listener(Self::decrease_font_size))
|
||||
.on_action(cx.listener(Self::reset_font_size))
|
||||
.on_action(cx.listener(Self::toggle_zoom))
|
||||
.on_action(cx.listener(|this, _: &ContinueThread, window, cx| {
|
||||
this.continue_conversation(window, cx);
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &ContinueWithBurnMode, window, cx| {
|
||||
this.thread.update(cx, |active_thread, cx| {
|
||||
active_thread.thread().update(cx, |thread, _cx| {
|
||||
thread.set_completion_mode(CompletionMode::Burn);
|
||||
});
|
||||
});
|
||||
this.continue_conversation(window, cx);
|
||||
}))
|
||||
.on_action(cx.listener(Self::toggle_burn_mode))
|
||||
.child(self.render_toolbar(window, cx))
|
||||
.children(self.render_upsell(window, cx))
|
||||
.children(self.render_trial_upsell(window, cx))
|
||||
.children(self.render_trial_end_upsell(window, cx))
|
||||
.map(|parent| match &self.active_view {
|
||||
ActiveView::Thread { .. } => parent
|
||||
.relative()
|
||||
.child(self.render_active_thread_or_empty_state(window, cx))
|
||||
.children(self.render_tool_use_limit_reached(window, cx))
|
||||
.children(self.render_tool_use_limit_reached(cx))
|
||||
.child(h_flex().child(self.message_editor.clone()))
|
||||
.children(self.render_last_error(cx))
|
||||
.child(self.render_drag_target(cx)),
|
||||
ActiveView::History => parent.child(self.history.clone()),
|
||||
ActiveView::TextThread {
|
||||
ActiveView::PromptEditor {
|
||||
context_editor,
|
||||
buffer_search_bar,
|
||||
..
|
||||
@@ -3299,9 +3099,9 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
struct Upsell;
|
||||
struct TrialUpsell;
|
||||
|
||||
impl Dismissable for Upsell {
|
||||
impl Dismissable for TrialUpsell {
|
||||
const KEY: &'static str = "dismissed-trial-upsell";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::context::ContextLoadResult;
|
||||
use crate::inline_prompt_editor::CodegenStatus;
|
||||
use crate::{context::load_context, context_store::ContextStore};
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::{Context as _, Result};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use client::telemetry::Telemetry;
|
||||
use collections::HashSet;
|
||||
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
|
||||
@@ -34,7 +34,6 @@ use std::{
|
||||
};
|
||||
use streaming_diff::{CharOperation, LineDiff, LineOperation, StreamingDiff};
|
||||
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
pub struct BufferCodegen {
|
||||
alternatives: Vec<Entity<CodegenAlternative>>,
|
||||
@@ -444,7 +443,7 @@ impl CodegenAlternative {
|
||||
}
|
||||
});
|
||||
|
||||
let temperature = AgentSettings::temperature_for_model(&model, cx);
|
||||
let temperature = AssistantSettings::temperature_for_model(&model, cx);
|
||||
|
||||
Ok(cx.spawn(async move |_cx| {
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
@@ -465,7 +464,6 @@ impl CodegenAlternative {
|
||||
LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
intent: Some(CompletionIntent::InlineAssist),
|
||||
mode: None,
|
||||
tools: Vec::new(),
|
||||
tool_choice: None,
|
||||
|
||||
@@ -766,7 +766,6 @@ pub(crate) fn insert_crease_for_mention(
|
||||
|
||||
let ids = editor.insert_creases(vec![crease.clone()], cx);
|
||||
editor.fold_creases(vec![crease], false, window, cx);
|
||||
|
||||
Some(ids[0])
|
||||
})
|
||||
}
|
||||
|
||||
@@ -322,10 +322,7 @@ impl ContextPickerCompletionProvider {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let new_text = format!(
|
||||
"{} ",
|
||||
selection_infos.iter().map(|(_, link, _)| link).join(" ")
|
||||
);
|
||||
let new_text = selection_infos.iter().map(|(_, link, _)| link).join(" ");
|
||||
|
||||
let callback = Arc::new({
|
||||
let context_store = context_store.clone();
|
||||
@@ -423,7 +420,7 @@ impl ContextPickerCompletionProvider {
|
||||
} else {
|
||||
IconName::MessageBubbles
|
||||
};
|
||||
let new_text = format!("{} ", MentionLink::for_thread(&thread_entry));
|
||||
let new_text = MentionLink::for_thread(&thread_entry);
|
||||
let new_text_len = new_text.len();
|
||||
Completion {
|
||||
replace_range: source_range.clone(),
|
||||
@@ -438,7 +435,7 @@ impl ContextPickerCompletionProvider {
|
||||
thread_entry.title().clone(),
|
||||
excerpt_id,
|
||||
source_range.start,
|
||||
new_text_len - 1,
|
||||
new_text_len,
|
||||
editor.clone(),
|
||||
context_store.clone(),
|
||||
move |window, cx| match &thread_entry {
|
||||
@@ -492,7 +489,7 @@ impl ContextPickerCompletionProvider {
|
||||
editor: Entity<Editor>,
|
||||
context_store: Entity<ContextStore>,
|
||||
) -> Completion {
|
||||
let new_text = format!("{} ", MentionLink::for_rule(&rules));
|
||||
let new_text = MentionLink::for_rule(&rules);
|
||||
let new_text_len = new_text.len();
|
||||
Completion {
|
||||
replace_range: source_range.clone(),
|
||||
@@ -507,7 +504,7 @@ impl ContextPickerCompletionProvider {
|
||||
rules.title.clone(),
|
||||
excerpt_id,
|
||||
source_range.start,
|
||||
new_text_len - 1,
|
||||
new_text_len,
|
||||
editor.clone(),
|
||||
context_store.clone(),
|
||||
move |_, cx| {
|
||||
@@ -529,7 +526,7 @@ impl ContextPickerCompletionProvider {
|
||||
context_store: Entity<ContextStore>,
|
||||
http_client: Arc<HttpClientWithUrl>,
|
||||
) -> Completion {
|
||||
let new_text = format!("{} ", MentionLink::for_fetch(&url_to_fetch));
|
||||
let new_text = MentionLink::for_fetch(&url_to_fetch);
|
||||
let new_text_len = new_text.len();
|
||||
Completion {
|
||||
replace_range: source_range.clone(),
|
||||
@@ -544,7 +541,7 @@ impl ContextPickerCompletionProvider {
|
||||
url_to_fetch.clone(),
|
||||
excerpt_id,
|
||||
source_range.start,
|
||||
new_text_len - 1,
|
||||
new_text_len,
|
||||
editor.clone(),
|
||||
context_store.clone(),
|
||||
move |_, cx| {
|
||||
@@ -553,7 +550,7 @@ impl ContextPickerCompletionProvider {
|
||||
let url_to_fetch = url_to_fetch.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
if let Some(context) = context_store
|
||||
.read_with(cx, |context_store, _| {
|
||||
.update(cx, |context_store, _| {
|
||||
context_store.get_url_context(url_to_fetch.clone())
|
||||
})
|
||||
.ok()?
|
||||
@@ -614,7 +611,7 @@ impl ContextPickerCompletionProvider {
|
||||
crease_icon_path.clone()
|
||||
};
|
||||
|
||||
let new_text = format!("{} ", MentionLink::for_file(&file_name, &full_path));
|
||||
let new_text = MentionLink::for_file(&file_name, &full_path);
|
||||
let new_text_len = new_text.len();
|
||||
Completion {
|
||||
replace_range: source_range.clone(),
|
||||
@@ -629,7 +626,7 @@ impl ContextPickerCompletionProvider {
|
||||
file_name,
|
||||
excerpt_id,
|
||||
source_range.start,
|
||||
new_text_len - 1,
|
||||
new_text_len,
|
||||
editor,
|
||||
context_store.clone(),
|
||||
move |_, cx| {
|
||||
@@ -685,7 +682,7 @@ impl ContextPickerCompletionProvider {
|
||||
label.push_str(" ", None);
|
||||
label.push_str(&file_name, comment_id);
|
||||
|
||||
let new_text = format!("{} ", MentionLink::for_symbol(&symbol.name, &full_path));
|
||||
let new_text = MentionLink::for_symbol(&symbol.name, &full_path);
|
||||
let new_text_len = new_text.len();
|
||||
Some(Completion {
|
||||
replace_range: source_range.clone(),
|
||||
@@ -700,7 +697,7 @@ impl ContextPickerCompletionProvider {
|
||||
symbol.name.clone().into(),
|
||||
excerpt_id,
|
||||
source_range.start,
|
||||
new_text_len - 1,
|
||||
new_text_len,
|
||||
editor.clone(),
|
||||
context_store.clone(),
|
||||
move |_, cx| {
|
||||
@@ -1216,7 +1213,7 @@ mod tests {
|
||||
assert_eq!(worktrees.len(), 1);
|
||||
worktrees.pop().unwrap()
|
||||
});
|
||||
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
|
||||
let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
|
||||
|
||||
let mut cx = VisualTestContext::from_window(*window.deref(), cx);
|
||||
|
||||
@@ -1289,7 +1286,7 @@ mod tests {
|
||||
.map(Entity::downgrade)
|
||||
});
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
|
||||
editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(
|
||||
workspace.downgrade(),
|
||||
context_store.downgrade(),
|
||||
None,
|
||||
@@ -1356,7 +1353,7 @@ mod tests {
|
||||
});
|
||||
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "Lorem [@one.txt](@file:dir/a/one.txt) ");
|
||||
assert_eq!(editor.text(cx), "Lorem [@one.txt](@file:dir/a/one.txt)",);
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
fold_ranges(editor, cx),
|
||||
@@ -1367,7 +1364,7 @@ mod tests {
|
||||
cx.simulate_input(" ");
|
||||
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "Lorem [@one.txt](@file:dir/a/one.txt) ");
|
||||
assert_eq!(editor.text(cx), "Lorem [@one.txt](@file:dir/a/one.txt) ",);
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
fold_ranges(editor, cx),
|
||||
@@ -1380,7 +1377,7 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum ",
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum ",
|
||||
);
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
@@ -1394,7 +1391,7 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum @file ",
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum @file ",
|
||||
);
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
@@ -1412,14 +1409,14 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt) "
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt)"
|
||||
);
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
fold_ranges(editor, cx),
|
||||
vec![
|
||||
Point::new(0, 6)..Point::new(0, 37),
|
||||
Point::new(0, 45)..Point::new(0, 80)
|
||||
Point::new(0, 44)..Point::new(0, 79)
|
||||
]
|
||||
);
|
||||
});
|
||||
@@ -1429,14 +1426,14 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt) \n@"
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt)\n@"
|
||||
);
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
fold_ranges(editor, cx),
|
||||
vec![
|
||||
Point::new(0, 6)..Point::new(0, 37),
|
||||
Point::new(0, 45)..Point::new(0, 80)
|
||||
Point::new(0, 44)..Point::new(0, 79)
|
||||
]
|
||||
);
|
||||
});
|
||||
@@ -1450,14 +1447,14 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt) \n[@six.txt](@file:dir/b/six.txt) "
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt)\n[@six.txt](@file:dir/b/six.txt)"
|
||||
);
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
fold_ranges(editor, cx),
|
||||
vec![
|
||||
Point::new(0, 6)..Point::new(0, 37),
|
||||
Point::new(0, 45)..Point::new(0, 80),
|
||||
Point::new(0, 44)..Point::new(0, 79),
|
||||
Point::new(1, 0)..Point::new(1, 31)
|
||||
]
|
||||
);
|
||||
|
||||
@@ -58,10 +58,9 @@ impl ContextStore {
|
||||
self.context_set.iter().map(|entry| entry.as_ref())
|
||||
}
|
||||
|
||||
pub fn clear(&mut self, cx: &mut Context<Self>) {
|
||||
pub fn clear(&mut self) {
|
||||
self.context_set.clear();
|
||||
self.context_thread_ids.clear();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn new_context_for_thread(
|
||||
|
||||
@@ -4,8 +4,8 @@ use std::ops::Range;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::{Context as _, Result};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use client::telemetry::Telemetry;
|
||||
use collections::{HashMap, HashSet, VecDeque, hash_map};
|
||||
use editor::display_map::EditorMargins;
|
||||
@@ -134,7 +134,7 @@ impl InlineAssistant {
|
||||
let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
let enabled = AgentSettings::get_global(cx).enabled;
|
||||
let enabled = AssistantSettings::get_global(cx).enabled;
|
||||
terminal_panel.update(cx, |terminal_panel, cx| {
|
||||
terminal_panel.set_assistant_enabled(enabled, cx)
|
||||
});
|
||||
@@ -219,7 +219,7 @@ impl InlineAssistant {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
if !settings.enabled {
|
||||
return;
|
||||
}
|
||||
@@ -1445,7 +1445,7 @@ impl InlineAssistant {
|
||||
style: BlockStyle::Flex,
|
||||
render: Arc::new(move |cx| {
|
||||
div()
|
||||
.block_mouse_except_scroll()
|
||||
.block_mouse_down()
|
||||
.bg(cx.theme().status().deleted_background)
|
||||
.size_full()
|
||||
.h(height as f32 * cx.window.line_height())
|
||||
@@ -1771,7 +1771,7 @@ impl CodeActionProvider for AssistantCodeActionProvider {
|
||||
_: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Vec<CodeAction>>> {
|
||||
if !AgentSettings::get_global(cx).enabled {
|
||||
if !AssistantSettings::get_global(cx).enabled {
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ use crate::terminal_codegen::TerminalCodegen;
|
||||
use crate::thread_store::{TextThreadStore, ThreadStore};
|
||||
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist};
|
||||
use crate::{RemoveAllContext, ToggleContextPicker};
|
||||
use assistant_context_editor::language_model_selector::ToggleModelSelector;
|
||||
use client::ErrorExt;
|
||||
use collections::VecDeque;
|
||||
use db::kvp::Dismissable;
|
||||
@@ -25,10 +24,10 @@ use gpui::{
|
||||
Focusable, FontWeight, Subscription, TextStyle, WeakEntity, Window, anchored, deferred, point,
|
||||
};
|
||||
use language_model::{LanguageModel, LanguageModelRegistry};
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
use parking_lot::Mutex;
|
||||
use settings::Settings;
|
||||
use std::cmp;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use theme::ThemeSettings;
|
||||
use ui::utils::WithRemSize;
|
||||
@@ -100,7 +99,7 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
v_flex()
|
||||
.key_context("PromptEditor")
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.block_mouse_except_scroll()
|
||||
.block_mouse_down()
|
||||
.gap_0p5()
|
||||
.border_y_1()
|
||||
.border_color(cx.theme().status().info_border)
|
||||
@@ -327,7 +326,9 @@ impl<T: 'static> PromptEditor<T> {
|
||||
EditorEvent::Edited { .. } => {
|
||||
if let Some(workspace) = window.root::<Workspace>().flatten() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let is_via_ssh = workspace.project().read(cx).is_via_ssh();
|
||||
let is_via_ssh = workspace
|
||||
.project()
|
||||
.update(cx, |project, _| project.is_via_ssh());
|
||||
|
||||
workspace
|
||||
.client()
|
||||
@@ -372,7 +373,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.context_store.update(cx, |store, cx| store.clear(cx));
|
||||
self.context_store.update(cx, |store, _cx| store.clear());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -891,7 +892,7 @@ impl PromptEditor<BufferCodegen> {
|
||||
|
||||
let prompt_editor_entity = prompt_editor.downgrade();
|
||||
prompt_editor.update(cx, |editor, _| {
|
||||
editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
|
||||
editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(
|
||||
workspace.clone(),
|
||||
context_store.downgrade(),
|
||||
thread_store.clone(),
|
||||
@@ -1062,7 +1063,7 @@ impl PromptEditor<TerminalCodegen> {
|
||||
|
||||
let prompt_editor_entity = prompt_editor.downgrade();
|
||||
prompt_editor.update(cx, |editor, _| {
|
||||
editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
|
||||
editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(
|
||||
workspace.clone(),
|
||||
context_store.downgrade(),
|
||||
thread_store.clone(),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::agent_model_selector::{AgentModelSelector, ModelType};
|
||||
@@ -9,8 +8,7 @@ use crate::ui::{
|
||||
AnimatedLabel, MaxModeTooltip,
|
||||
preview::{AgentPreview, UsageCallout},
|
||||
};
|
||||
use agent_settings::{AgentSettings, CompletionMode};
|
||||
use assistant_context_editor::language_model_selector::ToggleModelSelector;
|
||||
use assistant_settings::{AssistantSettings, CompletionMode};
|
||||
use buffer_diff::BufferDiff;
|
||||
use client::UserStore;
|
||||
use collections::{HashMap, HashSet};
|
||||
@@ -32,6 +30,7 @@ use language_model::{
|
||||
ConfiguredModel, LanguageModelRequestMessage, MessageContent, RequestUsage,
|
||||
ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
use multi_buffer;
|
||||
use project::Project;
|
||||
use prompt_store::PromptStore;
|
||||
@@ -42,7 +41,6 @@ use theme::ThemeSettings;
|
||||
use ui::{Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use util::{ResultExt as _, maybe};
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
|
||||
use crate::context_store::ContextStore;
|
||||
@@ -51,9 +49,8 @@ use crate::profile_selector::ProfileSelector;
|
||||
use crate::thread::{MessageCrease, Thread, TokenUsageRatio};
|
||||
use crate::thread_store::{TextThreadStore, ThreadStore};
|
||||
use crate::{
|
||||
ActiveThread, AgentDiffPane, Chat, ChatWithFollow, ExpandMessageEditor, Follow, NewThread,
|
||||
OpenAgentDiff, RemoveAllContext, ToggleBurnMode, ToggleContextPicker, ToggleProfileSelector,
|
||||
register_agent_preview,
|
||||
ActiveThread, AgentDiffPane, Chat, ExpandMessageEditor, Follow, NewThread, OpenAgentDiff,
|
||||
RemoveAllContext, ToggleContextPicker, ToggleProfileSelector, register_agent_preview,
|
||||
};
|
||||
|
||||
#[derive(RegisterComponent)]
|
||||
@@ -123,7 +120,7 @@ pub(crate) fn create_editor(
|
||||
|
||||
let editor_entity = editor.downgrade();
|
||||
editor.update(cx, |editor, _| {
|
||||
editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
|
||||
editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(
|
||||
workspace,
|
||||
context_store,
|
||||
Some(thread_store),
|
||||
@@ -281,7 +278,7 @@ impl MessageEditor {
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.context_store.update(cx, |store, cx| store.clear(cx));
|
||||
self.context_store.update(cx, |store, _cx| store.clear());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -305,21 +302,6 @@ impl MessageEditor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn chat_with_follow(
|
||||
&mut self,
|
||||
_: &ChatWithFollow,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.workspace
|
||||
.update(cx, |this, cx| {
|
||||
this.follow(CollaboratorId::Agent, window, cx)
|
||||
})
|
||||
.log_err();
|
||||
|
||||
self.chat(&Chat, window, cx);
|
||||
}
|
||||
|
||||
fn is_editor_empty(&self, cx: &App) -> bool {
|
||||
self.editor.read(cx).text(cx).trim().is_empty()
|
||||
}
|
||||
@@ -376,12 +358,7 @@ impl MessageEditor {
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.advance_prompt_id();
|
||||
thread.send_to_model(
|
||||
model,
|
||||
CompletionIntent::UserPrompt,
|
||||
Some(window_handle),
|
||||
cx,
|
||||
);
|
||||
thread.send_to_model(model, Some(window_handle), cx);
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
@@ -477,22 +454,6 @@ impl MessageEditor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_burn_mode(
|
||||
&mut self,
|
||||
_: &ToggleBurnMode,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.thread.update(cx, |thread, _cx| {
|
||||
let active_completion_mode = thread.completion_mode();
|
||||
|
||||
thread.set_completion_mode(match active_completion_mode {
|
||||
CompletionMode::Burn => CompletionMode::Normal,
|
||||
CompletionMode::Normal => CompletionMode::Burn,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn render_max_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
let thread = self.thread.read(cx);
|
||||
let model = thread.configured_model();
|
||||
@@ -501,24 +462,27 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
let active_completion_mode = thread.completion_mode();
|
||||
let burn_mode_enabled = active_completion_mode == CompletionMode::Burn;
|
||||
let icon = if burn_mode_enabled {
|
||||
IconName::ZedBurnModeOn
|
||||
} else {
|
||||
IconName::ZedBurnMode
|
||||
};
|
||||
let max_mode_enabled = active_completion_mode == CompletionMode::Max;
|
||||
|
||||
Some(
|
||||
IconButton::new("burn-mode", icon)
|
||||
Button::new("max-mode", "Max Mode")
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.icon(IconName::ZedMaxMode)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.toggle_state(burn_mode_enabled)
|
||||
.selected_icon_color(Color::Error)
|
||||
.on_click(cx.listener(|this, _event, window, cx| {
|
||||
this.toggle_burn_mode(&ToggleBurnMode, window, cx);
|
||||
.icon_position(IconPosition::Start)
|
||||
.toggle_state(max_mode_enabled)
|
||||
.on_click(cx.listener(move |this, _event, _window, cx| {
|
||||
this.thread.update(cx, |thread, _cx| {
|
||||
thread.set_completion_mode(match active_completion_mode {
|
||||
CompletionMode::Max => CompletionMode::Normal,
|
||||
CompletionMode::Normal => CompletionMode::Max,
|
||||
});
|
||||
});
|
||||
}))
|
||||
.tooltip(move |_window, cx| {
|
||||
cx.new(|_| MaxModeTooltip::new().selected(burn_mode_enabled))
|
||||
cx.new(|_| MaxModeTooltip::new().selected(max_mode_enabled))
|
||||
.into()
|
||||
})
|
||||
.into_any_element(),
|
||||
@@ -598,7 +562,6 @@ impl MessageEditor {
|
||||
v_flex()
|
||||
.key_context("MessageEditor")
|
||||
.on_action(cx.listener(Self::chat))
|
||||
.on_action(cx.listener(Self::chat_with_follow))
|
||||
.on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
|
||||
this.profile_selector
|
||||
.read(cx)
|
||||
@@ -613,7 +576,6 @@ impl MessageEditor {
|
||||
.on_action(cx.listener(Self::remove_all_context))
|
||||
.on_action(cx.listener(Self::move_up))
|
||||
.on_action(cx.listener(Self::expand_message_editor))
|
||||
.on_action(cx.listener(Self::toggle_burn_mode))
|
||||
.capture_action(cx.listener(Self::paste))
|
||||
.gap_2()
|
||||
.p_2()
|
||||
@@ -706,6 +668,7 @@ impl MessageEditor {
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(self.render_follow_toggle(cx))
|
||||
.children(self.render_max_mode_toggle(cx)),
|
||||
)
|
||||
@@ -879,7 +842,7 @@ impl MessageEditor {
|
||||
.border_b_0()
|
||||
.border_color(border_color)
|
||||
.rounded_t_md()
|
||||
.shadow(vec![gpui::BoxShadow {
|
||||
.shadow(smallvec::smallvec![gpui::BoxShadow {
|
||||
color: gpui::black().opacity(0.15),
|
||||
offset: point(px(1.), px(-1.)),
|
||||
blur_radius: px(3.),
|
||||
@@ -1204,10 +1167,9 @@ impl MessageEditor {
|
||||
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(cx)
|
||||
.new_context_for_thread(this.thread.read(cx), None);
|
||||
let new_context = this.context_store.read_with(cx, |context_store, cx| {
|
||||
context_store.new_context_for_thread(this.thread.read(cx), None)
|
||||
});
|
||||
load_context(new_context, &this.project, &this.prompt_store, cx)
|
||||
}) else {
|
||||
return;
|
||||
@@ -1286,13 +1248,12 @@ impl MessageEditor {
|
||||
let request = language_model::LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
intent: None,
|
||||
mode: None,
|
||||
messages: vec![request_message],
|
||||
tools: vec![],
|
||||
tool_choice: None,
|
||||
stop: vec![],
|
||||
temperature: AgentSettings::temperature_for_model(&model.model, cx),
|
||||
temperature: AssistantSettings::temperature_for_model(&model.model, cx),
|
||||
};
|
||||
|
||||
Some(model.model.count_tokens(request, cx))
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent_settings::{
|
||||
AgentDockPosition, AgentProfile, AgentProfileId, AgentSettings, GroupedAgentProfiles,
|
||||
use assistant_settings::{
|
||||
AgentProfile, AgentProfileId, AssistantDockPosition, AssistantSettings, GroupedAgentProfiles,
|
||||
builtin_profiles,
|
||||
};
|
||||
use fs::Fs;
|
||||
use gpui::{Action, Empty, Entity, FocusHandle, Subscription, WeakEntity, prelude::*};
|
||||
use gpui::{Action, Entity, FocusHandle, Subscription, WeakEntity, prelude::*};
|
||||
use language_model::LanguageModelRegistry;
|
||||
use settings::{Settings as _, SettingsStore, update_settings_file};
|
||||
use ui::{
|
||||
@@ -39,7 +39,7 @@ impl ProfileSelector {
|
||||
});
|
||||
|
||||
Self {
|
||||
profiles: GroupedAgentProfiles::from_settings(AgentSettings::get_global(cx)),
|
||||
profiles: GroupedAgentProfiles::from_settings(AssistantSettings::get_global(cx)),
|
||||
fs,
|
||||
thread,
|
||||
thread_store,
|
||||
@@ -54,7 +54,7 @@ impl ProfileSelector {
|
||||
}
|
||||
|
||||
fn refresh_profiles(&mut self, cx: &mut Context<Self>) {
|
||||
self.profiles = GroupedAgentProfiles::from_settings(AgentSettings::get_global(cx));
|
||||
self.profiles = GroupedAgentProfiles::from_settings(AssistantSettings::get_global(cx));
|
||||
}
|
||||
|
||||
fn build_context_menu(
|
||||
@@ -63,7 +63,7 @@ impl ProfileSelector {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<ContextMenu> {
|
||||
ContextMenu::build(window, cx, |mut menu, _window, cx| {
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
for (profile_id, profile) in self.profiles.builtin.iter() {
|
||||
menu = menu.item(self.menu_entry_for_profile(
|
||||
profile_id.clone(),
|
||||
@@ -100,7 +100,7 @@ impl ProfileSelector {
|
||||
&self,
|
||||
profile_id: AgentProfileId,
|
||||
profile: &AgentProfile,
|
||||
settings: &AgentSettings,
|
||||
settings: &AssistantSettings,
|
||||
_cx: &App,
|
||||
) -> ContextMenuEntry {
|
||||
let documentation = match profile.name.to_lowercase().as_str() {
|
||||
@@ -126,7 +126,7 @@ impl ProfileSelector {
|
||||
let thread_store = self.thread_store.clone();
|
||||
let profile_id = profile_id.clone();
|
||||
move |_window, cx| {
|
||||
update_settings_file::<AgentSettings>(fs.clone(), cx, {
|
||||
update_settings_file::<AssistantSettings>(fs.clone(), cx, {
|
||||
let profile_id = profile_id.clone();
|
||||
move |settings, _cx| {
|
||||
settings.set_profile(profile_id.clone());
|
||||
@@ -145,7 +145,7 @@ impl ProfileSelector {
|
||||
|
||||
impl Render for ProfileSelector {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
let profile_id = &settings.default_profile;
|
||||
let profile = settings.profiles.get(profile_id);
|
||||
|
||||
@@ -153,15 +153,17 @@ impl Render for ProfileSelector {
|
||||
.map(|profile| profile.name.clone())
|
||||
.unwrap_or_else(|| "Unknown".into());
|
||||
|
||||
let configured_model = self.thread.read(cx).configured_model().or_else(|| {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
model_registry.default_model()
|
||||
});
|
||||
let Some(configured_model) = configured_model else {
|
||||
return Empty.into_any_element();
|
||||
};
|
||||
let configured_model = self
|
||||
.thread
|
||||
.read_with(cx, |thread, _cx| thread.configured_model())
|
||||
.or_else(|| {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
model_registry.default_model()
|
||||
});
|
||||
let supports_tools =
|
||||
configured_model.map_or(false, |default| default.model.supports_tools());
|
||||
|
||||
if configured_model.model.supports_tools() {
|
||||
if supports_tools {
|
||||
let this = cx.entity().clone();
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
let trigger_button = Button::new("profile-selector-model", selected_profile)
|
||||
@@ -208,10 +210,10 @@ impl Render for ProfileSelector {
|
||||
}
|
||||
}
|
||||
|
||||
fn documentation_side(position: AgentDockPosition) -> DocumentationSide {
|
||||
fn documentation_side(position: AssistantDockPosition) -> DocumentationSide {
|
||||
match position {
|
||||
AgentDockPosition::Left => DocumentationSide::Right,
|
||||
AgentDockPosition::Bottom => DocumentationSide::Left,
|
||||
AgentDockPosition::Right => DocumentationSide::Left,
|
||||
AssistantDockPosition::Left => DocumentationSide::Right,
|
||||
AssistantDockPosition::Bottom => DocumentationSide::Left,
|
||||
AssistantDockPosition::Right => DocumentationSide::Left,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
These files changed since last read:
|
||||
@@ -1,6 +0,0 @@
|
||||
Generate a detailed summary of this conversation. Include:
|
||||
1. A brief overview of what was discussed
|
||||
2. Key facts or information discovered
|
||||
3. Outcomes or conclusions reached
|
||||
4. Any action items or next steps if any
|
||||
Format it in Markdown with headings and bullet points.
|
||||
@@ -1,4 +0,0 @@
|
||||
Generate a concise 3-7 word title for this conversation, omitting punctuation.
|
||||
Go straight to the title, without any preamble and prefix like `Here's a concise suggestion:...` or `Title:`.
|
||||
If the conversation is about a specific subject, include it in the title.
|
||||
Be descriptive. DO NOT speak in the first person.
|
||||
@@ -193,8 +193,7 @@ impl TerminalTransaction {
|
||||
});
|
||||
}
|
||||
|
||||
fn sanitize_input(mut input: String) -> String {
|
||||
input.retain(|c| c != '\r' && c != '\n');
|
||||
input
|
||||
fn sanitize_input(input: String) -> String {
|
||||
input.replace(['\r', '\n'], "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ use crate::inline_prompt_editor::{
|
||||
};
|
||||
use crate::terminal_codegen::{CLEAR_INPUT, CodegenEvent, TerminalCodegen};
|
||||
use crate::thread_store::{TextThreadStore, ThreadStore};
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::{Context as _, Result};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use client::telemetry::Telemetry;
|
||||
use collections::{HashMap, VecDeque};
|
||||
use editor::{MultiBuffer, actions::SelectAll};
|
||||
@@ -25,7 +25,6 @@ use terminal_view::TerminalView;
|
||||
use ui::prelude::*;
|
||||
use util::ResultExt;
|
||||
use workspace::{Toast, Workspace, notifications::NotificationId};
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
pub fn init(
|
||||
fs: Arc<dyn Fs>,
|
||||
@@ -272,7 +271,7 @@ impl TerminalInlineAssistant {
|
||||
.inline_assistant_model()
|
||||
.context("No inline assistant model")?;
|
||||
|
||||
let temperature = AgentSettings::temperature_for_model(&model, cx);
|
||||
let temperature = AssistantSettings::temperature_for_model(&model, cx);
|
||||
|
||||
Ok(cx.background_spawn(async move {
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
@@ -292,7 +291,6 @@ impl TerminalInlineAssistant {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
mode: None,
|
||||
intent: Some(CompletionIntent::TerminalInlineAssist),
|
||||
messages: vec![request_message],
|
||||
tools: Vec::new(),
|
||||
tool_choice: None,
|
||||
|
||||
@@ -4,8 +4,8 @@ use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use agent_settings::{AgentSettings, CompletionMode};
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_settings::{AssistantSettings, CompletionMode};
|
||||
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::HashMap;
|
||||
@@ -24,7 +24,7 @@ use language_model::{
|
||||
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
|
||||
LanguageModelToolResultContent, LanguageModelToolUseId, MessageContent,
|
||||
ModelRequestLimitReachedError, PaymentRequiredError, RequestUsage, Role, SelectedModel,
|
||||
StopReason, TokenUsage,
|
||||
StopReason, TokenUsage, WrappedTextContent,
|
||||
};
|
||||
use postage::stream::Stream as _;
|
||||
use project::Project;
|
||||
@@ -38,7 +38,7 @@ use thiserror::Error;
|
||||
use ui::Window;
|
||||
use util::{ResultExt as _, post_inc};
|
||||
use uuid::Uuid;
|
||||
use zed_llm_client::{CompletionIntent, CompletionRequestStatus};
|
||||
use zed_llm_client::CompletionRequestStatus;
|
||||
|
||||
use crate::ThreadStore;
|
||||
use crate::context::{AgentContext, AgentContextHandle, ContextLoadResult, LoadedContext};
|
||||
@@ -115,7 +115,6 @@ pub struct Message {
|
||||
pub segments: Vec<MessageSegment>,
|
||||
pub loaded_context: LoadedContext,
|
||||
pub creases: Vec<MessageCrease>,
|
||||
pub is_hidden: bool,
|
||||
}
|
||||
|
||||
impl Message {
|
||||
@@ -330,7 +329,7 @@ pub struct Thread {
|
||||
detailed_summary_task: Task<Option<()>>,
|
||||
detailed_summary_tx: postage::watch::Sender<DetailedSummaryState>,
|
||||
detailed_summary_rx: postage::watch::Receiver<DetailedSummaryState>,
|
||||
completion_mode: agent_settings::CompletionMode,
|
||||
completion_mode: assistant_settings::CompletionMode,
|
||||
messages: Vec<Message>,
|
||||
next_message_id: MessageId,
|
||||
last_prompt_id: PromptId,
|
||||
@@ -416,7 +415,7 @@ impl Thread {
|
||||
detailed_summary_task: Task::ready(None),
|
||||
detailed_summary_tx,
|
||||
detailed_summary_rx,
|
||||
completion_mode: AgentSettings::get_global(cx).preferred_completion_mode,
|
||||
completion_mode: AssistantSettings::get_global(cx).preferred_completion_mode,
|
||||
messages: Vec::new(),
|
||||
next_message_id: MessageId(0),
|
||||
last_prompt_id: PromptId::new(),
|
||||
@@ -494,7 +493,7 @@ impl Thread {
|
||||
|
||||
let completion_mode = serialized
|
||||
.completion_mode
|
||||
.unwrap_or_else(|| AgentSettings::get_global(cx).preferred_completion_mode);
|
||||
.unwrap_or_else(|| AssistantSettings::get_global(cx).preferred_completion_mode);
|
||||
|
||||
Self {
|
||||
id,
|
||||
@@ -541,7 +540,6 @@ impl Thread {
|
||||
context: None,
|
||||
})
|
||||
.collect(),
|
||||
is_hidden: message.is_hidden,
|
||||
})
|
||||
.collect(),
|
||||
next_message_id,
|
||||
@@ -562,7 +560,7 @@ impl Thread {
|
||||
cumulative_token_usage: serialized.cumulative_token_usage,
|
||||
exceeded_window_error: None,
|
||||
last_usage: None,
|
||||
tool_use_limit_reached: serialized.tool_use_limit_reached,
|
||||
tool_use_limit_reached: false,
|
||||
feedback: None,
|
||||
message_feedback: HashMap::default(),
|
||||
last_auto_capture_at: None,
|
||||
@@ -759,14 +757,6 @@ impl Thread {
|
||||
return;
|
||||
};
|
||||
|
||||
self.finalize_checkpoint(pending_checkpoint, cx);
|
||||
}
|
||||
|
||||
fn finalize_checkpoint(
|
||||
&mut self,
|
||||
pending_checkpoint: ThreadCheckpoint,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let git_store = self.project.read(cx).git_store().clone();
|
||||
let final_checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx));
|
||||
cx.spawn(async move |this, cx| match final_checkpoint.await {
|
||||
@@ -851,7 +841,7 @@ impl Thread {
|
||||
.get(ix + 1)
|
||||
.and_then(|message| {
|
||||
self.message(message.id)
|
||||
.map(|next_message| next_message.role == Role::User && !next_message.is_hidden)
|
||||
.map(|next_message| next_message.role == Role::User)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
@@ -891,7 +881,10 @@ impl Thread {
|
||||
|
||||
pub fn output_for_tool(&self, id: &LanguageModelToolUseId) -> Option<&Arc<str>> {
|
||||
match &self.tool_use.tool_result(id)?.content {
|
||||
LanguageModelToolResultContent::Text(text) => Some(text),
|
||||
LanguageModelToolResultContent::Text(text)
|
||||
| LanguageModelToolResultContent::WrappedText(WrappedTextContent { text, .. }) => {
|
||||
Some(text)
|
||||
}
|
||||
LanguageModelToolResultContent::Image(_) => {
|
||||
// TODO: We should display image
|
||||
None
|
||||
@@ -950,7 +943,6 @@ impl Thread {
|
||||
vec![MessageSegment::Text(text.into())],
|
||||
loaded_context.loaded_context,
|
||||
creases,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -966,20 +958,6 @@ impl Thread {
|
||||
message_id
|
||||
}
|
||||
|
||||
pub fn insert_invisible_continue_message(&mut self, cx: &mut Context<Self>) -> MessageId {
|
||||
let id = self.insert_message(
|
||||
Role::User,
|
||||
vec![MessageSegment::Text("Continue where you left off".into())],
|
||||
LoadedContext::default(),
|
||||
vec![],
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
self.pending_checkpoint = None;
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
pub fn insert_assistant_message(
|
||||
&mut self,
|
||||
segments: Vec<MessageSegment>,
|
||||
@@ -990,7 +968,6 @@ impl Thread {
|
||||
segments,
|
||||
LoadedContext::default(),
|
||||
Vec::new(),
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
@@ -1001,7 +978,6 @@ impl Thread {
|
||||
segments: Vec<MessageSegment>,
|
||||
loaded_context: LoadedContext,
|
||||
creases: Vec<MessageCrease>,
|
||||
is_hidden: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> MessageId {
|
||||
let id = self.next_message_id.post_inc();
|
||||
@@ -1011,7 +987,6 @@ impl Thread {
|
||||
segments,
|
||||
loaded_context,
|
||||
creases,
|
||||
is_hidden,
|
||||
});
|
||||
self.touch_updated_at();
|
||||
cx.emit(ThreadEvent::MessageAdded(id));
|
||||
@@ -1152,7 +1127,6 @@ impl Thread {
|
||||
label: crease.metadata.label.clone(),
|
||||
})
|
||||
.collect(),
|
||||
is_hidden: message.is_hidden,
|
||||
})
|
||||
.collect(),
|
||||
initial_project_snapshot,
|
||||
@@ -1168,7 +1142,6 @@ impl Thread {
|
||||
model: model.model.id().0.to_string(),
|
||||
}),
|
||||
completion_mode: Some(this.completion_mode),
|
||||
tool_use_limit_reached: this.tool_use_limit_reached,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1184,7 +1157,6 @@ impl Thread {
|
||||
pub fn send_to_model(
|
||||
&mut self,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
intent: CompletionIntent,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -1194,7 +1166,7 @@ impl Thread {
|
||||
|
||||
self.remaining_turns -= 1;
|
||||
|
||||
let request = self.to_completion_request(model.clone(), intent, cx);
|
||||
let request = self.to_completion_request(model.clone(), cx);
|
||||
|
||||
self.stream_completion(request, model, window, cx);
|
||||
}
|
||||
@@ -1214,19 +1186,17 @@ impl Thread {
|
||||
pub fn to_completion_request(
|
||||
&self,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
intent: CompletionIntent,
|
||||
cx: &mut Context<Self>,
|
||||
) -> LanguageModelRequest {
|
||||
let mut request = LanguageModelRequest {
|
||||
thread_id: Some(self.id.to_string()),
|
||||
prompt_id: Some(self.last_prompt_id.to_string()),
|
||||
intent: Some(intent),
|
||||
mode: None,
|
||||
messages: vec![],
|
||||
tools: Vec::new(),
|
||||
tool_choice: None,
|
||||
stop: Vec::new(),
|
||||
temperature: AgentSettings::temperature_for_model(&model, cx),
|
||||
temperature: AssistantSettings::temperature_for_model(&model, cx),
|
||||
};
|
||||
|
||||
let available_tools = self.available_tools(cx, model.clone());
|
||||
@@ -1374,20 +1344,18 @@ impl Thread {
|
||||
fn to_summarize_request(
|
||||
&self,
|
||||
model: &Arc<dyn LanguageModel>,
|
||||
intent: CompletionIntent,
|
||||
added_user_message: String,
|
||||
cx: &App,
|
||||
) -> LanguageModelRequest {
|
||||
let mut request = LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
intent: Some(intent),
|
||||
mode: None,
|
||||
messages: vec![],
|
||||
tools: Vec::new(),
|
||||
tool_choice: None,
|
||||
stop: Vec::new(),
|
||||
temperature: AgentSettings::temperature_for_model(model, cx),
|
||||
temperature: AssistantSettings::temperature_for_model(model, cx),
|
||||
};
|
||||
|
||||
for message in &self.messages {
|
||||
@@ -1428,7 +1396,7 @@ impl Thread {
|
||||
messages: &mut Vec<LanguageModelRequestMessage>,
|
||||
cx: &App,
|
||||
) {
|
||||
const STALE_FILES_HEADER: &str = include_str!("./prompts/stale_files_prompt_header.txt");
|
||||
const STALE_FILES_HEADER: &str = "These files changed since last read:";
|
||||
|
||||
let mut stale_message = String::new();
|
||||
|
||||
@@ -1440,7 +1408,7 @@ impl Thread {
|
||||
};
|
||||
|
||||
if stale_message.is_empty() {
|
||||
write!(&mut stale_message, "{}\n", STALE_FILES_HEADER.trim()).ok();
|
||||
write!(&mut stale_message, "{}\n", STALE_FILES_HEADER).ok();
|
||||
}
|
||||
|
||||
writeln!(&mut stale_message, "- {}", file.path().display()).ok();
|
||||
@@ -1725,43 +1693,6 @@ impl Thread {
|
||||
project.set_agent_location(None, cx);
|
||||
});
|
||||
}
|
||||
StopReason::Refusal => {
|
||||
thread.project.update(cx, |project, cx| {
|
||||
project.set_agent_location(None, cx);
|
||||
});
|
||||
|
||||
// Remove the turn that was refused.
|
||||
//
|
||||
// https://docs.anthropic.com/en/docs/test-and-evaluate/strengthen-guardrails/handle-streaming-refusals#reset-context-after-refusal
|
||||
{
|
||||
let mut messages_to_remove = Vec::new();
|
||||
|
||||
for (ix, message) in thread.messages.iter().enumerate().rev() {
|
||||
messages_to_remove.push(message.id);
|
||||
|
||||
if message.role == Role::User {
|
||||
if ix == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some(prev_message) = thread.messages.get(ix - 1) {
|
||||
if prev_message.role == Role::Assistant {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for message_id in messages_to_remove {
|
||||
thread.delete_message(message_id, cx);
|
||||
}
|
||||
}
|
||||
|
||||
cx.emit(ThreadEvent::ShowError(ThreadError::Message {
|
||||
header: "Language model refusal".into(),
|
||||
message: "Model refused to generate content for safety reasons.".into(),
|
||||
}));
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
thread.project.update(cx, |project, cx| {
|
||||
@@ -1805,7 +1736,6 @@ impl Thread {
|
||||
thread.cancel_last_completion(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
cx.emit(ThreadEvent::Stopped(result.map_err(Arc::new)));
|
||||
|
||||
if let Some((request_callback, (request, response_events))) = thread
|
||||
@@ -1854,14 +1784,12 @@ impl Thread {
|
||||
return;
|
||||
}
|
||||
|
||||
let added_user_message = include_str!("./prompts/summarize_thread_prompt.txt");
|
||||
let added_user_message = "Generate a concise 3-7 word title for this conversation, omitting punctuation. \
|
||||
Go straight to the title, without any preamble and prefix like `Here's a concise suggestion:...` or `Title:`. \
|
||||
If the conversation is about a specific subject, include it in the title. \
|
||||
Be descriptive. DO NOT speak in the first person.";
|
||||
|
||||
let request = self.to_summarize_request(
|
||||
&model.model,
|
||||
CompletionIntent::ThreadSummarization,
|
||||
added_user_message.into(),
|
||||
cx,
|
||||
);
|
||||
let request = self.to_summarize_request(&model.model, added_user_message.into(), cx);
|
||||
|
||||
self.summary = ThreadSummary::Generating;
|
||||
|
||||
@@ -1955,14 +1883,14 @@ impl Thread {
|
||||
return;
|
||||
}
|
||||
|
||||
let added_user_message = include_str!("./prompts/summarize_thread_detailed_prompt.txt");
|
||||
let added_user_message = "Generate a detailed summary of this conversation. Include:\n\
|
||||
1. A brief overview of what was discussed\n\
|
||||
2. Key facts or information discovered\n\
|
||||
3. Outcomes or conclusions reached\n\
|
||||
4. Any action items or next steps if any\n\
|
||||
Format it in Markdown with headings and bullet points.";
|
||||
|
||||
let request = self.to_summarize_request(
|
||||
&model,
|
||||
CompletionIntent::ThreadContextSummarization,
|
||||
added_user_message.into(),
|
||||
cx,
|
||||
);
|
||||
let request = self.to_summarize_request(&model, added_user_message.into(), cx);
|
||||
|
||||
*self.detailed_summary_tx.borrow_mut() = DetailedSummaryState::Generating {
|
||||
message_id: last_message_id,
|
||||
@@ -2054,8 +1982,7 @@ impl Thread {
|
||||
model: Arc<dyn LanguageModel>,
|
||||
) -> Vec<PendingToolUse> {
|
||||
self.auto_capture_telemetry(cx);
|
||||
let request =
|
||||
Arc::new(self.to_completion_request(model.clone(), CompletionIntent::ToolResults, cx));
|
||||
let request = Arc::new(self.to_completion_request(model.clone(), cx));
|
||||
let pending_tool_uses = self
|
||||
.tool_use
|
||||
.pending_tool_uses()
|
||||
@@ -2067,7 +1994,7 @@ impl Thread {
|
||||
for tool_use in pending_tool_uses.iter() {
|
||||
if let Some(tool) = self.tools.read(cx).tool(&tool_use.name, cx) {
|
||||
if tool.needs_confirmation(&tool_use.input, cx)
|
||||
&& !AgentSettings::get_global(cx).always_allow_tool_actions
|
||||
&& !AssistantSettings::get_global(cx).always_allow_tool_actions
|
||||
{
|
||||
self.tool_use.confirm_tool_use(
|
||||
tool_use.id.clone(),
|
||||
@@ -2251,7 +2178,7 @@ impl Thread {
|
||||
if self.all_tools_finished() {
|
||||
if let Some(ConfiguredModel { model, .. }) = self.configured_model.as_ref() {
|
||||
if !canceled {
|
||||
self.send_to_model(model.clone(), CompletionIntent::ToolResults, window, cx);
|
||||
self.send_to_model(model.clone(), window, cx);
|
||||
}
|
||||
self.auto_capture_telemetry(cx);
|
||||
}
|
||||
@@ -2284,17 +2211,10 @@ impl Thread {
|
||||
);
|
||||
}
|
||||
|
||||
self.finalize_pending_checkpoint(cx);
|
||||
|
||||
if canceled {
|
||||
cx.emit(ThreadEvent::CompletionCanceled);
|
||||
|
||||
// When canceled, we always want to insert the checkpoint.
|
||||
// (We skip over finalize_pending_checkpoint, because it
|
||||
// would conclude we didn't have anything to insert here.)
|
||||
if let Some(checkpoint) = self.pending_checkpoint.take() {
|
||||
self.insert_checkpoint(checkpoint, cx);
|
||||
}
|
||||
} else {
|
||||
self.finalize_pending_checkpoint(cx);
|
||||
}
|
||||
|
||||
canceled
|
||||
@@ -2598,7 +2518,11 @@ impl Thread {
|
||||
|
||||
writeln!(markdown, "**\n")?;
|
||||
match &tool_result.content {
|
||||
LanguageModelToolResultContent::Text(text) => {
|
||||
LanguageModelToolResultContent::Text(text)
|
||||
| LanguageModelToolResultContent::WrappedText(WrappedTextContent {
|
||||
text,
|
||||
..
|
||||
}) => {
|
||||
writeln!(markdown, "{text}")?;
|
||||
}
|
||||
LanguageModelToolResultContent::Image(image) => {
|
||||
@@ -2859,7 +2783,7 @@ struct PendingCompletion {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{ThreadStore, context::load_context, context_store::ContextStore, thread_store};
|
||||
use agent_settings::{AgentSettings, LanguageModelParameters};
|
||||
use assistant_settings::{AssistantSettings, LanguageModelParameters};
|
||||
use assistant_tool::ToolRegistry;
|
||||
use editor::EditorSettings;
|
||||
use gpui::TestAppContext;
|
||||
@@ -2890,8 +2814,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let context =
|
||||
context_store.read_with(cx, |store, _| store.context().next().cloned().unwrap());
|
||||
let context = context_store.update(cx, |store, _| store.context().next().cloned().unwrap());
|
||||
let loaded_context = cx
|
||||
.update(|cx| load_context(vec![context], &project, &None, cx))
|
||||
.await;
|
||||
@@ -2942,7 +2865,7 @@ fn main() {{
|
||||
|
||||
// Check message in request
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
});
|
||||
|
||||
assert_eq!(request.messages.len(), 2);
|
||||
@@ -3037,7 +2960,7 @@ fn main() {{
|
||||
|
||||
// Check entire request to make sure all contexts are properly included
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
});
|
||||
|
||||
// The request should contain all 3 messages
|
||||
@@ -3144,7 +3067,7 @@ fn main() {{
|
||||
|
||||
// Check message in request
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
});
|
||||
|
||||
assert_eq!(request.messages.len(), 2);
|
||||
@@ -3170,7 +3093,7 @@ fn main() {{
|
||||
|
||||
// Check that both messages appear in the request
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
});
|
||||
|
||||
assert_eq!(request.messages.len(), 3);
|
||||
@@ -3202,8 +3125,7 @@ fn main() {{
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let context =
|
||||
context_store.read_with(cx, |store, _| store.context().next().cloned().unwrap());
|
||||
let context = context_store.update(cx, |store, _| store.context().next().cloned().unwrap());
|
||||
let loaded_context = cx
|
||||
.update(|cx| load_context(vec![context], &project, &None, cx))
|
||||
.await;
|
||||
@@ -3215,7 +3137,7 @@ fn main() {{
|
||||
|
||||
// Create a request and check that it doesn't have a stale buffer warning yet
|
||||
let initial_request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
});
|
||||
|
||||
// Make sure we don't have a stale file warning yet
|
||||
@@ -3251,7 +3173,7 @@ fn main() {{
|
||||
|
||||
// Create a new request and check for the stale buffer warning
|
||||
let new_request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
});
|
||||
|
||||
// We should have a stale file warning as the last message
|
||||
@@ -3287,81 +3209,81 @@ fn main() {{
|
||||
|
||||
// Both model and provider
|
||||
cx.update(|cx| {
|
||||
AgentSettings::override_global(
|
||||
AgentSettings {
|
||||
AssistantSettings::override_global(
|
||||
AssistantSettings {
|
||||
model_parameters: vec![LanguageModelParameters {
|
||||
provider: Some(model.provider_id().0.to_string().into()),
|
||||
model: Some(model.id().0.clone()),
|
||||
temperature: Some(0.66),
|
||||
}],
|
||||
..AgentSettings::get_global(cx).clone()
|
||||
..AssistantSettings::get_global(cx).clone()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
});
|
||||
assert_eq!(request.temperature, Some(0.66));
|
||||
|
||||
// Only model
|
||||
cx.update(|cx| {
|
||||
AgentSettings::override_global(
|
||||
AgentSettings {
|
||||
AssistantSettings::override_global(
|
||||
AssistantSettings {
|
||||
model_parameters: vec![LanguageModelParameters {
|
||||
provider: None,
|
||||
model: Some(model.id().0.clone()),
|
||||
temperature: Some(0.66),
|
||||
}],
|
||||
..AgentSettings::get_global(cx).clone()
|
||||
..AssistantSettings::get_global(cx).clone()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
});
|
||||
assert_eq!(request.temperature, Some(0.66));
|
||||
|
||||
// Only provider
|
||||
cx.update(|cx| {
|
||||
AgentSettings::override_global(
|
||||
AgentSettings {
|
||||
AssistantSettings::override_global(
|
||||
AssistantSettings {
|
||||
model_parameters: vec![LanguageModelParameters {
|
||||
provider: Some(model.provider_id().0.to_string().into()),
|
||||
model: None,
|
||||
temperature: Some(0.66),
|
||||
}],
|
||||
..AgentSettings::get_global(cx).clone()
|
||||
..AssistantSettings::get_global(cx).clone()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
});
|
||||
assert_eq!(request.temperature, Some(0.66));
|
||||
|
||||
// Same model name, different provider
|
||||
cx.update(|cx| {
|
||||
AgentSettings::override_global(
|
||||
AgentSettings {
|
||||
AssistantSettings::override_global(
|
||||
AssistantSettings {
|
||||
model_parameters: vec![LanguageModelParameters {
|
||||
provider: Some("anthropic".into()),
|
||||
model: Some(model.id().0.clone()),
|
||||
temperature: Some(0.66),
|
||||
}],
|
||||
..AgentSettings::get_global(cx).clone()
|
||||
..AssistantSettings::get_global(cx).clone()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
});
|
||||
assert_eq!(request.temperature, None);
|
||||
}
|
||||
@@ -3393,12 +3315,7 @@ fn main() {{
|
||||
// Send a message
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.insert_user_message("Hi!", ContextLoadResult::default(), None, vec![], cx);
|
||||
thread.send_to_model(
|
||||
model.clone(),
|
||||
CompletionIntent::ThreadSummarization,
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
thread.send_to_model(model.clone(), None, cx);
|
||||
});
|
||||
|
||||
let fake_model = model.as_fake();
|
||||
@@ -3420,8 +3337,8 @@ fn main() {{
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
fake_model.stream_last_completion_response("Brief");
|
||||
fake_model.stream_last_completion_response(" Introduction");
|
||||
fake_model.stream_last_completion_response("Brief".into());
|
||||
fake_model.stream_last_completion_response(" Introduction".into());
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
@@ -3493,7 +3410,7 @@ fn main() {{
|
||||
vec![],
|
||||
cx,
|
||||
);
|
||||
thread.send_to_model(model.clone(), CompletionIntent::UserPrompt, None, cx);
|
||||
thread.send_to_model(model.clone(), None, cx);
|
||||
});
|
||||
|
||||
let fake_model = model.as_fake();
|
||||
@@ -3514,7 +3431,7 @@ fn main() {{
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
fake_model.stream_last_completion_response("A successful summary");
|
||||
fake_model.stream_last_completion_response("A successful summary".into());
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
@@ -3531,12 +3448,7 @@ fn main() {{
|
||||
) {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.insert_user_message("Hi!", ContextLoadResult::default(), None, vec![], cx);
|
||||
thread.send_to_model(
|
||||
model.clone(),
|
||||
CompletionIntent::ThreadSummarization,
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
thread.send_to_model(model.clone(), None, cx);
|
||||
});
|
||||
|
||||
let fake_model = model.as_fake();
|
||||
@@ -3561,7 +3473,7 @@ fn main() {{
|
||||
|
||||
fn simulate_successful_response(fake_model: &FakeLanguageModel, cx: &mut TestAppContext) {
|
||||
cx.run_until_parked();
|
||||
fake_model.stream_last_completion_response("Assistant response");
|
||||
fake_model.stream_last_completion_response("Assistant response".into());
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
}
|
||||
@@ -3572,7 +3484,7 @@ fn main() {{
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
AgentSettings::register(cx);
|
||||
AssistantSettings::register(cx);
|
||||
prompt_store::init(cx);
|
||||
thread_store::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
|
||||
@@ -4,8 +4,8 @@ use std::path::{Path, PathBuf};
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent_settings::{AgentProfile, AgentProfileId, AgentSettings, CompletionMode};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_settings::{AgentProfile, AgentProfileId, AssistantSettings, CompletionMode};
|
||||
use assistant_tool::{ToolId, ToolSource, ToolWorkingSet};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::HashMap;
|
||||
@@ -485,13 +485,13 @@ impl ThreadStore {
|
||||
}
|
||||
|
||||
fn load_default_profile(&self, cx: &mut Context<Self>) {
|
||||
let assistant_settings = AgentSettings::get_global(cx);
|
||||
let assistant_settings = AssistantSettings::get_global(cx);
|
||||
|
||||
self.load_profile_by_id(assistant_settings.default_profile.clone(), cx);
|
||||
}
|
||||
|
||||
pub fn load_profile_by_id(&self, profile_id: AgentProfileId, cx: &mut Context<Self>) {
|
||||
let assistant_settings = AgentSettings::get_global(cx);
|
||||
let assistant_settings = AssistantSettings::get_global(cx);
|
||||
|
||||
if let Some(profile) = assistant_settings.profiles.get(&profile_id) {
|
||||
self.load_profile(profile.clone(), cx);
|
||||
@@ -676,8 +676,6 @@ pub struct SerializedThread {
|
||||
pub model: Option<SerializedLanguageModel>,
|
||||
#[serde(default)]
|
||||
pub completion_mode: Option<CompletionMode>,
|
||||
#[serde(default)]
|
||||
pub tool_use_limit_reached: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
@@ -759,8 +757,6 @@ pub struct SerializedMessage {
|
||||
pub context: String,
|
||||
#[serde(default)]
|
||||
pub creases: Vec<SerializedCrease>,
|
||||
#[serde(default)]
|
||||
pub is_hidden: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -819,7 +815,6 @@ impl LegacySerializedThread {
|
||||
exceeded_window_error: None,
|
||||
model: None,
|
||||
completion_mode: None,
|
||||
tool_use_limit_reached: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -845,7 +840,6 @@ impl LegacySerializedMessage {
|
||||
tool_results: self.tool_results,
|
||||
context: String::new(),
|
||||
creases: Vec::new(),
|
||||
is_hidden: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3
crates/agent/src/trial_markdown.md
Normal file
3
crates/agent/src/trial_markdown.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Build better with Zed Pro
|
||||
|
||||
Try [Zed Pro](https://zed.dev/pricing) for free for 14 days - no credit card required. Only $20/month afterward. Cancel anytime.
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::ToggleBurnMode;
|
||||
use gpui::{Context, FontWeight, IntoElement, Render, Window};
|
||||
use ui::{KeyBinding, prelude::*, tooltip_container};
|
||||
use gpui::{Context, IntoElement, Render, Window};
|
||||
use ui::{prelude::*, tooltip_container};
|
||||
|
||||
pub struct MaxModeTooltip {
|
||||
selected: bool,
|
||||
@@ -19,48 +18,38 @@ impl MaxModeTooltip {
|
||||
|
||||
impl Render for MaxModeTooltip {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let (icon, color) = if self.selected {
|
||||
(IconName::ZedBurnModeOn, Color::Error)
|
||||
} else {
|
||||
(IconName::ZedBurnMode, Color::Default)
|
||||
};
|
||||
|
||||
let turned_on = h_flex()
|
||||
.h_4()
|
||||
.px_1()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().text_accent.opacity(0.1))
|
||||
.rounded_sm()
|
||||
.child(
|
||||
Label::new("ON")
|
||||
.size(LabelSize::XSmall)
|
||||
.weight(FontWeight::SEMIBOLD)
|
||||
.color(Color::Accent),
|
||||
);
|
||||
|
||||
let title = h_flex()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(icon).size(IconSize::Small).color(color))
|
||||
.child(Label::new("Burn Mode"))
|
||||
.when(self.selected, |title| title.child(turned_on));
|
||||
|
||||
let keybinding = KeyBinding::for_action(&ToggleBurnMode, window, cx)
|
||||
.map(|kb| kb.size(rems_from_px(12.)));
|
||||
|
||||
tooltip_container(window, cx, |this, _, _| {
|
||||
this
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(title)
|
||||
.children(keybinding)
|
||||
)
|
||||
this.gap_1()
|
||||
.map(|header| if self.selected {
|
||||
header.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(IconName::ZedMaxMode).size(IconSize::Small).color(Color::Accent))
|
||||
.child(Label::new("Zed's Max Mode"))
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(Icon::new(IconName::Check).size(IconSize::XSmall).color(Color::Accent))
|
||||
.child(Label::new("Turned On").size(LabelSize::XSmall).color(Color::Accent))
|
||||
)
|
||||
)
|
||||
} else {
|
||||
header.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(IconName::ZedMaxMode).size(IconSize::Small))
|
||||
.child(Label::new("Zed's Max Mode"))
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.max_w_64()
|
||||
.max_w_72()
|
||||
.child(
|
||||
Label::new("Enables models to use large context windows, unlimited tool calls, and other capabilities for expanded reasoning.")
|
||||
Label::new("This mode enables models to use large context windows, unlimited tool calls, and other capabilities for expanded reasoning, offering an unfettered agentic experience.")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
)
|
||||
|
||||
@@ -34,6 +34,7 @@ pub enum AnthropicModelMode {
|
||||
pub enum Model {
|
||||
#[serde(rename = "claude-3-5-sonnet", alias = "claude-3-5-sonnet-latest")]
|
||||
Claude3_5Sonnet,
|
||||
#[default]
|
||||
#[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
|
||||
Claude3_7Sonnet,
|
||||
#[serde(
|
||||
@@ -41,21 +42,6 @@ pub enum Model {
|
||||
alias = "claude-3-7-sonnet-thinking-latest"
|
||||
)]
|
||||
Claude3_7SonnetThinking,
|
||||
#[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")]
|
||||
ClaudeOpus4,
|
||||
#[serde(
|
||||
rename = "claude-opus-4-thinking",
|
||||
alias = "claude-opus-4-thinking-latest"
|
||||
)]
|
||||
ClaudeOpus4Thinking,
|
||||
#[default]
|
||||
#[serde(rename = "claude-sonnet-4", alias = "claude-sonnet-4-latest")]
|
||||
ClaudeSonnet4,
|
||||
#[serde(
|
||||
rename = "claude-sonnet-4-thinking",
|
||||
alias = "claude-sonnet-4-thinking-latest"
|
||||
)]
|
||||
ClaudeSonnet4Thinking,
|
||||
#[serde(rename = "claude-3-5-haiku", alias = "claude-3-5-haiku-latest")]
|
||||
Claude3_5Haiku,
|
||||
#[serde(rename = "claude-3-opus", alias = "claude-3-opus-latest")]
|
||||
@@ -103,14 +89,6 @@ impl Model {
|
||||
Ok(Self::Claude3Sonnet)
|
||||
} else if id.starts_with("claude-3-haiku") {
|
||||
Ok(Self::Claude3Haiku)
|
||||
} else if id.starts_with("claude-opus-4-thinking") {
|
||||
Ok(Self::ClaudeOpus4Thinking)
|
||||
} else if id.starts_with("claude-opus-4") {
|
||||
Ok(Self::ClaudeOpus4)
|
||||
} else if id.starts_with("claude-sonnet-4-thinking") {
|
||||
Ok(Self::ClaudeSonnet4Thinking)
|
||||
} else if id.starts_with("claude-sonnet-4") {
|
||||
Ok(Self::ClaudeSonnet4)
|
||||
} else {
|
||||
anyhow::bail!("invalid model id {id}");
|
||||
}
|
||||
@@ -118,10 +96,6 @@ impl Model {
|
||||
|
||||
pub fn id(&self) -> &str {
|
||||
match self {
|
||||
Model::ClaudeOpus4 => "claude-opus-4-latest",
|
||||
Model::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest",
|
||||
Model::ClaudeSonnet4 => "claude-sonnet-4-latest",
|
||||
Model::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest",
|
||||
Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
|
||||
Model::Claude3_7Sonnet => "claude-3-7-sonnet-latest",
|
||||
Model::Claude3_7SonnetThinking => "claude-3-7-sonnet-thinking-latest",
|
||||
@@ -136,8 +110,6 @@ impl Model {
|
||||
/// The id of the model that should be used for making API requests
|
||||
pub fn request_id(&self) -> &str {
|
||||
match self {
|
||||
Model::ClaudeOpus4 | Model::ClaudeOpus4Thinking => "claude-opus-4-20250514",
|
||||
Model::ClaudeSonnet4 | Model::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514",
|
||||
Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
|
||||
Model::Claude3_7Sonnet | Model::Claude3_7SonnetThinking => "claude-3-7-sonnet-latest",
|
||||
Model::Claude3_5Haiku => "claude-3-5-haiku-latest",
|
||||
@@ -150,10 +122,6 @@ impl Model {
|
||||
|
||||
pub fn display_name(&self) -> &str {
|
||||
match self {
|
||||
Model::ClaudeOpus4 => "Claude Opus 4",
|
||||
Model::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
|
||||
Model::ClaudeSonnet4 => "Claude Sonnet 4",
|
||||
Model::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
|
||||
Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
|
||||
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
|
||||
Self::Claude3_7SonnetThinking => "Claude 3.7 Sonnet Thinking",
|
||||
@@ -169,11 +137,7 @@ impl Model {
|
||||
|
||||
pub fn cache_configuration(&self) -> Option<AnthropicModelCacheConfiguration> {
|
||||
match self {
|
||||
Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::Claude3_5Sonnet
|
||||
Self::Claude3_5Sonnet
|
||||
| Self::Claude3_5Haiku
|
||||
| Self::Claude3_7Sonnet
|
||||
| Self::Claude3_7SonnetThinking
|
||||
@@ -192,11 +156,7 @@ impl Model {
|
||||
|
||||
pub fn max_token_count(&self) -> usize {
|
||||
match self {
|
||||
Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::Claude3_5Sonnet
|
||||
Self::Claude3_5Sonnet
|
||||
| Self::Claude3_5Haiku
|
||||
| Self::Claude3_7Sonnet
|
||||
| Self::Claude3_7SonnetThinking
|
||||
@@ -213,11 +173,7 @@ impl Model {
|
||||
Self::Claude3_5Sonnet
|
||||
| Self::Claude3_7Sonnet
|
||||
| Self::Claude3_7SonnetThinking
|
||||
| Self::Claude3_5Haiku
|
||||
| Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking => 8_192,
|
||||
| Self::Claude3_5Haiku => 8_192,
|
||||
Self::Custom {
|
||||
max_output_tokens, ..
|
||||
} => max_output_tokens.unwrap_or(4_096),
|
||||
@@ -226,11 +182,7 @@ impl Model {
|
||||
|
||||
pub fn default_temperature(&self) -> f32 {
|
||||
match self {
|
||||
Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::Claude3_5Sonnet
|
||||
Self::Claude3_5Sonnet
|
||||
| Self::Claude3_7Sonnet
|
||||
| Self::Claude3_7SonnetThinking
|
||||
| Self::Claude3_5Haiku
|
||||
@@ -249,14 +201,10 @@ impl Model {
|
||||
Self::Claude3_5Sonnet
|
||||
| Self::Claude3_7Sonnet
|
||||
| Self::Claude3_5Haiku
|
||||
| Self::ClaudeOpus4
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::Claude3Opus
|
||||
| Self::Claude3Sonnet
|
||||
| Self::Claude3Haiku => AnthropicModelMode::Default,
|
||||
Self::Claude3_7SonnetThinking
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeSonnet4Thinking => AnthropicModelMode::Thinking {
|
||||
Self::Claude3_7SonnetThinking => AnthropicModelMode::Thinking {
|
||||
budget_tokens: Some(4_096),
|
||||
},
|
||||
Self::Custom { mode, .. } => mode.clone(),
|
||||
|
||||
@@ -12,8 +12,8 @@ workspace = true
|
||||
path = "src/assistant_context_editor.rs"
|
||||
|
||||
[dependencies]
|
||||
agent_settings.workspace = true
|
||||
anyhow.workspace = true
|
||||
assistant_settings.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
assistant_slash_commands.workspace = true
|
||||
chrono.workspace = true
|
||||
@@ -22,7 +22,6 @@ clock.workspace = true
|
||||
collections.workspace = true
|
||||
context_server.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
@@ -30,16 +29,15 @@ gpui.workspace = true
|
||||
indexed_docs.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
language_model_selector.workspace = true
|
||||
log.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
open_ai.workspace = true
|
||||
ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
picker.workspace = true
|
||||
project.workspace = true
|
||||
prompt_store.workspace = true
|
||||
proto.workspace = true
|
||||
regex.workspace = true
|
||||
rope.workspace = true
|
||||
rpc.workspace = true
|
||||
@@ -57,7 +55,6 @@ uuid.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
language_model = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -2,8 +2,6 @@ mod context;
|
||||
mod context_editor;
|
||||
mod context_history;
|
||||
mod context_store;
|
||||
pub mod language_model_selector;
|
||||
mod max_mode_tooltip;
|
||||
mod slash_command;
|
||||
mod slash_command_picker;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#[cfg(test)]
|
||||
mod context_tests;
|
||||
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use assistant_slash_command::{
|
||||
SlashCommandContent, SlashCommandEvent, SlashCommandLine, SlashCommandOutputSection,
|
||||
SlashCommandResult, SlashCommandWorkingSet,
|
||||
@@ -29,7 +29,6 @@ use paths::contexts_dir;
|
||||
use project::Project;
|
||||
use prompt_store::PromptBuilder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
cmp::{Ordering, max},
|
||||
@@ -45,7 +44,6 @@ use text::{BufferSnapshot, ToPoint};
|
||||
use ui::IconName;
|
||||
use util::{ResultExt, TryFutureExt, post_inc};
|
||||
use uuid::Uuid;
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub struct ContextId(String);
|
||||
@@ -684,7 +682,6 @@ pub struct AssistantContext {
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
project: Option<Entity<Project>>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
completion_mode: agent_settings::CompletionMode,
|
||||
}
|
||||
|
||||
trait ContextAnnotation {
|
||||
@@ -721,14 +718,6 @@ impl AssistantContext {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn completion_mode(&self) -> agent_settings::CompletionMode {
|
||||
self.completion_mode
|
||||
}
|
||||
|
||||
pub fn set_completion_mode(&mut self, completion_mode: agent_settings::CompletionMode) {
|
||||
self.completion_mode = completion_mode;
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
id: ContextId,
|
||||
replica_id: ReplicaId,
|
||||
@@ -775,7 +764,6 @@ impl AssistantContext {
|
||||
pending_cache_warming_task: Task::ready(None),
|
||||
_subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
|
||||
pending_save: Task::ready(Ok(())),
|
||||
completion_mode: AgentSettings::get_global(cx).preferred_completion_mode,
|
||||
path: None,
|
||||
buffer,
|
||||
telemetry,
|
||||
@@ -1742,8 +1730,9 @@ impl AssistantContext {
|
||||
merge_same_roles,
|
||||
} => {
|
||||
if !merge_same_roles && Some(role) != last_role {
|
||||
let buffer = this.buffer.read(cx);
|
||||
let offset = insert_position.to_offset(buffer);
|
||||
let offset = this.buffer.read_with(cx, |buffer, _cx| {
|
||||
insert_position.to_offset(buffer)
|
||||
});
|
||||
this.insert_message_at_offset(
|
||||
offset,
|
||||
role,
|
||||
@@ -2215,7 +2204,6 @@ impl AssistantContext {
|
||||
StopReason::ToolUse => {}
|
||||
StopReason::EndTurn => {}
|
||||
StopReason::MaxTokens => {}
|
||||
StopReason::Refusal => {}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -2273,13 +2261,13 @@ impl AssistantContext {
|
||||
let mut completion_request = LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
intent: Some(CompletionIntent::UserPrompt),
|
||||
mode: None,
|
||||
messages: Vec::new(),
|
||||
tools: Vec::new(),
|
||||
tool_choice: None,
|
||||
stop: Vec::new(),
|
||||
temperature: model.and_then(|model| AgentSettings::temperature_for_model(model, cx)),
|
||||
temperature: model
|
||||
.and_then(|model| AssistantSettings::temperature_for_model(model, cx)),
|
||||
};
|
||||
for message in self.messages(cx) {
|
||||
if message.status != MessageStatus::Done {
|
||||
@@ -2334,15 +2322,7 @@ impl AssistantContext {
|
||||
completion_request.messages.push(request_message);
|
||||
}
|
||||
}
|
||||
let supports_max_mode = if let Some(model) = model {
|
||||
model.supports_max_mode()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if supports_max_mode {
|
||||
completion_request.mode = Some(self.completion_mode.into());
|
||||
}
|
||||
completion_request
|
||||
}
|
||||
|
||||
|
||||
@@ -1210,8 +1210,8 @@ async fn test_summarization(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
fake_model.stream_last_completion_response("Brief");
|
||||
fake_model.stream_last_completion_response(" Introduction");
|
||||
fake_model.stream_last_completion_response("Brief".into());
|
||||
fake_model.stream_last_completion_response(" Introduction".into());
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
@@ -1274,7 +1274,7 @@ async fn test_thread_summary_error_retry(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
fake_model.stream_last_completion_response("A successful summary");
|
||||
fake_model.stream_last_completion_response("A successful summary".into());
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
@@ -1356,7 +1356,7 @@ fn setup_context_editor_with_fake_model(
|
||||
|
||||
fn simulate_successful_response(fake_model: &FakeLanguageModel, cx: &mut TestAppContext) {
|
||||
cx.run_until_parked();
|
||||
fake_model.stream_last_completion_response("Assistant response");
|
||||
fake_model.stream_last_completion_response("Assistant response".into());
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
}
|
||||
@@ -1386,7 +1386,7 @@ fn init_test(cx: &mut App) {
|
||||
LanguageModelRegistry::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
agent_settings::init(cx);
|
||||
assistant_settings::init(cx);
|
||||
Project::init_settings(cx);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
use crate::{
|
||||
language_model_selector::{
|
||||
LanguageModelSelector, ToggleModelSelector, language_model_selector,
|
||||
},
|
||||
max_mode_tooltip::MaxModeTooltip,
|
||||
};
|
||||
use agent_settings::{AgentSettings, CompletionMode};
|
||||
use anyhow::Result;
|
||||
use assistant_settings::AssistantSettings;
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
|
||||
use assistant_slash_commands::{
|
||||
DefaultSlashCommand, DocsSlashCommand, DocsSlashCommandArgs, FileSlashCommand,
|
||||
@@ -42,8 +36,11 @@ use language_model::{
|
||||
LanguageModelImage, LanguageModelProvider, LanguageModelProviderTosView, LanguageModelRegistry,
|
||||
Role,
|
||||
};
|
||||
use language_model_selector::{
|
||||
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use picker::{Picker, popover_menu::PickerPopoverMenu};
|
||||
use picker::Picker;
|
||||
use project::{Project, Worktree};
|
||||
use project::{ProjectPath, lsp_store::LocalLspAdapterDelegate};
|
||||
use rope::Point;
|
||||
@@ -54,7 +51,6 @@ use std::{
|
||||
cmp,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
@@ -238,7 +234,7 @@ impl ContextEditor {
|
||||
editor.set_show_breakpoints(false, cx);
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_completion_provider(Some(Rc::new(completion_provider)));
|
||||
editor.set_completion_provider(Some(Box::new(completion_provider)));
|
||||
editor.set_menu_inline_completions_policy(MenuInlineCompletionsPolicy::Never);
|
||||
editor.set_collaboration_hub(Box::new(project.clone()));
|
||||
|
||||
@@ -283,10 +279,10 @@ impl ContextEditor {
|
||||
slash_menu_handle: Default::default(),
|
||||
dragged_file_worktrees: Vec::new(),
|
||||
language_model_selector: cx.new(|cx| {
|
||||
language_model_selector(
|
||||
LanguageModelSelector::new(
|
||||
|cx| LanguageModelRegistry::read_global(cx).default_model(),
|
||||
move |model, cx| {
|
||||
update_settings_file::<AgentSettings>(
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
@@ -1906,7 +1902,7 @@ impl ContextEditor {
|
||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||
let client = this
|
||||
.workspace
|
||||
.read_with(cx, |workspace, _| workspace.client().clone())
|
||||
.update(cx, |workspace, _| workspace.client().clone())
|
||||
.log_err();
|
||||
|
||||
if let Some(client) = client {
|
||||
@@ -2011,17 +2007,17 @@ impl ContextEditor {
|
||||
None => (ButtonStyle::Filled, None),
|
||||
};
|
||||
|
||||
Button::new("send_button", "Send")
|
||||
.label_size(LabelSize::Small)
|
||||
ButtonLike::new("send_button")
|
||||
.disabled(self.sending_disabled(cx))
|
||||
.style(style)
|
||||
.when_some(tooltip, |button, tooltip| {
|
||||
button.tooltip(move |_, _| tooltip.clone())
|
||||
})
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.key_binding(
|
||||
.child(Label::new("Send"))
|
||||
.children(
|
||||
KeyBinding::for_action_in(&Assist, &focus_handle, window, cx)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
.map(|binding| binding.into_any_element()),
|
||||
)
|
||||
.on_click(move |_event, window, cx| {
|
||||
focus_handle.dispatch_action(&Assist, window, cx);
|
||||
@@ -2061,50 +2057,7 @@ impl ContextEditor {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_max_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
let context = self.context().read(cx);
|
||||
let active_model = LanguageModelRegistry::read_global(cx)
|
||||
.default_model()
|
||||
.map(|default| default.model)?;
|
||||
if !active_model.supports_max_mode() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let active_completion_mode = context.completion_mode();
|
||||
let burn_mode_enabled = active_completion_mode == CompletionMode::Burn;
|
||||
let icon = if burn_mode_enabled {
|
||||
IconName::ZedBurnModeOn
|
||||
} else {
|
||||
IconName::ZedBurnMode
|
||||
};
|
||||
|
||||
Some(
|
||||
IconButton::new("burn-mode", icon)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.toggle_state(burn_mode_enabled)
|
||||
.selected_icon_color(Color::Error)
|
||||
.on_click(cx.listener(move |this, _event, _window, cx| {
|
||||
this.context().update(cx, |context, _cx| {
|
||||
context.set_completion_mode(match active_completion_mode {
|
||||
CompletionMode::Burn => CompletionMode::Normal,
|
||||
CompletionMode::Normal => CompletionMode::Burn,
|
||||
});
|
||||
});
|
||||
}))
|
||||
.tooltip(move |_window, cx| {
|
||||
cx.new(|_| MaxModeTooltip::new().selected(burn_mode_enabled))
|
||||
.into()
|
||||
})
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_language_model_selector(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
fn render_language_model_selector(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let active_model = LanguageModelRegistry::read_global(cx)
|
||||
.default_model()
|
||||
.map(|default| default.model);
|
||||
@@ -2114,7 +2067,7 @@ impl ContextEditor {
|
||||
None => SharedString::from("No model selected"),
|
||||
};
|
||||
|
||||
PickerPopoverMenu::new(
|
||||
LanguageModelSelectorPopoverMenu::new(
|
||||
self.language_model_selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
.style(ButtonStyle::Subtle)
|
||||
@@ -2142,10 +2095,8 @@ impl ContextEditor {
|
||||
)
|
||||
},
|
||||
gpui::Corner::BottomLeft,
|
||||
cx,
|
||||
)
|
||||
.with_handle(self.language_model_selector_menu_handle.clone())
|
||||
.render(window, cx)
|
||||
}
|
||||
|
||||
fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
@@ -2551,7 +2502,6 @@ impl Render for ContextEditor {
|
||||
let provider = LanguageModelRegistry::read_global(cx)
|
||||
.default_model()
|
||||
.map(|default| default.provider);
|
||||
|
||||
let accept_terms = if self.show_accept_terms {
|
||||
provider.as_ref().and_then(|provider| {
|
||||
provider.render_accept_terms(LanguageModelProviderTosView::PromptEditorPopup, cx)
|
||||
@@ -2561,8 +2511,6 @@ impl Render for ContextEditor {
|
||||
};
|
||||
|
||||
let language_model_selector = self.language_model_selector_menu_handle.clone();
|
||||
let max_mode_toggle = self.render_max_mode_toggle(cx);
|
||||
|
||||
v_flex()
|
||||
.key_context("ContextEditor")
|
||||
.capture_action(cx.listener(ContextEditor::cancel))
|
||||
@@ -2602,28 +2550,31 @@ impl Render for ContextEditor {
|
||||
})
|
||||
.children(self.render_last_error(cx))
|
||||
.child(
|
||||
h_flex()
|
||||
.relative()
|
||||
.py_2()
|
||||
.pl_1p5()
|
||||
.pr_2()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(self.render_inject_context_menu(cx))
|
||||
.when_some(max_mode_toggle, |this, element| this.child(element)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(self.render_language_model_selector(window, cx))
|
||||
.child(self.render_send_button(window, cx)),
|
||||
),
|
||||
h_flex().w_full().relative().child(
|
||||
h_flex()
|
||||
.p_2()
|
||||
.w_full()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(self.render_inject_context_menu(cx))
|
||||
.child(ui::Divider::vertical())
|
||||
.child(
|
||||
div()
|
||||
.pl_0p5()
|
||||
.child(self.render_language_model_selector(cx)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_end()
|
||||
.child(self.render_send_button(window, cx)),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -3414,7 +3365,7 @@ mod tests {
|
||||
LanguageModelRegistry::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
agent_settings::init(cx);
|
||||
assistant_settings::init(cx);
|
||||
Project::init_settings(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
workspace::init_settings(cx);
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
use gpui::{Context, FontWeight, IntoElement, Render, Window};
|
||||
use ui::{prelude::*, tooltip_container};
|
||||
|
||||
pub struct MaxModeTooltip {
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
impl MaxModeTooltip {
|
||||
pub fn new() -> Self {
|
||||
Self { selected: false }
|
||||
}
|
||||
|
||||
pub fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for MaxModeTooltip {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let (icon, color) = if self.selected {
|
||||
(IconName::ZedBurnModeOn, Color::Error)
|
||||
} else {
|
||||
(IconName::ZedBurnMode, Color::Default)
|
||||
};
|
||||
|
||||
let turned_on = h_flex()
|
||||
.h_4()
|
||||
.px_1()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().text_accent.opacity(0.1))
|
||||
.rounded_sm()
|
||||
.child(
|
||||
Label::new("ON")
|
||||
.size(LabelSize::XSmall)
|
||||
.weight(FontWeight::SEMIBOLD)
|
||||
.color(Color::Accent),
|
||||
);
|
||||
|
||||
let title = h_flex()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(icon).size(IconSize::Small).color(color))
|
||||
.child(Label::new("Burn Mode"))
|
||||
.when(self.selected, |title| title.child(turned_on));
|
||||
|
||||
tooltip_container(window, cx, |this, _, _| {
|
||||
this
|
||||
.child(title)
|
||||
.child(
|
||||
div()
|
||||
.max_w_64()
|
||||
.child(
|
||||
Label::new("Enables models to use large context windows, unlimited tool calls, and other capabilities for expanded reasoning.")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -338,7 +338,7 @@ where
|
||||
|
||||
let handle = self
|
||||
.active_context_editor
|
||||
.read_with(cx, |this, _| this.slash_menu_handle.clone())
|
||||
.update(cx, |this, _| this.slash_menu_handle.clone())
|
||||
.ok();
|
||||
PopoverMenu::new("model-switcher")
|
||||
.menu(move |_window, _cx| Some(picker_view.clone()))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "agent_settings"
|
||||
name = "assistant_settings"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
@@ -9,7 +9,7 @@ license = "GPL-3.0-or-later"
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/agent_settings.rs"
|
||||
path = "src/assistant_settings.rs"
|
||||
|
||||
[dependencies]
|
||||
anthropic = { workspace = true, features = ["schemars"] }
|
||||
@@ -24,7 +24,7 @@ pub struct GroupedAgentProfiles {
|
||||
}
|
||||
|
||||
impl GroupedAgentProfiles {
|
||||
pub fn from_settings(settings: &crate::AgentSettings) -> Self {
|
||||
pub fn from_settings(settings: &crate::AssistantSettings) -> Self {
|
||||
let mut builtin = IndexMap::default();
|
||||
let mut custom = IndexMap::default();
|
||||
|
||||
@@ -8,7 +8,7 @@ use anyhow::{Result, bail};
|
||||
use collections::IndexMap;
|
||||
use deepseek::Model as DeepseekModel;
|
||||
use gpui::{App, Pixels, SharedString};
|
||||
use language_model::LanguageModel;
|
||||
use language_model::{CloudModel, LanguageModel};
|
||||
use lmstudio::Model as LmStudioModel;
|
||||
use mistral::Model as MistralModel;
|
||||
use ollama::Model as OllamaModel;
|
||||
@@ -19,26 +19,18 @@ use settings::{Settings, SettingsSources};
|
||||
pub use crate::agent_profile::*;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
AgentSettings::register(cx);
|
||||
AssistantSettings::register(cx);
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AgentDockPosition {
|
||||
pub enum AssistantDockPosition {
|
||||
Left,
|
||||
#[default]
|
||||
Right,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DefaultView {
|
||||
#[default]
|
||||
Thread,
|
||||
TextThread,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum NotifyWhenAgentWaiting {
|
||||
@@ -51,9 +43,9 @@ pub enum NotifyWhenAgentWaiting {
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(tag = "name", rename_all = "snake_case")]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub enum AgentProviderContentV1 {
|
||||
pub enum AssistantProviderContentV1 {
|
||||
#[serde(rename = "zed.dev")]
|
||||
ZedDotDev { default_model: Option<String> },
|
||||
ZedDotDev { default_model: Option<CloudModel> },
|
||||
#[serde(rename = "openai")]
|
||||
OpenAi {
|
||||
default_model: Option<OpenAiModel>,
|
||||
@@ -88,10 +80,10 @@ pub enum AgentProviderContentV1 {
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct AgentSettings {
|
||||
pub struct AssistantSettings {
|
||||
pub enabled: bool,
|
||||
pub button: bool,
|
||||
pub dock: AgentDockPosition,
|
||||
pub dock: AssistantDockPosition,
|
||||
pub default_width: Pixels,
|
||||
pub default_height: Pixels,
|
||||
pub default_model: LanguageModelSelection,
|
||||
@@ -101,11 +93,9 @@ pub struct AgentSettings {
|
||||
pub inline_alternatives: Vec<LanguageModelSelection>,
|
||||
pub using_outdated_settings_version: bool,
|
||||
pub default_profile: AgentProfileId,
|
||||
pub default_view: DefaultView,
|
||||
pub profiles: IndexMap<AgentProfileId, AgentProfile>,
|
||||
pub always_allow_tool_actions: bool,
|
||||
pub notify_when_agent_waiting: NotifyWhenAgentWaiting,
|
||||
pub play_sound_when_agent_done: bool,
|
||||
pub stream_edits: bool,
|
||||
pub single_file_review: bool,
|
||||
pub model_parameters: Vec<LanguageModelParameters>,
|
||||
@@ -113,7 +103,7 @@ pub struct AgentSettings {
|
||||
pub enable_feedback: bool,
|
||||
}
|
||||
|
||||
impl AgentSettings {
|
||||
impl AssistantSettings {
|
||||
pub fn temperature_for_model(model: &Arc<dyn LanguageModel>, cx: &App) -> Option<f32> {
|
||||
let settings = Self::get_global(cx);
|
||||
settings
|
||||
@@ -168,56 +158,58 @@ impl LanguageModelParameters {
|
||||
}
|
||||
}
|
||||
|
||||
/// Agent panel settings
|
||||
/// Assistant panel settings
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
|
||||
pub struct AgentSettingsContent {
|
||||
pub struct AssistantSettingsContent {
|
||||
#[serde(flatten)]
|
||||
pub inner: Option<AgentSettingsContentInner>,
|
||||
pub inner: Option<AssistantSettingsContentInner>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum AgentSettingsContentInner {
|
||||
Versioned(Box<VersionedAgentSettingsContent>),
|
||||
Legacy(LegacyAgentSettingsContent),
|
||||
pub enum AssistantSettingsContentInner {
|
||||
Versioned(Box<VersionedAssistantSettingsContent>),
|
||||
Legacy(LegacyAssistantSettingsContent),
|
||||
}
|
||||
|
||||
impl AgentSettingsContentInner {
|
||||
fn for_v2(content: AgentSettingsContentV2) -> Self {
|
||||
AgentSettingsContentInner::Versioned(Box::new(VersionedAgentSettingsContent::V2(content)))
|
||||
impl AssistantSettingsContentInner {
|
||||
fn for_v2(content: AssistantSettingsContentV2) -> Self {
|
||||
AssistantSettingsContentInner::Versioned(Box::new(VersionedAssistantSettingsContent::V2(
|
||||
content,
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for AgentSettingsContent {
|
||||
impl JsonSchema for AssistantSettingsContent {
|
||||
fn schema_name() -> String {
|
||||
VersionedAgentSettingsContent::schema_name()
|
||||
VersionedAssistantSettingsContent::schema_name()
|
||||
}
|
||||
|
||||
fn json_schema(r#gen: &mut schemars::r#gen::SchemaGenerator) -> Schema {
|
||||
VersionedAgentSettingsContent::json_schema(r#gen)
|
||||
VersionedAssistantSettingsContent::json_schema(r#gen)
|
||||
}
|
||||
|
||||
fn is_referenceable() -> bool {
|
||||
VersionedAgentSettingsContent::is_referenceable()
|
||||
VersionedAssistantSettingsContent::is_referenceable()
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentSettingsContent {
|
||||
impl AssistantSettingsContent {
|
||||
pub fn is_version_outdated(&self) -> bool {
|
||||
match &self.inner {
|
||||
Some(AgentSettingsContentInner::Versioned(settings)) => match **settings {
|
||||
VersionedAgentSettingsContent::V1(_) => true,
|
||||
VersionedAgentSettingsContent::V2(_) => false,
|
||||
Some(AssistantSettingsContentInner::Versioned(settings)) => match **settings {
|
||||
VersionedAssistantSettingsContent::V1(_) => true,
|
||||
VersionedAssistantSettingsContent::V2(_) => false,
|
||||
},
|
||||
Some(AgentSettingsContentInner::Legacy(_)) => true,
|
||||
Some(AssistantSettingsContentInner::Legacy(_)) => true,
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn upgrade(&self) -> AgentSettingsContentV2 {
|
||||
fn upgrade(&self) -> AssistantSettingsContentV2 {
|
||||
match &self.inner {
|
||||
Some(AgentSettingsContentInner::Versioned(settings)) => match **settings {
|
||||
VersionedAgentSettingsContent::V1(ref settings) => AgentSettingsContentV2 {
|
||||
Some(AssistantSettingsContentInner::Versioned(settings)) => match **settings {
|
||||
VersionedAssistantSettingsContent::V1(ref settings) => AssistantSettingsContentV2 {
|
||||
enabled: settings.enabled,
|
||||
button: settings.button,
|
||||
dock: settings.dock,
|
||||
@@ -227,49 +219,54 @@ impl AgentSettingsContent {
|
||||
.provider
|
||||
.clone()
|
||||
.and_then(|provider| match provider {
|
||||
AgentProviderContentV1::ZedDotDev { default_model } => default_model
|
||||
.map(|model| LanguageModelSelection {
|
||||
AssistantProviderContentV1::ZedDotDev { default_model } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "zed.dev".into(),
|
||||
model,
|
||||
}),
|
||||
AgentProviderContentV1::OpenAi { default_model, .. } => default_model
|
||||
.map(|model| LanguageModelSelection {
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::OpenAi { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "openai".into(),
|
||||
model: model.id().to_string(),
|
||||
}),
|
||||
AgentProviderContentV1::Anthropic { default_model, .. } => {
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::Anthropic { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "anthropic".into(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AgentProviderContentV1::Ollama { default_model, .. } => default_model
|
||||
.map(|model| LanguageModelSelection {
|
||||
AssistantProviderContentV1::Ollama { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "ollama".into(),
|
||||
model: model.id().to_string(),
|
||||
}),
|
||||
AgentProviderContentV1::LmStudio { default_model, .. } => default_model
|
||||
.map(|model| LanguageModelSelection {
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::LmStudio { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "lmstudio".into(),
|
||||
model: model.id().to_string(),
|
||||
}),
|
||||
AgentProviderContentV1::DeepSeek { default_model, .. } => default_model
|
||||
.map(|model| LanguageModelSelection {
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::DeepSeek { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "deepseek".into(),
|
||||
model: model.id().to_string(),
|
||||
}),
|
||||
AgentProviderContentV1::Mistral { default_model, .. } => default_model
|
||||
.map(|model| LanguageModelSelection {
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::Mistral { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "mistral".into(),
|
||||
model: model.id().to_string(),
|
||||
}),
|
||||
})
|
||||
}
|
||||
}),
|
||||
inline_assistant_model: None,
|
||||
commit_message_model: None,
|
||||
thread_summary_model: None,
|
||||
inline_alternatives: None,
|
||||
default_profile: None,
|
||||
default_view: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
@@ -278,11 +275,10 @@ impl AgentSettingsContent {
|
||||
model_parameters: Vec::new(),
|
||||
preferred_completion_mode: None,
|
||||
enable_feedback: None,
|
||||
play_sound_when_agent_done: None,
|
||||
},
|
||||
VersionedAgentSettingsContent::V2(ref settings) => settings.clone(),
|
||||
VersionedAssistantSettingsContent::V2(ref settings) => settings.clone(),
|
||||
},
|
||||
Some(AgentSettingsContentInner::Legacy(settings)) => AgentSettingsContentV2 {
|
||||
Some(AssistantSettingsContentInner::Legacy(settings)) => AssistantSettingsContentV2 {
|
||||
enabled: None,
|
||||
button: settings.button,
|
||||
dock: settings.dock,
|
||||
@@ -302,7 +298,6 @@ impl AgentSettingsContent {
|
||||
thread_summary_model: None,
|
||||
inline_alternatives: None,
|
||||
default_profile: None,
|
||||
default_view: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
@@ -311,30 +306,31 @@ impl AgentSettingsContent {
|
||||
model_parameters: Vec::new(),
|
||||
preferred_completion_mode: None,
|
||||
enable_feedback: None,
|
||||
play_sound_when_agent_done: None,
|
||||
},
|
||||
None => AgentSettingsContentV2::default(),
|
||||
None => AssistantSettingsContentV2::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_dock(&mut self, dock: AgentDockPosition) {
|
||||
pub fn set_dock(&mut self, dock: AssistantDockPosition) {
|
||||
match &mut self.inner {
|
||||
Some(AgentSettingsContentInner::Versioned(settings)) => match **settings {
|
||||
VersionedAgentSettingsContent::V1(ref mut settings) => {
|
||||
Some(AssistantSettingsContentInner::Versioned(settings)) => match **settings {
|
||||
VersionedAssistantSettingsContent::V1(ref mut settings) => {
|
||||
settings.dock = Some(dock);
|
||||
}
|
||||
VersionedAgentSettingsContent::V2(ref mut settings) => {
|
||||
VersionedAssistantSettingsContent::V2(ref mut settings) => {
|
||||
settings.dock = Some(dock);
|
||||
}
|
||||
},
|
||||
Some(AgentSettingsContentInner::Legacy(settings)) => {
|
||||
Some(AssistantSettingsContentInner::Legacy(settings)) => {
|
||||
settings.dock = Some(dock);
|
||||
}
|
||||
None => {
|
||||
self.inner = Some(AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
|
||||
dock: Some(dock),
|
||||
..Default::default()
|
||||
}))
|
||||
self.inner = Some(AssistantSettingsContentInner::for_v2(
|
||||
AssistantSettingsContentV2 {
|
||||
dock: Some(dock),
|
||||
..Default::default()
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -344,99 +340,105 @@ impl AgentSettingsContent {
|
||||
let provider = language_model.provider_id().0.to_string();
|
||||
|
||||
match &mut self.inner {
|
||||
Some(AgentSettingsContentInner::Versioned(settings)) => match **settings {
|
||||
VersionedAgentSettingsContent::V1(ref mut settings) => match provider.as_ref() {
|
||||
"zed.dev" => {
|
||||
log::warn!("attempted to set zed.dev model on outdated settings");
|
||||
}
|
||||
"anthropic" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AgentProviderContentV1::Anthropic { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AgentProviderContentV1::Anthropic {
|
||||
default_model: AnthropicModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"ollama" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AgentProviderContentV1::Ollama { api_url, .. }) => api_url.clone(),
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AgentProviderContentV1::Ollama {
|
||||
default_model: Some(ollama::Model::new(
|
||||
&model,
|
||||
None,
|
||||
None,
|
||||
Some(language_model.supports_tools()),
|
||||
)),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"lmstudio" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AgentProviderContentV1::LmStudio { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AgentProviderContentV1::LmStudio {
|
||||
default_model: Some(lmstudio::Model::new(&model, None, None, false)),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"openai" => {
|
||||
let (api_url, available_models) = match &settings.provider {
|
||||
Some(AgentProviderContentV1::OpenAi {
|
||||
Some(AssistantSettingsContentInner::Versioned(settings)) => match **settings {
|
||||
VersionedAssistantSettingsContent::V1(ref mut settings) => {
|
||||
match provider.as_ref() {
|
||||
"zed.dev" => {
|
||||
log::warn!("attempted to set zed.dev model on outdated settings");
|
||||
}
|
||||
"anthropic" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::Anthropic { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::Anthropic {
|
||||
default_model: AnthropicModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"ollama" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::Ollama { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::Ollama {
|
||||
default_model: Some(ollama::Model::new(
|
||||
&model,
|
||||
None,
|
||||
None,
|
||||
Some(language_model.supports_tools()),
|
||||
)),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"lmstudio" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::LmStudio { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::LmStudio {
|
||||
default_model: Some(lmstudio::Model::new(&model, None, None)),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"openai" => {
|
||||
let (api_url, available_models) = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::OpenAi {
|
||||
api_url,
|
||||
available_models,
|
||||
..
|
||||
}) => (api_url.clone(), available_models.clone()),
|
||||
_ => (None, None),
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::OpenAi {
|
||||
default_model: OpenAiModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
available_models,
|
||||
..
|
||||
}) => (api_url.clone(), available_models.clone()),
|
||||
_ => (None, None),
|
||||
};
|
||||
settings.provider = Some(AgentProviderContentV1::OpenAi {
|
||||
default_model: OpenAiModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
available_models,
|
||||
});
|
||||
});
|
||||
}
|
||||
"deepseek" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::DeepSeek { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::DeepSeek {
|
||||
default_model: DeepseekModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
"deepseek" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AgentProviderContentV1::DeepSeek { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AgentProviderContentV1::DeepSeek {
|
||||
default_model: DeepseekModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
VersionedAgentSettingsContent::V2(ref mut settings) => {
|
||||
}
|
||||
VersionedAssistantSettingsContent::V2(ref mut settings) => {
|
||||
settings.default_model = Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
});
|
||||
}
|
||||
},
|
||||
Some(AgentSettingsContentInner::Legacy(settings)) => {
|
||||
Some(AssistantSettingsContentInner::Legacy(settings)) => {
|
||||
if let Ok(model) = OpenAiModel::from_id(&language_model.id().0) {
|
||||
settings.default_open_ai_model = Some(model);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.inner = Some(AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
}),
|
||||
..Default::default()
|
||||
}));
|
||||
self.inner = Some(AssistantSettingsContentInner::for_v2(
|
||||
AssistantSettingsContentV2 {
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -465,15 +467,15 @@ impl AgentSettingsContent {
|
||||
|
||||
pub fn v2_setting(
|
||||
&mut self,
|
||||
f: impl FnOnce(&mut AgentSettingsContentV2) -> anyhow::Result<()>,
|
||||
f: impl FnOnce(&mut AssistantSettingsContentV2) -> anyhow::Result<()>,
|
||||
) -> anyhow::Result<()> {
|
||||
match self.inner.get_or_insert_with(|| {
|
||||
AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
|
||||
AssistantSettingsContentInner::for_v2(AssistantSettingsContentV2 {
|
||||
..Default::default()
|
||||
})
|
||||
}) {
|
||||
AgentSettingsContentInner::Versioned(boxed) => {
|
||||
if let VersionedAgentSettingsContent::V2(ref mut settings) = **boxed {
|
||||
AssistantSettingsContentInner::Versioned(boxed) => {
|
||||
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
|
||||
f(settings)
|
||||
} else {
|
||||
Ok(())
|
||||
@@ -502,14 +504,6 @@ impl AgentSettingsContent {
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub fn set_play_sound_when_agent_done(&mut self, allow: bool) {
|
||||
self.v2_setting(|setting| {
|
||||
setting.play_sound_when_agent_done = Some(allow);
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub fn set_single_file_review(&mut self, allow: bool) {
|
||||
self.v2_setting(|setting| {
|
||||
setting.single_file_review = Some(allow);
|
||||
@@ -566,16 +560,16 @@ impl AgentSettingsContent {
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[serde(tag = "version")]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub enum VersionedAgentSettingsContent {
|
||||
pub enum VersionedAssistantSettingsContent {
|
||||
#[serde(rename = "1")]
|
||||
V1(AgentSettingsContentV1),
|
||||
V1(AssistantSettingsContentV1),
|
||||
#[serde(rename = "2")]
|
||||
V2(AgentSettingsContentV2),
|
||||
V2(AssistantSettingsContentV2),
|
||||
}
|
||||
|
||||
impl Default for VersionedAgentSettingsContent {
|
||||
impl Default for VersionedAssistantSettingsContent {
|
||||
fn default() -> Self {
|
||||
Self::V2(AgentSettingsContentV2 {
|
||||
Self::V2(AssistantSettingsContentV2 {
|
||||
enabled: None,
|
||||
button: None,
|
||||
dock: None,
|
||||
@@ -587,7 +581,6 @@ impl Default for VersionedAgentSettingsContent {
|
||||
thread_summary_model: None,
|
||||
inline_alternatives: None,
|
||||
default_profile: None,
|
||||
default_view: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
@@ -596,31 +589,30 @@ impl Default for VersionedAgentSettingsContent {
|
||||
model_parameters: Vec::new(),
|
||||
preferred_completion_mode: None,
|
||||
enable_feedback: None,
|
||||
play_sound_when_agent_done: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct AgentSettingsContentV2 {
|
||||
/// Whether the Agent is enabled.
|
||||
pub struct AssistantSettingsContentV2 {
|
||||
/// Whether the Assistant is enabled.
|
||||
///
|
||||
/// Default: true
|
||||
enabled: Option<bool>,
|
||||
/// Whether to show the agent panel button in the status bar.
|
||||
/// Whether to show the assistant panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
button: Option<bool>,
|
||||
/// Where to dock the agent panel.
|
||||
/// Where to dock the assistant.
|
||||
///
|
||||
/// Default: right
|
||||
dock: Option<AgentDockPosition>,
|
||||
/// Default width in pixels when the agent panel is docked to the left or right.
|
||||
dock: Option<AssistantDockPosition>,
|
||||
/// Default width in pixels when the assistant is docked to the left or right.
|
||||
///
|
||||
/// Default: 640
|
||||
default_width: Option<f32>,
|
||||
/// Default height in pixels when the agent panel is docked to the bottom.
|
||||
/// Default height in pixels when the assistant is docked to the bottom.
|
||||
///
|
||||
/// Default: 320
|
||||
default_height: Option<f32>,
|
||||
@@ -638,10 +630,6 @@ pub struct AgentSettingsContentV2 {
|
||||
///
|
||||
/// Default: write
|
||||
default_profile: Option<AgentProfileId>,
|
||||
/// Which view type to show by default in the agent panel.
|
||||
///
|
||||
/// Default: "thread"
|
||||
default_view: Option<DefaultView>,
|
||||
/// The available agent profiles.
|
||||
pub profiles: Option<IndexMap<AgentProfileId, AgentProfileContent>>,
|
||||
/// Whenever a tool action would normally wait for your confirmation
|
||||
@@ -653,10 +641,6 @@ pub struct AgentSettingsContentV2 {
|
||||
///
|
||||
/// Default: "primary_screen"
|
||||
notify_when_agent_waiting: Option<NotifyWhenAgentWaiting>,
|
||||
/// Whether to play a sound when the agent has either completed its response, or needs user input.
|
||||
///
|
||||
/// Default: false
|
||||
play_sound_when_agent_done: Option<bool>,
|
||||
/// Whether to stream edits from the agent as they are received.
|
||||
///
|
||||
/// Default: false
|
||||
@@ -674,6 +658,7 @@ pub struct AgentSettingsContentV2 {
|
||||
/// Default: []
|
||||
#[serde(default)]
|
||||
model_parameters: Vec<LanguageModelParameters>,
|
||||
|
||||
/// What completion mode to enable for new threads
|
||||
///
|
||||
/// Default: normal
|
||||
@@ -689,14 +674,14 @@ pub struct AgentSettingsContentV2 {
|
||||
pub enum CompletionMode {
|
||||
#[default]
|
||||
Normal,
|
||||
Burn,
|
||||
Max,
|
||||
}
|
||||
|
||||
impl From<CompletionMode> for zed_llm_client::CompletionMode {
|
||||
fn from(value: CompletionMode) -> Self {
|
||||
match value {
|
||||
CompletionMode::Normal => zed_llm_client::CompletionMode::Normal,
|
||||
CompletionMode::Burn => zed_llm_client::CompletionMode::Max,
|
||||
CompletionMode::Max => zed_llm_client::CompletionMode::Max,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -774,50 +759,50 @@ pub struct ContextServerPresetContent {
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct AgentSettingsContentV1 {
|
||||
/// Whether the Agent is enabled.
|
||||
pub struct AssistantSettingsContentV1 {
|
||||
/// Whether the Assistant is enabled.
|
||||
///
|
||||
/// Default: true
|
||||
enabled: Option<bool>,
|
||||
/// Whether to show the Agent panel button in the status bar.
|
||||
/// Whether to show the assistant panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
button: Option<bool>,
|
||||
/// Where to dock the Agent.
|
||||
/// Where to dock the assistant.
|
||||
///
|
||||
/// Default: right
|
||||
dock: Option<AgentDockPosition>,
|
||||
/// Default width in pixels when the Agent is docked to the left or right.
|
||||
dock: Option<AssistantDockPosition>,
|
||||
/// Default width in pixels when the assistant is docked to the left or right.
|
||||
///
|
||||
/// Default: 640
|
||||
default_width: Option<f32>,
|
||||
/// Default height in pixels when the Agent is docked to the bottom.
|
||||
/// Default height in pixels when the assistant is docked to the bottom.
|
||||
///
|
||||
/// Default: 320
|
||||
default_height: Option<f32>,
|
||||
/// The provider of the Agent service.
|
||||
/// The provider of the assistant service.
|
||||
///
|
||||
/// This can be "openai", "anthropic", "ollama", "lmstudio", "deepseek", "zed.dev"
|
||||
/// each with their respective default models and configurations.
|
||||
provider: Option<AgentProviderContentV1>,
|
||||
provider: Option<AssistantProviderContentV1>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct LegacyAgentSettingsContent {
|
||||
/// Whether to show the Agent panel button in the status bar.
|
||||
pub struct LegacyAssistantSettingsContent {
|
||||
/// Whether to show the assistant panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
pub button: Option<bool>,
|
||||
/// Where to dock the Agent.
|
||||
/// Where to dock the assistant.
|
||||
///
|
||||
/// Default: right
|
||||
pub dock: Option<AgentDockPosition>,
|
||||
/// Default width in pixels when the Agent is docked to the left or right.
|
||||
pub dock: Option<AssistantDockPosition>,
|
||||
/// Default width in pixels when the assistant is docked to the left or right.
|
||||
///
|
||||
/// Default: 640
|
||||
pub default_width: Option<f32>,
|
||||
/// Default height in pixels when the Agent is docked to the bottom.
|
||||
/// Default height in pixels when the assistant is docked to the bottom.
|
||||
///
|
||||
/// Default: 320
|
||||
pub default_height: Option<f32>,
|
||||
@@ -831,20 +816,20 @@ pub struct LegacyAgentSettingsContent {
|
||||
pub openai_api_url: Option<String>,
|
||||
}
|
||||
|
||||
impl Settings for AgentSettings {
|
||||
impl Settings for AssistantSettings {
|
||||
const KEY: Option<&'static str> = Some("agent");
|
||||
|
||||
const FALLBACK_KEY: Option<&'static str> = Some("assistant");
|
||||
|
||||
const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
|
||||
|
||||
type FileContent = AgentSettingsContent;
|
||||
type FileContent = AssistantSettingsContent;
|
||||
|
||||
fn load(
|
||||
sources: SettingsSources<Self::FileContent>,
|
||||
_: &mut gpui::App,
|
||||
) -> anyhow::Result<Self> {
|
||||
let mut settings = AgentSettings::default();
|
||||
let mut settings = AssistantSettings::default();
|
||||
|
||||
for value in sources.defaults_and_customizations() {
|
||||
if value.is_version_outdated() {
|
||||
@@ -882,14 +867,9 @@ impl Settings for AgentSettings {
|
||||
&mut settings.notify_when_agent_waiting,
|
||||
value.notify_when_agent_waiting,
|
||||
);
|
||||
merge(
|
||||
&mut settings.play_sound_when_agent_done,
|
||||
value.play_sound_when_agent_done,
|
||||
);
|
||||
merge(&mut settings.stream_edits, value.stream_edits);
|
||||
merge(&mut settings.single_file_review, value.single_file_review);
|
||||
merge(&mut settings.default_profile, value.default_profile);
|
||||
merge(&mut settings.default_view, value.default_view);
|
||||
merge(
|
||||
&mut settings.preferred_completion_mode,
|
||||
value.preferred_completion_mode,
|
||||
@@ -939,25 +919,28 @@ impl Settings for AgentSettings {
|
||||
.and_then(|b| b.as_bool())
|
||||
{
|
||||
match &mut current.inner {
|
||||
Some(AgentSettingsContentInner::Versioned(versioned)) => match versioned.as_mut() {
|
||||
VersionedAgentSettingsContent::V1(setting) => {
|
||||
setting.enabled = Some(b);
|
||||
setting.button = Some(b);
|
||||
}
|
||||
Some(AssistantSettingsContentInner::Versioned(versioned)) => {
|
||||
match versioned.as_mut() {
|
||||
VersionedAssistantSettingsContent::V1(setting) => {
|
||||
setting.enabled = Some(b);
|
||||
setting.button = Some(b);
|
||||
}
|
||||
|
||||
VersionedAgentSettingsContent::V2(setting) => {
|
||||
setting.enabled = Some(b);
|
||||
setting.button = Some(b);
|
||||
VersionedAssistantSettingsContent::V2(setting) => {
|
||||
setting.enabled = Some(b);
|
||||
setting.button = Some(b);
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(AgentSettingsContentInner::Legacy(setting)) => setting.button = Some(b),
|
||||
}
|
||||
Some(AssistantSettingsContentInner::Legacy(setting)) => setting.button = Some(b),
|
||||
None => {
|
||||
current.inner =
|
||||
Some(AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
|
||||
current.inner = Some(AssistantSettingsContentInner::for_v2(
|
||||
AssistantSettingsContentV2 {
|
||||
enabled: Some(b),
|
||||
button: Some(b),
|
||||
..Default::default()
|
||||
}));
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -979,7 +962,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_deserialize_agent_settings_with_version(cx: &mut TestAppContext) {
|
||||
async fn test_deserialize_assistant_settings_with_version(cx: &mut TestAppContext) {
|
||||
let fs = fs::FakeFs::new(cx.executor().clone());
|
||||
fs.create_dir(paths::settings_file().parent().unwrap())
|
||||
.await
|
||||
@@ -988,51 +971,51 @@ mod tests {
|
||||
cx.update(|cx| {
|
||||
let test_settings = settings::SettingsStore::test(cx);
|
||||
cx.set_global(test_settings);
|
||||
AgentSettings::register(cx);
|
||||
AssistantSettings::register(cx);
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
assert!(!AgentSettings::get_global(cx).using_outdated_settings_version);
|
||||
assert!(!AssistantSettings::get_global(cx).using_outdated_settings_version);
|
||||
assert_eq!(
|
||||
AgentSettings::get_global(cx).default_model,
|
||||
AssistantSettings::get_global(cx).default_model,
|
||||
LanguageModelSelection {
|
||||
provider: "zed.dev".into(),
|
||||
model: "claude-sonnet-4".into(),
|
||||
model: "claude-3-7-sonnet-latest".into(),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
settings::SettingsStore::global(cx).update_settings_file::<AgentSettings>(
|
||||
settings::SettingsStore::global(cx).update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
|settings, _| {
|
||||
*settings = AgentSettingsContent {
|
||||
inner: Some(AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
|
||||
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,
|
||||
default_profile: None,
|
||||
default_view: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
play_sound_when_agent_done: None,
|
||||
notify_when_agent_waiting: None,
|
||||
stream_edits: None,
|
||||
single_file_review: None,
|
||||
enable_feedback: None,
|
||||
model_parameters: Vec::new(),
|
||||
preferred_completion_mode: None,
|
||||
})),
|
||||
*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,
|
||||
default_profile: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
stream_edits: None,
|
||||
single_file_review: None,
|
||||
enable_feedback: None,
|
||||
model_parameters: Vec::new(),
|
||||
preferred_completion_mode: None,
|
||||
},
|
||||
)),
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -1044,14 +1027,14 @@ mod tests {
|
||||
assert!(raw_settings_value.contains(r#""version": "2""#));
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AgentSettingsTest {
|
||||
agent: AgentSettingsContent,
|
||||
struct AssistantSettingsTest {
|
||||
agent: AssistantSettingsContent,
|
||||
}
|
||||
|
||||
let agent_settings: AgentSettingsTest =
|
||||
let assistant_settings: AssistantSettingsTest =
|
||||
serde_json_lenient::from_str(&raw_settings_value).unwrap();
|
||||
|
||||
assert!(!agent_settings.agent.is_version_outdated());
|
||||
assert!(!assistant_settings.agent.is_version_outdated());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -1076,27 +1059,29 @@ mod tests {
|
||||
.set_user_settings(user_settings_content, cx)
|
||||
.unwrap();
|
||||
cx.set_global(test_settings);
|
||||
AgentSettings::register(cx);
|
||||
AssistantSettings::register(cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let agent_settings = cx.update(|cx| AgentSettings::get_global(cx).clone());
|
||||
assert!(agent_settings.enabled);
|
||||
assert!(!agent_settings.using_outdated_settings_version);
|
||||
assert_eq!(agent_settings.default_model.model, "gpt-99");
|
||||
let assistant_settings = cx.update(|cx| AssistantSettings::get_global(cx).clone());
|
||||
assert!(assistant_settings.enabled);
|
||||
assert!(!assistant_settings.using_outdated_settings_version);
|
||||
assert_eq!(assistant_settings.default_model.model, "gpt-99");
|
||||
|
||||
cx.update_global::<SettingsStore, _>(|settings_store, cx| {
|
||||
settings_store.update_user_settings::<AgentSettings>(cx, |settings| {
|
||||
*settings = AgentSettingsContent {
|
||||
inner: Some(AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
|
||||
enabled: Some(false),
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: "xai".to_owned().into(),
|
||||
model: "grok".to_owned(),
|
||||
}),
|
||||
..Default::default()
|
||||
})),
|
||||
settings_store.update_user_settings::<AssistantSettings>(cx, |settings| {
|
||||
*settings = AssistantSettingsContent {
|
||||
inner: Some(AssistantSettingsContentInner::for_v2(
|
||||
AssistantSettingsContentV2 {
|
||||
enabled: Some(false),
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: "xai".to_owned().into(),
|
||||
model: "grok".to_owned(),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
)),
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -1106,12 +1091,12 @@ mod tests {
|
||||
let settings = cx.update(|cx| SettingsStore::global(cx).raw_user_settings().clone());
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AgentSettingsTest {
|
||||
assistant: AgentSettingsContent,
|
||||
struct AssistantSettingsTest {
|
||||
assistant: AssistantSettingsContent,
|
||||
agent: Option<serde_json_lenient::Value>,
|
||||
}
|
||||
|
||||
let agent_settings: AgentSettingsTest = serde_json::from_value(settings).unwrap();
|
||||
assert!(agent_settings.agent.is_none());
|
||||
let assistant_settings: AssistantSettingsTest = serde_json::from_value(settings).unwrap();
|
||||
assert!(assistant_settings.agent.is_none());
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ use anyhow::Result;
|
||||
use futures::StreamExt;
|
||||
use futures::stream::{self, BoxStream};
|
||||
use gpui::{App, SharedString, Task, WeakEntity, Window};
|
||||
use language::HighlightId;
|
||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
|
||||
pub use language_model::Role;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -17,7 +16,6 @@ use std::{
|
||||
ops::Range,
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
use ui::ActiveTheme;
|
||||
use workspace::{Workspace, ui::IconName};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
@@ -327,18 +325,6 @@ impl SlashCommandLine {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_label_for_command(command_name: &str, arguments: &[&str], cx: &App) -> CodeLabel {
|
||||
let mut label = CodeLabel::default();
|
||||
label.push_str(command_name, None);
|
||||
label.push_str(" ", None);
|
||||
label.push_str(
|
||||
&arguments.join(" "),
|
||||
cx.theme().syntax().highlight_id("comment").map(HighlightId),
|
||||
);
|
||||
label.filter_range = 0..command_name.len();
|
||||
label
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
@@ -35,6 +35,7 @@ rope.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
terminal_view.workspace = true
|
||||
text.workspace = true
|
||||
toml.workspace = true
|
||||
ui.workspace = true
|
||||
@@ -44,6 +45,6 @@ worktree.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
settings.workspace = true
|
||||
zlog.workspace = true
|
||||
|
||||
@@ -12,6 +12,11 @@ mod selection_command;
|
||||
mod streaming_example_command;
|
||||
mod symbols_command;
|
||||
mod tab_command;
|
||||
mod terminal_command;
|
||||
|
||||
use gpui::App;
|
||||
use language::{CodeLabel, HighlightId};
|
||||
use ui::ActiveTheme as _;
|
||||
|
||||
pub use crate::cargo_workspace_command::*;
|
||||
pub use crate::context_server_command::*;
|
||||
@@ -27,5 +32,16 @@ pub use crate::selection_command::*;
|
||||
pub use crate::streaming_example_command::*;
|
||||
pub use crate::symbols_command::*;
|
||||
pub use crate::tab_command::*;
|
||||
pub use crate::terminal_command::*;
|
||||
|
||||
use assistant_slash_command::create_label_for_command;
|
||||
pub fn create_label_for_command(command_name: &str, arguments: &[&str], cx: &App) -> CodeLabel {
|
||||
let mut label = CodeLabel::default();
|
||||
label.push_str(command_name, None);
|
||||
label.push_str(" ", None);
|
||||
label.push_str(
|
||||
&arguments.join(" "),
|
||||
cx.theme().syntax().highlight_id("comment").map(HighlightId),
|
||||
);
|
||||
label.filter_range = 0..command_name.len();
|
||||
label
|
||||
}
|
||||
|
||||
@@ -587,7 +587,9 @@ mod test {
|
||||
use super::collect_files;
|
||||
|
||||
pub fn init_test(cx: &mut gpui::TestAppContext) {
|
||||
zlog::init_test();
|
||||
if std::env::var("RUST_LOG").is_ok() {
|
||||
env_logger::try_init().ok();
|
||||
}
|
||||
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use crate::{TerminalView, terminal_panel::TerminalPanel};
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
@@ -9,10 +8,11 @@ use assistant_slash_command::{
|
||||
};
|
||||
use gpui::{App, Entity, Task, WeakEntity};
|
||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
|
||||
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
|
||||
use ui::prelude::*;
|
||||
use workspace::{Workspace, dock::Panel};
|
||||
|
||||
use assistant_slash_command::create_label_for_command;
|
||||
use super::create_label_for_command;
|
||||
|
||||
pub struct TerminalSlashCommand;
|
||||
|
||||
@@ -37,6 +37,7 @@ buffer_diff = { workspace = true, features = ["test-support"] }
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
clock = { workspace = true, features = ["test-support"] }
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
language_model = { workspace = true, features = ["test-support"] }
|
||||
@@ -47,4 +48,3 @@ rand.workspace = true
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
text = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
zlog.workspace = true
|
||||
|
||||
@@ -415,38 +415,14 @@ impl ActionLog {
|
||||
self.project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
|
||||
} else {
|
||||
// For a file created by AI with no pre-existing content,
|
||||
// only delete the file if we're certain it contains only AI content
|
||||
// with no edits from the user.
|
||||
|
||||
let initial_version = tracked_buffer.version.clone();
|
||||
let current_version = buffer.read(cx).version();
|
||||
|
||||
let current_content = buffer.read(cx).text();
|
||||
let tracked_content = tracked_buffer.snapshot.text();
|
||||
|
||||
let is_ai_only_content =
|
||||
initial_version == current_version && current_content == tracked_content;
|
||||
|
||||
if is_ai_only_content {
|
||||
buffer
|
||||
.read(cx)
|
||||
.entry_id(cx)
|
||||
.and_then(|entry_id| {
|
||||
self.project.update(cx, |project, cx| {
|
||||
project.delete_entry(entry_id, false, cx)
|
||||
})
|
||||
})
|
||||
.unwrap_or(Task::ready(Ok(())))
|
||||
} else {
|
||||
// Not sure how to disentangle edits made by the user
|
||||
// from edits made by the AI at this point.
|
||||
// For now, preserve both to avoid data loss.
|
||||
//
|
||||
// TODO: Better solution (disable "Reject" after user makes some
|
||||
// edit or find a way to differentiate between AI and user edits)
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
buffer
|
||||
.read(cx)
|
||||
.entry_id(cx)
|
||||
.and_then(|entry_id| {
|
||||
self.project
|
||||
.update(cx, |project, cx| project.delete_entry(entry_id, false, cx))
|
||||
})
|
||||
.unwrap_or(Task::ready(Ok(())))
|
||||
};
|
||||
|
||||
self.tracked_buffers.remove(&buffer);
|
||||
@@ -741,7 +717,9 @@ mod tests {
|
||||
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
zlog::init_test();
|
||||
if std::env::var("RUST_LOG").is_ok() {
|
||||
env_logger::init();
|
||||
}
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
@@ -1600,6 +1578,7 @@ mod tests {
|
||||
project.find_project_path("dir/new_file", cx)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(file_path, cx))
|
||||
.await
|
||||
@@ -1642,72 +1621,6 @@ mod tests {
|
||||
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_reject_created_file_with_user_edits(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], 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/new_file", cx)
|
||||
})
|
||||
.unwrap();
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(file_path, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// AI creates file with initial content
|
||||
cx.update(|cx| {
|
||||
action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
|
||||
buffer.update(cx, |buffer, cx| buffer.set_text("ai content", cx));
|
||||
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
||||
});
|
||||
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// User makes additional edits
|
||||
cx.update(|cx| {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(10..10, "\nuser added this line")], None, cx);
|
||||
});
|
||||
});
|
||||
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
|
||||
|
||||
// Reject all
|
||||
action_log
|
||||
.update(cx, |log, cx| {
|
||||
log.reject_edits_in_ranges(
|
||||
buffer.clone(),
|
||||
vec![Point::new(0, 0)..Point::new(100, 0)],
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
// File should still contain all the content
|
||||
assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
|
||||
|
||||
let content = buffer.read_with(cx, |buffer, _| buffer.text());
|
||||
assert_eq!(content, "ai content\nuser added this line");
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
async fn test_random_diffs(mut rng: StdRng, cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
@@ -15,10 +15,10 @@ path = "src/assistant_tools.rs"
|
||||
eval = []
|
||||
|
||||
[dependencies]
|
||||
agent_settings.workspace = true
|
||||
aho-corasick.workspace = true
|
||||
anyhow.workspace = true
|
||||
assistant_settings.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
async-watch.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
chrono.workspace = true
|
||||
collections.workspace = true
|
||||
@@ -36,13 +36,11 @@ itertools.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
markdown.workspace = true
|
||||
open.workspace = true
|
||||
paths.workspace = true
|
||||
portable-pty.workspace = true
|
||||
project.workspace = true
|
||||
prompt_store.workspace = true
|
||||
regex.workspace = true
|
||||
rust-embed.workspace = true
|
||||
schemars.workspace = true
|
||||
@@ -65,7 +63,6 @@ workspace.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
lsp = { workspace = true, features = ["test-support"] }
|
||||
client = { workspace = true, features = ["test-support"] }
|
||||
clock = { workspace = true, features = ["test-support"] }
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -96,7 +96,7 @@ fn register_web_search_tool(registry: &Entity<LanguageModelRegistry>, cx: &mut A
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use agent_settings::AgentSettings;
|
||||
use assistant_settings::AssistantSettings;
|
||||
use client::Client;
|
||||
use clock::FakeSystemClock;
|
||||
use http_client::FakeHttpClient;
|
||||
@@ -133,7 +133,7 @@ mod tests {
|
||||
#[gpui::test]
|
||||
fn test_builtin_tool_schema_compatibility(cx: &mut App) {
|
||||
settings::init(cx);
|
||||
AgentSettings::register(cx);
|
||||
AssistantSettings::register(cx);
|
||||
|
||||
let client = Client::new(
|
||||
Arc::new(FakeSystemClock::new()),
|
||||
|
||||
9
crates/assistant_tools/src/batch_tool/description.md
Normal file
9
crates/assistant_tools/src/batch_tool/description.md
Normal file
@@ -0,0 +1,9 @@
|
||||
Invoke multiple other tool calls either sequentially or concurrently.
|
||||
|
||||
This tool is useful when you need to perform several operations at once, improving efficiency by reducing the number of back-and-forth interactions needed to complete complex tasks.
|
||||
|
||||
If the tool calls are set to be run sequentially, then each tool call within the batch is executed in the order provided. If it's set to run concurrently, then they may run in a different order. Regardless, all tool calls will have the same permissions and context as if they were called individually.
|
||||
|
||||
This tool should never be used to run a total of one tool. Instead, just run that one tool directly. You can run batches within batches if desired, which is a way you can mix concurrent and sequential tool call execution.
|
||||
|
||||
When it's possible to run tools in a batch, you should run as many as possible in the batch, up to a maximum of 32. For example, don't run multiple consecutive batches of 10 when you could instead run one batch of 30.
|
||||
19
crates/assistant_tools/src/code_action_tool/description.md
Normal file
19
crates/assistant_tools/src/code_action_tool/description.md
Normal file
@@ -0,0 +1,19 @@
|
||||
A tool for applying code actions to specific sections of your code. It uses language servers to provide refactoring capabilities similar to what you'd find in an IDE.
|
||||
|
||||
This tool can:
|
||||
- List all available code actions for a selected text range
|
||||
- Execute a specific code action on that range
|
||||
- Rename symbols across your codebase. This tool is the preferred way to rename things, and you should always prefer to rename code symbols using this tool rather than using textual find/replace when both are available.
|
||||
|
||||
Use this tool when you want to:
|
||||
- Discover what code actions are available for a piece of code
|
||||
- Apply automatic fixes and code transformations
|
||||
- Rename variables, functions, or other symbols consistently throughout your project
|
||||
- Clean up imports, implement interfaces, or perform other language-specific operations
|
||||
|
||||
- If unsure what actions are available, call the tool without specifying an action to get a list
|
||||
- For common operations, you can directly specify actions like "quickfix.all" or "source.organizeImports"
|
||||
- For renaming, use the special "textDocument/rename" action and provide the new name in the arguments field
|
||||
- Be specific with your text range and context to ensure the tool identifies the correct code location
|
||||
|
||||
The tool will automatically save any changes it makes to your files.
|
||||
39
crates/assistant_tools/src/code_symbols_tool/description.md
Normal file
39
crates/assistant_tools/src/code_symbols_tool/description.md
Normal file
@@ -0,0 +1,39 @@
|
||||
Returns either an outline of the public code symbols in the entire project (grouped by file) or else an outline of both the public and private code symbols within a particular file.
|
||||
|
||||
When a path is provided, this tool returns a hierarchical outline of code symbols for that specific file.
|
||||
When no path is provided, it returns a list of all public code symbols in the project, organized by file.
|
||||
|
||||
You can also provide an optional regular expression which filters the output by only showing code symbols which match that regex.
|
||||
|
||||
Results are paginated with 2000 entries per page. Use the optional 'offset' parameter to request subsequent pages.
|
||||
|
||||
Markdown headings indicate the structure of the output; just like
|
||||
with markdown headings, the more # symbols there are at the beginning of a line,
|
||||
the deeper it is in the hierarchy.
|
||||
|
||||
Each code symbol entry ends with a line number or range, which tells you what portion of the
|
||||
underlying source code file corresponds to that part of the outline. You can use
|
||||
that line information with other tools, to strategically read portions of the source code.
|
||||
|
||||
For example, you can use this tool to find a relevant symbol in the project, then get the outline of the file which contains that symbol, then use the line number information from that file's outline to read different sections of that file, without having to read the entire file all at once (which can be slow, or use a lot of tokens).
|
||||
|
||||
<example>
|
||||
# class Foo [L123-136]
|
||||
## method do_something(arg1, arg2) [L124-126]
|
||||
## method process_data(data) [L128-135]
|
||||
# class Bar [L145-161]
|
||||
## method initialize() [L146-149]
|
||||
## method update_state(new_state) [L160]
|
||||
## private method _validate_state(state) [L161-162]
|
||||
</example>
|
||||
|
||||
This example shows how tree-sitter outlines the structure of source code:
|
||||
|
||||
1. `class Foo` is defined on lines 123-136
|
||||
- It contains a method `do_something` spanning lines 124-126
|
||||
- It also has a method `process_data` spanning lines 128-135
|
||||
|
||||
2. `class Bar` is defined on lines 145-161
|
||||
- It has an `initialize` method spanning lines 146-149
|
||||
- It has an `update_state` method on line 160
|
||||
- It has a private method `_validate_state` spanning lines 161-162
|
||||
9
crates/assistant_tools/src/contents_tool/description.md
Normal file
9
crates/assistant_tools/src/contents_tool/description.md
Normal file
@@ -0,0 +1,9 @@
|
||||
Reads the contents of a path on the filesystem.
|
||||
|
||||
If the path is a directory, this lists all files and directories within that path.
|
||||
If the path is a file, this returns the file's contents.
|
||||
|
||||
When reading a file, if the file is too big and no line range is specified, an outline of the file's code symbols is listed instead, which can be used to request specific line ranges in a subsequent call.
|
||||
|
||||
Similarly, if a directory has too many entries to show at once, a subset of entries will be shown,
|
||||
and subsequent requests can use starting and ending line numbers to get other subsets.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,234 +0,0 @@
|
||||
use regex::Regex;
|
||||
use smallvec::SmallVec;
|
||||
use std::cell::LazyCell;
|
||||
use util::debug_panic;
|
||||
|
||||
const START_MARKER: LazyCell<Regex> = LazyCell::new(|| Regex::new(r"\n?```\S*\n").unwrap());
|
||||
const END_MARKER: LazyCell<Regex> = LazyCell::new(|| Regex::new(r"(^|\n)```\s*$").unwrap());
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CreateFileParserEvent {
|
||||
NewTextChunk { chunk: String },
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CreateFileParser {
|
||||
state: ParserState,
|
||||
buffer: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum ParserState {
|
||||
Pending,
|
||||
WithinText,
|
||||
Finishing,
|
||||
Finished,
|
||||
}
|
||||
|
||||
impl CreateFileParser {
|
||||
pub fn new() -> Self {
|
||||
CreateFileParser {
|
||||
state: ParserState::Pending,
|
||||
buffer: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, chunk: Option<&str>) -> SmallVec<[CreateFileParserEvent; 1]> {
|
||||
if chunk.is_none() {
|
||||
self.state = ParserState::Finishing;
|
||||
}
|
||||
|
||||
let chunk = chunk.unwrap_or_default();
|
||||
|
||||
self.buffer.push_str(chunk);
|
||||
|
||||
let mut edit_events = SmallVec::new();
|
||||
loop {
|
||||
match &mut self.state {
|
||||
ParserState::Pending => {
|
||||
if let Some(m) = START_MARKER.find(&self.buffer) {
|
||||
self.buffer.drain(..m.end());
|
||||
self.state = ParserState::WithinText;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
ParserState::WithinText => {
|
||||
let text = self.buffer.trim_end_matches(&['`', '\n', ' ']);
|
||||
let text_len = text.len();
|
||||
|
||||
if text_len > 0 {
|
||||
edit_events.push(CreateFileParserEvent::NewTextChunk {
|
||||
chunk: self.buffer.drain(..text_len).collect(),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
ParserState::Finishing => {
|
||||
if let Some(m) = END_MARKER.find(&self.buffer) {
|
||||
self.buffer.drain(m.start()..);
|
||||
}
|
||||
if !self.buffer.is_empty() {
|
||||
if !self.buffer.ends_with('\n') {
|
||||
self.buffer.push('\n');
|
||||
}
|
||||
edit_events.push(CreateFileParserEvent::NewTextChunk {
|
||||
chunk: self.buffer.drain(..).collect(),
|
||||
});
|
||||
}
|
||||
self.state = ParserState::Finished;
|
||||
break;
|
||||
}
|
||||
ParserState::Finished => debug_panic!("Can't call parser after finishing"),
|
||||
}
|
||||
}
|
||||
edit_events
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use indoc::indoc;
|
||||
use rand::prelude::*;
|
||||
use std::cmp;
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_happy_path(mut rng: StdRng) {
|
||||
let mut parser = CreateFileParser::new();
|
||||
assert_eq!(
|
||||
parse_random_chunks("```\nHello world\n```", &mut parser, &mut rng),
|
||||
"Hello world".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_cut_prefix(mut rng: StdRng) {
|
||||
let mut parser = CreateFileParser::new();
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
indoc! {"
|
||||
Let me write this file for you:
|
||||
|
||||
```
|
||||
Hello world
|
||||
```
|
||||
|
||||
"},
|
||||
&mut parser,
|
||||
&mut rng
|
||||
),
|
||||
"Hello world".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_language_name_on_fences(mut rng: StdRng) {
|
||||
let mut parser = CreateFileParser::new();
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
indoc! {"
|
||||
```rust
|
||||
Hello world
|
||||
```
|
||||
|
||||
"},
|
||||
&mut parser,
|
||||
&mut rng
|
||||
),
|
||||
"Hello world".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_leave_suffix(mut rng: StdRng) {
|
||||
let mut parser = CreateFileParser::new();
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
indoc! {"
|
||||
Let me write this file for you:
|
||||
|
||||
```
|
||||
Hello world
|
||||
```
|
||||
|
||||
The end
|
||||
"},
|
||||
&mut parser,
|
||||
&mut rng
|
||||
),
|
||||
// This output is marlformed, so we're doing our best effort
|
||||
"Hello world\n```\n\nThe end\n".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_inner_fences(mut rng: StdRng) {
|
||||
let mut parser = CreateFileParser::new();
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
indoc! {"
|
||||
Let me write this file for you:
|
||||
|
||||
```
|
||||
```
|
||||
Hello world
|
||||
```
|
||||
```
|
||||
"},
|
||||
&mut parser,
|
||||
&mut rng
|
||||
),
|
||||
// This output is marlformed, so we're doing our best effort
|
||||
"```\nHello world\n```\n".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
fn test_empty_file(mut rng: StdRng) {
|
||||
let mut parser = CreateFileParser::new();
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
indoc! {"
|
||||
```
|
||||
```
|
||||
"},
|
||||
&mut parser,
|
||||
&mut rng
|
||||
),
|
||||
"".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
fn parse_random_chunks(input: &str, parser: &mut CreateFileParser, rng: &mut StdRng) -> String {
|
||||
let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50));
|
||||
let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);
|
||||
chunk_indices.sort();
|
||||
chunk_indices.push(input.len());
|
||||
|
||||
let chunk_indices = chunk_indices
|
||||
.into_iter()
|
||||
.map(Some)
|
||||
.chain(vec![None])
|
||||
.collect::<Vec<Option<usize>>>();
|
||||
|
||||
let mut edit = String::default();
|
||||
let mut last_ix = 0;
|
||||
for chunk_ix in chunk_indices {
|
||||
let mut chunk = None;
|
||||
if let Some(chunk_ix) = chunk_ix {
|
||||
chunk = Some(&input[last_ix..chunk_ix]);
|
||||
last_ix = chunk_ix;
|
||||
}
|
||||
|
||||
for event in parser.push(chunk) {
|
||||
match event {
|
||||
CreateFileParserEvent::NewTextChunk { chunk } => {
|
||||
edit.push_str(&chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
edit
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,16 @@ use derive_more::{Add, AddAssign};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smallvec::SmallVec;
|
||||
use std::{mem, ops::Range};
|
||||
use std::{cmp, mem, ops::Range};
|
||||
|
||||
const OLD_TEXT_END_TAG: &str = "</old_text>";
|
||||
const NEW_TEXT_END_TAG: &str = "</new_text>";
|
||||
const EDITS_END_TAG: &str = "</edits>";
|
||||
const END_TAGS: [&str; 3] = [OLD_TEXT_END_TAG, NEW_TEXT_END_TAG, EDITS_END_TAG];
|
||||
const END_TAG_LEN: usize = OLD_TEXT_END_TAG.len();
|
||||
const _: () = debug_assert!(OLD_TEXT_END_TAG.len() == NEW_TEXT_END_TAG.len());
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum EditParserEvent {
|
||||
OldTextChunk { chunk: String, done: bool },
|
||||
OldText(String),
|
||||
NewTextChunk { chunk: String, done: bool },
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ pub struct EditParser {
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum EditParserState {
|
||||
Pending,
|
||||
WithinOldText { start: bool },
|
||||
WithinOldText,
|
||||
AfterOldText,
|
||||
WithinNewText { start: bool },
|
||||
}
|
||||
@@ -56,23 +56,20 @@ impl EditParser {
|
||||
EditParserState::Pending => {
|
||||
if let Some(start) = self.buffer.find("<old_text>") {
|
||||
self.buffer.drain(..start + "<old_text>".len());
|
||||
self.state = EditParserState::WithinOldText { start: true };
|
||||
self.state = EditParserState::WithinOldText;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
EditParserState::WithinOldText { start } => {
|
||||
if !self.buffer.is_empty() {
|
||||
if *start && self.buffer.starts_with('\n') {
|
||||
self.buffer.remove(0);
|
||||
}
|
||||
*start = false;
|
||||
}
|
||||
|
||||
EditParserState::WithinOldText => {
|
||||
if let Some(tag_range) = self.find_end_tag() {
|
||||
let mut chunk = self.buffer[..tag_range.start].to_string();
|
||||
if chunk.ends_with('\n') {
|
||||
chunk.pop();
|
||||
let mut start = 0;
|
||||
if self.buffer.starts_with('\n') {
|
||||
start = 1;
|
||||
}
|
||||
let mut old_text = self.buffer[start..tag_range.start].to_string();
|
||||
if old_text.ends_with('\n') {
|
||||
old_text.pop();
|
||||
}
|
||||
|
||||
self.metrics.tags += 1;
|
||||
@@ -82,14 +79,8 @@ impl EditParser {
|
||||
|
||||
self.buffer.drain(..tag_range.end);
|
||||
self.state = EditParserState::AfterOldText;
|
||||
edit_events.push(EditParserEvent::OldTextChunk { chunk, done: true });
|
||||
edit_events.push(EditParserEvent::OldText(old_text));
|
||||
} else {
|
||||
if !self.ends_with_tag_prefix() {
|
||||
edit_events.push(EditParserEvent::OldTextChunk {
|
||||
chunk: mem::take(&mut self.buffer),
|
||||
done: false,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -124,7 +115,10 @@ impl EditParser {
|
||||
self.state = EditParserState::Pending;
|
||||
edit_events.push(EditParserEvent::NewTextChunk { chunk, done: true });
|
||||
} else {
|
||||
if !self.ends_with_tag_prefix() {
|
||||
let mut end_prefixes = (1..END_TAG_LEN)
|
||||
.flat_map(|i| [&NEW_TEXT_END_TAG[..i], &OLD_TEXT_END_TAG[..i]])
|
||||
.chain(["\n"]);
|
||||
if end_prefixes.all(|prefix| !self.buffer.ends_with(&prefix)) {
|
||||
edit_events.push(EditParserEvent::NewTextChunk {
|
||||
chunk: mem::take(&mut self.buffer),
|
||||
done: false,
|
||||
@@ -139,19 +133,16 @@ impl EditParser {
|
||||
}
|
||||
|
||||
fn find_end_tag(&self) -> Option<Range<usize>> {
|
||||
let (tag, start_ix) = END_TAGS
|
||||
.iter()
|
||||
.flat_map(|tag| Some((tag, self.buffer.find(tag)?)))
|
||||
.min_by_key(|(_, ix)| *ix)?;
|
||||
Some(start_ix..start_ix + tag.len())
|
||||
}
|
||||
|
||||
fn ends_with_tag_prefix(&self) -> bool {
|
||||
let mut end_prefixes = END_TAGS
|
||||
.iter()
|
||||
.flat_map(|tag| (1..tag.len()).map(move |i| &tag[..i]))
|
||||
.chain(["\n"]);
|
||||
end_prefixes.any(|prefix| self.buffer.ends_with(&prefix))
|
||||
let old_text_end_tag_ix = self.buffer.find(OLD_TEXT_END_TAG);
|
||||
let new_text_end_tag_ix = self.buffer.find(NEW_TEXT_END_TAG);
|
||||
let start_ix = if let Some((old_text_ix, new_text_ix)) =
|
||||
old_text_end_tag_ix.zip(new_text_end_tag_ix)
|
||||
{
|
||||
cmp::min(old_text_ix, new_text_ix)
|
||||
} else {
|
||||
old_text_end_tag_ix.or(new_text_end_tag_ix)?
|
||||
};
|
||||
Some(start_ix..start_ix + END_TAG_LEN)
|
||||
}
|
||||
|
||||
pub fn finish(self) -> EditParserMetrics {
|
||||
@@ -382,35 +373,6 @@ mod tests {
|
||||
mismatched_tags: 4
|
||||
}
|
||||
);
|
||||
|
||||
let mut parser = EditParser::new();
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
// Reduced from an actual Opus 4 output
|
||||
indoc! {"
|
||||
<edits>
|
||||
<old_text>
|
||||
Lorem
|
||||
</old_text>
|
||||
<new_text>
|
||||
LOREM
|
||||
</edits>
|
||||
"},
|
||||
&mut parser,
|
||||
&mut rng
|
||||
),
|
||||
vec![Edit {
|
||||
old_text: "Lorem".to_string(),
|
||||
new_text: "LOREM".to_string(),
|
||||
},]
|
||||
);
|
||||
assert_eq!(
|
||||
parser.finish(),
|
||||
EditParserMetrics {
|
||||
tags: 2,
|
||||
mismatched_tags: 1
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, PartialEq, Eq)]
|
||||
@@ -425,35 +387,26 @@ mod tests {
|
||||
chunk_indices.sort();
|
||||
chunk_indices.push(input.len());
|
||||
|
||||
let mut old_text = Some(String::new());
|
||||
let mut new_text = None;
|
||||
let mut pending_edit = Edit::default();
|
||||
let mut edits = Vec::new();
|
||||
let mut last_ix = 0;
|
||||
for chunk_ix in chunk_indices {
|
||||
for event in parser.push(&input[last_ix..chunk_ix]) {
|
||||
match event {
|
||||
EditParserEvent::OldTextChunk { chunk, done } => {
|
||||
old_text.as_mut().unwrap().push_str(&chunk);
|
||||
if done {
|
||||
pending_edit.old_text = old_text.take().unwrap();
|
||||
new_text = Some(String::new());
|
||||
}
|
||||
EditParserEvent::OldText(old_text) => {
|
||||
pending_edit.old_text = old_text;
|
||||
}
|
||||
EditParserEvent::NewTextChunk { chunk, done } => {
|
||||
new_text.as_mut().unwrap().push_str(&chunk);
|
||||
pending_edit.new_text.push_str(&chunk);
|
||||
if done {
|
||||
pending_edit.new_text = new_text.take().unwrap();
|
||||
edits.push(pending_edit);
|
||||
pending_edit = Edit::default();
|
||||
old_text = Some(String::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
last_ix = chunk_ix;
|
||||
}
|
||||
|
||||
edits
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ use language_model::{
|
||||
LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, SelectedModel,
|
||||
};
|
||||
use project::Project;
|
||||
use prompt_store::{ModelContext, ProjectContext, PromptBuilder, WorktreeContext};
|
||||
use rand::prelude::*;
|
||||
use reqwest_client::ReqwestClient;
|
||||
use serde_json::json;
|
||||
@@ -34,30 +33,13 @@ use util::path;
|
||||
#[test]
|
||||
#[cfg_attr(not(feature = "eval"), ignore)]
|
||||
fn eval_extract_handle_command_output() {
|
||||
// Test how well agent generates multiple edit hunks.
|
||||
//
|
||||
// Model | Pass rate
|
||||
// ----------------------------|----------
|
||||
// claude-3.7-sonnet | 0.98
|
||||
// gemini-2.5-pro | 0.86
|
||||
// gemini-2.5-flash | 0.11
|
||||
// gpt-4.1 | 1.00
|
||||
|
||||
let input_file_path = "root/blame.rs";
|
||||
let input_file_content = include_str!("evals/fixtures/extract_handle_command_output/before.rs");
|
||||
let possible_diffs = vec![
|
||||
include_str!("evals/fixtures/extract_handle_command_output/possible-01.diff"),
|
||||
include_str!("evals/fixtures/extract_handle_command_output/possible-02.diff"),
|
||||
include_str!("evals/fixtures/extract_handle_command_output/possible-03.diff"),
|
||||
include_str!("evals/fixtures/extract_handle_command_output/possible-04.diff"),
|
||||
include_str!("evals/fixtures/extract_handle_command_output/possible-05.diff"),
|
||||
include_str!("evals/fixtures/extract_handle_command_output/possible-06.diff"),
|
||||
include_str!("evals/fixtures/extract_handle_command_output/possible-07.diff"),
|
||||
];
|
||||
let output_file_content = include_str!("evals/fixtures/extract_handle_command_output/after.rs");
|
||||
let edit_description = "Extract `handle_command_output` method from `run_git_blame`.";
|
||||
eval(
|
||||
100,
|
||||
0.7, // Taking the lower bar for Gemini
|
||||
0.95,
|
||||
EvalInput::from_conversation(
|
||||
vec![
|
||||
message(
|
||||
@@ -66,7 +48,6 @@ fn eval_extract_handle_command_output() {
|
||||
Read the `{input_file_path}` file and extract a method in
|
||||
the final stanza of `run_git_blame` to deal with command failures,
|
||||
call it `handle_command_output` and take the std::process::Output as the only parameter.
|
||||
Do not document the method and do not add any comments.
|
||||
|
||||
Add it right next to `run_git_blame` and copy it verbatim from `run_git_blame`.
|
||||
"})],
|
||||
@@ -101,7 +82,7 @@ fn eval_extract_handle_command_output() {
|
||||
),
|
||||
],
|
||||
Some(input_file_content.into()),
|
||||
EvalAssertion::assert_diff_any(possible_diffs),
|
||||
EvalAssertion::assert_eq(output_file_content),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -163,15 +144,6 @@ fn eval_delete_run_git_blame() {
|
||||
#[test]
|
||||
#[cfg_attr(not(feature = "eval"), ignore)]
|
||||
fn eval_translate_doc_comments() {
|
||||
// Results for 2025-05-22
|
||||
//
|
||||
// Model | Pass rate
|
||||
// ============================================
|
||||
//
|
||||
// claude-3.7-sonnet |
|
||||
// gemini-2.5-pro-preview-03-25 | 1.0
|
||||
// gemini-2.5-flash-preview-04-17 |
|
||||
// gpt-4.1 |
|
||||
let input_file_path = "root/canvas.rs";
|
||||
let input_file_content = include_str!("evals/fixtures/translate_doc_comments/before.rs");
|
||||
let edit_description = "Translate all doc comments to Italian";
|
||||
@@ -225,15 +197,6 @@ fn eval_translate_doc_comments() {
|
||||
#[test]
|
||||
#[cfg_attr(not(feature = "eval"), ignore)]
|
||||
fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
|
||||
// Results for 2025-05-22
|
||||
//
|
||||
// Model | Pass rate
|
||||
// ============================================
|
||||
//
|
||||
// claude-3.7-sonnet | 0.98
|
||||
// gemini-2.5-pro-preview-03-25 | 0.99
|
||||
// gemini-2.5-flash-preview-04-17 |
|
||||
// gpt-4.1 |
|
||||
let input_file_path = "root/lib.rs";
|
||||
let input_file_content =
|
||||
include_str!("evals/fixtures/use_wasi_sdk_in_compile_parser_to_wasm/before.rs");
|
||||
@@ -350,15 +313,6 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
|
||||
#[test]
|
||||
#[cfg_attr(not(feature = "eval"), ignore)]
|
||||
fn eval_disable_cursor_blinking() {
|
||||
// Results for 2025-05-22
|
||||
//
|
||||
// Model | Pass rate
|
||||
// ============================================
|
||||
//
|
||||
// claude-3.7-sonnet |
|
||||
// gemini-2.5-pro-preview-03-25 | 1.0
|
||||
// gemini-2.5-flash-preview-04-17 |
|
||||
// gpt-4.1 |
|
||||
let input_file_path = "root/editor.rs";
|
||||
let input_file_content = include_str!("evals/fixtures/disable_cursor_blinking/before.rs");
|
||||
let edit_description = "Comment out the call to `BlinkManager::enable`";
|
||||
@@ -433,15 +387,6 @@ fn eval_disable_cursor_blinking() {
|
||||
#[test]
|
||||
#[cfg_attr(not(feature = "eval"), ignore)]
|
||||
fn eval_from_pixels_constructor() {
|
||||
// Results for 2025-05-22
|
||||
//
|
||||
// Model | Pass rate
|
||||
// ============================================
|
||||
//
|
||||
// claude-3.7-sonnet |
|
||||
// gemini-2.5-pro-preview-03-25 | 0.94
|
||||
// gemini-2.5-flash-preview-04-17 |
|
||||
// gpt-4.1 |
|
||||
let input_file_path = "root/canvas.rs";
|
||||
let input_file_content = include_str!("evals/fixtures/from_pixels_constructor/before.rs");
|
||||
let edit_description = "Implement from_pixels constructor and add tests.";
|
||||
@@ -633,20 +578,11 @@ fn eval_from_pixels_constructor() {
|
||||
#[test]
|
||||
#[cfg_attr(not(feature = "eval"), ignore)]
|
||||
fn eval_zode() {
|
||||
// Results for 2025-05-22
|
||||
//
|
||||
// Model | Pass rate
|
||||
// ============================================
|
||||
//
|
||||
// claude-3.7-sonnet | 1.0
|
||||
// gemini-2.5-pro-preview-03-25 | 1.0
|
||||
// gemini-2.5-flash-preview-04-17 | 1.0
|
||||
// gpt-4.1 | 1.0
|
||||
let input_file_path = "root/zode.py";
|
||||
let input_content = None;
|
||||
let edit_description = "Create the main Zode CLI script";
|
||||
eval(
|
||||
50,
|
||||
200,
|
||||
1.,
|
||||
EvalInput::from_conversation(
|
||||
vec![
|
||||
@@ -712,7 +648,7 @@ fn eval_zode() {
|
||||
let invalid_starts = [' ', '`', '\n'];
|
||||
let mut message = String::new();
|
||||
for start in invalid_starts {
|
||||
if sample.text_after.starts_with(start) {
|
||||
if sample.text.starts_with(start) {
|
||||
message.push_str(&format!("The sample starts with a {:?}\n", start));
|
||||
break;
|
||||
}
|
||||
@@ -739,15 +675,6 @@ fn eval_zode() {
|
||||
#[test]
|
||||
#[cfg_attr(not(feature = "eval"), ignore)]
|
||||
fn eval_add_overwrite_test() {
|
||||
// Results for 2025-05-22
|
||||
//
|
||||
// Model | Pass rate
|
||||
// ============================================
|
||||
//
|
||||
// claude-3.7-sonnet | 0.16
|
||||
// gemini-2.5-pro-preview-03-25 | 0.35
|
||||
// gemini-2.5-flash-preview-04-17 |
|
||||
// gpt-4.1 |
|
||||
let input_file_path = "root/action_log.rs";
|
||||
let input_file_content = include_str!("evals/fixtures/add_overwrite_test/before.rs");
|
||||
let edit_description = "Add a new test for overwriting a file in action_log.rs";
|
||||
@@ -968,21 +895,52 @@ fn eval_add_overwrite_test() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(not(feature = "eval"), ignore)]
|
||||
#[ignore] // until we figure out the mystery described in the comments
|
||||
// #[cfg_attr(not(feature = "eval"), ignore)]
|
||||
fn eval_create_empty_file() {
|
||||
// Check that Edit Agent can create a file without writing its
|
||||
// thoughts into it. This issue is not specific to empty files, but
|
||||
// it's easier to reproduce with them.
|
||||
//
|
||||
// Results for 2025-05-21:
|
||||
// NOTE: For some mysterious reason, I could easily reproduce this
|
||||
// issue roughly 90% of the time in actual Zed. However, once I
|
||||
// extract the exact LLM request before the failure point and
|
||||
// generate from that, the reproduction rate drops to 2%!
|
||||
//
|
||||
// Things I've tried to make sure it's not a fluke: disabling prompt
|
||||
// caching, capturing the LLM request via a proxy server, running the
|
||||
// prompt on Claude separately from evals. Every time it was mostly
|
||||
// giving good outcomes, which doesn't match my actual experience in
|
||||
// Zed.
|
||||
//
|
||||
// At some point I discovered that simply adding one insignificant
|
||||
// space or a newline to the prompt suddenly results in an outcome I
|
||||
// tried to reproduce almost perfectly.
|
||||
//
|
||||
// This weirdness happens even outside of the Zed code base and even
|
||||
// when using a different subscription. The result is the same: an
|
||||
// extra newline or space changes the model behavior significantly
|
||||
// enough, so that the pass rate drops from 99% to 0-3%
|
||||
//
|
||||
// I have no explanation to this.
|
||||
//
|
||||
//
|
||||
// Model | Pass rate
|
||||
// ============================================
|
||||
//
|
||||
// claude-3.7-sonnet | 1.00
|
||||
// --------------------------------------------
|
||||
// Prompt version: 2025-05-19
|
||||
// --------------------------------------------
|
||||
//
|
||||
// claude-3.7-sonnet | 0.98
|
||||
// + one extra space in prompt | 0.00
|
||||
// + original prompt again | 0.99
|
||||
// + extra newline | 0.03
|
||||
// gemini-2.5-pro-preview-03-25 | 1.00
|
||||
// gemini-2.5-flash-preview-04-17 | 1.00
|
||||
// + one extra space | 1.00
|
||||
// gpt-4.1 | 1.00
|
||||
// + one extra space | 1.00
|
||||
//
|
||||
//
|
||||
// TODO: gpt-4.1-mini errored 38 times:
|
||||
@@ -991,8 +949,8 @@ fn eval_create_empty_file() {
|
||||
let input_file_content = None;
|
||||
let expected_output_content = String::new();
|
||||
eval(
|
||||
100,
|
||||
0.99,
|
||||
1,
|
||||
1.0,
|
||||
EvalInput::from_conversation(
|
||||
vec![
|
||||
message(User, [text("Create a second empty todo file ")]),
|
||||
@@ -1143,8 +1101,7 @@ impl EvalInput {
|
||||
|
||||
#[derive(Clone)]
|
||||
struct EvalSample {
|
||||
text_before: String,
|
||||
text_after: String,
|
||||
text: String,
|
||||
edit_output: EditAgentOutput,
|
||||
diff: String,
|
||||
}
|
||||
@@ -1201,7 +1158,7 @@ impl EvalAssertion {
|
||||
let expected = expected.into();
|
||||
Self::new(async move |sample, _judge, _cx| {
|
||||
Ok(EvalAssertionOutcome {
|
||||
score: if strip_empty_lines(&sample.text_after) == strip_empty_lines(&expected) {
|
||||
score: if strip_empty_lines(&sample.text) == strip_empty_lines(&expected) {
|
||||
100
|
||||
} else {
|
||||
0
|
||||
@@ -1211,22 +1168,6 @@ impl EvalAssertion {
|
||||
})
|
||||
}
|
||||
|
||||
fn assert_diff_any(expected_diffs: Vec<impl Into<String>>) -> Self {
|
||||
let expected_diffs: Vec<String> = expected_diffs.into_iter().map(Into::into).collect();
|
||||
Self::new(async move |sample, _judge, _cx| {
|
||||
let matches = expected_diffs.iter().any(|possible_diff| {
|
||||
let expected =
|
||||
language::apply_diff_patch(&sample.text_before, possible_diff).unwrap();
|
||||
strip_empty_lines(&expected) == strip_empty_lines(&sample.text_after)
|
||||
});
|
||||
|
||||
Ok(EvalAssertionOutcome {
|
||||
score: if matches { 100 } else { 0 },
|
||||
message: None,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn judge_diff(assertions: &'static str) -> Self {
|
||||
Self::new(async move |sample, judge, cx| {
|
||||
let prompt = DiffJudgeTemplate {
|
||||
@@ -1311,7 +1252,7 @@ fn eval(iterations: usize, expected_pass_ratio: f32, mut eval: EvalInput) {
|
||||
if output.assertion.score < 80 {
|
||||
failed_count += 1;
|
||||
failed_evals
|
||||
.entry(output.sample.text_after.clone())
|
||||
.entry(output.sample.text.clone())
|
||||
.or_insert(Vec::new())
|
||||
.push(output);
|
||||
}
|
||||
@@ -1481,7 +1422,7 @@ impl EditAgentTest {
|
||||
model.provider_id() == selected_model.provider
|
||||
&& model.id() == selected_model.model
|
||||
})
|
||||
.expect("Model not found");
|
||||
.unwrap();
|
||||
let provider = models.provider(&model.provider_id()).unwrap();
|
||||
(provider, model)
|
||||
})?;
|
||||
@@ -1501,62 +1442,26 @@ impl EditAgentTest {
|
||||
.update(cx, |project, cx| project.open_buffer(path, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let tools = cx.update(|cx| {
|
||||
ToolRegistry::default_global(cx)
|
||||
.tools()
|
||||
.into_iter()
|
||||
.filter_map(|tool| {
|
||||
let input_schema = tool
|
||||
.input_schema(self.agent.model.tool_input_format())
|
||||
.ok()?;
|
||||
Some(LanguageModelRequestTool {
|
||||
name: tool.name(),
|
||||
description: tool.description(),
|
||||
input_schema,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
let tool_names = tools
|
||||
.iter()
|
||||
.map(|tool| tool.name.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let worktrees = vec![WorktreeContext {
|
||||
root_name: "root".to_string(),
|
||||
rules_file: None,
|
||||
}];
|
||||
let prompt_builder = PromptBuilder::new(None)?;
|
||||
let project_context = ProjectContext::new(worktrees, Vec::default());
|
||||
let system_prompt = prompt_builder.generate_assistant_system_prompt(
|
||||
&project_context,
|
||||
&ModelContext {
|
||||
available_tools: tool_names,
|
||||
},
|
||||
)?;
|
||||
|
||||
let has_system_prompt = eval
|
||||
.conversation
|
||||
.first()
|
||||
.map_or(false, |msg| msg.role == Role::System);
|
||||
let messages = if has_system_prompt {
|
||||
eval.conversation
|
||||
} else {
|
||||
[LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: vec![MessageContent::Text(system_prompt)],
|
||||
cache: true,
|
||||
}]
|
||||
.into_iter()
|
||||
.chain(eval.conversation)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
let conversation = LanguageModelRequest {
|
||||
messages,
|
||||
tools,
|
||||
messages: eval.conversation,
|
||||
tools: cx.update(|cx| {
|
||||
ToolRegistry::default_global(cx)
|
||||
.tools()
|
||||
.into_iter()
|
||||
.filter_map(|tool| {
|
||||
let input_schema = tool
|
||||
.input_schema(self.agent.model.tool_input_format())
|
||||
.ok()?;
|
||||
Some(LanguageModelRequestTool {
|
||||
name: tool.name(),
|
||||
description: tool.description(),
|
||||
input_schema,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let edit_output = if matches!(eval.edit_file_input.mode, EditFileMode::Edit) {
|
||||
if let Some(input_content) = eval.input_content.as_deref() {
|
||||
buffer.update(cx, |buffer, cx| buffer.set_text(input_content, cx));
|
||||
@@ -1585,8 +1490,7 @@ impl EditAgentTest {
|
||||
eval.input_content.as_deref().unwrap_or_default(),
|
||||
&buffer_text,
|
||||
),
|
||||
text_before: eval.input_content.unwrap_or_default(),
|
||||
text_after: buffer_text,
|
||||
text: buffer_text,
|
||||
};
|
||||
let assertion = eval
|
||||
.assertion
|
||||
|
||||
@@ -676,7 +676,9 @@ mod tests {
|
||||
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
zlog::init_test();
|
||||
if std::env::var("RUST_LOG").is_ok() {
|
||||
env_logger::init();
|
||||
}
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
|
||||
@@ -7698,7 +7698,7 @@ impl Editor {
|
||||
.gap_1()
|
||||
// Workaround: For some reason, there's a gap if we don't do this
|
||||
.ml(-BORDER_WIDTH)
|
||||
.shadow(vec![gpui::BoxShadow {
|
||||
.shadow(smallvec![gpui::BoxShadow {
|
||||
color: gpui::black().opacity(0.05),
|
||||
offset: point(px(1.), px(1.)),
|
||||
blur_radius: px(2.),
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
use crate::commit::get_messages;
|
||||
use crate::{GitRemote, Oid};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::AsyncWriteExt;
|
||||
use gpui::SharedString;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::process::Stdio;
|
||||
use std::{ops::Range, path::Path};
|
||||
use text::Rope;
|
||||
use time::OffsetDateTime;
|
||||
use time::UtcOffset;
|
||||
use time::macros::format_description;
|
||||
|
||||
pub use git2 as libgit;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Blame {
|
||||
pub entries: Vec<BlameEntry>,
|
||||
pub messages: HashMap<Oid, String>,
|
||||
pub remote_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ParsedCommitMessage {
|
||||
pub message: SharedString,
|
||||
pub permalink: Option<url::Url>,
|
||||
pub pull_request: Option<crate::hosting_provider::PullRequest>,
|
||||
pub remote: Option<GitRemote>,
|
||||
}
|
||||
|
||||
impl Blame {
|
||||
pub async fn for_path(
|
||||
git_binary: &Path,
|
||||
working_directory: &Path,
|
||||
path: &Path,
|
||||
content: &Rope,
|
||||
remote_url: Option<String>,
|
||||
) -> Result<Self> {
|
||||
let output = run_git_blame(git_binary, working_directory, path, content).await?;
|
||||
let mut entries = parse_git_blame(&output)?;
|
||||
entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start));
|
||||
|
||||
let mut unique_shas = HashSet::default();
|
||||
|
||||
for entry in entries.iter_mut() {
|
||||
unique_shas.insert(entry.sha);
|
||||
}
|
||||
|
||||
let shas = unique_shas.into_iter().collect::<Vec<_>>();
|
||||
let messages = get_messages(working_directory, &shas)
|
||||
.await
|
||||
.context("failed to get commit messages")?;
|
||||
|
||||
Ok(Self {
|
||||
entries,
|
||||
messages,
|
||||
remote_url,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const GIT_BLAME_NO_COMMIT_ERROR: &str = "fatal: no such ref: HEAD";
|
||||
const GIT_BLAME_NO_PATH: &str = "fatal: no such path";
|
||||
|
||||
async fn run_git_blame(
|
||||
git_binary: &Path,
|
||||
working_directory: &Path,
|
||||
path: &Path,
|
||||
contents: &Rope,
|
||||
) -> Result<String> {
|
||||
let mut child = util::command::new_smol_command(git_binary)
|
||||
.current_dir(working_directory)
|
||||
.arg("blame")
|
||||
.arg("--incremental")
|
||||
.arg("--contents")
|
||||
.arg("-")
|
||||
.arg(path.as_os_str())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.context("starting git blame process")?;
|
||||
|
||||
let stdin = child
|
||||
.stdin
|
||||
.as_mut()
|
||||
.context("failed to get pipe to stdin of git blame command")?;
|
||||
|
||||
for chunk in contents.chunks() {
|
||||
stdin.write_all(chunk.as_bytes()).await?;
|
||||
}
|
||||
stdin.flush().await?;
|
||||
|
||||
let output = child.output().await.context("reading git blame output")?;
|
||||
|
||||
handle_command_output(output)
|
||||
}
|
||||
|
||||
fn handle_command_output(output: std::process::Output) -> Result<String> {
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let trimmed = stderr.trim();
|
||||
if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
|
||||
return Ok(String::new());
|
||||
}
|
||||
anyhow::bail!("git blame process failed: {stderr}");
|
||||
}
|
||||
|
||||
Ok(String::from_utf8(output.stdout)?)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct BlameEntry {
|
||||
pub sha: Oid,
|
||||
|
||||
pub range: Range<u32>,
|
||||
|
||||
pub original_line_number: u32,
|
||||
|
||||
pub author: Option<String>,
|
||||
pub author_mail: Option<String>,
|
||||
pub author_time: Option<i64>,
|
||||
pub author_tz: Option<String>,
|
||||
|
||||
pub committer_name: Option<String>,
|
||||
pub committer_email: Option<String>,
|
||||
pub committer_time: Option<i64>,
|
||||
pub committer_tz: Option<String>,
|
||||
|
||||
pub summary: Option<String>,
|
||||
|
||||
pub previous: Option<String>,
|
||||
pub filename: String,
|
||||
}
|
||||
|
||||
impl BlameEntry {
|
||||
// Returns a BlameEntry by parsing the first line of a `git blame --incremental`
|
||||
// entry. The line MUST have this format:
|
||||
//
|
||||
// <40-byte-hex-sha1> <sourceline> <resultline> <num-lines>
|
||||
fn new_from_blame_line(line: &str) -> Result<BlameEntry> {
|
||||
let mut parts = line.split_whitespace();
|
||||
|
||||
let sha = parts
|
||||
.next()
|
||||
.and_then(|line| line.parse::<Oid>().ok())
|
||||
.with_context(|| format!("parsing sha from {line}"))?;
|
||||
|
||||
let original_line_number = parts
|
||||
.next()
|
||||
.and_then(|line| line.parse::<u32>().ok())
|
||||
.with_context(|| format!("parsing original line number from {line}"))?;
|
||||
let final_line_number = parts
|
||||
.next()
|
||||
.and_then(|line| line.parse::<u32>().ok())
|
||||
.with_context(|| format!("parsing final line number from {line}"))?;
|
||||
|
||||
let line_count = parts
|
||||
.next()
|
||||
.and_then(|line| line.parse::<u32>().ok())
|
||||
.with_context(|| format!("parsing line count from {line}"))?;
|
||||
|
||||
let start_line = final_line_number.saturating_sub(1);
|
||||
let end_line = start_line + line_count;
|
||||
let range = start_line..end_line;
|
||||
|
||||
Ok(Self {
|
||||
sha,
|
||||
range,
|
||||
original_line_number,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn author_offset_date_time(&self) -> Result<time::OffsetDateTime> {
|
||||
if let (Some(author_time), Some(author_tz)) = (self.author_time, &self.author_tz) {
|
||||
let format = format_description!("[offset_hour][offset_minute]");
|
||||
let offset = UtcOffset::parse(author_tz, &format)?;
|
||||
let date_time_utc = OffsetDateTime::from_unix_timestamp(author_time)?;
|
||||
|
||||
Ok(date_time_utc.to_offset(offset))
|
||||
} else {
|
||||
// Directly return current time in UTC if there's no committer time or timezone
|
||||
Ok(time::OffsetDateTime::now_utc())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parse_git_blame parses the output of `git blame --incremental`, which returns
|
||||
// all the blame-entries for a given path incrementally, as it finds them.
|
||||
//
|
||||
// Each entry *always* starts with:
|
||||
//
|
||||
// <40-byte-hex-sha1> <sourceline> <resultline> <num-lines>
|
||||
//
|
||||
// Each entry *always* ends with:
|
||||
//
|
||||
// filename <whitespace-quoted-filename-goes-here>
|
||||
//
|
||||
// Line numbers are 1-indexed.
|
||||
//
|
||||
// A `git blame --incremental` entry looks like this:
|
||||
//
|
||||
// 6ad46b5257ba16d12c5ca9f0d4900320959df7f4 2 2 1
|
||||
// author Joe Schmoe
|
||||
// author-mail <joe.schmoe@example.com>
|
||||
// author-time 1709741400
|
||||
// author-tz +0100
|
||||
// committer Joe Schmoe
|
||||
// committer-mail <joe.schmoe@example.com>
|
||||
// committer-time 1709741400
|
||||
// committer-tz +0100
|
||||
// summary Joe's cool commit
|
||||
// previous 486c2409237a2c627230589e567024a96751d475 index.js
|
||||
// filename index.js
|
||||
//
|
||||
// If the entry has the same SHA as an entry that was already printed then no
|
||||
// signature information is printed:
|
||||
//
|
||||
// 6ad46b5257ba16d12c5ca9f0d4900320959df7f4 3 4 1
|
||||
// previous 486c2409237a2c627230589e567024a96751d475 index.js
|
||||
// filename index.js
|
||||
//
|
||||
// More about `--incremental` output: https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-blame.html
|
||||
fn parse_git_blame(output: &str) -> Result<Vec<BlameEntry>> {
|
||||
let mut entries: Vec<BlameEntry> = Vec::new();
|
||||
let mut index: HashMap<Oid, usize> = HashMap::default();
|
||||
|
||||
let mut current_entry: Option<BlameEntry> = None;
|
||||
|
||||
for line in output.lines() {
|
||||
let mut done = false;
|
||||
|
||||
match &mut current_entry {
|
||||
None => {
|
||||
let mut new_entry = BlameEntry::new_from_blame_line(line)?;
|
||||
|
||||
if let Some(existing_entry) = index
|
||||
.get(&new_entry.sha)
|
||||
.and_then(|slot| entries.get(*slot))
|
||||
{
|
||||
new_entry.author.clone_from(&existing_entry.author);
|
||||
new_entry
|
||||
.author_mail
|
||||
.clone_from(&existing_entry.author_mail);
|
||||
new_entry.author_time = existing_entry.author_time;
|
||||
new_entry.author_tz.clone_from(&existing_entry.author_tz);
|
||||
new_entry
|
||||
.committer_name
|
||||
.clone_from(&existing_entry.committer_name);
|
||||
new_entry
|
||||
.committer_email
|
||||
.clone_from(&existing_entry.committer_email);
|
||||
new_entry.committer_time = existing_entry.committer_time;
|
||||
new_entry
|
||||
.committer_tz
|
||||
.clone_from(&existing_entry.committer_tz);
|
||||
new_entry.summary.clone_from(&existing_entry.summary);
|
||||
}
|
||||
|
||||
current_entry.replace(new_entry);
|
||||
}
|
||||
Some(entry) => {
|
||||
let Some((key, value)) = line.split_once(' ') else {
|
||||
continue;
|
||||
};
|
||||
let is_committed = !entry.sha.is_zero();
|
||||
match key {
|
||||
"filename" => {
|
||||
entry.filename = value.into();
|
||||
done = true;
|
||||
}
|
||||
"previous" => entry.previous = Some(value.into()),
|
||||
|
||||
"summary" if is_committed => entry.summary = Some(value.into()),
|
||||
"author" if is_committed => entry.author = Some(value.into()),
|
||||
"author-mail" if is_committed => entry.author_mail = Some(value.into()),
|
||||
"author-time" if is_committed => {
|
||||
entry.author_time = Some(value.parse::<i64>()?)
|
||||
}
|
||||
"author-tz" if is_committed => entry.author_tz = Some(value.into()),
|
||||
|
||||
"committer" if is_committed => entry.committer_name = Some(value.into()),
|
||||
"committer-mail" if is_committed => entry.committer_email = Some(value.into()),
|
||||
"committer-time" if is_committed => {
|
||||
entry.committer_time = Some(value.parse::<i64>()?)
|
||||
}
|
||||
"committer-tz" if is_committed => entry.committer_tz = Some(value.into()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if done {
|
||||
if let Some(entry) = current_entry.take() {
|
||||
index.insert(entry.sha, entries.len());
|
||||
|
||||
// We only want annotations that have a commit.
|
||||
if !entry.sha.is_zero() {
|
||||
entries.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::BlameEntry;
|
||||
use super::parse_git_blame;
|
||||
|
||||
fn read_test_data(filename: &str) -> String {
|
||||
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
path.push("test_data");
|
||||
path.push(filename);
|
||||
|
||||
std::fs::read_to_string(&path)
|
||||
.unwrap_or_else(|_| panic!("Could not read test data at {:?}. Is it generated?", path))
|
||||
}
|
||||
|
||||
fn assert_eq_golden(entries: &Vec<BlameEntry>, golden_filename: &str) {
|
||||
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
path.push("test_data");
|
||||
path.push("golden");
|
||||
path.push(format!("{}.json", golden_filename));
|
||||
|
||||
let mut have_json =
|
||||
serde_json::to_string_pretty(&entries).expect("could not serialize entries to JSON");
|
||||
// We always want to save with a trailing newline.
|
||||
have_json.push('\n');
|
||||
|
||||
let update = std::env::var("UPDATE_GOLDEN")
|
||||
.map(|val| val.eq_ignore_ascii_case("true"))
|
||||
.unwrap_or(false);
|
||||
|
||||
if update {
|
||||
std::fs::create_dir_all(path.parent().unwrap())
|
||||
.expect("could not create golden test data directory");
|
||||
std::fs::write(&path, have_json).expect("could not write out golden data");
|
||||
} else {
|
||||
let want_json =
|
||||
std::fs::read_to_string(&path).unwrap_or_else(|_| {
|
||||
panic!("could not read golden test data file at {:?}. Did you run the test with UPDATE_GOLDEN=true before?", path);
|
||||
}).replace("\r\n", "\n");
|
||||
|
||||
pretty_assertions::assert_eq!(have_json, want_json, "wrong blame entries");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_git_blame_not_committed() {
|
||||
let output = read_test_data("blame_incremental_not_committed");
|
||||
let entries = parse_git_blame(&output).unwrap();
|
||||
assert_eq_golden(&entries, "blame_incremental_not_committed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_git_blame_simple() {
|
||||
let output = read_test_data("blame_incremental_simple");
|
||||
let entries = parse_git_blame(&output).unwrap();
|
||||
assert_eq_golden(&entries, "blame_incremental_simple");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_git_blame_complex() {
|
||||
let output = read_test_data("blame_incremental_complex");
|
||||
let entries = parse_git_blame(&output).unwrap();
|
||||
assert_eq_golden(&entries, "blame_incremental_complex");
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
@@ -94,6 +94,10 @@
|
||||
|
||||
let output = child.output().await.context("reading git blame output")?;
|
||||
|
||||
+ handle_command_output(output)
|
||||
+}
|
||||
+
|
||||
+fn handle_command_output(output: std::process::Output) -> Result<String> {
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let trimmed = stderr.trim();
|
||||
@@ -1,26 +0,0 @@
|
||||
@@ -95,15 +95,19 @@
|
||||
let output = child.output().await.context("reading git blame output")?;
|
||||
|
||||
if !output.status.success() {
|
||||
- let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
- let trimmed = stderr.trim();
|
||||
- if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
|
||||
- return Ok(String::new());
|
||||
- }
|
||||
- anyhow::bail!("git blame process failed: {stderr}");
|
||||
+ return handle_command_output(output);
|
||||
}
|
||||
|
||||
Ok(String::from_utf8(output.stdout)?)
|
||||
+}
|
||||
+
|
||||
+fn handle_command_output(output: std::process::Output) -> Result<String> {
|
||||
+ let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
+ let trimmed = stderr.trim();
|
||||
+ if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
|
||||
+ return Ok(String::new());
|
||||
+ }
|
||||
+ anyhow::bail!("git blame process failed: {stderr}");
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
|
||||
@@ -1,11 +0,0 @@
|
||||
@@ -93,7 +93,10 @@
|
||||
stdin.flush().await?;
|
||||
|
||||
let output = child.output().await.context("reading git blame output")?;
|
||||
+ handle_command_output(output)
|
||||
+}
|
||||
|
||||
+fn handle_command_output(output: std::process::Output) -> Result<String> {
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let trimmed = stderr.trim();
|
||||
@@ -1,24 +0,0 @@
|
||||
@@ -93,17 +93,20 @@
|
||||
stdin.flush().await?;
|
||||
|
||||
let output = child.output().await.context("reading git blame output")?;
|
||||
+ handle_command_output(&output)?;
|
||||
+ Ok(String::from_utf8(output.stdout)?)
|
||||
+}
|
||||
|
||||
+fn handle_command_output(output: &std::process::Output) -> Result<()> {
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let trimmed = stderr.trim();
|
||||
if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
|
||||
- return Ok(String::new());
|
||||
+ return Ok(());
|
||||
}
|
||||
anyhow::bail!("git blame process failed: {stderr}");
|
||||
}
|
||||
-
|
||||
- Ok(String::from_utf8(output.stdout)?)
|
||||
+ Ok(())
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
|
||||
@@ -1,26 +0,0 @@
|
||||
@@ -95,15 +95,19 @@
|
||||
let output = child.output().await.context("reading git blame output")?;
|
||||
|
||||
if !output.status.success() {
|
||||
- let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
- let trimmed = stderr.trim();
|
||||
- if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
|
||||
- return Ok(String::new());
|
||||
- }
|
||||
- anyhow::bail!("git blame process failed: {stderr}");
|
||||
+ return handle_command_output(&output);
|
||||
}
|
||||
|
||||
Ok(String::from_utf8(output.stdout)?)
|
||||
+}
|
||||
+
|
||||
+fn handle_command_output(output: &std::process::Output) -> Result<String> {
|
||||
+ let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
+ let trimmed = stderr.trim();
|
||||
+ if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
|
||||
+ return Ok(String::new());
|
||||
+ }
|
||||
+ anyhow::bail!("git blame process failed: {stderr}");
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
|
||||
@@ -1,23 +0,0 @@
|
||||
@@ -93,7 +93,12 @@
|
||||
stdin.flush().await?;
|
||||
|
||||
let output = child.output().await.context("reading git blame output")?;
|
||||
+ handle_command_output(&output)?;
|
||||
|
||||
+ Ok(String::from_utf8(output.stdout)?)
|
||||
+}
|
||||
+
|
||||
+fn handle_command_output(output: &std::process::Output) -> Result<String> {
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let trimmed = stderr.trim();
|
||||
@@ -102,8 +107,7 @@
|
||||
}
|
||||
anyhow::bail!("git blame process failed: {stderr}");
|
||||
}
|
||||
-
|
||||
- Ok(String::from_utf8(output.stdout)?)
|
||||
+ Ok(String::from_utf8_lossy(&output.stdout).into_owned())
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
|
||||
@@ -1,26 +0,0 @@
|
||||
@@ -95,15 +95,19 @@
|
||||
let output = child.output().await.context("reading git blame output")?;
|
||||
|
||||
if !output.status.success() {
|
||||
- let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
- let trimmed = stderr.trim();
|
||||
- if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
|
||||
- return Ok(String::new());
|
||||
- }
|
||||
- anyhow::bail!("git blame process failed: {stderr}");
|
||||
+ return handle_command_output(output);
|
||||
}
|
||||
|
||||
Ok(String::from_utf8(output.stdout)?)
|
||||
+}
|
||||
+
|
||||
+fn handle_command_output(output: std::process::Output) -> Result<String> {
|
||||
+ let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
+ let trimmed = stderr.trim();
|
||||
+ if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
|
||||
+ return Ok(String::new());
|
||||
+ }
|
||||
+ anyhow::bail!("git blame process failed: {stderr}");
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
|
||||
@@ -1,26 +0,0 @@
|
||||
@@ -95,15 +95,19 @@
|
||||
let output = child.output().await.context("reading git blame output")?;
|
||||
|
||||
if !output.status.success() {
|
||||
- let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
- let trimmed = stderr.trim();
|
||||
- if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
|
||||
- return Ok(String::new());
|
||||
- }
|
||||
- anyhow::bail!("git blame process failed: {stderr}");
|
||||
+ return handle_command_output(output);
|
||||
}
|
||||
|
||||
Ok(String::from_utf8(output.stdout)?)
|
||||
+}
|
||||
+
|
||||
+fn handle_command_output(output: std::process::Output) -> Result<String> {
|
||||
+ let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
+ let trimmed = stderr.trim();
|
||||
+ if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
|
||||
+ return Ok(String::new());
|
||||
+ }
|
||||
+ anyhow::bail!("git blame process failed: {stderr}")
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
|
||||
@@ -1,694 +0,0 @@
|
||||
use language::{Point, TextBufferSnapshot};
|
||||
use std::{cmp, ops::Range};
|
||||
|
||||
const REPLACEMENT_COST: u32 = 1;
|
||||
const INSERTION_COST: u32 = 3;
|
||||
const DELETION_COST: u32 = 10;
|
||||
|
||||
/// A streaming fuzzy matcher that can process text chunks incrementally
|
||||
/// and return the best match found so far at each step.
|
||||
pub struct StreamingFuzzyMatcher {
|
||||
snapshot: TextBufferSnapshot,
|
||||
query_lines: Vec<String>,
|
||||
incomplete_line: String,
|
||||
best_match: Option<Range<usize>>,
|
||||
matrix: SearchMatrix,
|
||||
}
|
||||
|
||||
impl StreamingFuzzyMatcher {
|
||||
pub fn new(snapshot: TextBufferSnapshot) -> Self {
|
||||
let buffer_line_count = snapshot.max_point().row as usize + 1;
|
||||
Self {
|
||||
snapshot,
|
||||
query_lines: Vec::new(),
|
||||
incomplete_line: String::new(),
|
||||
best_match: None,
|
||||
matrix: SearchMatrix::new(buffer_line_count + 1),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the query lines.
|
||||
pub fn query_lines(&self) -> &[String] {
|
||||
&self.query_lines
|
||||
}
|
||||
|
||||
/// Push a new chunk of text and get the best match found so far.
|
||||
///
|
||||
/// This method accumulates text chunks and processes complete lines.
|
||||
/// Partial lines are buffered internally until a newline is received.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns `Some(range)` if a match has been found with the accumulated
|
||||
/// query so far, or `None` if no suitable match exists yet.
|
||||
pub fn push(&mut self, chunk: &str) -> Option<Range<usize>> {
|
||||
// Add the chunk to our incomplete line buffer
|
||||
self.incomplete_line.push_str(chunk);
|
||||
|
||||
if let Some((last_pos, _)) = self.incomplete_line.match_indices('\n').next_back() {
|
||||
let complete_part = &self.incomplete_line[..=last_pos];
|
||||
|
||||
// Split into lines and add to query_lines
|
||||
for line in complete_part.lines() {
|
||||
self.query_lines.push(line.to_string());
|
||||
}
|
||||
|
||||
self.incomplete_line.replace_range(..last_pos + 1, "");
|
||||
|
||||
self.best_match = self.resolve_location_fuzzy();
|
||||
}
|
||||
|
||||
self.best_match.clone()
|
||||
}
|
||||
|
||||
/// Finish processing and return the final best match.
|
||||
///
|
||||
/// This processes any remaining incomplete line before returning the final
|
||||
/// match result.
|
||||
pub fn finish(&mut self) -> Option<Range<usize>> {
|
||||
// Process any remaining incomplete line
|
||||
if !self.incomplete_line.is_empty() {
|
||||
self.query_lines.push(self.incomplete_line.clone());
|
||||
self.best_match = self.resolve_location_fuzzy();
|
||||
}
|
||||
|
||||
self.best_match.clone()
|
||||
}
|
||||
|
||||
fn resolve_location_fuzzy(&mut self) -> Option<Range<usize>> {
|
||||
let new_query_line_count = self.query_lines.len();
|
||||
let old_query_line_count = self.matrix.rows.saturating_sub(1);
|
||||
if new_query_line_count == old_query_line_count {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.matrix.resize_rows(new_query_line_count + 1);
|
||||
|
||||
// Process only the new query lines
|
||||
for row in old_query_line_count..new_query_line_count {
|
||||
let query_line = self.query_lines[row].trim();
|
||||
let leading_deletion_cost = (row + 1) as u32 * DELETION_COST;
|
||||
|
||||
self.matrix.set(
|
||||
row + 1,
|
||||
0,
|
||||
SearchState::new(leading_deletion_cost, SearchDirection::Up),
|
||||
);
|
||||
|
||||
let mut buffer_lines = self.snapshot.as_rope().chunks().lines();
|
||||
let mut col = 0;
|
||||
while let Some(buffer_line) = buffer_lines.next() {
|
||||
let buffer_line = buffer_line.trim();
|
||||
let up = SearchState::new(
|
||||
self.matrix
|
||||
.get(row, col + 1)
|
||||
.cost
|
||||
.saturating_add(DELETION_COST),
|
||||
SearchDirection::Up,
|
||||
);
|
||||
let left = SearchState::new(
|
||||
self.matrix
|
||||
.get(row + 1, col)
|
||||
.cost
|
||||
.saturating_add(INSERTION_COST),
|
||||
SearchDirection::Left,
|
||||
);
|
||||
let diagonal = SearchState::new(
|
||||
if query_line == buffer_line {
|
||||
self.matrix.get(row, col).cost
|
||||
} else if fuzzy_eq(query_line, buffer_line) {
|
||||
self.matrix.get(row, col).cost + REPLACEMENT_COST
|
||||
} else {
|
||||
self.matrix
|
||||
.get(row, col)
|
||||
.cost
|
||||
.saturating_add(DELETION_COST + INSERTION_COST)
|
||||
},
|
||||
SearchDirection::Diagonal,
|
||||
);
|
||||
self.matrix
|
||||
.set(row + 1, col + 1, up.min(left).min(diagonal));
|
||||
col += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Traceback to find the best match
|
||||
let buffer_line_count = self.snapshot.max_point().row as usize + 1;
|
||||
let mut buffer_row_end = buffer_line_count as u32;
|
||||
let mut best_cost = u32::MAX;
|
||||
for col in 1..=buffer_line_count {
|
||||
let cost = self.matrix.get(new_query_line_count, col).cost;
|
||||
if cost < best_cost {
|
||||
best_cost = cost;
|
||||
buffer_row_end = col as u32;
|
||||
}
|
||||
}
|
||||
|
||||
let mut matched_lines = 0;
|
||||
let mut query_row = new_query_line_count;
|
||||
let mut buffer_row_start = buffer_row_end;
|
||||
while query_row > 0 && buffer_row_start > 0 {
|
||||
let current = self.matrix.get(query_row, buffer_row_start as usize);
|
||||
match current.direction {
|
||||
SearchDirection::Diagonal => {
|
||||
query_row -= 1;
|
||||
buffer_row_start -= 1;
|
||||
matched_lines += 1;
|
||||
}
|
||||
SearchDirection::Up => {
|
||||
query_row -= 1;
|
||||
}
|
||||
SearchDirection::Left => {
|
||||
buffer_row_start -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let matched_buffer_row_count = buffer_row_end - buffer_row_start;
|
||||
let matched_ratio = matched_lines as f32
|
||||
/ (matched_buffer_row_count as f32).max(new_query_line_count as f32);
|
||||
if matched_ratio >= 0.8 {
|
||||
let buffer_start_ix = self
|
||||
.snapshot
|
||||
.point_to_offset(Point::new(buffer_row_start, 0));
|
||||
let buffer_end_ix = self.snapshot.point_to_offset(Point::new(
|
||||
buffer_row_end - 1,
|
||||
self.snapshot.line_len(buffer_row_end - 1),
|
||||
));
|
||||
Some(buffer_start_ix..buffer_end_ix)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fuzzy_eq(left: &str, right: &str) -> bool {
|
||||
const THRESHOLD: f64 = 0.8;
|
||||
|
||||
let min_levenshtein = left.len().abs_diff(right.len());
|
||||
let min_normalized_levenshtein =
|
||||
1. - (min_levenshtein as f64 / cmp::max(left.len(), right.len()) as f64);
|
||||
if min_normalized_levenshtein < THRESHOLD {
|
||||
return false;
|
||||
}
|
||||
|
||||
strsim::normalized_levenshtein(left, right) >= THRESHOLD
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum SearchDirection {
|
||||
Up,
|
||||
Left,
|
||||
Diagonal,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct SearchState {
|
||||
cost: u32,
|
||||
direction: SearchDirection,
|
||||
}
|
||||
|
||||
impl SearchState {
|
||||
fn new(cost: u32, direction: SearchDirection) -> Self {
|
||||
Self { cost, direction }
|
||||
}
|
||||
}
|
||||
|
||||
struct SearchMatrix {
|
||||
cols: usize,
|
||||
rows: usize,
|
||||
data: Vec<SearchState>,
|
||||
}
|
||||
|
||||
impl SearchMatrix {
|
||||
fn new(cols: usize) -> Self {
|
||||
SearchMatrix {
|
||||
cols,
|
||||
rows: 0,
|
||||
data: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn resize_rows(&mut self, needed_rows: usize) {
|
||||
debug_assert!(needed_rows > self.rows);
|
||||
self.rows = needed_rows;
|
||||
self.data.resize(
|
||||
self.rows * self.cols,
|
||||
SearchState::new(0, SearchDirection::Diagonal),
|
||||
);
|
||||
}
|
||||
|
||||
fn get(&self, row: usize, col: usize) -> SearchState {
|
||||
debug_assert!(row < self.rows && col < self.cols);
|
||||
self.data[row * self.cols + col]
|
||||
}
|
||||
|
||||
fn set(&mut self, row: usize, col: usize, state: SearchState) {
|
||||
debug_assert!(row < self.rows && col < self.cols);
|
||||
self.data[row * self.cols + col] = state;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use indoc::indoc;
|
||||
use language::{BufferId, TextBuffer};
|
||||
use rand::prelude::*;
|
||||
use util::test::{generate_marked_text, marked_text_ranges};
|
||||
|
||||
#[test]
|
||||
fn test_empty_query() {
|
||||
let buffer = TextBuffer::new(
|
||||
0,
|
||||
BufferId::new(1).unwrap(),
|
||||
"Hello world\nThis is a test\nFoo bar baz",
|
||||
);
|
||||
let snapshot = buffer.snapshot();
|
||||
|
||||
let mut finder = StreamingFuzzyMatcher::new(snapshot.clone());
|
||||
assert_eq!(push(&mut finder, ""), None);
|
||||
assert_eq!(finish(finder), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_streaming_exact_match() {
|
||||
let buffer = TextBuffer::new(
|
||||
0,
|
||||
BufferId::new(1).unwrap(),
|
||||
"Hello world\nThis is a test\nFoo bar baz",
|
||||
);
|
||||
let snapshot = buffer.snapshot();
|
||||
|
||||
let mut finder = StreamingFuzzyMatcher::new(snapshot.clone());
|
||||
|
||||
// Push partial query
|
||||
assert_eq!(push(&mut finder, "This"), None);
|
||||
|
||||
// Complete the line
|
||||
assert_eq!(
|
||||
push(&mut finder, " is a test\n"),
|
||||
Some("This is a test".to_string())
|
||||
);
|
||||
|
||||
// Finish should return the same result
|
||||
assert_eq!(finish(finder), Some("This is a test".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_streaming_fuzzy_match() {
|
||||
let buffer = TextBuffer::new(
|
||||
0,
|
||||
BufferId::new(1).unwrap(),
|
||||
indoc! {"
|
||||
function foo(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
function bar(x, y) {
|
||||
return x * y;
|
||||
}
|
||||
"},
|
||||
);
|
||||
let snapshot = buffer.snapshot();
|
||||
|
||||
let mut finder = StreamingFuzzyMatcher::new(snapshot.clone());
|
||||
|
||||
// Push a fuzzy query that should match the first function
|
||||
assert_eq!(
|
||||
push(&mut finder, "function foo(a, c) {\n").as_deref(),
|
||||
Some("function foo(a, b) {")
|
||||
);
|
||||
assert_eq!(
|
||||
push(&mut finder, " return a + c;\n}\n").as_deref(),
|
||||
Some(concat!(
|
||||
"function foo(a, b) {\n",
|
||||
" return a + b;\n",
|
||||
"}"
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_incremental_improvement() {
|
||||
let buffer = TextBuffer::new(
|
||||
0,
|
||||
BufferId::new(1).unwrap(),
|
||||
"Line 1\nLine 2\nLine 3\nLine 4\nLine 5",
|
||||
);
|
||||
let snapshot = buffer.snapshot();
|
||||
|
||||
let mut finder = StreamingFuzzyMatcher::new(snapshot.clone());
|
||||
|
||||
// No match initially
|
||||
assert_eq!(push(&mut finder, "Lin"), None);
|
||||
|
||||
// Get a match when we complete a line
|
||||
assert_eq!(push(&mut finder, "e 3\n"), Some("Line 3".to_string()));
|
||||
|
||||
// The match might change if we add more specific content
|
||||
assert_eq!(
|
||||
push(&mut finder, "Line 4\n"),
|
||||
Some("Line 3\nLine 4".to_string())
|
||||
);
|
||||
assert_eq!(finish(finder), Some("Line 3\nLine 4".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_incomplete_lines_buffering() {
|
||||
let buffer = TextBuffer::new(
|
||||
0,
|
||||
BufferId::new(1).unwrap(),
|
||||
indoc! {"
|
||||
The quick brown fox
|
||||
jumps over the lazy dog
|
||||
Pack my box with five dozen liquor jugs
|
||||
"},
|
||||
);
|
||||
let snapshot = buffer.snapshot();
|
||||
|
||||
let mut finder = StreamingFuzzyMatcher::new(snapshot.clone());
|
||||
|
||||
// Push text in small chunks across line boundaries
|
||||
assert_eq!(push(&mut finder, "jumps "), None); // No newline yet
|
||||
assert_eq!(push(&mut finder, "over the"), None); // Still no newline
|
||||
assert_eq!(push(&mut finder, " lazy"), None); // Still incomplete
|
||||
|
||||
// Complete the line
|
||||
assert_eq!(
|
||||
push(&mut finder, " dog\n"),
|
||||
Some("jumps over the lazy dog".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiline_fuzzy_match() {
|
||||
let buffer = TextBuffer::new(
|
||||
0,
|
||||
BufferId::new(1).unwrap(),
|
||||
indoc! {r#"
|
||||
impl Display for User {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "User: {} ({})", self.name, self.email)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for User {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.debug_struct("User")
|
||||
.field("name", &self.name)
|
||||
.field("email", &self.email)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
"#},
|
||||
);
|
||||
let snapshot = buffer.snapshot();
|
||||
|
||||
let mut finder = StreamingFuzzyMatcher::new(snapshot.clone());
|
||||
|
||||
assert_eq!(
|
||||
push(&mut finder, "impl Debug for User {\n"),
|
||||
Some("impl Debug for User {".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
push(
|
||||
&mut finder,
|
||||
" fn fmt(&self, f: &mut Formatter) -> Result {\n"
|
||||
)
|
||||
.as_deref(),
|
||||
Some(concat!(
|
||||
"impl Debug for User {\n",
|
||||
" fn fmt(&self, f: &mut Formatter) -> fmt::Result {"
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
push(&mut finder, " f.debug_struct(\"User\")\n").as_deref(),
|
||||
Some(concat!(
|
||||
"impl Debug for User {\n",
|
||||
" fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n",
|
||||
" f.debug_struct(\"User\")"
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
push(
|
||||
&mut finder,
|
||||
" .field(\"name\", &self.username)\n"
|
||||
)
|
||||
.as_deref(),
|
||||
Some(concat!(
|
||||
"impl Debug for User {\n",
|
||||
" fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n",
|
||||
" f.debug_struct(\"User\")\n",
|
||||
" .field(\"name\", &self.name)"
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
finish(finder).as_deref(),
|
||||
Some(concat!(
|
||||
"impl Debug for User {\n",
|
||||
" fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n",
|
||||
" f.debug_struct(\"User\")\n",
|
||||
" .field(\"name\", &self.name)"
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_resolve_location_single_line(mut rng: StdRng) {
|
||||
assert_location_resolution(
|
||||
concat!(
|
||||
" Lorem\n",
|
||||
"« ipsum»\n",
|
||||
" dolor sit amet\n",
|
||||
" consecteur",
|
||||
),
|
||||
"ipsum",
|
||||
&mut rng,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_resolve_location_multiline(mut rng: StdRng) {
|
||||
assert_location_resolution(
|
||||
concat!(
|
||||
" Lorem\n",
|
||||
"« ipsum\n",
|
||||
" dolor sit amet»\n",
|
||||
" consecteur",
|
||||
),
|
||||
"ipsum\ndolor sit amet",
|
||||
&mut rng,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_resolve_location_function_with_typo(mut rng: StdRng) {
|
||||
assert_location_resolution(
|
||||
indoc! {"
|
||||
«fn foo1(a: usize) -> usize {
|
||||
40
|
||||
}»
|
||||
|
||||
fn foo2(b: usize) -> usize {
|
||||
42
|
||||
}
|
||||
"},
|
||||
"fn foo1(a: usize) -> u32 {\n40\n}",
|
||||
&mut rng,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_resolve_location_class_methods(mut rng: StdRng) {
|
||||
assert_location_resolution(
|
||||
indoc! {"
|
||||
class Something {
|
||||
one() { return 1; }
|
||||
« two() { return 2222; }
|
||||
three() { return 333; }
|
||||
four() { return 4444; }
|
||||
five() { return 5555; }
|
||||
six() { return 6666; }»
|
||||
seven() { return 7; }
|
||||
eight() { return 8; }
|
||||
}
|
||||
"},
|
||||
indoc! {"
|
||||
two() { return 2222; }
|
||||
four() { return 4444; }
|
||||
five() { return 5555; }
|
||||
six() { return 6666; }
|
||||
"},
|
||||
&mut rng,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_resolve_location_imports_no_match(mut rng: StdRng) {
|
||||
assert_location_resolution(
|
||||
indoc! {"
|
||||
use std::ops::Range;
|
||||
use std::sync::Mutex;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env,
|
||||
ffi::{OsStr, OsString},
|
||||
fs,
|
||||
io::{BufRead, BufReader},
|
||||
mem,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
sync::LazyLock,
|
||||
time::SystemTime,
|
||||
};
|
||||
"},
|
||||
indoc! {"
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::fmt::Write as _;
|
||||
use std::fs;
|
||||
use std::io::{BufReader, Read, Write};
|
||||
use std::mem;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
"},
|
||||
&mut rng,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_resolve_location_nested_closure(mut rng: StdRng) {
|
||||
assert_location_resolution(
|
||||
indoc! {"
|
||||
impl Foo {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
subscriptions: vec![
|
||||
cx.observe_window_activation(window, |editor, window, cx| {
|
||||
let active = window.is_window_active();
|
||||
editor.blink_manager.update(cx, |blink_manager, cx| {
|
||||
if active {
|
||||
blink_manager.enable(cx);
|
||||
} else {
|
||||
blink_manager.disable(cx);
|
||||
}
|
||||
});
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
"},
|
||||
concat!(
|
||||
" editor.blink_manager.update(cx, |blink_manager, cx| {\n",
|
||||
" blink_manager.enable(cx);\n",
|
||||
" });",
|
||||
),
|
||||
&mut rng,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_resolve_location_tool_invocation(mut rng: StdRng) {
|
||||
assert_location_resolution(
|
||||
indoc! {r#"
|
||||
let tool = cx
|
||||
.update(|cx| working_set.tool(&tool_name, cx))
|
||||
.map_err(|err| {
|
||||
anyhow!("Failed to look up tool '{}': {}", tool_name, err)
|
||||
})?;
|
||||
|
||||
let Some(tool) = tool else {
|
||||
return Err(anyhow!("Tool '{}' not found", tool_name));
|
||||
};
|
||||
|
||||
let project = project.clone();
|
||||
let action_log = action_log.clone();
|
||||
let messages = messages.clone();
|
||||
let tool_result = 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);
|
||||
"#},
|
||||
concat!(
|
||||
"let tool_result = cx\n",
|
||||
" .update(|cx| tool.run(invocation.input, &messages, project, action_log, cx))\n",
|
||||
" .output;",
|
||||
),
|
||||
&mut rng,
|
||||
);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_location_resolution(text_with_expected_range: &str, query: &str, rng: &mut StdRng) {
|
||||
let (text, expected_ranges) = marked_text_ranges(text_with_expected_range, false);
|
||||
let buffer = TextBuffer::new(0, BufferId::new(1).unwrap(), text.clone());
|
||||
let snapshot = buffer.snapshot();
|
||||
|
||||
let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone());
|
||||
|
||||
// Split query into random chunks
|
||||
let chunks = to_random_chunks(rng, query);
|
||||
|
||||
// Push chunks incrementally
|
||||
for chunk in &chunks {
|
||||
matcher.push(chunk);
|
||||
}
|
||||
|
||||
let result = matcher.finish();
|
||||
|
||||
// If no expected ranges, we expect no match
|
||||
if expected_ranges.is_empty() {
|
||||
assert_eq!(
|
||||
result, None,
|
||||
"Expected no match for query: {:?}, but found: {:?}",
|
||||
query, result
|
||||
);
|
||||
} else {
|
||||
let mut actual_ranges = Vec::new();
|
||||
if let Some(range) = result {
|
||||
actual_ranges.push(range);
|
||||
}
|
||||
|
||||
let text_with_actual_range = generate_marked_text(&text, &actual_ranges, false);
|
||||
pretty_assertions::assert_eq!(
|
||||
text_with_actual_range,
|
||||
text_with_expected_range,
|
||||
"Query: {:?}, Chunks: {:?}",
|
||||
query,
|
||||
chunks
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn to_random_chunks(rng: &mut StdRng, input: &str) -> Vec<String> {
|
||||
let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50));
|
||||
let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);
|
||||
chunk_indices.sort();
|
||||
chunk_indices.push(input.len());
|
||||
|
||||
let mut chunks = Vec::new();
|
||||
let mut last_ix = 0;
|
||||
for chunk_ix in chunk_indices {
|
||||
chunks.push(input[last_ix..chunk_ix].to_string());
|
||||
last_ix = chunk_ix;
|
||||
}
|
||||
chunks
|
||||
}
|
||||
|
||||
fn push(finder: &mut StreamingFuzzyMatcher, chunk: &str) -> Option<String> {
|
||||
finder
|
||||
.push(chunk)
|
||||
.map(|range| finder.snapshot.text_for_range(range).collect::<String>())
|
||||
}
|
||||
|
||||
fn finish(mut finder: StreamingFuzzyMatcher) -> Option<String> {
|
||||
let snapshot = finder.snapshot.clone();
|
||||
finder
|
||||
.finish()
|
||||
.map(|range| snapshot.text_for_range(range).collect::<String>())
|
||||
}
|
||||
}
|
||||
@@ -9,31 +9,24 @@ use assistant_tool::{
|
||||
ToolUseStatus,
|
||||
};
|
||||
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
|
||||
use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey};
|
||||
use editor::{Editor, EditorMode, MultiBuffer, PathKey};
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task,
|
||||
Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, EntityId, Task,
|
||||
TextStyleRefinement, WeakEntity, pulsating_between,
|
||||
};
|
||||
use indoc::formatdoc;
|
||||
use language::{
|
||||
Anchor, Buffer, Capability, LanguageRegistry, LineEnding, OffsetRangeExt, Point, Rope,
|
||||
TextBuffer,
|
||||
language_settings::{self, FormatOnSave, SoftWrap},
|
||||
Anchor, Buffer, Capability, LanguageRegistry, LineEnding, OffsetRangeExt, Rope, TextBuffer,
|
||||
language_settings::SoftWrap,
|
||||
};
|
||||
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
|
||||
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
|
||||
use project::{
|
||||
Project, ProjectPath,
|
||||
lsp_store::{FormatTrigger, LspFormatTarget},
|
||||
};
|
||||
use project::{Project, ProjectPath};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
collections::HashSet,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
@@ -105,7 +98,7 @@ pub enum EditFileMode {
|
||||
pub struct EditFileToolOutput {
|
||||
pub original_path: PathBuf,
|
||||
pub new_text: String,
|
||||
pub old_text: Arc<String>,
|
||||
pub old_text: String,
|
||||
pub raw_output: Option<EditAgentOutput>,
|
||||
}
|
||||
|
||||
@@ -194,10 +187,8 @@ impl Tool for EditFileTool {
|
||||
});
|
||||
|
||||
let card_clone = card.clone();
|
||||
let action_log_clone = action_log.clone();
|
||||
let task = cx.spawn(async move |cx: &mut AsyncApp| {
|
||||
let edit_agent =
|
||||
EditAgent::new(model, project.clone(), action_log_clone, Templates::new());
|
||||
let edit_agent = EditAgent::new(model, project.clone(), action_log, Templates::new());
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
@@ -209,14 +200,10 @@ impl Tool for EditFileTool {
|
||||
let old_text = cx
|
||||
.background_spawn({
|
||||
let old_snapshot = old_snapshot.clone();
|
||||
async move { Arc::new(old_snapshot.text()) }
|
||||
async move { old_snapshot.text() }
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Some(card) = card_clone.as_ref() {
|
||||
card.update(cx, |card, cx| card.initialize(buffer.clone(), cx))?;
|
||||
}
|
||||
|
||||
let (output, mut events) = if matches!(input.mode, EditFileMode::Edit) {
|
||||
edit_agent.edit(
|
||||
buffer.clone(),
|
||||
@@ -238,78 +225,54 @@ impl Tool for EditFileTool {
|
||||
match event {
|
||||
EditAgentOutputEvent::Edited => {
|
||||
if let Some(card) = card_clone.as_ref() {
|
||||
card.update(cx, |card, cx| card.update_diff(cx))?;
|
||||
}
|
||||
}
|
||||
EditAgentOutputEvent::UnresolvedEditRange => hallucinated_old_text = true,
|
||||
EditAgentOutputEvent::ResolvingEditRange(range) => {
|
||||
if let Some(card) = card_clone.as_ref() {
|
||||
card.update(cx, |card, cx| card.reveal_range(range, cx))?;
|
||||
let new_snapshot =
|
||||
buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
let new_text = cx
|
||||
.background_spawn({
|
||||
let new_snapshot = new_snapshot.clone();
|
||||
async move { new_snapshot.text() }
|
||||
})
|
||||
.await;
|
||||
card.update(cx, |card, cx| {
|
||||
card.set_diff(
|
||||
project_path.path.clone(),
|
||||
old_text.clone(),
|
||||
new_text,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
EditAgentOutputEvent::OldTextNotFound(_) => hallucinated_old_text = true,
|
||||
}
|
||||
}
|
||||
let agent_output = output.await?;
|
||||
|
||||
// If format_on_save is enabled, format the buffer
|
||||
let format_on_save_enabled = buffer
|
||||
.read_with(cx, |buffer, cx| {
|
||||
let settings = language_settings::language_settings(
|
||||
buffer.language().map(|l| l.name()),
|
||||
buffer.file(),
|
||||
cx,
|
||||
);
|
||||
!matches!(settings.format_on_save, FormatOnSave::Off)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
if format_on_save_enabled {
|
||||
let format_task = project.update(cx, |project, cx| {
|
||||
project.format(
|
||||
HashSet::from_iter([buffer.clone()]),
|
||||
LspFormatTarget::Buffers,
|
||||
false, // Don't push to history since the tool did it.
|
||||
FormatTrigger::Save,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
format_task.await.log_err();
|
||||
}
|
||||
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
|
||||
.await?;
|
||||
|
||||
// Notify the action log that we've edited the buffer (*after* formatting has completed).
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.buffer_edited(buffer.clone(), cx);
|
||||
})?;
|
||||
|
||||
let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
let (new_text, diff) = cx
|
||||
.background_spawn({
|
||||
let new_snapshot = new_snapshot.clone();
|
||||
let old_text = old_text.clone();
|
||||
async move {
|
||||
let new_text = new_snapshot.text();
|
||||
let diff = language::unified_diff(&old_text, &new_text);
|
||||
|
||||
(new_text, diff)
|
||||
}
|
||||
})
|
||||
.await;
|
||||
let new_text = cx.background_spawn({
|
||||
let new_snapshot = new_snapshot.clone();
|
||||
async move { new_snapshot.text() }
|
||||
});
|
||||
let diff = cx.background_spawn(async move {
|
||||
language::unified_diff(&old_snapshot.text(), &new_snapshot.text())
|
||||
});
|
||||
let (new_text, diff) = futures::join!(new_text, diff);
|
||||
|
||||
let output = EditFileToolOutput {
|
||||
original_path: project_path.path.to_path_buf(),
|
||||
new_text: new_text.clone(),
|
||||
old_text,
|
||||
old_text: old_text.clone(),
|
||||
raw_output: Some(agent_output),
|
||||
};
|
||||
|
||||
if let Some(card) = card_clone {
|
||||
card.update(cx, |card, cx| {
|
||||
card.update_diff(cx);
|
||||
card.finalize(cx)
|
||||
card.set_diff(project_path.path.clone(), old_text, new_text, cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
@@ -319,15 +282,12 @@ impl Tool for EditFileTool {
|
||||
anyhow::ensure!(
|
||||
!hallucinated_old_text,
|
||||
formatdoc! {"
|
||||
Some edits were produced but none of them could be applied.
|
||||
Read the relevant sections of {input_path} again so that
|
||||
I can perform the requested edits.
|
||||
"}
|
||||
Some edits were produced but none of them could be applied.
|
||||
Read the relevant sections of {input_path} again so that
|
||||
I can perform the requested edits.
|
||||
"}
|
||||
);
|
||||
Ok(ToolResultOutput {
|
||||
content: ToolResultContent::Text("No edits were made.".into()),
|
||||
output: serde_json::to_value(output).ok(),
|
||||
})
|
||||
Ok("No edits were made.".to_string().into())
|
||||
} else {
|
||||
Ok(ToolResultOutput {
|
||||
content: ToolResultContent::Text(format!(
|
||||
@@ -358,48 +318,16 @@ impl Tool for EditFileTool {
|
||||
};
|
||||
|
||||
let card = cx.new(|cx| {
|
||||
EditFileToolCard::new(output.original_path.clone(), project.clone(), window, cx)
|
||||
let mut card = EditFileToolCard::new(output.original_path.clone(), project, window, cx);
|
||||
card.set_diff(
|
||||
output.original_path.into(),
|
||||
output.old_text,
|
||||
output.new_text,
|
||||
cx,
|
||||
);
|
||||
card
|
||||
});
|
||||
|
||||
cx.spawn({
|
||||
let path: Arc<Path> = output.original_path.into();
|
||||
let language_registry = project.read(cx).languages().clone();
|
||||
let card = card.clone();
|
||||
async move |cx| {
|
||||
let buffer =
|
||||
build_buffer(output.new_text, path.clone(), &language_registry, cx).await?;
|
||||
let buffer_diff =
|
||||
build_buffer_diff(output.old_text.clone(), &buffer, &language_registry, cx)
|
||||
.await?;
|
||||
card.update(cx, |card, cx| {
|
||||
card.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<_>>();
|
||||
|
||||
multibuffer.set_excerpts_for_path(
|
||||
PathKey::for_buffer(&buffer, cx),
|
||||
buffer,
|
||||
diff_hunk_ranges,
|
||||
editor::DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
cx,
|
||||
);
|
||||
multibuffer.add_diff(buffer_diff, cx);
|
||||
let end = multibuffer.len(cx);
|
||||
card.total_lines =
|
||||
Some(multibuffer.snapshot(cx).offset_to_point(end).row + 1);
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
Some(card.into())
|
||||
}
|
||||
}
|
||||
@@ -474,15 +402,12 @@ pub struct EditFileToolCard {
|
||||
editor: Entity<Editor>,
|
||||
multibuffer: Entity<MultiBuffer>,
|
||||
project: Entity<Project>,
|
||||
buffer: Option<Entity<Buffer>>,
|
||||
base_text: Option<Arc<String>>,
|
||||
buffer_diff: Option<Entity<BufferDiff>>,
|
||||
revealed_ranges: Vec<Range<Anchor>>,
|
||||
diff_task: Option<Task<Result<()>>>,
|
||||
preview_expanded: bool,
|
||||
error_expanded: Option<Entity<Markdown>>,
|
||||
full_height_expanded: bool,
|
||||
total_lines: Option<u32>,
|
||||
editor_unique_id: EntityId,
|
||||
}
|
||||
|
||||
impl EditFileToolCard {
|
||||
@@ -503,9 +428,7 @@ impl EditFileToolCard {
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.disable_inline_diagnostics();
|
||||
editor.disable_expand_excerpt_buttons(cx);
|
||||
// Keep horizontal scrollbar so user can scroll horizontally if needed
|
||||
editor.set_show_vertical_scrollbar(false, cx);
|
||||
editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx);
|
||||
editor.disable_scrollbars_and_minimap(window, cx);
|
||||
editor.set_soft_wrap_mode(SoftWrap::None, cx);
|
||||
editor.scroll_manager.set_forbid_vertical_scroll(true);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
@@ -517,14 +440,11 @@ impl EditFileToolCard {
|
||||
editor
|
||||
});
|
||||
Self {
|
||||
editor_unique_id: editor.entity_id(),
|
||||
path,
|
||||
project,
|
||||
editor,
|
||||
multibuffer,
|
||||
buffer: None,
|
||||
base_text: None,
|
||||
buffer_diff: None,
|
||||
revealed_ranges: Vec::new(),
|
||||
diff_task: None,
|
||||
preview_expanded: true,
|
||||
error_expanded: None,
|
||||
@@ -533,184 +453,46 @@ impl EditFileToolCard {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialize(&mut self, buffer: Entity<Buffer>, cx: &mut App) {
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let base_text = buffer_snapshot.text();
|
||||
let language_registry = buffer.read(cx).language_registry();
|
||||
let text_snapshot = buffer.read(cx).text_snapshot();
|
||||
|
||||
// Create a buffer diff with the current text as the base
|
||||
let buffer_diff = cx.new(|cx| {
|
||||
let mut diff = BufferDiff::new(&text_snapshot, cx);
|
||||
let _ = diff.set_base_text(
|
||||
buffer_snapshot.clone(),
|
||||
language_registry,
|
||||
text_snapshot,
|
||||
cx,
|
||||
);
|
||||
diff
|
||||
});
|
||||
|
||||
self.buffer = Some(buffer.clone());
|
||||
self.base_text = Some(base_text.into());
|
||||
self.buffer_diff = Some(buffer_diff.clone());
|
||||
|
||||
// Add the diff to the multibuffer
|
||||
self.multibuffer
|
||||
.update(cx, |multibuffer, cx| multibuffer.add_diff(buffer_diff, cx));
|
||||
pub fn has_diff(&self) -> bool {
|
||||
self.total_lines.is_some()
|
||||
}
|
||||
|
||||
pub fn is_loading(&self) -> bool {
|
||||
self.total_lines.is_none()
|
||||
}
|
||||
|
||||
pub fn update_diff(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(buffer) = self.buffer.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Some(buffer_diff) = self.buffer_diff.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let buffer = buffer.clone();
|
||||
let buffer_diff = buffer_diff.clone();
|
||||
let base_text = self.base_text.clone();
|
||||
self.diff_task = Some(cx.spawn(async move |this, cx| {
|
||||
let text_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot())?;
|
||||
let diff_snapshot = BufferDiff::update_diff(
|
||||
buffer_diff.clone(),
|
||||
text_snapshot.clone(),
|
||||
base_text,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
buffer_diff.update(cx, |diff, cx| {
|
||||
diff.set_snapshot(diff_snapshot, &text_snapshot, cx)
|
||||
})?;
|
||||
this.update(cx, |this, cx| this.update_visible_ranges(cx))
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn reveal_range(&mut self, range: Range<Anchor>, cx: &mut Context<Self>) {
|
||||
self.revealed_ranges.push(range);
|
||||
self.update_visible_ranges(cx);
|
||||
}
|
||||
|
||||
fn update_visible_ranges(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(buffer) = self.buffer.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let ranges = self.excerpt_ranges(cx);
|
||||
self.total_lines = self.multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.set_excerpts_for_path(
|
||||
PathKey::for_buffer(buffer, cx),
|
||||
buffer.clone(),
|
||||
ranges,
|
||||
editor::DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
cx,
|
||||
);
|
||||
let end = multibuffer.len(cx);
|
||||
Some(multibuffer.snapshot(cx).offset_to_point(end).row + 1)
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn excerpt_ranges(&self, cx: &App) -> Vec<Range<Point>> {
|
||||
let Some(buffer) = self.buffer.as_ref() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(diff) = self.buffer_diff.as_ref() else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let buffer = buffer.read(cx);
|
||||
let diff = diff.read(cx);
|
||||
let mut ranges = diff
|
||||
.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, cx)
|
||||
.map(|diff_hunk| diff_hunk.buffer_range.to_point(&buffer))
|
||||
.collect::<Vec<_>>();
|
||||
ranges.extend(
|
||||
self.revealed_ranges
|
||||
.iter()
|
||||
.map(|range| range.to_point(&buffer)),
|
||||
);
|
||||
ranges.sort_unstable_by_key(|range| (range.start, Reverse(range.end)));
|
||||
|
||||
// Merge adjacent ranges
|
||||
let mut ranges = ranges.into_iter().peekable();
|
||||
let mut merged_ranges = Vec::new();
|
||||
while let Some(mut range) = ranges.next() {
|
||||
while let Some(next_range) = ranges.peek() {
|
||||
if range.end >= next_range.start {
|
||||
range.end = range.end.max(next_range.end);
|
||||
ranges.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
merged_ranges.push(range);
|
||||
}
|
||||
merged_ranges
|
||||
}
|
||||
|
||||
pub fn finalize(&mut self, cx: &mut Context<Self>) -> Result<()> {
|
||||
let ranges = self.excerpt_ranges(cx);
|
||||
let buffer = self.buffer.take().context("card was already finalized")?;
|
||||
let base_text = self
|
||||
.base_text
|
||||
.take()
|
||||
.context("card was already finalized")?;
|
||||
pub 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?;
|
||||
|
||||
// Replace the buffer in the multibuffer with the snapshot
|
||||
let buffer = cx.new(|cx| {
|
||||
let language = buffer.read(cx).language().cloned();
|
||||
let buffer = TextBuffer::new_normalized(
|
||||
0,
|
||||
cx.entity_id().as_non_zero_u64().into(),
|
||||
buffer.read(cx).line_ending(),
|
||||
buffer.read(cx).as_rope().clone(),
|
||||
);
|
||||
let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite);
|
||||
buffer.set_language(language, cx);
|
||||
buffer
|
||||
});
|
||||
|
||||
let buffer_diff = cx.spawn({
|
||||
let buffer = buffer.clone();
|
||||
let language_registry = language_registry.clone();
|
||||
async move |_this, cx| {
|
||||
build_buffer_diff(base_text, &buffer, &language_registry, cx).await
|
||||
}
|
||||
});
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let buffer_diff = buffer_diff.await?;
|
||||
this.update(cx, |this, cx| {
|
||||
this.multibuffer.update(cx, |multibuffer, cx| {
|
||||
let path_key = PathKey::for_buffer(&buffer, 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<_>>();
|
||||
multibuffer.clear(cx);
|
||||
multibuffer.set_excerpts_for_path(
|
||||
path_key,
|
||||
PathKey::for_buffer(&buffer, cx),
|
||||
buffer,
|
||||
ranges,
|
||||
diff_hunk_ranges,
|
||||
editor::DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
cx,
|
||||
);
|
||||
multibuffer.add_diff(buffer_diff.clone(), cx);
|
||||
multibuffer.add_diff(buffer_diff, cx);
|
||||
let end = multibuffer.len(cx);
|
||||
Some(multibuffer.snapshot(cx).offset_to_point(end).row + 1)
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -728,7 +510,7 @@ impl ToolCard for EditFileToolCard {
|
||||
};
|
||||
|
||||
let path_label_button = h_flex()
|
||||
.id(("edit-tool-path-label-button", self.editor.entity_id()))
|
||||
.id(("edit-tool-path-label-button", self.editor_unique_id))
|
||||
.w_full()
|
||||
.max_w_full()
|
||||
.px_1()
|
||||
@@ -827,7 +609,7 @@ impl ToolCard for EditFileToolCard {
|
||||
)
|
||||
.child(
|
||||
Disclosure::new(
|
||||
("edit-file-error-disclosure", self.editor.entity_id()),
|
||||
("edit-file-error-disclosure", self.editor_unique_id),
|
||||
self.error_expanded.is_some(),
|
||||
)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
@@ -849,10 +631,10 @@ impl ToolCard for EditFileToolCard {
|
||||
),
|
||||
)
|
||||
})
|
||||
.when(error_message.is_none() && !self.is_loading(), |header| {
|
||||
.when(error_message.is_none() && self.has_diff(), |header| {
|
||||
header.child(
|
||||
Disclosure::new(
|
||||
("edit-file-disclosure", self.editor.entity_id()),
|
||||
("edit-file-disclosure", self.editor_unique_id),
|
||||
self.preview_expanded,
|
||||
)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
@@ -988,10 +770,10 @@ impl ToolCard for EditFileToolCard {
|
||||
),
|
||||
)
|
||||
})
|
||||
.when(self.is_loading() && error_message.is_none(), |card| {
|
||||
.when(!self.has_diff() && error_message.is_none(), |card| {
|
||||
card.child(waiting_for_diff)
|
||||
})
|
||||
.when(self.preview_expanded && !self.is_loading(), |card| {
|
||||
.when(self.preview_expanded && self.has_diff(), |card| {
|
||||
card.child(
|
||||
v_flex()
|
||||
.relative()
|
||||
@@ -1013,7 +795,7 @@ impl ToolCard for EditFileToolCard {
|
||||
.when(is_collapsible, |card| {
|
||||
card.child(
|
||||
h_flex()
|
||||
.id(("expand-button", self.editor.entity_id()))
|
||||
.id(("expand-button", self.editor_unique_id))
|
||||
.flex_none()
|
||||
.cursor_pointer()
|
||||
.h_5()
|
||||
@@ -1087,23 +869,19 @@ async fn build_buffer(
|
||||
}
|
||||
|
||||
async fn build_buffer_diff(
|
||||
old_text: Arc<String>,
|
||||
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 old_text_rope = cx
|
||||
.background_spawn({
|
||||
let old_text = old_text.clone();
|
||||
async move { Rope::from(old_text.as_str()) }
|
||||
})
|
||||
.await;
|
||||
let base_buffer = cx
|
||||
.update(|cx| {
|
||||
Buffer::build_snapshot(
|
||||
old_text_rope,
|
||||
old_text.clone().into(),
|
||||
buffer.language().cloned(),
|
||||
Some(language_registry.clone()),
|
||||
cx,
|
||||
@@ -1115,7 +893,7 @@ async fn build_buffer_diff(
|
||||
.update(|cx| {
|
||||
BufferDiffSnapshot::new_with_base_buffer(
|
||||
buffer.text.clone(),
|
||||
Some(old_text),
|
||||
Some(old_text.into()),
|
||||
base_buffer,
|
||||
cx,
|
||||
)
|
||||
@@ -1140,8 +918,8 @@ async fn build_buffer_diff(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use client::TelemetrySettings;
|
||||
use fs::{FakeFs, Fs};
|
||||
use gpui::{TestAppContext, UpdateGlobal};
|
||||
use fs::FakeFs;
|
||||
use gpui::TestAppContext;
|
||||
use language_model::fake_provider::FakeLanguageModel;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
@@ -1351,340 +1129,4 @@ mod tests {
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_format_on_save(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/root", json!({"src": {}})).await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
// Set up a Rust language with LSP formatting support
|
||||
let rust_language = Arc::new(language::Language::new(
|
||||
language::LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: language::LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
None,
|
||||
));
|
||||
|
||||
// Register the language and fake LSP
|
||||
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
||||
language_registry.add(rust_language);
|
||||
|
||||
let mut fake_language_servers = language_registry.register_fake_lsp(
|
||||
"Rust",
|
||||
language::FakeLspAdapter {
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
document_formatting_provider: Some(lsp::OneOf::Left(true)),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
// Create the file
|
||||
fs.save(
|
||||
path!("/root/src/main.rs").as_ref(),
|
||||
&"initial content".into(),
|
||||
language::LineEnding::Unix,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Open the buffer to trigger LSP initialization
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer(path!("/root/src/main.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Register the buffer with language servers
|
||||
let _handle = project.update(cx, |project, cx| {
|
||||
project.register_buffer_with_language_servers(&buffer, cx)
|
||||
});
|
||||
|
||||
const UNFORMATTED_CONTENT: &str = "fn main() {println!(\"Hello!\");}\n";
|
||||
const FORMATTED_CONTENT: &str =
|
||||
"This file was formatted by the fake formatter in the test.\n";
|
||||
|
||||
// Get the fake language server and set up formatting handler
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>({
|
||||
|_, _| async move {
|
||||
Ok(Some(vec![lsp::TextEdit {
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(1, 0)),
|
||||
new_text: FORMATTED_CONTENT.to_string(),
|
||||
}]))
|
||||
}
|
||||
});
|
||||
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
|
||||
// First, test with format_on_save enabled
|
||||
cx.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<language::language_settings::AllLanguageSettings>(
|
||||
cx,
|
||||
|settings| {
|
||||
settings.defaults.format_on_save = Some(FormatOnSave::On);
|
||||
settings.defaults.formatter =
|
||||
Some(language::language_settings::SelectedFormatter::Auto);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Have the model stream unformatted content
|
||||
let edit_result = {
|
||||
let edit_task = cx.update(|cx| {
|
||||
let input = serde_json::to_value(EditFileToolInput {
|
||||
display_description: "Create main function".into(),
|
||||
path: "root/src/main.rs".into(),
|
||||
mode: EditFileMode::Overwrite,
|
||||
})
|
||||
.unwrap();
|
||||
Arc::new(EditFileTool)
|
||||
.run(
|
||||
input,
|
||||
Arc::default(),
|
||||
project.clone(),
|
||||
action_log.clone(),
|
||||
model.clone(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
.output
|
||||
});
|
||||
|
||||
// Stream the unformatted content
|
||||
cx.executor().run_until_parked();
|
||||
model.stream_last_completion_response(UNFORMATTED_CONTENT.to_string());
|
||||
model.end_last_completion_stream();
|
||||
|
||||
edit_task.await
|
||||
};
|
||||
assert!(edit_result.is_ok());
|
||||
|
||||
// Wait for any async operations (e.g. formatting) to complete
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
// Read the file to verify it was formatted automatically
|
||||
let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
|
||||
assert_eq!(
|
||||
// Ignore carriage returns on Windows
|
||||
new_content.replace("\r\n", "\n"),
|
||||
FORMATTED_CONTENT,
|
||||
"Code should be formatted when format_on_save is enabled"
|
||||
);
|
||||
|
||||
let stale_buffer_count = action_log.read_with(cx, |log, cx| log.stale_buffers(cx).count());
|
||||
|
||||
assert_eq!(
|
||||
stale_buffer_count, 0,
|
||||
"BUG: Buffer is incorrectly marked as stale after format-on-save. Found {} stale buffers. \
|
||||
This causes the agent to think the file was modified externally when it was just formatted.",
|
||||
stale_buffer_count
|
||||
);
|
||||
|
||||
// Next, test with format_on_save disabled
|
||||
cx.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<language::language_settings::AllLanguageSettings>(
|
||||
cx,
|
||||
|settings| {
|
||||
settings.defaults.format_on_save = Some(FormatOnSave::Off);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Stream unformatted edits again
|
||||
let edit_result = {
|
||||
let edit_task = cx.update(|cx| {
|
||||
let input = serde_json::to_value(EditFileToolInput {
|
||||
display_description: "Update main function".into(),
|
||||
path: "root/src/main.rs".into(),
|
||||
mode: EditFileMode::Overwrite,
|
||||
})
|
||||
.unwrap();
|
||||
Arc::new(EditFileTool)
|
||||
.run(
|
||||
input,
|
||||
Arc::default(),
|
||||
project.clone(),
|
||||
action_log.clone(),
|
||||
model.clone(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
.output
|
||||
});
|
||||
|
||||
// Stream the unformatted content
|
||||
cx.executor().run_until_parked();
|
||||
model.stream_last_completion_response(UNFORMATTED_CONTENT.to_string());
|
||||
model.end_last_completion_stream();
|
||||
|
||||
edit_task.await
|
||||
};
|
||||
assert!(edit_result.is_ok());
|
||||
|
||||
// Wait for any async operations (e.g. formatting) to complete
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
// Verify the file was not formatted
|
||||
let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
|
||||
assert_eq!(
|
||||
// Ignore carriage returns on Windows
|
||||
new_content.replace("\r\n", "\n"),
|
||||
UNFORMATTED_CONTENT,
|
||||
"Code should not be formatted when format_on_save is disabled"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_remove_trailing_whitespace(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/root", json!({"src": {}})).await;
|
||||
|
||||
// Create a simple file with trailing whitespace
|
||||
fs.save(
|
||||
path!("/root/src/main.rs").as_ref(),
|
||||
&"initial content".into(),
|
||||
language::LineEnding::Unix,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
|
||||
// First, test with remove_trailing_whitespace_on_save enabled
|
||||
cx.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<language::language_settings::AllLanguageSettings>(
|
||||
cx,
|
||||
|settings| {
|
||||
settings.defaults.remove_trailing_whitespace_on_save = Some(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const CONTENT_WITH_TRAILING_WHITESPACE: &str =
|
||||
"fn main() { \n println!(\"Hello!\"); \n}\n";
|
||||
|
||||
// Have the model stream content that contains trailing whitespace
|
||||
let edit_result = {
|
||||
let edit_task = cx.update(|cx| {
|
||||
let input = serde_json::to_value(EditFileToolInput {
|
||||
display_description: "Create main function".into(),
|
||||
path: "root/src/main.rs".into(),
|
||||
mode: EditFileMode::Overwrite,
|
||||
})
|
||||
.unwrap();
|
||||
Arc::new(EditFileTool)
|
||||
.run(
|
||||
input,
|
||||
Arc::default(),
|
||||
project.clone(),
|
||||
action_log.clone(),
|
||||
model.clone(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
.output
|
||||
});
|
||||
|
||||
// Stream the content with trailing whitespace
|
||||
cx.executor().run_until_parked();
|
||||
model.stream_last_completion_response(CONTENT_WITH_TRAILING_WHITESPACE.to_string());
|
||||
model.end_last_completion_stream();
|
||||
|
||||
edit_task.await
|
||||
};
|
||||
assert!(edit_result.is_ok());
|
||||
|
||||
// Wait for any async operations (e.g. formatting) to complete
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
// Read the file to verify trailing whitespace was removed automatically
|
||||
assert_eq!(
|
||||
// Ignore carriage returns on Windows
|
||||
fs.load(path!("/root/src/main.rs").as_ref())
|
||||
.await
|
||||
.unwrap()
|
||||
.replace("\r\n", "\n"),
|
||||
"fn main() {\n println!(\"Hello!\");\n}\n",
|
||||
"Trailing whitespace should be removed when remove_trailing_whitespace_on_save is enabled"
|
||||
);
|
||||
|
||||
// Next, test with remove_trailing_whitespace_on_save disabled
|
||||
cx.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<language::language_settings::AllLanguageSettings>(
|
||||
cx,
|
||||
|settings| {
|
||||
settings.defaults.remove_trailing_whitespace_on_save = Some(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Stream edits again with trailing whitespace
|
||||
let edit_result = {
|
||||
let edit_task = cx.update(|cx| {
|
||||
let input = serde_json::to_value(EditFileToolInput {
|
||||
display_description: "Update main function".into(),
|
||||
path: "root/src/main.rs".into(),
|
||||
mode: EditFileMode::Overwrite,
|
||||
})
|
||||
.unwrap();
|
||||
Arc::new(EditFileTool)
|
||||
.run(
|
||||
input,
|
||||
Arc::default(),
|
||||
project.clone(),
|
||||
action_log.clone(),
|
||||
model.clone(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
.output
|
||||
});
|
||||
|
||||
// Stream the content with trailing whitespace
|
||||
cx.executor().run_until_parked();
|
||||
model.stream_last_completion_response(CONTENT_WITH_TRAILING_WHITESPACE.to_string());
|
||||
model.end_last_completion_stream();
|
||||
|
||||
edit_task.await
|
||||
};
|
||||
assert!(edit_result.is_ok());
|
||||
|
||||
// Wait for any async operations (e.g. formatting) to complete
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
// Verify the file still has trailing whitespace
|
||||
// Read the file again - it should still have trailing whitespace
|
||||
let final_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
|
||||
assert_eq!(
|
||||
// Ignore carriage returns on Windows
|
||||
final_content.replace("\r\n", "\n"),
|
||||
CONTENT_WITH_TRAILING_WHITESPACE,
|
||||
"Trailing whitespace should remain when remove_trailing_whitespace_on_save is disabled"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::{borrow::Cow, cell::RefCell};
|
||||
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
@@ -39,11 +39,10 @@ impl FetchTool {
|
||||
}
|
||||
|
||||
async fn build_message(http_client: Arc<HttpClientWithUrl>, url: &str) -> Result<String> {
|
||||
let url = if !url.starts_with("https://") && !url.starts_with("http://") {
|
||||
Cow::Owned(format!("https://{url}"))
|
||||
} else {
|
||||
Cow::Borrowed(url)
|
||||
};
|
||||
let mut url = url.to_owned();
|
||||
if !url.starts_with("https://") && !url.starts_with("http://") {
|
||||
url = format!("https://{url}");
|
||||
}
|
||||
|
||||
let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
|
||||
|
||||
@@ -157,7 +156,8 @@ impl Tool for FetchTool {
|
||||
|
||||
let text = cx.background_spawn({
|
||||
let http_client = self.http_client.clone();
|
||||
async move { Self::build_message(http_client, &input.url).await }
|
||||
let url = input.url.clone();
|
||||
async move { Self::build_message(http_client, &url).await }
|
||||
});
|
||||
|
||||
cx.foreground_executor()
|
||||
|
||||
@@ -119,16 +119,14 @@ impl Tool for FindPathTool {
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
for mat in matches.iter().skip(offset).take(RESULTS_PER_PAGE) {
|
||||
write!(&mut message, "\n{}", mat.display()).unwrap();
|
||||
}
|
||||
|
||||
let output = FindPathToolOutput {
|
||||
glob,
|
||||
paths: matches,
|
||||
paths: matches.clone(),
|
||||
};
|
||||
|
||||
for mat in matches.into_iter().skip(offset).take(RESULTS_PER_PAGE) {
|
||||
write!(&mut message, "\n{}", mat.display()).unwrap();
|
||||
}
|
||||
Ok(ToolResultOutput {
|
||||
content: ToolResultContent::Text(message),
|
||||
output: Some(serde_json::to_value(output)?),
|
||||
@@ -237,6 +235,8 @@ impl ToolCard for FindPathToolCard {
|
||||
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()
|
||||
@@ -310,7 +310,7 @@ impl ToolCard for FindPathToolCard {
|
||||
.gap_1()
|
||||
.child(
|
||||
ToolCallCardHeader::new(IconName::SearchCode, matches_label)
|
||||
.with_code_path(&self.glob)
|
||||
.with_code_path(glob_label)
|
||||
.disclosure_slot(
|
||||
Disclosure::new("path-search-disclosure", self.expanded)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
|
||||
@@ -125,287 +125,18 @@ impl Tool for ListDirectoryTool {
|
||||
return Task::ready(Err(anyhow!("{} is not a directory.", input.path))).into();
|
||||
}
|
||||
|
||||
let mut folders = Vec::new();
|
||||
let mut files = Vec::new();
|
||||
|
||||
for entry in worktree.child_entries(&project_path.path) {
|
||||
let full_path = Path::new(worktree.root_name())
|
||||
.join(&entry.path)
|
||||
.display()
|
||||
.to_string();
|
||||
if entry.is_dir() {
|
||||
folders.push(full_path);
|
||||
} else {
|
||||
files.push(full_path);
|
||||
}
|
||||
}
|
||||
|
||||
let mut output = String::new();
|
||||
|
||||
if !folders.is_empty() {
|
||||
writeln!(output, "# Folders:\n{}", folders.join("\n")).unwrap();
|
||||
for entry in worktree.child_entries(&project_path.path) {
|
||||
writeln!(
|
||||
output,
|
||||
"{}",
|
||||
Path::new(worktree.root_name()).join(&entry.path).display(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
if !files.is_empty() {
|
||||
writeln!(output, "\n# Files:\n{}", files.join("\n")).unwrap();
|
||||
}
|
||||
|
||||
if output.is_empty() {
|
||||
writeln!(output, "{} is empty.", input.path).unwrap();
|
||||
return Task::ready(Ok(format!("{} is empty.", input.path).into())).into();
|
||||
}
|
||||
|
||||
Task::ready(Ok(output.into())).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use assistant_tool::Tool;
|
||||
use gpui::{AppContext, TestAppContext};
|
||||
use indoc::indoc;
|
||||
use language_model::fake_provider::FakeLanguageModel;
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use util::path;
|
||||
|
||||
fn platform_paths(path_str: &str) -> String {
|
||||
if cfg!(target_os = "windows") {
|
||||
path_str.replace("/", "\\")
|
||||
} else {
|
||||
path_str.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_list_directory_separates_files_and_dirs(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/project",
|
||||
json!({
|
||||
"src": {
|
||||
"main.rs": "fn main() {}",
|
||||
"lib.rs": "pub fn hello() {}",
|
||||
"models": {
|
||||
"user.rs": "struct User {}",
|
||||
"post.rs": "struct Post {}"
|
||||
},
|
||||
"utils": {
|
||||
"helper.rs": "pub fn help() {}"
|
||||
}
|
||||
},
|
||||
"tests": {
|
||||
"integration_test.rs": "#[test] fn test() {}"
|
||||
},
|
||||
"README.md": "# Project",
|
||||
"Cargo.toml": "[package]"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let tool = Arc::new(ListDirectoryTool);
|
||||
|
||||
// Test listing root directory
|
||||
let input = json!({
|
||||
"path": "project"
|
||||
});
|
||||
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
tool.clone().run(
|
||||
input,
|
||||
Arc::default(),
|
||||
project.clone(),
|
||||
action_log.clone(),
|
||||
model.clone(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.output
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let content = result.content.as_str().unwrap();
|
||||
assert_eq!(
|
||||
content,
|
||||
platform_paths(indoc! {"
|
||||
# Folders:
|
||||
project/src
|
||||
project/tests
|
||||
|
||||
# Files:
|
||||
project/Cargo.toml
|
||||
project/README.md
|
||||
"})
|
||||
);
|
||||
|
||||
// Test listing src directory
|
||||
let input = json!({
|
||||
"path": "project/src"
|
||||
});
|
||||
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
tool.clone().run(
|
||||
input,
|
||||
Arc::default(),
|
||||
project.clone(),
|
||||
action_log.clone(),
|
||||
model.clone(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.output
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let content = result.content.as_str().unwrap();
|
||||
assert_eq!(
|
||||
content,
|
||||
platform_paths(indoc! {"
|
||||
# Folders:
|
||||
project/src/models
|
||||
project/src/utils
|
||||
|
||||
# Files:
|
||||
project/src/lib.rs
|
||||
project/src/main.rs
|
||||
"})
|
||||
);
|
||||
|
||||
// Test listing directory with only files
|
||||
let input = json!({
|
||||
"path": "project/tests"
|
||||
});
|
||||
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
tool.clone().run(
|
||||
input,
|
||||
Arc::default(),
|
||||
project.clone(),
|
||||
action_log.clone(),
|
||||
model.clone(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.output
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let content = result.content.as_str().unwrap();
|
||||
assert!(!content.contains("# Folders:"));
|
||||
assert!(content.contains("# Files:"));
|
||||
assert!(content.contains(&platform_paths("project/tests/integration_test.rs")));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_list_directory_empty_directory(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/project",
|
||||
json!({
|
||||
"empty_dir": {}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let tool = Arc::new(ListDirectoryTool);
|
||||
|
||||
let input = json!({
|
||||
"path": "project/empty_dir"
|
||||
});
|
||||
|
||||
let result = cx
|
||||
.update(|cx| tool.run(input, Arc::default(), project, action_log, model, None, cx))
|
||||
.output
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let content = result.content.as_str().unwrap();
|
||||
assert_eq!(content, "project/empty_dir is empty.\n");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_list_directory_error_cases(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/project",
|
||||
json!({
|
||||
"file.txt": "content"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let tool = Arc::new(ListDirectoryTool);
|
||||
|
||||
// Test non-existent path
|
||||
let input = json!({
|
||||
"path": "project/nonexistent"
|
||||
});
|
||||
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
tool.clone().run(
|
||||
input,
|
||||
Arc::default(),
|
||||
project.clone(),
|
||||
action_log.clone(),
|
||||
model.clone(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.output
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("Path not found"));
|
||||
|
||||
// Test trying to list a file instead of directory
|
||||
let input = json!({
|
||||
"path": "project/file.txt"
|
||||
});
|
||||
|
||||
let result = cx
|
||||
.update(|cx| tool.run(input, Arc::default(), project, action_log, model, None, cx))
|
||||
.output
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(
|
||||
result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("is not a directory")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
15
crates/assistant_tools/src/rename_tool/description.md
Normal file
15
crates/assistant_tools/src/rename_tool/description.md
Normal file
@@ -0,0 +1,15 @@
|
||||
Renames a symbol across your codebase using the language server's semantic knowledge.
|
||||
|
||||
This tool performs a rename refactoring operation on a specified symbol. It uses the project's language server to analyze the code and perform the rename correctly across all files where the symbol is referenced.
|
||||
|
||||
Unlike a simple find and replace, this tool understands the semantic meaning of the code, so it only renames the specific symbol you specify and not unrelated text that happens to have the same name.
|
||||
|
||||
Examples of symbols you can rename:
|
||||
- Variables
|
||||
- Functions
|
||||
- Classes/structs
|
||||
- Fields/properties
|
||||
- Methods
|
||||
- Interfaces/traits
|
||||
|
||||
The language server handles updating all references to the renamed symbol throughout the codebase.
|
||||
11
crates/assistant_tools/src/symbol_info_tool/description.md
Normal file
11
crates/assistant_tools/src/symbol_info_tool/description.md
Normal file
@@ -0,0 +1,11 @@
|
||||
Gives detailed information about code symbols in your project such as variables, functions, classes, interface, traits, and other programming constructs, using the editor's integrated Language Server Protocol (LSP) servers.
|
||||
|
||||
This tool is the preferred way to do things like:
|
||||
* Find out where a code symbol is first declared (or first defined - that is, assigned)
|
||||
* Find all the places where a code symbol is referenced
|
||||
* Find the type definition for a code symbol
|
||||
* Find a code symbol's implementation
|
||||
|
||||
This tool gives more reliable answers than things like regex searches, because it can account for relevant semantics like aliases. It should be used over textual search tools (e.g. regex) when searching for information about code symbols that this tool supports directly.
|
||||
|
||||
This tool should not be used when you need to search for something that is not a code symbol.
|
||||
@@ -1,15 +1,12 @@
|
||||
You are an expert engineer and your task is to write a new file from scratch.
|
||||
|
||||
You MUST respond with the file's content wrapped in triple backticks (```).
|
||||
The backticks should be on their own line.
|
||||
The text you output will be saved verbatim as the content of the file.
|
||||
Tool calls have been disabled.
|
||||
Start your response with ```.
|
||||
|
||||
<file_path>
|
||||
<file_to_edit>
|
||||
{{path}}
|
||||
</file_path>
|
||||
</file_to_edit>
|
||||
|
||||
<edit_description>
|
||||
{{edit_description}}
|
||||
</edit_description>
|
||||
|
||||
You MUST respond directly with the file's content, without explanations, additional text or triple backticks.
|
||||
The text you output will be saved verbatim as the content of the file.
|
||||
|
||||
@@ -27,58 +27,20 @@ NEW TEXT 3 HERE
|
||||
</edits>
|
||||
```
|
||||
|
||||
# File Editing Instructions
|
||||
|
||||
- Use `<old_text>` and `<new_text>` tags to replace content
|
||||
- `<old_text>` must exactly match existing file content, including indentation
|
||||
- `<old_text>` must come from the actual file, not an outline
|
||||
- `<old_text>` cannot be empty
|
||||
- Be minimal with replacements:
|
||||
- For unique lines, include only those lines
|
||||
- For non-unique lines, include enough context to identify them
|
||||
- Do not escape quotes, newlines, or other characters within tags
|
||||
- For multiple occurrences, repeat the same tag pair for each instance
|
||||
- Edits are sequential - each assumes previous edits are already applied
|
||||
- Only edit the specified file
|
||||
- Always close all tags properly
|
||||
|
||||
|
||||
{{!-- The following example adds almost 10% pass rate for Gemini 2.5.
|
||||
Claude and gpt-4.1 don't really need it. --}}
|
||||
<example>
|
||||
<edits>
|
||||
|
||||
<old_text>
|
||||
struct User {
|
||||
name: String,
|
||||
email: String,
|
||||
}
|
||||
</old_text>
|
||||
<new_text>
|
||||
struct User {
|
||||
name: String,
|
||||
email: String,
|
||||
active: bool,
|
||||
}
|
||||
</new_text>
|
||||
|
||||
<old_text>
|
||||
let user = User {
|
||||
name: String::from("John"),
|
||||
email: String::from("john@example.com"),
|
||||
};
|
||||
</old_text>
|
||||
<new_text>
|
||||
let user = User {
|
||||
name: String::from("John"),
|
||||
email: String::from("john@example.com"),
|
||||
active: true,
|
||||
};
|
||||
</new_text>
|
||||
|
||||
</edits>
|
||||
</example>
|
||||
Rules for editing:
|
||||
|
||||
- `old_text` represents lines in the input file that will be replaced with `new_text`.
|
||||
- `old_text` MUST exactly match the existing file content, character for character, including indentation.
|
||||
- `old_text` MUST NEVER come from the outline, but from actual lines in the file.
|
||||
- Strive to be minimal in the lines you replace in `old_text`:
|
||||
- If the lines you want to replace are unique, you MUST include just those in the `old_text`.
|
||||
- If the lines you want to replace are NOT unique, you MUST include enough context around them in `old_text` to distinguish them from other lines.
|
||||
- If you want to replace many occurrences of the same text, repeat the same `old_text`/`new_text` pair multiple times and I will apply them sequentially, one occurrence at a time.
|
||||
- When reporting multiple edits, each edit assumes the previous one has already been applied! Therefore, you must ensure `old_text` doesn't reference text that has already been modified by a previous edit.
|
||||
- Don't explain the edits, just report them.
|
||||
- Only edit the file specified in `<file_to_edit>` and NEVER include edits to other files!
|
||||
- If you open an <old_text> tag, you MUST close it using </old_text>
|
||||
- If you open an <new_text> tag, you MUST close it using </new_text>
|
||||
|
||||
<file_to_edit>
|
||||
{{path}}
|
||||
|
||||
@@ -182,8 +182,9 @@ impl Tool for TerminalTool {
|
||||
let mut child = pair.slave.spawn_command(cmd)?;
|
||||
let mut reader = pair.master.try_clone_reader()?;
|
||||
drop(pair);
|
||||
let mut content = String::new();
|
||||
reader.read_to_string(&mut content)?;
|
||||
let mut content = Vec::new();
|
||||
reader.read_to_end(&mut content)?;
|
||||
let mut content = String::from_utf8(content)?;
|
||||
// Massage the pty output a bit to try to match what the terminal codepath gives us
|
||||
LineEnding::normalize(&mut content);
|
||||
content = content
|
||||
@@ -274,7 +275,7 @@ impl Tool for TerminalTool {
|
||||
let exit_status = terminal
|
||||
.update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
|
||||
.await;
|
||||
let (content, content_line_count) = terminal.read_with(cx, |terminal, _| {
|
||||
let (content, content_line_count) = terminal.update(cx, |terminal, _| {
|
||||
(terminal.get_content(), terminal.total_lines())
|
||||
})?;
|
||||
|
||||
@@ -672,7 +673,8 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
fn init_test(executor: &BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
zlog::init_test();
|
||||
zlog::init();
|
||||
zlog::init_output_stdout();
|
||||
|
||||
executor.allow_parking();
|
||||
cx.update(|cx| {
|
||||
|
||||
@@ -166,7 +166,7 @@ impl ToolCard for WebSearchToolCard {
|
||||
.gap_1()
|
||||
.children(response.results.iter().enumerate().map(|(index, result)| {
|
||||
let title = result.title.clone();
|
||||
let url = SharedString::from(result.url.clone());
|
||||
let url = result.url.clone();
|
||||
|
||||
Button::new(("result", index), title)
|
||||
.label_size(LabelSize::Small)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user