Compare commits

..

38 Commits

Author SHA1 Message Date
Cole Miller
9697177213 node_runtime: Bump minimum version for system node to match copilot's requirement (#39632)
Copilot now requires 22.x. See the last min node version bump:
https://github.com/zed-industries/zed/pull/27912

Closes #39461

<img width="1040" height="97" alt="image"
src="https://github.com/user-attachments/assets/8f0490e3-b9b5-45fd-b7f1-321691b862f0"
/>

Release Notes:

- Zed will no longer use `node` from your `$PATH` if it's older than
22.x (previously, the minimum version was 20.x). Instead, it will fall
back to its bundled `node`. This fixes being unable to use Copilot if an
older `node` was installed system-wide.
2025-10-06 17:34:29 -04:00
Zed Bot
c6ee7bc5a4 Bump to 0.206.7 for @maxdeviant 2025-10-06 17:51:28 +00:00
Piotr Osiewicz
b444b7f567 python: Fix user settings not getting passed on for Ty (#39174)
Closes #39144

Release Notes:

- python: Fixed user settings not being respected with Ty language
server.
2025-10-06 19:23:42 +02:00
Marshall Bowers
598ba645d7 x_ai: Add support for Grok 4 Fast (#39492)
This PR adds support for Grok 4 Fast.

Release Notes:

- Added support for Grok 4 Fast models.

Co-authored-by: David Kleingeld <davidsk@zed.dev>
2025-10-06 12:57:42 -04:00
Jowell Young
667ff5b8fa x_ai: Add support for tools and images with custom models (#38792)
After the change, we can add "supports_images", "supports_tools" and
"parallel_tool_calls" properties to set up new models. Our
`settings.json` will be as follows:
```json
  "language_models": {
     "x_ai": {
       "api_url": "https://api.x.ai/v1",
       "available_models": [
         {
           "name": "grok-4-fast-reasoning",
           "display_name": "Grok 4 Fast Reasoning",
           "max_tokens": 2000000,
           "max_output_tokens": 64000,
           "supports_tools": true,
           "parallel_tool_calls": true,
         },
         {
           "name": "grok-4-fast-non-reasoning",
           "display_name": "Grok 4 Fast Non-Reasoning",
           "max_tokens": 2000000,
           "max_output_tokens": 64000,
           "supports_images": true,
         }
       ]
     }
   }

```

Closes https://github.com/zed-industries/zed/issues/38752

Release Notes:

- xAI: Added support for for configuring tool and image support for
custom model configurations
2025-10-06 12:57:37 -04:00
Marshall Bowers
c97a7506ef language_models: Send a header indicating that the client supports xAI models (#38931)
This PR adds an `x-zed-client-supports-x-ai` header to the `GET /models`
request sent to Cloud to indicate that the client supports xAI models.

Release Notes:

- N/A
2025-10-06 12:56:38 -04:00
Marshall Bowers
67566ad381 x_ai: Fix Model::from_id for Grok 4 (#38930)
This PR fixes `x_ai::Model::from_id`, which was not properly handling
`grok-4`.

Release Notes:

- N/A
2025-10-06 12:56:14 -04:00
Marshall Bowers
8afabc18a7 language_models: Add xAI support to Zed Cloud provider (#38928)
This PR adds xAI support to the Zed Cloud provider.

Release Notes:

- N/A
2025-10-06 12:56:08 -04:00
Joseph T. Lyons
f6695400b1 v0.206.x stable 2025-10-01 11:27:38 -04:00
Smit Barmase
da48536e7d copilot: Ensure minimum Node version (#38945)
Closes #38918

Release Notes:

- N/A
2025-10-01 15:59:27 +05:30
Bennet Bo Fenner
7eacefd8ac worktree: Remove unwrap in BackgroundScanner::update_ignore_status (#39191)
We've seen this panic come up in the last two weeks, which might be
caused by #33592. However, we are not sure what paths can cause this
`unwrap()` to fail. Therefore adding some logging around this, so that
the next time someone opens a bug report we can further diagnose the
issue.

Fixes ZED-1F6

Release Notes:

- Fixed an issue where Zed could crash when including specific paths in
a global `.gitignore` files
2025-10-01 11:54:33 +02:00
Ben Kunkle
cd9eb77ace Fix bug in code action formatter handling (#39246)
Closes #39112

Release Notes:

- Fixed an issue when using code actions on format where specifying
multiple code actions in the same code actions block that resolved to
code actions from different language servers could result in conflicting
edits being applied and mangled buffer text.
2025-09-30 22:08:58 -04:00
Joseph T. Lyons
62f2df90ed zed 0.206.6 2025-09-30 15:17:53 -04:00
Anthony Eid
6412621855 debugger: Fix python debug scenario not showing up in code actions (#39224)
The bug happened because the Python locator was checking for a quote
before the ZED task variable. Removing that part of the check fixed the
issue.

Closes #39179 

Release Notes:

- Fix Python debug tasks not showing up in code actions or debug picker
2025-09-30 13:01:10 -06:00
Lukas Wirth
bc42327f8e terminals: Remove (now) incorrect alacritty workaround for task spawning 2025-09-30 20:32:24 +02:00
Sergei Zharinov
fa163e7049 gpui: Respect font smoothing on macOS (#39197)
- Closes #38847
- See also: #37622 and #38467

Release Notes:

- Fonts are now rendered in accordance with the `AppleFontSmoothing`
setting.
2025-09-30 18:31:32 +03:00
Ben Brandt
5041b51d49 acp: Notify of latest agent version only after successful download (#39201)
Before we would notify the user even if the download failed. We also
we're overwriting the directory, which means a user could be stuck in a
loop if a previous download failed

Release Notes:

- acp: Fix user seeing update prompt in a loop because of a previous
failed download
2025-09-30 15:54:20 +02:00
Conrad Irwin
b801ab8c9c Fix panic in UnwrapSyntaxNode (#39139)
Closes #39139
Fixes ZED-1HY

Release Notes:

- Fixed a panic in UnwrapSyntaxNode in multi-buffers
2025-09-29 15:13:46 -06:00
Joseph T. Lyons
e2b5fbc4fd zed 0.206.5 2025-09-29 17:02:11 -04:00
Richard Feldman
8e75ee673f Default to Sonnet 4.5 in BYOK (#39132)
<img width="381" height="204" alt="Screenshot 2025-09-29 at 2 29 58 PM"
src="https://github.com/user-attachments/assets/c7aaf0b0-b09b-4ed9-8113-8d7b18eefc2f"
/>


Release Notes:

- Claude Sonnet 4.5 and 4.5 Thinking are now the recommended Anthropic
models
2025-09-29 16:55:37 -04:00
Richard Feldman
3dcae67cb5 Add Sonnet 4.5 support (#39127)
Release Notes:

- Added support for Claude Sonnet 4.5 for Bring-Your-Own-Key (BYOK)
2025-09-29 16:55:37 -04:00
Zed Bot
e1876a1486 Bump to 0.206.4 for @ConradIrwin 2025-09-29 17:54:16 +00:00
Ben Brandt
c81b4c8f7e acp: Add NO_PROXY if not set otherwise to not proxy localhost urls (#39100)
Since we might run MCP servers locally for an agent, we don't want to
use the proxy for those.
We set this if the user has set a proxy, but not a custom NO_PROXY env
var.

Closes #38839

Release Notes:

- acp: Don't run local mcp servers through proxy, if set
2025-09-29 13:47:11 +02:00
Lukas Wirth
d54783bccd acp_thread: Fix terminal tool incorrectly redirecting stdin to /dev/null 2025-09-29 11:39:44 +02:00
Lukas Wirth
8baaea6ae3 auto_update: Unmount update disk image in the background (#38867)
Release Notes:

- Fixed potentially temporarily hanging on macOS when updating the app
2025-09-27 19:55:45 +02:00
Joseph T. Lyons
3ff1eef029 zed 0.206.3 2025-09-26 14:23:08 -04:00
Conrad Irwin
2828705f39 Revert "Fix arrow function detection in TypeScript/JavaScript outline (#38411)" (#38982)
This reverts commit 1bbf98aea6.

We found that #38411 caused problems where anonymous functions are
included too many times in the outline. We'd like to figure out a better
fix before shipping this to stable.

Fixes #38956

Release Notes:

- (preview only) revert changes to outline view
2025-09-26 13:55:06 -04:00
Smit Barmase
a3f2838380 editor: Fix predict edit at cursor action when show_edit_predictions is false (#38821)
Closes #37601 

Regressed in https://github.com/zed-industries/zed/pull/36469. 

Edit: Original issue https://github.com/zed-industries/zed/issues/25744
is fixed for Zeta in this PR. For Copilot, it will be covered in a
follow-up. In the case of Copilot, even after discarding, we still get a
prediction on suggest, which is a bug.

Release Notes:

- Fixed issue where predict edit at cursor didn't work when
`show_edit_predictions` is `false`.
2025-09-26 06:23:27 -04:00
Derek Nguyen
aeddd518a6 python: Fix ty archive extraction on Linux (#38917)
Closes #38553 
Release Notes:

- Fixed wrong AssetKind specified on linux for ty 


As discussed in the linked issue. All of the non windows assets for ty
are `tar.gz` files. This change applies that fix.
2025-09-25 19:31:55 -04:00
Joseph T. Lyons
5e4d3970d1 zed 0.206.2 2025-09-25 11:49:48 -04:00
Ben Brandt
0f0f9c93ce acp: Use ACP error types in read_text_file (#38863)
- Map path lookup and internal failures to acp::Error 
- Return INVALID_PARAMS for reads beyond EOF

Release Notes:

- acp: Return more informative error types from `read_text_file` to
agents
2025-09-25 14:09:43 +02:00
Ben Brandt
4ee165dd60 acp: Fix read_text_file erroring on empty files (#38856)
The previous validation was too strict and didn't permit reading empty
files.

Addresses: https://github.com/google-gemini/gemini-cli/issues/9280

Release Notes:

- acp: Fix `read_text_file` returning errors for empty files
2025-09-25 12:13:29 +02:00
Zed Bot
94d19baf4e Bump to 0.206.1 for @ConradIrwin 2025-09-25 03:12:47 +00:00
Conrad Irwin
70cb2f5d55 Whitespace map more (#38827)
Release Notes:

- N/A
2025-09-24 21:09:20 -06:00
Conrad Irwin
15baa8f5ed Only allow single chars for whitespace map (#38825)
Release Notes:

- Only allow single characters in the whitespace map
2025-09-24 16:30:50 -06:00
Lukas Wirth
c68345f502 editor: Fix invalid anchors in hover_links::surrounding_filename (#38766)
Fixes ZED-1K3

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-24 13:09:55 +02:00
Lukas Wirth
72007a075a editor: Prevent panics in BlockChunks if the block spans more than 128 lines (#38763)
Not an ideal fix, but a proper one will require restructuring the
iterator state (which would be easier if Rust had first class
generators)
Fixes ZED-1MB

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-24 13:09:55 +02:00
Joseph T. Lyons
c68602612f v0.206.x preview 2025-09-23 16:14:50 -04:00
907 changed files with 28262 additions and 152028 deletions

View File

@@ -10,15 +10,3 @@
# Here, we opted to use `[target.'cfg(all())']` instead of `[build]` because `[target.'**']` is guaranteed to be cumulative.
[target.'cfg(all())']
rustflags = ["-D", "warnings"]
# Use Mold on Linux, because it's faster than GNU ld and LLD.
#
# We no longer set this in the default `config.toml` so that developers can opt in to Wild, which
# is faster than Mold, in their own ~/.cargo/config.toml.
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
[target.aarch64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]

View File

@@ -4,9 +4,16 @@ rustflags = ["-C", "symbol-mangling-version=v0", "--cfg", "tokio_unstable"]
[alias]
xtask = "run --package xtask --"
perf-test = ["test", "--profile", "release-fast", "--lib", "--bins", "--tests", "--all-features", "--config", "target.'cfg(true)'.runner='cargo run -p perf --release'", "--config", "target.'cfg(true)'.rustflags=[\"--cfg\", \"perf_enabled\"]"]
# Keep similar flags here to share some ccache
perf-compare = ["run", "--profile", "release-fast", "-p", "perf", "--config", "target.'cfg(true)'.rustflags=[\"--cfg\", \"perf_enabled\"]", "--", "compare"]
perf-test = ["test", "--profile", "release-fast", "--lib", "--bins", "--tests", "--config", "target.'cfg(true)'.runner='cargo run -p perf --release'", "--config", "target.'cfg(true)'.rustflags=[\"--cfg\", \"perf_enabled\"]"]
perf-compare = ["run", "--release", "-p", "perf", "--", "compare"]
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
[target.aarch64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
[target.'cfg(target_os = "windows")']
rustflags = [

View File

@@ -24,9 +24,9 @@ workspace-members = [
third-party = [
{ name = "reqwest", version = "0.11.27" },
# build of remote_server should not include scap / its x11 dependency
{ name = "zed-scap", git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197176fb19cd0f9b180dd36eaac", version = "0.0.8-zed" },
{ name = "scap", git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7" },
# build of remote_server should not need to include on libalsa through rodio
{ name = "rodio", git = "https://github.com/RustAudio/rodio" },
{ name = "rodio", git = "https://github.com/RustAudio/rodio", branch = "better_wav_output"},
]
[final-excludes]
@@ -37,6 +37,8 @@ workspace-members = [
"zed_glsl",
"zed_html",
"zed_proto",
"zed_ruff",
"slash_commands_example",
"zed_snippets",
"zed_test_extension",
]

View File

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

View File

@@ -20,4 +20,4 @@ runs:
- name: Run tests
shell: bash -euxo pipefail {0}
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
run: cargo nextest run --workspace --no-fail-fast

View File

@@ -24,4 +24,4 @@ runs:
shell: powershell
working-directory: ${{ inputs.working-directory }}
run: |
cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
cargo nextest run --workspace --no-fail-fast

View File

@@ -826,9 +826,8 @@ jobs:
timeout-minutes: 120
name: Create a Windows installer
runs-on: [self-32vcpu-windows-2022]
if: |
( startsWith(github.ref, 'refs/tags/v')
|| contains(github.event.pull_request.labels.*.name, 'run-bundling') )
if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
# if: (startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling'))
needs: [windows_tests]
env:
AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }}
@@ -866,12 +865,13 @@ jobs:
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
with:
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-x86_64.exe
name: ZedEditorUserSetup-x64-${{ github.event.pull_request.head.sha || github.sha }}.exe
path: ${{ env.SETUP_PATH }}
- name: Upload Artifacts to release
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
if: ${{ !(contains(github.event.pull_request.labels.*.name, 'run-bundling')) }}
# Re-enable when we are ready to publish windows preview releases
if: ${{ !(contains(github.event.pull_request.labels.*.name, 'run-bundling')) && env.RELEASE_CHANNEL == 'preview' }} # upload only preview
with:
draft: true
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}

View File

@@ -1,48 +0,0 @@
name: Community Champion Auto Labeler
on:
issues:
types: [opened]
pull_request_target:
types: [opened]
jobs:
label_community_champion:
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
steps:
- name: Check if author is a community champion and apply label
uses: actions/github-script@v7
with:
script: |
const communityChampionBody = `${{ secrets.COMMUNITY_CHAMPIONS }}`;
const communityChampions = communityChampionBody
.split('\n')
.map(handle => handle.trim().toLowerCase());
let author;
if (context.eventName === 'issues') {
author = context.payload.issue.user.login;
} else if (context.eventName === 'pull_request_target') {
author = context.payload.pull_request.user.login;
}
if (!author || !communityChampions.includes(author.toLowerCase())) {
return;
}
const issueNumber = context.payload.issue?.number || context.payload.pull_request?.number;
try {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: ['community champion']
});
console.log(`Applied 'community champion' label to #${issueNumber} by ${author}`);
} catch (error) {
console.error(`Failed to apply label: ${error.message}`);
}

View File

@@ -38,28 +38,8 @@ jobs:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_RELEASE_NOTES }}
content: ${{ steps.get-content.outputs.string }}
publish-winget:
runs-on:
- ubuntu-latest
steps:
- name: Set Package Name
id: set-package-name
run: |
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
PACKAGE_NAME=ZedIndustries.Zed.Preview
else
PACKAGE_NAME=ZedIndustries.Zed
fi
echo "PACKAGE_NAME=$PACKAGE_NAME" >> "$GITHUB_OUTPUT"
- uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f # v2
with:
identifier: ${{ steps.set-package-name.outputs.PACKAGE_NAME }}
max-versions-to-keep: 5
token: ${{ secrets.WINGET_TOKEN }}
send_release_notes_email:
if: false && github.repository_owner == 'zed-industries' && !github.event.release.prerelease
if: github.repository_owner == 'zed-industries' && !github.event.release.prerelease
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4

33
.github/workflows/issue_response.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: Issue Response
on:
schedule:
- cron: "0 12 * * 2"
workflow_dispatch:
jobs:
issue-response:
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
with:
version: 9
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "20"
cache: "pnpm"
cache-dependency-path: "script/issue_response/pnpm-lock.yaml"
- run: pnpm install --dir script/issue_response
- name: Run Issue Response
run: pnpm run --dir script/issue_response start
env:
ISSUE_RESPONSE_GITHUB_TOKEN: ${{ secrets.ISSUE_RESPONSE_GITHUB_TOKEN }}
SLACK_ISSUE_RESPONSE_WEBHOOK_URL: ${{ secrets.SLACK_ISSUE_RESPONSE_WEBHOOK_URL }}

View File

@@ -63,7 +63,6 @@ Although there are few hard and fast rules, typically we don't merge:
- New file icons. Zed's default icon theme consists of icons that are hand-designed to fit together in a cohesive manner, please don't submit PRs with off-the-shelf SVGs.
- Giant refactorings.
- Non-trivial changes with no tests.
- Stylistic code changes that do not alter any app logic. Reducing allocations, removing `.unwrap()`s, fixing typos is great; making code "more readable" — maybe not so much.
- Features where (in our subjective opinion) the extra complexity isn't worth it for the number of people who will benefit.
- Anything that seems completely AI generated.

2824
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -90,8 +90,9 @@ members = [
"crates/image_viewer",
"crates/inspector_ui",
"crates/install_cli",
"crates/jj",
"crates/jj_ui",
"crates/journal",
"crates/json_schema_store",
"crates/keymap_editor",
"crates/language",
"crates/language_extension",
@@ -152,7 +153,6 @@ members = [
"crates/settings",
"crates/settings_macros",
"crates/settings_profile_selector",
"crates/settings_ui",
"crates/snippet",
"crates/snippet_provider",
"crates/snippets_ui",
@@ -164,7 +164,6 @@ members = [
"crates/sum_tree",
"crates/supermaven",
"crates/supermaven_api",
"crates/codestral",
"crates/svg_preview",
"crates/system_specs",
"crates/tab_switcher",
@@ -213,7 +212,9 @@ members = [
"extensions/glsl",
"extensions/html",
"extensions/proto",
"extensions/ruff",
"extensions/slash-commands-example",
"extensions/snippets",
"extensions/test-extension",
#
@@ -274,7 +275,7 @@ cloud_llm_client = { path = "crates/cloud_llm_client" }
cloud_zeta2_prompt = { path = "crates/cloud_zeta2_prompt" }
collab = { path = "crates/collab" }
collab_ui = { path = "crates/collab_ui" }
collections = { path = "crates/collections", version = "0.1.0" }
collections = { path = "crates/collections" }
command_palette = { path = "crates/command_palette" }
command_palette_hooks = { path = "crates/command_palette_hooks" }
component = { path = "crates/component" }
@@ -290,7 +291,6 @@ debug_adapter_extension = { path = "crates/debug_adapter_extension" }
debugger_tools = { path = "crates/debugger_tools" }
debugger_ui = { path = "crates/debugger_ui" }
deepseek = { path = "crates/deepseek" }
derive_refineable = { path = "crates/refineable/derive_refineable" }
diagnostics = { path = "crates/diagnostics" }
editor = { path = "crates/editor" }
extension = { path = "crates/extension" }
@@ -322,8 +322,9 @@ edit_prediction_context = { path = "crates/edit_prediction_context" }
zeta2_tools = { path = "crates/zeta2_tools" }
inspector_ui = { path = "crates/inspector_ui" }
install_cli = { path = "crates/install_cli" }
jj = { path = "crates/jj" }
jj_ui = { path = "crates/jj_ui" }
journal = { path = "crates/journal" }
json_schema_store = { path = "crates/json_schema_store" }
keymap_editor = { path = "crates/keymap_editor" }
language = { path = "crates/language" }
language_extension = { path = "crates/language_extension" }
@@ -378,7 +379,7 @@ remote_server = { path = "crates/remote_server" }
repl = { path = "crates/repl" }
reqwest_client = { path = "crates/reqwest_client" }
rich_text = { path = "crates/rich_text" }
rodio = { git = "https://github.com/RustAudio/rodio" }
rodio = { git = "https://github.com/RustAudio/rodio", branch = "better_wav_output"}
rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" }
rules_library = { path = "crates/rules_library" }
@@ -386,7 +387,6 @@ search = { path = "crates/search" }
semantic_version = { path = "crates/semantic_version" }
session = { path = "crates/session" }
settings = { path = "crates/settings" }
settings_macros = { path = "crates/settings_macros" }
settings_ui = { path = "crates/settings_ui" }
snippet = { path = "crates/snippet" }
snippet_provider = { path = "crates/snippet_provider" }
@@ -399,7 +399,6 @@ streaming_diff = { path = "crates/streaming_diff" }
sum_tree = { path = "crates/sum_tree" }
supermaven = { path = "crates/supermaven" }
supermaven_api = { path = "crates/supermaven_api" }
codestral = { path = "crates/codestral" }
system_specs = { path = "crates/system_specs" }
tab_switcher = { path = "crates/tab_switcher" }
task = { path = "crates/task" }
@@ -475,9 +474,10 @@ backtrace = "0.3"
base64 = "0.22"
bincode = "1.2.1"
bitflags = "2.6.0"
blade-graphics = { version = "0.7.0" }
blade-macros = { version = "0.3.0" }
blade-util = { version = "0.3.0" }
blade-graphics = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" }
blade-util = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" }
blake3 = "1.5.3"
bytes = "1.0"
cargo_metadata = "0.19"
cargo_toml = "0.21"
@@ -514,7 +514,6 @@ futures-lite = "1.13"
git2 = { version = "0.20.1", default-features = false }
globset = "0.4"
handlebars = "4.3"
hashbrown = "0.15.3"
heck = "0.5"
heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
hex = "0.4.3"
@@ -530,6 +529,7 @@ indexmap = { version = "2.7.0", features = ["serde"] }
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"
@@ -550,7 +550,6 @@ nanoid = "0.4"
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
nix = "0.29"
num-format = "0.4.4"
num-traits = "0.2"
objc = "0.2"
objc2-foundation = { version = "0.3", default-features = false, features = [
"NSArray",
@@ -607,8 +606,7 @@ rand = "0.9"
rayon = "1.8"
ref-cast = "1.0.24"
regex = "1.5"
# WARNING: If you change this, you must also publish a new version of zed-reqwest to crates.io
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "c15662463bda39148ba154100dd44d3fba5873a4", default-features = false, features = [
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c770a32f1998d6e999cef3e59e0013e6c4415", default-features = false, features = [
"charset",
"http2",
"macos-system-configuration",
@@ -616,17 +614,17 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "c15662
"rustls-tls-native-roots",
"socks",
"stream",
], package = "zed-reqwest", version = "0.12.15-zed" }
] }
rsa = "0.9.6"
runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [
"async-dispatcher-runtime",
] }
rust-embed = { version = "8.4", features = ["include-exclude"] }
rustc-demangle = "0.1.23"
rustc-hash = "2.1.0"
rustls = { version = "0.23.26" }
rustls-platform-verifier = "0.5.0"
# WARNING: If you change this, you must also publish a new version of zed-scap to crates.io
scap = { git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197176fb19cd0f9b180dd36eaac", default-features = false, package = "zed-scap", version = "0.0.8-zed" }
scap = { git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7", default-features = false }
schemars = { version = "1.0", features = ["indexmap2"] }
semver = "1.0"
serde = { version = "1.0.221", features = ["derive", "rc"] }
@@ -652,9 +650,9 @@ 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", "visit-mut"] }
syn = { version = "2.0.101", features = ["full", "extra-traits"] }
sys-locale = "0.3.1"
sysinfo = "0.37.0"
sysinfo = "0.31.0"
take-until = "0.2.0"
tempfile = "3.20.0"
thiserror = "2.0.12"
@@ -670,9 +668,8 @@ tiny_http = "0.8"
tokio = { version = "1" }
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"] }
toml = "0.8"
toml_edit = { version = "0.22", default-features = false, features = ["display", "parse", "serde"] }
tower-http = "0.4.4"
tree-sitter = { version = "0.25.10", features = ["wasm"] }
tree-sitter = { version = "0.25.6", features = ["wasm"] }
tree-sitter-bash = "0.25.0"
tree-sitter-c = "0.23"
tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "5cb9b693cfd7bfacab1d9ff4acac1a4150700609" }
@@ -689,11 +686,11 @@ tree-sitter-html = "0.23"
tree-sitter-jsdoc = "0.23"
tree-sitter-json = "0.24"
tree-sitter-md = { git = "https://github.com/tree-sitter-grammars/tree-sitter-markdown", rev = "9a23c1a96c0513d8fc6520972beedd419a973539" }
tree-sitter-python = "0.25"
tree-sitter-python = { git = "https://github.com/zed-industries/tree-sitter-python", rev = "218fcbf3fda3d029225f3dec005cb497d111b35e" }
tree-sitter-regex = "0.24"
tree-sitter-ruby = "0.23"
tree-sitter-rust = "0.24"
tree-sitter-typescript = { git = "https://github.com/zed-industries/tree-sitter-typescript", rev = "e2c53597d6a5d9cf7bbe8dccde576fe1e46c5899" } # https://github.com/tree-sitter/tree-sitter-typescript/pull/347
tree-sitter-typescript = "0.23"
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "baff0b51c64ef6a1fb1f8390f3ad6015b83ec13a" }
unicase = "2.6"
unicode-script = "0.5.7"
@@ -816,7 +813,6 @@ image_viewer = { codegen-units = 1 }
edit_prediction_button = { codegen-units = 1 }
install_cli = { codegen-units = 1 }
journal = { codegen-units = 1 }
json_schema_store = { codegen-units = 1 }
lmstudio = { codegen-units = 1 }
menu = { codegen-units = 1 }
notifications = { codegen-units = 1 }
@@ -868,7 +864,6 @@ todo = "deny"
declare_interior_mutable_const = "deny"
redundant_clone = "deny"
disallowed_methods = "deny"
# We currently do not restrict any style rules
# as it slows down shipping code to Zed.

2
Cross.toml Normal file
View File

@@ -0,0 +1,2 @@
[build]
dockerfile = "Dockerfile-cross"

17
Dockerfile-cross Normal file
View File

@@ -0,0 +1,17 @@
# syntax=docker/dockerfile:1
ARG CROSS_BASE_IMAGE
FROM ${CROSS_BASE_IMAGE}
WORKDIR /app
ARG TZ=Etc/UTC \
LANG=C.UTF-8 \
LC_ALL=C.UTF-8 \
DEBIAN_FRONTEND=noninteractive
ENV CARGO_TERM_COLOR=always
COPY script/install-mold script/
RUN ./script/install-mold "2.34.0"
COPY script/remote-server script/
RUN ./script/remote-server
COPY . .

View File

@@ -1 +0,0 @@
.rules

View File

@@ -1,3 +1,9 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.2806 4.66818L8.26042 1.76982C8.09921 1.67673 7.9003 1.67673 7.73909 1.76982L2.71918 4.66818C2.58367 4.74642 2.5 4.89112 2.5 5.04785V10.8924C2.5 11.0489 2.58367 11.1938 2.71918 11.2721L7.73934 14.1704C7.90054 14.2635 8.09946 14.2635 8.26066 14.1704L13.2808 11.2721C13.4163 11.1938 13.5 11.0491 13.5 10.8924V5.04785C13.5 4.89136 13.4163 4.74642 13.2808 4.66818H13.2806ZM12.9653 5.28212L8.11901 13.676C8.08626 13.7326 7.99977 13.7095 7.99977 13.6439V8.14771C7.99977 8.03788 7.94107 7.9363 7.84586 7.88115L3.08613 5.13317C3.02957 5.10041 3.05266 5.0139 3.11818 5.0139H12.8106C12.9483 5.0139 13.0343 5.1631 12.9655 5.28236H12.9653V5.28212Z" fill="#C4CAD4"/>
<path opacity="0.6" d="M3.5 11V5.5L8.5 8L3.5 11Z" fill="black"/>
<path opacity="0.4" d="M8.5 14L3.5 11L8.5 8V14Z" fill="black"/>
<path opacity="0.6" d="M8.5 5.5H3.5L8.5 2.5L8.5 5.5Z" fill="black"/>
<path opacity="0.8" d="M8.5 5.5V2.5L13.5 5.5H8.5Z" fill="black"/>
<path opacity="0.2" d="M13.5 11L8.5 14L11 9.5L13.5 11Z" fill="black"/>
<path opacity="0.5" d="M13.5 11L11 9.5L13.5 5V11Z" fill="black"/>
<path d="M3.5 11V5L8.5 2.11325L13.5 5V11L8.5 13.8868L3.5 11Z" stroke="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 769 B

After

Width:  |  Height:  |  Size: 583 B

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.1645 4.45825L5.20344 9.52074C4.98225 9.74193 4.85798 10.0419 4.85798 10.3548C4.85798 10.6676 4.98225 10.9676 5.20344 11.1888C5.42464 11.41 5.72464 11.5342 6.03746 11.5342C6.35028 11.5342 6.65028 11.41 6.87148 11.1888L11.8326 6.12629C12.2749 5.68397 12.5234 5.08407 12.5234 4.45854C12.5234 3.83302 12.2749 3.23311 11.8326 2.7908C11.3902 2.34849 10.7903 2.1 10.1648 2.1C9.53928 2.1 8.93938 2.34849 8.49707 2.7908L3.55663 7.83265C3.22373 8.16017 2.95897 8.55037 2.77762 8.98072C2.59628 9.41108 2.50193 9.87308 2.50003 10.3401C2.49813 10.8071 2.58871 11.2698 2.76654 11.7017C2.94438 12.1335 3.20595 12.5258 3.53618 12.856C3.8664 13.1863 4.25873 13.4478 4.69055 13.6257C5.12237 13.8035 5.58513 13.8941 6.05213 13.8922C6.51913 13.8903 6.98114 13.7959 7.41149 13.6146C7.84185 13.4332 8.23204 13.1685 8.55957 12.8356L13.5 7.79373" stroke="#C4CAD4" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,4 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.125 9.25001L3 6.125L6.125 3" stroke="#C4CAD4" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 6.125H9.56251C10.0139 6.125 10.4609 6.21391 10.878 6.38666C11.295 6.55942 11.674 6.81262 11.9932 7.13182C12.3124 7.45102 12.5656 7.82997 12.7383 8.24703C12.9111 8.66408 13 9.11108 13 9.5625C13 10.0139 12.9111 10.4609 12.7383 10.878C12.5656 11.295 12.3124 11.674 11.9932 11.9932C11.674 12.3124 11.295 12.5656 10.878 12.7383C10.4609 12.9111 10.0139 13 9.56251 13H7.375" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2.6 5v3.6h3.6"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M13.4 11A5.4 5.4 0 0 0 8 5.6a5.4 5.4 0 0 0-3.6 1.38L2.6 8.6"/></svg>

Before

Width:  |  Height:  |  Size: 692 B

After

Width:  |  Height:  |  Size: 339 B

View File

@@ -31,7 +31,6 @@
"ctrl--": ["zed::DecreaseBufferFontSize", { "persist": false }],
"ctrl-0": ["zed::ResetBufferFontSize", { "persist": false }],
"ctrl-,": "zed::OpenSettings",
"ctrl-alt-,": "zed::OpenSettingsFile",
"ctrl-q": "zed::Quit",
"f4": "debugger::Start",
"shift-f5": "debugger::Stop",
@@ -251,7 +250,7 @@
"alt-enter": "agent::ContinueWithBurnMode",
"ctrl-y": "agent::AllowOnce",
"ctrl-alt-y": "agent::AllowAlways",
"ctrl-alt-z": "agent::RejectOnce"
"ctrl-d": "agent::RejectOnce"
}
},
{
@@ -370,8 +369,7 @@
"bindings": {
"new": "rules_library::NewRule",
"ctrl-n": "rules_library::NewRule",
"ctrl-shift-s": "rules_library::ToggleDefaultRule",
"ctrl-w": "workspace::CloseWindow"
"ctrl-shift-s": "rules_library::ToggleDefaultRule"
}
},
{
@@ -527,15 +525,15 @@
"ctrl-k ctrl-l": "editor::ToggleFold",
"ctrl-k ctrl-[": "editor::FoldRecursive",
"ctrl-k ctrl-]": "editor::UnfoldRecursive",
"ctrl-k ctrl-1": "editor::FoldAtLevel_1",
"ctrl-k ctrl-2": "editor::FoldAtLevel_2",
"ctrl-k ctrl-3": "editor::FoldAtLevel_3",
"ctrl-k ctrl-4": "editor::FoldAtLevel_4",
"ctrl-k ctrl-5": "editor::FoldAtLevel_5",
"ctrl-k ctrl-6": "editor::FoldAtLevel_6",
"ctrl-k ctrl-7": "editor::FoldAtLevel_7",
"ctrl-k ctrl-8": "editor::FoldAtLevel_8",
"ctrl-k ctrl-9": "editor::FoldAtLevel_9",
"ctrl-k ctrl-1": ["editor::FoldAtLevel", 1],
"ctrl-k ctrl-2": ["editor::FoldAtLevel", 2],
"ctrl-k ctrl-3": ["editor::FoldAtLevel", 3],
"ctrl-k ctrl-4": ["editor::FoldAtLevel", 4],
"ctrl-k ctrl-5": ["editor::FoldAtLevel", 5],
"ctrl-k ctrl-6": ["editor::FoldAtLevel", 6],
"ctrl-k ctrl-7": ["editor::FoldAtLevel", 7],
"ctrl-k ctrl-8": ["editor::FoldAtLevel", 8],
"ctrl-k ctrl-9": ["editor::FoldAtLevel", 9],
"ctrl-k ctrl-0": "editor::FoldAll",
"ctrl-k ctrl-j": "editor::UnfoldAll",
"ctrl-space": "editor::ShowCompletions",
@@ -621,7 +619,7 @@
"ctrl-shift-f": "pane::DeploySearch",
"ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
"ctrl-shift-t": "pane::ReopenClosedItem",
"ctrl-k ctrl-s": "zed::OpenKeymap",
"ctrl-k ctrl-s": "zed::OpenKeymapEditor",
"ctrl-k ctrl-t": "theme_selector::Toggle",
"ctrl-alt-super-p": "settings_profile_selector::Toggle",
"ctrl-t": "project_symbols::Toggle",
@@ -1229,6 +1227,9 @@
"context": "Onboarding",
"use_key_equivalents": true,
"bindings": {
"ctrl-1": "onboarding::ActivateBasicsPage",
"ctrl-2": "onboarding::ActivateEditingPage",
"ctrl-3": "onboarding::ActivateAISetupPage",
"ctrl-enter": "onboarding::Finish",
"alt-shift-l": "onboarding::SignIn",
"alt-shift-a": "onboarding::OpenAccount"
@@ -1240,44 +1241,5 @@
"bindings": {
"ctrl-shift-enter": "workspace::OpenWithSystem"
}
},
{
"context": "SettingsWindow",
"use_key_equivalents": true,
"bindings": {
"ctrl-w": "workspace::CloseWindow",
"escape": "workspace::CloseWindow",
"ctrl-m": "settings_editor::Minimize",
"ctrl-f": "search::FocusSearch",
"left": "settings_editor::ToggleFocusNav",
"ctrl-shift-e": "settings_editor::ToggleFocusNav",
// todo(settings_ui): cut this down based on the max files and overflow UI
"ctrl-1": ["settings_editor::FocusFile", 0],
"ctrl-2": ["settings_editor::FocusFile", 1],
"ctrl-3": ["settings_editor::FocusFile", 2],
"ctrl-4": ["settings_editor::FocusFile", 3],
"ctrl-5": ["settings_editor::FocusFile", 4],
"ctrl-6": ["settings_editor::FocusFile", 5],
"ctrl-7": ["settings_editor::FocusFile", 6],
"ctrl-8": ["settings_editor::FocusFile", 7],
"ctrl-9": ["settings_editor::FocusFile", 8],
"ctrl-0": ["settings_editor::FocusFile", 9],
"ctrl-pageup": "settings_editor::FocusPreviousFile",
"ctrl-pagedown": "settings_editor::FocusNextFile"
}
},
{
"context": "SettingsWindow > NavigationMenu",
"use_key_equivalents": true,
"bindings": {
"up": "settings_editor::FocusPreviousNavEntry",
"down": "settings_editor::FocusNextNavEntry",
"right": "settings_editor::ExpandNavEntry",
"left": "settings_editor::CollapseNavEntry",
"pageup": "settings_editor::FocusPreviousRootNavEntry",
"pagedown": "settings_editor::FocusNextRootNavEntry",
"home": "settings_editor::FocusFirstNavEntry",
"end": "settings_editor::FocusLastNavEntry"
}
}
]

View File

@@ -40,7 +40,6 @@
"cmd--": ["zed::DecreaseBufferFontSize", { "persist": false }],
"cmd-0": ["zed::ResetBufferFontSize", { "persist": false }],
"cmd-,": "zed::OpenSettings",
"cmd-alt-,": "zed::OpenSettingsFile",
"cmd-q": "zed::Quit",
"cmd-h": "zed::Hide",
"alt-cmd-h": "zed::HideOthers",
@@ -290,7 +289,7 @@
"alt-enter": "agent::ContinueWithBurnMode",
"cmd-y": "agent::AllowOnce",
"cmd-alt-y": "agent::AllowAlways",
"cmd-alt-z": "agent::RejectOnce"
"cmd-d": "agent::RejectOnce"
}
},
{
@@ -582,15 +581,15 @@
"cmd-k cmd-l": "editor::ToggleFold",
"cmd-k cmd-[": "editor::FoldRecursive",
"cmd-k cmd-]": "editor::UnfoldRecursive",
"cmd-k cmd-1": "editor::FoldAtLevel_1",
"cmd-k cmd-2": "editor::FoldAtLevel_2",
"cmd-k cmd-3": "editor::FoldAtLevel_3",
"cmd-k cmd-4": "editor::FoldAtLevel_4",
"cmd-k cmd-5": "editor::FoldAtLevel_5",
"cmd-k cmd-6": "editor::FoldAtLevel_6",
"cmd-k cmd-7": "editor::FoldAtLevel_7",
"cmd-k cmd-8": "editor::FoldAtLevel_8",
"cmd-k cmd-9": "editor::FoldAtLevel_9",
"cmd-k cmd-1": ["editor::FoldAtLevel", 1],
"cmd-k cmd-2": ["editor::FoldAtLevel", 2],
"cmd-k cmd-3": ["editor::FoldAtLevel", 3],
"cmd-k cmd-4": ["editor::FoldAtLevel", 4],
"cmd-k cmd-5": ["editor::FoldAtLevel", 5],
"cmd-k cmd-6": ["editor::FoldAtLevel", 6],
"cmd-k cmd-7": ["editor::FoldAtLevel", 7],
"cmd-k cmd-8": ["editor::FoldAtLevel", 8],
"cmd-k cmd-9": ["editor::FoldAtLevel", 9],
"cmd-k cmd-0": "editor::FoldAll",
"cmd-k cmd-j": "editor::UnfoldAll",
// Using `ctrl-space` / `ctrl-shift-space` in Zed requires disabling the macOS global shortcut.
@@ -690,7 +689,7 @@
"cmd-shift-f": "pane::DeploySearch",
"cmd-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
"cmd-shift-t": "pane::ReopenClosedItem",
"cmd-k cmd-s": "zed::OpenKeymap",
"cmd-k cmd-s": "zed::OpenKeymapEditor",
"cmd-k cmd-t": "theme_selector::Toggle",
"ctrl-alt-cmd-p": "settings_profile_selector::Toggle",
"cmd-t": "project_symbols::Toggle",
@@ -1334,7 +1333,10 @@
"context": "Onboarding",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "onboarding::Finish",
"cmd-1": "onboarding::ActivateBasicsPage",
"cmd-2": "onboarding::ActivateEditingPage",
"cmd-3": "onboarding::ActivateAISetupPage",
"cmd-escape": "onboarding::Finish",
"alt-tab": "onboarding::SignIn",
"alt-shift-a": "onboarding::OpenAccount"
}
@@ -1345,44 +1347,5 @@
"bindings": {
"ctrl-shift-enter": "workspace::OpenWithSystem"
}
},
{
"context": "SettingsWindow",
"use_key_equivalents": true,
"bindings": {
"cmd-w": "workspace::CloseWindow",
"escape": "workspace::CloseWindow",
"cmd-m": "settings_editor::Minimize",
"cmd-f": "search::FocusSearch",
"left": "settings_editor::ToggleFocusNav",
"cmd-shift-e": "settings_editor::ToggleFocusNav",
// todo(settings_ui): cut this down based on the max files and overflow UI
"ctrl-1": ["settings_editor::FocusFile", 0],
"ctrl-2": ["settings_editor::FocusFile", 1],
"ctrl-3": ["settings_editor::FocusFile", 2],
"ctrl-4": ["settings_editor::FocusFile", 3],
"ctrl-5": ["settings_editor::FocusFile", 4],
"ctrl-6": ["settings_editor::FocusFile", 5],
"ctrl-7": ["settings_editor::FocusFile", 6],
"ctrl-8": ["settings_editor::FocusFile", 7],
"ctrl-9": ["settings_editor::FocusFile", 8],
"ctrl-0": ["settings_editor::FocusFile", 9],
"cmd-{": "settings_editor::FocusPreviousFile",
"cmd-}": "settings_editor::FocusNextFile"
}
},
{
"context": "SettingsWindow > NavigationMenu",
"use_key_equivalents": true,
"bindings": {
"up": "settings_editor::FocusPreviousNavEntry",
"down": "settings_editor::FocusNextNavEntry",
"right": "settings_editor::ExpandNavEntry",
"left": "settings_editor::CollapseNavEntry",
"pageup": "settings_editor::FocusPreviousRootNavEntry",
"pagedown": "settings_editor::FocusNextRootNavEntry",
"home": "settings_editor::FocusFirstNavEntry",
"end": "settings_editor::FocusLastNavEntry"
}
}
]

View File

@@ -30,7 +30,6 @@
"ctrl--": ["zed::DecreaseBufferFontSize", { "persist": false }],
"ctrl-0": ["zed::ResetBufferFontSize", { "persist": false }],
"ctrl-,": "zed::OpenSettings",
"ctrl-alt-,": "zed::OpenSettingsFile",
"ctrl-q": "zed::Quit",
"f4": "debugger::Start",
"shift-f5": "debugger::Stop",
@@ -134,7 +133,7 @@
"ctrl-k z": "editor::ToggleSoftWrap",
"ctrl-f": "buffer_search::Deploy",
"ctrl-h": "buffer_search::DeployReplace",
"ctrl-shift-.": "agent::QuoteSelection",
"ctrl-shift-.": "assistant::QuoteSelection",
"ctrl-shift-,": "assistant::InsertIntoEditor",
"shift-alt-e": "editor::SelectEnclosingSymbol",
"ctrl-shift-backspace": "editor::GoToPreviousChange",
@@ -244,7 +243,7 @@
"ctrl-shift-i": "agent::ToggleOptionsMenu",
// "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
"ctrl-shift-.": "agent::QuoteSelection",
"ctrl-shift-.": "assistant::QuoteSelection",
"shift-alt-e": "agent::RemoveAllContext",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-enter": "agent::ContinueThread",
@@ -252,7 +251,7 @@
"alt-enter": "agent::ContinueWithBurnMode",
"ctrl-y": "agent::AllowOnce",
"ctrl-alt-y": "agent::AllowAlways",
"ctrl-alt-z": "agent::RejectOnce"
"ctrl-d": "agent::RejectOnce"
}
},
{
@@ -346,7 +345,7 @@
}
},
{
"context": "AcpThread > Editor && !use_modifier_to_send",
"context": "AcpThread > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "agent::Chat",
@@ -356,17 +355,6 @@
"shift-tab": "agent::CycleModeSelector"
}
},
{
"context": "AcpThread > Editor && use_modifier_to_send",
"use_key_equivalents": true,
"bindings": {
"ctrl-enter": "agent::Chat",
"ctrl-shift-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
"shift-tab": "agent::CycleModeSelector"
}
},
{
"context": "ThreadHistory",
"use_key_equivalents": true,
@@ -379,8 +367,7 @@
"use_key_equivalents": true,
"bindings": {
"ctrl-n": "rules_library::NewRule",
"ctrl-shift-s": "rules_library::ToggleDefaultRule",
"ctrl-w": "workspace::CloseWindow"
"ctrl-shift-s": "rules_library::ToggleDefaultRule"
}
},
{
@@ -536,15 +523,15 @@
"ctrl-k ctrl-l": "editor::ToggleFold",
"ctrl-k ctrl-[": "editor::FoldRecursive",
"ctrl-k ctrl-]": "editor::UnfoldRecursive",
"ctrl-k ctrl-1": "editor::FoldAtLevel_1",
"ctrl-k ctrl-2": "editor::FoldAtLevel_2",
"ctrl-k ctrl-3": "editor::FoldAtLevel_3",
"ctrl-k ctrl-4": "editor::FoldAtLevel_4",
"ctrl-k ctrl-5": "editor::FoldAtLevel_5",
"ctrl-k ctrl-6": "editor::FoldAtLevel_6",
"ctrl-k ctrl-7": "editor::FoldAtLevel_7",
"ctrl-k ctrl-8": "editor::FoldAtLevel_8",
"ctrl-k ctrl-9": "editor::FoldAtLevel_9",
"ctrl-k ctrl-1": ["editor::FoldAtLevel", 1],
"ctrl-k ctrl-2": ["editor::FoldAtLevel", 2],
"ctrl-k ctrl-3": ["editor::FoldAtLevel", 3],
"ctrl-k ctrl-4": ["editor::FoldAtLevel", 4],
"ctrl-k ctrl-5": ["editor::FoldAtLevel", 5],
"ctrl-k ctrl-6": ["editor::FoldAtLevel", 6],
"ctrl-k ctrl-7": ["editor::FoldAtLevel", 7],
"ctrl-k ctrl-8": ["editor::FoldAtLevel", 8],
"ctrl-k ctrl-9": ["editor::FoldAtLevel", 9],
"ctrl-k ctrl-0": "editor::FoldAll",
"ctrl-k ctrl-j": "editor::UnfoldAll",
"ctrl-space": "editor::ShowCompletions",
@@ -620,10 +607,12 @@
"shift-alt--": ["workspace::DecreaseActiveDockSize", { "px": 0 }],
"shift-alt-=": ["workspace::IncreaseActiveDockSize", { "px": 0 }],
"shift-alt-0": "workspace::ResetOpenDocksSize",
"ctrl-shift-alt--": ["workspace::DecreaseOpenDocksSize", { "px": 0 }],
"ctrl-shift-alt-=": ["workspace::IncreaseOpenDocksSize", { "px": 0 }],
"ctrl-shift-f": "pane::DeploySearch",
"ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
"ctrl-shift-t": "pane::ReopenClosedItem",
"ctrl-k ctrl-s": "zed::OpenKeymap",
"ctrl-k ctrl-s": "zed::OpenKeymapEditor",
"ctrl-k ctrl-t": "theme_selector::Toggle",
"ctrl-alt-super-p": "settings_profile_selector::Toggle",
"ctrl-t": "project_symbols::Toggle",
@@ -1123,7 +1112,6 @@
"alt-f": ["terminal::SendText", "\u001bf"],
"alt-.": ["terminal::SendText", "\u001b."],
"ctrl-delete": ["terminal::SendText", "\u001bd"],
"ctrl-n": "workspace::NewTerminal",
// Overrides for conflicting keybindings
"ctrl-b": ["terminal::SendKeystroke", "ctrl-b"],
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
@@ -1257,48 +1245,12 @@
"context": "Onboarding",
"use_key_equivalents": true,
"bindings": {
"ctrl-1": "onboarding::ActivateBasicsPage",
"ctrl-2": "onboarding::ActivateEditingPage",
"ctrl-3": "onboarding::ActivateAISetupPage",
"ctrl-enter": "onboarding::Finish",
"alt-shift-l": "onboarding::SignIn",
"shift-alt-a": "onboarding::OpenAccount"
}
},
{
"context": "SettingsWindow",
"use_key_equivalents": true,
"bindings": {
"ctrl-w": "workspace::CloseWindow",
"escape": "workspace::CloseWindow",
"ctrl-m": "settings_editor::Minimize",
"ctrl-f": "search::FocusSearch",
"left": "settings_editor::ToggleFocusNav",
"ctrl-shift-e": "settings_editor::ToggleFocusNav",
// todo(settings_ui): cut this down based on the max files and overflow UI
"ctrl-1": ["settings_editor::FocusFile", 0],
"ctrl-2": ["settings_editor::FocusFile", 1],
"ctrl-3": ["settings_editor::FocusFile", 2],
"ctrl-4": ["settings_editor::FocusFile", 3],
"ctrl-5": ["settings_editor::FocusFile", 4],
"ctrl-6": ["settings_editor::FocusFile", 5],
"ctrl-7": ["settings_editor::FocusFile", 6],
"ctrl-8": ["settings_editor::FocusFile", 7],
"ctrl-9": ["settings_editor::FocusFile", 8],
"ctrl-0": ["settings_editor::FocusFile", 9],
"ctrl-pageup": "settings_editor::FocusPreviousFile",
"ctrl-pagedown": "settings_editor::FocusNextFile"
}
},
{
"context": "SettingsWindow > NavigationMenu",
"use_key_equivalents": true,
"bindings": {
"up": "settings_editor::FocusPreviousNavEntry",
"down": "settings_editor::FocusNextNavEntry",
"right": "settings_editor::ExpandNavEntry",
"left": "settings_editor::CollapseNavEntry",
"pageup": "settings_editor::FocusPreviousRootNavEntry",
"pagedown": "settings_editor::FocusNextRootNavEntry",
"home": "settings_editor::FocusFirstNavEntry",
"end": "settings_editor::FocusLastNavEntry"
}
}
]

View File

@@ -1,7 +1,7 @@
[
{
"bindings": {
"ctrl-alt-s": "zed::OpenSettingsFile",
"ctrl-alt-s": "zed::OpenSettings",
"ctrl-{": "pane::ActivatePreviousItem",
"ctrl-}": "pane::ActivateNextItem",
"shift-escape": null, // Unmap workspace::zoom

View File

@@ -4,7 +4,6 @@
// from the command palette.
[
{
"context": "!GitPanel",
"bindings": {
"ctrl-g": "menu::Cancel"
}

View File

@@ -240,7 +240,6 @@
"delete": "vim::DeleteRight",
"g shift-j": "vim::JoinLinesNoWhitespace",
"y": "vim::PushYank",
"shift-y": "vim::YankLine",
"x": "vim::DeleteRight",
"shift-x": "vim::DeleteLeft",
"ctrl-a": "vim::Increment",
@@ -427,7 +426,6 @@
";": "vim::HelixCollapseSelection",
":": "command_palette::Toggle",
"m": "vim::PushHelixMatch",
"s": "vim::HelixSelectRegex",
"]": ["vim::PushHelixNext", { "around": true }],
"[": ["vim::PushHelixPrevious", { "around": true }],
"left": "vim::WrappingLeft",
@@ -580,18 +578,18 @@
// "q": "vim::AnyQuotes",
"q": "vim::MiniQuotes",
"|": "vim::VerticalBars",
"(": ["vim::Parentheses", { "opening": true }],
"(": "vim::Parentheses",
")": "vim::Parentheses",
"b": "vim::Parentheses",
// "b": "vim::AnyBrackets",
// "b": "vim::MiniBrackets",
"[": ["vim::SquareBrackets", { "opening": true }],
"[": "vim::SquareBrackets",
"]": "vim::SquareBrackets",
"r": "vim::SquareBrackets",
"{": ["vim::CurlyBrackets", { "opening": true }],
"{": "vim::CurlyBrackets",
"}": "vim::CurlyBrackets",
"shift-b": "vim::CurlyBrackets",
"<": ["vim::AngleBrackets", { "opening": true }],
"<": "vim::AngleBrackets",
">": "vim::AngleBrackets",
"a": "vim::Argument",
"i": "vim::IndentObj",
@@ -884,12 +882,10 @@
"/": "project_panel::NewSearchInDirectory",
"d": "project_panel::NewDirectory",
"enter": "project_panel::OpenPermanent",
"escape": "vim::ToggleProjectPanelFocus",
"escape": "project_panel::ToggleFocus",
"h": "project_panel::CollapseSelectedEntry",
"j": "vim::MenuSelectNext",
"k": "vim::MenuSelectPrevious",
"down": "vim::MenuSelectNext",
"up": "vim::MenuSelectPrevious",
"j": "menu::SelectNext",
"k": "menu::SelectPrevious",
"l": "project_panel::ExpandSelectedEntry",
"shift-d": "project_panel::Delete",
"shift-r": "project_panel::Rename",
@@ -908,22 +904,7 @@
"{": "project_panel::SelectPrevDirectory",
"shift-g": "menu::SelectLast",
"g g": "menu::SelectFirst",
"-": "project_panel::SelectParent",
"ctrl-u": "project_panel::ScrollUp",
"ctrl-d": "project_panel::ScrollDown",
"z t": "project_panel::ScrollCursorTop",
"z z": "project_panel::ScrollCursorCenter",
"z b": "project_panel::ScrollCursorBottom",
"0": ["vim::Number", 0],
"1": ["vim::Number", 1],
"2": ["vim::Number", 2],
"3": ["vim::Number", 3],
"4": ["vim::Number", 4],
"5": ["vim::Number", 5],
"6": ["vim::Number", 6],
"7": ["vim::Number", 7],
"8": ["vim::Number", 8],
"9": ["vim::Number", 9]
"-": "project_panel::SelectParent"
}
},
{

View File

@@ -29,9 +29,7 @@ Generate {{content_type}} based on the following prompt:
Match the indentation in the original file in the inserted {{content_type}}, don't include any indentation on blank lines.
Return ONLY the {{content_type}} to insert. Do NOT include any XML tags like <document>, <insert_here>, or any surrounding markup from the input.
Respond with a code block containing the {{content_type}} to insert. Replace \{{INSERTED_CODE}} with your actual {{content_type}}:
Immediately start with the following format with no remarks:
```
\{{INSERTED_CODE}}
@@ -68,9 +66,7 @@ Only make changes that are necessary to fulfill the prompt, leave everything els
Start at the indentation level in the original file in the rewritten {{content_type}}. Don't stop until you've rewritten the entire section, even if you have no more changes to make, always write out the whole section with no unnecessary elisions.
Return ONLY the rewritten {{content_type}}. Do NOT include any XML tags like <document>, <rewrite_this>, or any surrounding markup from the input.
Respond with a code block containing the rewritten {{content_type}}. Replace \{{REWRITTEN_CODE}} with your actual rewritten {{content_type}}:
Immediately start with the following format with no remarks:
```
\{{REWRITTEN_CODE}}

View File

@@ -1,8 +1,5 @@
{
"$schema": "zed://schemas/settings",
/// The displayed name of this project. If not set or empty, the root directory name
/// will be displayed.
"project_name": "",
"project_name": null,
// The name of the Zed theme to use for the UI.
//
// `mode` is one of:
@@ -75,10 +72,8 @@
"ui_font_weight": 400,
// The default font size for text in the UI
"ui_font_size": 16,
// The default font size for agent responses in the agent panel. Falls back to the UI font size if unset.
"agent_ui_font_size": null,
// The default font size for user messages in the agent panel.
"agent_buffer_font_size": 12,
// The default font size for text in the agent panel. Falls back to the UI font size if unset.
"agent_font_size": null,
// How much to fade out unused code.
"unnecessary_code_fade": 0.3,
// Active pane styling settings.
@@ -120,7 +115,6 @@
// Whether to enable vim modes and key bindings.
"vim_mode": false,
// Whether to enable helix mode and key bindings.
// Enabling this mode will automatically enable vim mode.
"helix_mode": false,
// Whether to show the informational hover box when moving the mouse
// over symbols in the editor.
@@ -397,6 +391,8 @@
"use_system_window_tabs": false,
// Titlebar related settings
"title_bar": {
// When to show the title bar: "always" | "never" | "hide_in_full_screen".
"show": "always",
// Whether to show the branch icon beside branch switcher in the titlebar.
"show_branch_icon": false,
// Whether to show the branch name button in the titlebar.
@@ -417,33 +413,15 @@
"experimental.rodio_audio": false,
// Requires 'rodio_audio: true'
//
// Automatically increase or decrease you microphone's volume. This affects how
// loud you sound to others.
//
// Recommended: off (default)
// Microphones are too quite in zed, until everyone is on experimental
// audio and has auto speaker volume on this will make you very loud
// compared to other speakers.
"experimental.auto_microphone_volume": false,
// Use the new audio systems automatic gain control for your microphone.
// This affects how loud you sound to others.
"experimental.control_input_volume": false,
// Requires 'rodio_audio: true'
//
// Automatically increate or decrease the volume of other call members.
// This only affects how things sound for you.
"experimental.auto_speaker_volume": true,
// Requires 'rodio_audio: true'
//
// Remove background noises. Works great for typing, cars, dogs, AC. Does
// not work well on music.
"experimental.denoise": true,
// Requires 'rodio_audio: true'
//
// Use audio parameters compatible with the previous versions of
// experimental audio and non-experimental audio. When this is false you
// will sound strange to anyone not on the latest experimental audio. In
// the future we will migrate by setting this to false
//
// You need to rejoin a call for this setting to apply
"experimental.legacy_audio_compatible": true
// Use the new audio systems automatic gain control on everyone in the
// call. This makes call members who are too quite louder and those who are
// too loud quieter. This only affects how things sound for you.
"experimental.control_output_volume": false
},
// Scrollbar related settings
"scrollbar": {
@@ -722,9 +700,7 @@
// Whether to enable drag-and-drop operations in the project panel.
"drag_and_drop": true,
// Whether to hide the root entry when only one folder is open in the window.
"hide_root": false,
// Whether to hide the hidden entries in the project panel.
"hide_hidden": false
"hide_root": false
},
"outline_panel": {
// Whether to show the outline panel button in the status bar
@@ -906,7 +882,6 @@
"now": true,
"find_path": true,
"read_file": true,
"open": true,
"grep": true,
"terminal": true,
"thinking": true,
@@ -918,6 +893,7 @@
// We don't know which of the context server tools are safe for the "Ask" profile, so we don't enable them by default.
// "enable_all_context_servers": true,
"tools": {
"contents": true,
"diagnostics": true,
"fetch": true,
"list_directory": true,
@@ -1104,31 +1080,25 @@
// Removes any lines containing only whitespace at the end of the file and
// ensures just one newline at the end.
"ensure_final_newline_on_save": true,
// Whether or not to perform a buffer format before saving: [on, off]
// Whether or not to perform a buffer format before saving: [on, off, prettier, language_server]
// Keep in mind, if the autosave with delay is enabled, format_on_save will be ignored
"format_on_save": "on",
// How to perform a buffer format. This setting can take multiple values:
// How to perform a buffer format. This setting can take 4 values:
//
// 1. Default. Format files using Zed's Prettier integration (if applicable),
// or falling back to formatting via language server:
// "formatter": "auto"
// 2. Format code using the current language server:
// 1. Format code using the current language server:
// "formatter": "language_server"
// 3. Format code using a specific language server:
// "formatter": {"language_server": {"name": "ruff"}}
// 4. Format code using an external command:
// 2. Format code using an external command:
// "formatter": {
// "external": {
// "command": "prettier",
// "arguments": ["--stdin-filepath", "{buffer_path}"]
// }
// }
// 5. Format code using Zed's Prettier integration:
// 3. Format code using Zed's Prettier integration:
// "formatter": "prettier"
// 6. Format code using a code action
// "formatter": {"code_action": "source.fixAll.eslint"}
// 7. An array of any format step specified above to apply in order
// "formatter": [{"code_action": "source.fixAll.eslint"}, "prettier"]
// 4. Default. Format files using Zed's Prettier integration (if applicable),
// or falling back to formatting via language server:
// "formatter": "auto"
"formatter": "auto",
// How to soft-wrap long lines of text.
// Possible values:
@@ -1240,10 +1210,6 @@
// 2. Hide the gutter
// "git_gutter": "hide"
"git_gutter": "tracked_files",
/// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter.
///
/// Default: 0
"gutter_debounce": 0,
// Control whether the git blame information is shown inline,
// in the currently focused line.
"inline_blame": {
@@ -1259,9 +1225,6 @@
// The minimum column number to show the inline blame information at
"min_column": 0
},
"blame": {
"show_avatar": true
},
// Control which information is shown in the branch picker.
"branch_picker": {
"show_author_name": true
@@ -1320,18 +1283,15 @@
// "proxy": "",
// "proxy_no_verify": false
// },
// Whether edit predictions are enabled when editing text threads.
// This setting has no effect if globally disabled.
"enabled_in_text_threads": true,
"copilot": {
"enterprise_uri": null,
"proxy": null,
"proxy_no_verify": null
},
"codestral": {
"model": null,
"max_tokens": null
},
// Whether edit predictions are enabled when editing text threads.
// This setting has no effect if globally disabled.
"enabled_in_text_threads": true
}
},
// Settings specific to journaling
"journal": {
@@ -1345,8 +1305,6 @@
},
// Status bar-related settings.
"status_bar": {
// Whether to show the status bar.
"experimental.show": true,
// Whether to show the active language button in the status bar.
"active_language_button": true,
// Whether to show the cursor position button in the status bar.
@@ -1413,8 +1371,8 @@
// 4. A box drawn around the following character
// "hollow"
//
// Default: "block"
"cursor_shape": "block",
// Default: not set, defaults to "block"
"cursor_shape": null,
// Set whether Alternate Scroll mode (code: ?1007) is active by default.
// Alternate Scroll mode converts mouse scroll events into up / down key
// presses when in the alternate screen (e.g. when running applications
@@ -1436,8 +1394,8 @@
// Whether or not selecting text in the terminal will automatically
// copy to the system clipboard.
"copy_on_select": false,
// Whether to keep the text selection after copying it to the clipboard.
"keep_selection_on_copy": true,
// Whether to keep the text selection after copying it to the clipboard
"keep_selection_on_copy": false,
// Whether to show the terminal button in the status bar
"button": true,
// Any key-value pairs added to this list will be added to the terminal's
@@ -1456,7 +1414,7 @@
// "line_height": {
// "custom": 2
// },
"line_height": "standard",
"line_height": "comfortable",
// Activate the python virtual environment, if one is found, in the
// terminal's working directory (as resolved by the working_directory
// setting). Set this to "off" to disable this behavior.
@@ -1476,7 +1434,7 @@
//
// The shell running in the terminal needs to be configured to emit the title.
// Example: `echo -e "\e]2;New Title\007";`
"breadcrumbs": false
"breadcrumbs": true
},
// Scrollbar-related settings
"scrollbar": {
@@ -1582,14 +1540,6 @@
"auto_install_extensions": {
"html": true
},
// The capabilities granted to extensions.
//
// This list can be customized to restrict what extensions are able to do.
"granted_extension_capabilities": [
{ "kind": "process:exec", "command": "*", "args": ["**"] },
{ "kind": "download_file", "host": "*", "path": ["**"] },
{ "kind": "npm:install", "package": "*" }
],
// Controls how completions are processed for this language.
"completions": {
// Controls how words are completed.
@@ -1888,19 +1838,21 @@
// Allows to enable/disable formatting with Prettier
// and configure default Prettier, used when no project-level Prettier installation is found.
"prettier": {
// Enables or disables formatting with Prettier for any given language.
"allowed": false,
// Forces Prettier integration to use a specific parser name when formatting files with the language.
"plugins": [],
// Default Prettier options, in the format as in package.json section for Prettier.
// If project installs Prettier via its package.json, these options will be ignored.
// // Whether to consider prettier formatter or not when attempting to format a file.
"allowed": false
//
// // Use regular Prettier json configuration.
// // If Prettier is allowed, Zed will use this for its Prettier instance for any applicable file, if
// // the project has no other Prettier installed.
// "plugins": [],
//
// // Use regular Prettier json configuration.
// // If Prettier is allowed, Zed will use this for its Prettier instance for any applicable file, if
// // the project has no other Prettier installed.
// "trailingComma": "es5",
// "tabWidth": 4,
// "semi": false,
// "singleQuote": true
// Forces Prettier integration to use a specific parser name when formatting files with the language
// when set to a non-empty string.
"parser": ""
},
// Settings for auto-closing of JSX tags.
"jsx_tag_auto_close": {
@@ -2050,7 +2002,7 @@
// Examples:
// "profiles": {
// "Presenting": {
// "agent_ui_font_size": 20.0,
// "agent_font_size": 20.0,
// "buffer_font_size": 20.0,
// "theme": "One Light",
// "ui_font_size": 20.0
@@ -2063,7 +2015,7 @@
// }
// }
// }
"profiles": {},
"profiles": [],
// A map of log scopes to the desired log level.
// Useful for filtering out noisy logs or enabling more verbose logging.

Binary file not shown.

View File

@@ -192,7 +192,7 @@
"font_weight": null
},
"comment": {
"color": "#5c6773ff",
"color": "#abb5be8c",
"font_style": null,
"font_weight": null
},
@@ -239,7 +239,7 @@
"hint": {
"color": "#628b80ff",
"font_style": null,
"font_weight": null
"font_weight": 700
},
"keyword": {
"color": "#ff8f3fff",
@@ -583,7 +583,7 @@
"font_weight": null
},
"comment": {
"color": "#abb0b6ff",
"color": "#787b8099",
"font_style": null,
"font_weight": null
},
@@ -630,7 +630,7 @@
"hint": {
"color": "#8ca7c2ff",
"font_style": null,
"font_weight": null
"font_weight": 700
},
"keyword": {
"color": "#fa8d3eff",
@@ -974,7 +974,7 @@
"font_weight": null
},
"comment": {
"color": "#5c6773ff",
"color": "#b8cfe680",
"font_style": null,
"font_weight": null
},
@@ -1021,7 +1021,7 @@
"hint": {
"color": "#7399a3ff",
"font_style": null,
"font_weight": null
"font_weight": 700
},
"keyword": {
"color": "#ffad65ff",

View File

@@ -248,7 +248,7 @@
"hint": {
"color": "#8c957dff",
"font_style": null,
"font_weight": null
"font_weight": 700
},
"keyword": {
"color": "#fb4833ff",
@@ -653,7 +653,7 @@
"hint": {
"color": "#8c957dff",
"font_style": null,
"font_weight": null
"font_weight": 700
},
"keyword": {
"color": "#fb4833ff",
@@ -1058,7 +1058,7 @@
"hint": {
"color": "#8c957dff",
"font_style": null,
"font_weight": null
"font_weight": 700
},
"keyword": {
"color": "#fb4833ff",
@@ -1463,7 +1463,7 @@
"hint": {
"color": "#677562ff",
"font_style": null,
"font_weight": null
"font_weight": 700
},
"keyword": {
"color": "#9d0006ff",
@@ -1868,7 +1868,7 @@
"hint": {
"color": "#677562ff",
"font_style": null,
"font_weight": null
"font_weight": 700
},
"keyword": {
"color": "#9d0006ff",
@@ -2273,7 +2273,7 @@
"hint": {
"color": "#677562ff",
"font_style": null,
"font_weight": null
"font_weight": 700
},
"keyword": {
"color": "#9d0006ff",

View File

@@ -244,7 +244,7 @@
"hint": {
"color": "#788ca6ff",
"font_style": null,
"font_weight": null
"font_weight": 700
},
"keyword": {
"color": "#b477cfff",
@@ -643,7 +643,7 @@
"hint": {
"color": "#7274a7ff",
"font_style": null,
"font_weight": null
"font_weight": 700
},
"keyword": {
"color": "#a449abff",

View File

@@ -5,16 +5,3 @@ ignore-interior-mutability = [
# and Hash impls do not use fields with interior mutability.
"agent::context::AgentContextKey"
]
disallowed-methods = [
{ path = "std::process::Command::spawn", reason = "Spawning `std::process::Command` can block the current thread for an unknown duration", replacement = "smol::process::Command::spawn" },
{ path = "std::process::Command::output", reason = "Spawning `std::process::Command` can block the current thread for an unknown duration", replacement = "smol::process::Command::output" },
{ path = "std::process::Command::status", reason = "Spawning `std::process::Command` can block the current thread for an unknown duration", replacement = "smol::process::Command::status" },
{ path = "serde_json::from_reader", reason = "Parsing from a buffer is much slower than first reading the buffer into a Vec/String, see https://github.com/serde-rs/json/issues/160#issuecomment-253446892. Use `serde_json::from_slice` instead." },
{ path = "serde_json_lenient::from_reader", reason = "Parsing from a buffer is much slower than first reading the buffer into a Vec/String, see https://github.com/serde-rs/json/issues/160#issuecomment-253446892, Use `serde_json_lenient::from_slice` instead." },
]
disallowed-types = [
# { path = "std::collections::HashMap", replacement = "collections::HashMap" },
# { path = "std::collections::HashSet", replacement = "collections::HashSet" },
# { path = "indexmap::IndexSet", replacement = "collections::IndexSet" },
# { path = "indexmap::IndexMap", replacement = "collections::IndexMap" },
]

View File

@@ -3,7 +3,6 @@ mod diff;
mod mention;
mod terminal;
use ::terminal::terminal_settings::TerminalSettings;
use agent_settings::AgentSettings;
use collections::HashSet;
pub use connection::*;
@@ -12,7 +11,7 @@ use language::language_settings::FormatOnSave;
pub use mention::*;
use project::lsp_store::{FormatTrigger, LspFormatTarget};
use serde::{Deserialize, Serialize};
use settings::{Settings as _, SettingsLocation};
use settings::Settings as _;
use task::{Shell, ShellBuilder};
pub use terminal::*;
@@ -35,7 +34,7 @@ use std::rc::Rc;
use std::time::{Duration, Instant};
use std::{fmt::Display, mem, path::PathBuf, sync::Arc};
use ui::App;
use util::{ResultExt, get_default_system_shell_preferring_bash};
use util::{ResultExt, get_default_system_shell};
use uuid::Uuid;
#[derive(Debug)]
@@ -574,7 +573,7 @@ impl ToolCallContent {
))),
acp::ToolCallContent::Diff { diff } => Ok(Self::Diff(cx.new(|cx| {
Diff::finalized(
diff.path.to_string_lossy().into_owned(),
diff.path,
diff.old_text,
diff.new_text,
language_registry,
@@ -788,8 +787,6 @@ pub struct AcpThread {
prompt_capabilities: acp::PromptCapabilities,
_observe_prompt_capabilities: Task<anyhow::Result<()>>,
terminals: HashMap<acp::TerminalId, Entity<Terminal>>,
pending_terminal_output: HashMap<acp::TerminalId, Vec<Vec<u8>>>,
pending_terminal_exit: HashMap<acp::TerminalId, acp::TerminalExitStatus>,
}
#[derive(Debug)]
@@ -812,126 +809,6 @@ pub enum AcpThreadEvent {
impl EventEmitter<AcpThreadEvent> for AcpThread {}
#[derive(Debug, Clone)]
pub enum TerminalProviderEvent {
Created {
terminal_id: acp::TerminalId,
label: String,
cwd: Option<PathBuf>,
output_byte_limit: Option<u64>,
terminal: Entity<::terminal::Terminal>,
},
Output {
terminal_id: acp::TerminalId,
data: Vec<u8>,
},
TitleChanged {
terminal_id: acp::TerminalId,
title: String,
},
Exit {
terminal_id: acp::TerminalId,
status: acp::TerminalExitStatus,
},
}
#[derive(Debug, Clone)]
pub enum TerminalProviderCommand {
WriteInput {
terminal_id: acp::TerminalId,
bytes: Vec<u8>,
},
Resize {
terminal_id: acp::TerminalId,
cols: u16,
rows: u16,
},
Close {
terminal_id: acp::TerminalId,
},
}
impl AcpThread {
pub fn on_terminal_provider_event(
&mut self,
event: TerminalProviderEvent,
cx: &mut Context<Self>,
) {
match event {
TerminalProviderEvent::Created {
terminal_id,
label,
cwd,
output_byte_limit,
terminal,
} => {
let entity = self.register_terminal_created(
terminal_id.clone(),
label,
cwd,
output_byte_limit,
terminal,
cx,
);
if let Some(mut chunks) = self.pending_terminal_output.remove(&terminal_id) {
for data in chunks.drain(..) {
entity.update(cx, |term, cx| {
term.inner().update(cx, |inner, cx| {
inner.write_output(&data, cx);
})
});
}
}
if let Some(_status) = self.pending_terminal_exit.remove(&terminal_id) {
entity.update(cx, |_term, cx| {
cx.notify();
});
}
cx.notify();
}
TerminalProviderEvent::Output { terminal_id, data } => {
if let Some(entity) = self.terminals.get(&terminal_id) {
entity.update(cx, |term, cx| {
term.inner().update(cx, |inner, cx| {
inner.write_output(&data, cx);
})
});
} else {
self.pending_terminal_output
.entry(terminal_id)
.or_default()
.push(data);
}
}
TerminalProviderEvent::TitleChanged { terminal_id, title } => {
if let Some(entity) = self.terminals.get(&terminal_id) {
entity.update(cx, |term, cx| {
term.inner().update(cx, |inner, cx| {
inner.breadcrumb_text = title;
cx.emit(::terminal::Event::BreadcrumbsChanged);
})
});
}
}
TerminalProviderEvent::Exit {
terminal_id,
status,
} => {
if let Some(entity) = self.terminals.get(&terminal_id) {
entity.update(cx, |_term, cx| {
cx.notify();
});
} else {
self.pending_terminal_exit.insert(terminal_id, status);
}
}
}
}
}
#[derive(PartialEq, Eq, Debug)]
pub enum ThreadStatus {
Idle,
@@ -1009,8 +886,6 @@ impl AcpThread {
prompt_capabilities,
_observe_prompt_capabilities: task,
terminals: HashMap::default(),
pending_terminal_output: HashMap::default(),
pending_terminal_exit: HashMap::default(),
}
}
@@ -2086,24 +1961,16 @@ impl AcpThread {
) -> Task<Result<Entity<Terminal>>> {
let env = match &cwd {
Some(dir) => self.project.update(cx, |project, cx| {
let worktree = project.find_worktree(dir.as_path(), cx);
let shell = TerminalSettings::get(
worktree.as_ref().map(|(worktree, path)| SettingsLocation {
worktree_id: worktree.read(cx).id(),
path: &path,
}),
cx,
)
.shell
.clone();
project.directory_environment(&shell, dir.as_path().into(), cx)
project.directory_environment(dir.as_path().into(), cx)
}),
None => Task::ready(None).shared(),
};
let env = cx.spawn(async move |_, _| {
let mut env = env.await.unwrap_or_default();
// Disables paging for `git` and hopefully other commands
env.insert("PAGER".into(), "".into());
if cfg!(unix) {
env.insert("PAGER".into(), "cat".into());
}
for var in extra_env {
env.insert(var.name, var.value);
}
@@ -2112,24 +1979,24 @@ impl AcpThread {
let project = self.project.clone();
let language_registry = project.read(cx).languages().clone();
let is_windows = project.read(cx).path_style(cx).is_windows();
let terminal_id = acp::TerminalId(Uuid::new_v4().to_string().into());
let terminal_task = cx.spawn({
let terminal_id = terminal_id.clone();
async move |_this, cx| {
let env = env.await;
let shell = project
.update(cx, |project, cx| {
project
.remote_client()
.and_then(|r| r.read(cx).default_system_shell())
})?
.unwrap_or_else(|| get_default_system_shell_preferring_bash());
let (task_command, task_args) =
ShellBuilder::new(&Shell::Program(shell), is_windows)
.redirect_stdin_to_dev_null()
.build(Some(command.clone()), &args);
let (task_command, task_args) = ShellBuilder::new(
project
.update(cx, |project, cx| {
project
.remote_client()
.and_then(|r| r.read(cx).default_system_shell())
})?
.as_deref(),
&Shell::Program(get_default_system_shell()),
)
.redirect_stdin_to_dev_null()
.build(Some(command.clone()), &args);
let terminal = project
.update(cx, |project, cx| {
project.create_terminal_task(
@@ -2212,32 +2079,6 @@ impl AcpThread {
pub fn emit_load_error(&mut self, error: LoadError, cx: &mut Context<Self>) {
cx.emit(AcpThreadEvent::LoadError(error));
}
pub fn register_terminal_created(
&mut self,
terminal_id: acp::TerminalId,
command_label: String,
working_dir: Option<PathBuf>,
output_byte_limit: Option<u64>,
terminal: Entity<::terminal::Terminal>,
cx: &mut Context<Self>,
) -> Entity<Terminal> {
let language_registry = self.project.read(cx).languages().clone();
let entity = cx.new(|cx| {
Terminal::new(
terminal_id.clone(),
&command_label,
working_dir.clone(),
output_byte_limit.map(|l| l as usize),
terminal,
language_registry,
cx,
)
});
self.terminals.insert(terminal_id.clone(), entity.clone());
entity
}
}
fn markdown_for_raw_output(
@@ -2314,145 +2155,6 @@ mod tests {
});
}
#[gpui::test]
async fn test_terminal_output_buffered_before_created_renders(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await;
let connection = Rc::new(FakeAgentConnection::new());
let thread = cx
.update(|cx| connection.new_thread(project, std::path::Path::new(path!("/test")), cx))
.await
.unwrap();
let terminal_id = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
// Send Output BEFORE Created - should be buffered by acp_thread
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Output {
terminal_id: terminal_id.clone(),
data: b"hello buffered".to_vec(),
},
cx,
);
});
// Create a display-only terminal and then send Created
let lower = cx.new(|cx| {
let builder = ::terminal::TerminalBuilder::new_display_only(
::terminal::terminal_settings::CursorShape::default(),
::terminal::terminal_settings::AlternateScroll::On,
None,
0,
)
.unwrap();
builder.subscribe(cx)
});
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Created {
terminal_id: terminal_id.clone(),
label: "Buffered Test".to_string(),
cwd: None,
output_byte_limit: None,
terminal: lower.clone(),
},
cx,
);
});
// After Created, buffered Output should have been flushed into the renderer
let content = thread.read_with(cx, |thread, cx| {
let term = thread.terminal(terminal_id.clone()).unwrap();
term.read_with(cx, |t, cx| t.inner().read(cx).get_content())
});
assert!(
content.contains("hello buffered"),
"expected buffered output to render, got: {content}"
);
}
#[gpui::test]
async fn test_terminal_output_and_exit_buffered_before_created(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await;
let connection = Rc::new(FakeAgentConnection::new());
let thread = cx
.update(|cx| connection.new_thread(project, std::path::Path::new(path!("/test")), cx))
.await
.unwrap();
let terminal_id = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
// Send Output BEFORE Created
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Output {
terminal_id: terminal_id.clone(),
data: b"pre-exit data".to_vec(),
},
cx,
);
});
// Send Exit BEFORE Created
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Exit {
terminal_id: terminal_id.clone(),
status: acp::TerminalExitStatus {
exit_code: Some(0),
signal: None,
meta: None,
},
},
cx,
);
});
// Now create a display-only lower-level terminal and send Created
let lower = cx.new(|cx| {
let builder = ::terminal::TerminalBuilder::new_display_only(
::terminal::terminal_settings::CursorShape::default(),
::terminal::terminal_settings::AlternateScroll::On,
None,
0,
)
.unwrap();
builder.subscribe(cx)
});
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Created {
terminal_id: terminal_id.clone(),
label: "Buffered Exit Test".to_string(),
cwd: None,
output_byte_limit: None,
terminal: lower.clone(),
},
cx,
);
});
// Output should be present after Created (flushed from buffer)
let content = thread.read_with(cx, |thread, cx| {
let term = thread.terminal(terminal_id.clone()).unwrap();
term.read_with(cx, |t, cx| t.inner().read(cx).get_content())
});
assert!(
content.contains("pre-exit data"),
"expected pre-exit data to render, got: {content}"
);
}
#[gpui::test]
async fn test_push_user_content_block(cx: &mut gpui::TestAppContext) {
init_test(cx);

View File

@@ -6,7 +6,12 @@ use itertools::Itertools;
use language::{
Anchor, Buffer, Capability, LanguageRegistry, OffsetRangeExt as _, Point, Rope, TextBuffer,
};
use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
use std::{
cmp::Reverse,
ops::Range,
path::{Path, PathBuf},
sync::Arc,
};
use util::ResultExt;
pub enum Diff {
@@ -16,7 +21,7 @@ pub enum Diff {
impl Diff {
pub fn finalized(
path: String,
path: PathBuf,
old_text: Option<String>,
new_text: String,
language_registry: Arc<LanguageRegistry>,
@@ -31,7 +36,7 @@ impl Diff {
let buffer = new_buffer.clone();
async move |_, cx| {
let language = language_registry
.load_language_for_file_path(Path::new(&path))
.language_for_file_path(&path)
.await
.log_err();
@@ -147,15 +152,12 @@ impl Diff {
let path = match self {
Diff::Pending(PendingDiff {
new_buffer: buffer, ..
}) => buffer
.read(cx)
.file()
.map(|file| file.path().display(file.path_style(cx))),
Diff::Finalized(FinalizedDiff { path, .. }) => Some(path.as_str().into()),
}) => buffer.read(cx).file().map(|file| file.path().as_ref()),
Diff::Finalized(FinalizedDiff { path, .. }) => Some(path.as_path()),
};
format!(
"Diff: {}\n```\n{}\n```\n",
path.unwrap_or("untitled".into()),
path.unwrap_or(Path::new("untitled")).display(),
buffer_text
)
}
@@ -242,8 +244,8 @@ impl PendingDiff {
.new_buffer
.read(cx)
.file()
.map(|file| file.path().display(file.path_style(cx)))
.unwrap_or("untitled".into())
.map(|file| file.path().as_ref())
.unwrap_or(Path::new("untitled"))
.into();
// Replace the buffer in the multibuffer with the snapshot
@@ -346,7 +348,7 @@ impl PendingDiff {
}
pub struct FinalizedDiff {
path: String,
path: PathBuf,
base_text: Arc<String>,
new_buffer: Entity<Buffer>,
multibuffer: Entity<MultiBuffer>,

View File

@@ -126,39 +126,6 @@ impl MentionUri {
abs_path: None,
line_range,
})
} else if let Some(name) = path.strip_prefix("/agent/symbol/") {
let fragment = url
.fragment()
.context("Missing fragment for untitled buffer selection")?;
let line_range = parse_line_range(fragment)?;
let path =
single_query_param(&url, "path")?.context("Missing path for symbol")?;
Ok(Self::Symbol {
name: name.to_string(),
abs_path: path.into(),
line_range,
})
} else if path.starts_with("/agent/file") {
let path =
single_query_param(&url, "path")?.context("Missing path for file")?;
Ok(Self::File {
abs_path: path.into(),
})
} else if path.starts_with("/agent/directory") {
let path =
single_query_param(&url, "path")?.context("Missing path for directory")?;
Ok(Self::Directory {
abs_path: path.into(),
})
} else if path.starts_with("/agent/selection") {
let fragment = url.fragment().context("Missing fragment for selection")?;
let line_range = parse_line_range(fragment)?;
let path =
single_query_param(&url, "path")?.context("Missing path for selection")?;
Ok(Self::Selection {
abs_path: Some(path.into()),
line_range,
})
} else {
bail!("invalid zed url: {:?}", input);
}
@@ -213,29 +180,20 @@ impl MentionUri {
pub fn to_uri(&self) -> Url {
match self {
MentionUri::File { abs_path } => {
let mut url = Url::parse("zed:///").unwrap();
url.set_path("/agent/file");
url.query_pairs_mut()
.append_pair("path", &abs_path.to_string_lossy());
url
Url::from_file_path(abs_path).expect("mention path should be absolute")
}
MentionUri::PastedImage => Url::parse("zed:///agent/pasted-image").unwrap(),
MentionUri::Directory { abs_path } => {
let mut url = Url::parse("zed:///").unwrap();
url.set_path("/agent/directory");
url.query_pairs_mut()
.append_pair("path", &abs_path.to_string_lossy());
url
Url::from_directory_path(abs_path).expect("mention path should be absolute")
}
MentionUri::Symbol {
abs_path,
name,
line_range,
} => {
let mut url = Url::parse("zed:///").unwrap();
url.set_path(&format!("/agent/symbol/{name}"));
url.query_pairs_mut()
.append_pair("path", &abs_path.to_string_lossy());
let mut url =
Url::from_file_path(abs_path).expect("mention path should be absolute");
url.query_pairs_mut().append_pair("symbol", name);
url.set_fragment(Some(&format!(
"L{}:{}",
line_range.start() + 1,
@@ -244,16 +202,15 @@ impl MentionUri {
url
}
MentionUri::Selection {
abs_path,
abs_path: path,
line_range,
} => {
let mut url = Url::parse("zed:///").unwrap();
if let Some(abs_path) = abs_path {
url.set_path("/agent/selection");
url.query_pairs_mut()
.append_pair("path", &abs_path.to_string_lossy());
let mut url = if let Some(path) = path {
Url::from_file_path(path).expect("mention path should be absolute")
} else {
let mut url = Url::parse("zed:///").unwrap();
url.set_path("/agent/untitled-buffer");
url
};
url.set_fragment(Some(&format!(
"L{}:{}",
@@ -338,32 +295,37 @@ mod tests {
#[test]
fn test_parse_file_uri() {
let old_uri = uri!("file:///path/to/file.rs");
let parsed = MentionUri::parse(old_uri).unwrap();
let file_uri = uri!("file:///path/to/file.rs");
let parsed = MentionUri::parse(file_uri).unwrap();
match &parsed {
MentionUri::File { abs_path } => {
assert_eq!(abs_path.to_str().unwrap(), path!("/path/to/file.rs"));
}
_ => panic!("Expected File variant"),
}
let new_uri = parsed.to_uri().to_string();
assert!(new_uri.starts_with("zed:///agent/file"));
assert_eq!(MentionUri::parse(&new_uri).unwrap(), parsed);
assert_eq!(parsed.to_uri().to_string(), file_uri);
}
#[test]
fn test_parse_directory_uri() {
let old_uri = uri!("file:///path/to/dir/");
let parsed = MentionUri::parse(old_uri).unwrap();
let file_uri = uri!("file:///path/to/dir/");
let parsed = MentionUri::parse(file_uri).unwrap();
match &parsed {
MentionUri::Directory { abs_path } => {
assert_eq!(abs_path.to_str().unwrap(), path!("/path/to/dir/"));
}
_ => panic!("Expected Directory variant"),
}
let new_uri = parsed.to_uri().to_string();
assert!(new_uri.starts_with("zed:///agent/directory"));
assert_eq!(MentionUri::parse(&new_uri).unwrap(), parsed);
assert_eq!(parsed.to_uri().to_string(), file_uri);
}
#[test]
fn test_to_directory_uri_with_slash() {
let uri = MentionUri::Directory {
abs_path: PathBuf::from(path!("/path/to/dir/")),
};
let expected = uri!("file:///path/to/dir/");
assert_eq!(uri.to_uri().to_string(), expected);
}
#[test]
@@ -371,15 +333,14 @@ mod tests {
let uri = MentionUri::Directory {
abs_path: PathBuf::from(path!("/path/to/dir")),
};
let uri_string = uri.to_uri().to_string();
assert!(uri_string.starts_with("zed:///agent/directory"));
assert_eq!(MentionUri::parse(&uri_string).unwrap(), uri);
let expected = uri!("file:///path/to/dir/");
assert_eq!(uri.to_uri().to_string(), expected);
}
#[test]
fn test_parse_symbol_uri() {
let old_uri = uri!("file:///path/to/file.rs?symbol=MySymbol#L10:20");
let parsed = MentionUri::parse(old_uri).unwrap();
let symbol_uri = uri!("file:///path/to/file.rs?symbol=MySymbol#L10:20");
let parsed = MentionUri::parse(symbol_uri).unwrap();
match &parsed {
MentionUri::Symbol {
abs_path: path,
@@ -393,15 +354,13 @@ mod tests {
}
_ => panic!("Expected Symbol variant"),
}
let new_uri = parsed.to_uri().to_string();
assert!(new_uri.starts_with("zed:///agent/symbol/MySymbol"));
assert_eq!(MentionUri::parse(&new_uri).unwrap(), parsed);
assert_eq!(parsed.to_uri().to_string(), symbol_uri);
}
#[test]
fn test_parse_selection_uri() {
let old_uri = uri!("file:///path/to/file.rs#L5:15");
let parsed = MentionUri::parse(old_uri).unwrap();
let selection_uri = uri!("file:///path/to/file.rs#L5:15");
let parsed = MentionUri::parse(selection_uri).unwrap();
match &parsed {
MentionUri::Selection {
abs_path: path,
@@ -416,9 +375,7 @@ mod tests {
}
_ => panic!("Expected Selection variant"),
}
let new_uri = parsed.to_uri().to_string();
assert!(new_uri.starts_with("zed:///agent/selection"));
assert_eq!(MentionUri::parse(&new_uri).unwrap(), parsed);
assert_eq!(parsed.to_uri().to_string(), selection_uri);
}
#[test]

View File

@@ -4,26 +4,22 @@ use std::{
fmt::Display,
rc::{Rc, Weak},
sync::Arc,
time::Duration,
};
use agent_client_protocol as acp;
use collections::HashMap;
use gpui::{
App, ClipboardItem, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment,
ListState, StyleRefinement, Subscription, Task, TextStyleRefinement, Window, actions, list,
prelude::*,
App, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment, ListState,
StyleRefinement, Subscription, Task, TextStyleRefinement, Window, actions, list, prelude::*,
};
use language::LanguageRegistry;
use markdown::{CodeBlockRenderer, Markdown, MarkdownElement, MarkdownStyle};
use project::Project;
use settings::Settings;
use theme::ThemeSettings;
use ui::{Tooltip, prelude::*};
use ui::prelude::*;
use util::ResultExt as _;
use workspace::{
Item, ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
};
use workspace::{Item, Workspace};
actions!(dev, [OpenAcpLogs]);
@@ -231,34 +227,6 @@ impl AcpTools {
cx.notify();
}
fn serialize_observed_messages(&self) -> Option<String> {
let connection = self.watched_connection.as_ref()?;
let messages: Vec<serde_json::Value> = connection
.messages
.iter()
.filter_map(|message| {
let params = match &message.params {
Ok(Some(params)) => params.clone(),
Ok(None) => serde_json::Value::Null,
Err(err) => serde_json::to_value(err).ok()?,
};
Some(serde_json::json!({
"_direction": match message.direction {
acp::StreamMessageDirection::Incoming => "incoming",
acp::StreamMessageDirection::Outgoing => "outgoing",
},
"_type": message.message_type.to_string().to_lowercase(),
"id": message.request_id,
"method": message.name.to_string(),
"params": params,
}))
})
.collect();
serde_json::to_string_pretty(&messages).ok()
}
fn render_message(
&mut self,
index: usize,
@@ -524,92 +492,3 @@ impl Render for AcpTools {
})
}
}
pub struct AcpToolsToolbarItemView {
acp_tools: Option<Entity<AcpTools>>,
just_copied: bool,
}
impl AcpToolsToolbarItemView {
pub fn new() -> Self {
Self {
acp_tools: None,
just_copied: false,
}
}
}
impl Render for AcpToolsToolbarItemView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let Some(acp_tools) = self.acp_tools.as_ref() else {
return Empty.into_any_element();
};
let acp_tools = acp_tools.clone();
h_flex()
.gap_2()
.child(
IconButton::new(
"copy_all_messages",
if self.just_copied {
IconName::Check
} else {
IconName::Copy
},
)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text(if self.just_copied {
"Copied!"
} else {
"Copy All Messages"
}))
.disabled(
acp_tools
.read(cx)
.watched_connection
.as_ref()
.is_none_or(|connection| connection.messages.is_empty()),
)
.on_click(cx.listener(move |this, _, _window, cx| {
if let Some(content) = acp_tools.read(cx).serialize_observed_messages() {
cx.write_to_clipboard(ClipboardItem::new_string(content));
this.just_copied = true;
cx.spawn(async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(2)).await;
this.update(cx, |this, cx| {
this.just_copied = false;
cx.notify();
})
})
.detach();
}
})),
)
.into_any()
}
}
impl EventEmitter<ToolbarItemEvent> for AcpToolsToolbarItemView {}
impl ToolbarItemView for AcpToolsToolbarItemView {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
_window: &mut Window,
cx: &mut Context<Self>,
) -> ToolbarItemLocation {
if let Some(item) = active_pane_item
&& let Some(acp_tools) = item.downcast::<AcpTools>()
{
self.acp_tools = Some(acp_tools);
cx.notify();
return ToolbarItemLocation::PrimaryRight;
}
if self.acp_tools.take().is_some() {
cx.notify();
}
ToolbarItemLocation::Hidden
}
}

View File

@@ -8,7 +8,10 @@ use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint};
use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};
use std::{cmp, ops::Range, sync::Arc};
use text::{Edit, Patch, Rope};
use util::{RangeExt, ResultExt as _};
use util::{
RangeExt, ResultExt as _,
paths::{PathStyle, RemotePathBuf},
};
/// Tracks actions performed by tools in a thread
pub struct ActionLog {
@@ -59,13 +62,7 @@ impl ActionLog {
let file_path = buffer
.read(cx)
.file()
.map(|file| {
let mut path = file.full_path(cx).to_string_lossy().into_owned();
if file.path_style(cx).is_windows() {
path = path.replace('\\', "/");
}
path
})
.map(|file| RemotePathBuf::new(file.full_path(cx), PathStyle::Posix).to_proto())
.unwrap_or_else(|| format!("buffer_{}", buffer.entity_id()));
let mut result = String::new();
@@ -2304,7 +2301,7 @@ mod tests {
.await;
fs.set_head_for_repo(
path!("/project/.git").as_ref(),
&[("file.txt", "a\nb\nc\nd\ne\nf\ng\nh\ni\nj".into())],
&[("file.txt".into(), "a\nb\nc\nd\ne\nf\ng\nh\ni\nj".into())],
"0000000",
);
cx.run_until_parked();
@@ -2387,7 +2384,7 @@ mod tests {
// - Ignores the last line edit (j stays as j)
fs.set_head_for_repo(
path!("/project/.git").as_ref(),
&[("file.txt", "A\nb\nc\nf\nG\nh\ni\nj".into())],
&[("file.txt".into(), "A\nb\nc\nf\nG\nh\ni\nj".into())],
"0000001",
);
cx.run_until_parked();
@@ -2418,7 +2415,10 @@ mod tests {
// Make another commit that accepts the NEW line but with different content
fs.set_head_for_repo(
path!("/project/.git").as_ref(),
&[("file.txt", "A\nb\nc\nf\nGGG\nh\nDIFFERENT\ni\nj".into())],
&[(
"file.txt".into(),
"A\nb\nc\nf\nGGG\nh\nDIFFERENT\ni\nj".into(),
)],
"0000002",
);
cx.run_until_parked();
@@ -2444,7 +2444,7 @@ mod tests {
// Final commit that accepts all remaining edits
fs.set_head_for_repo(
path!("/project/.git").as_ref(),
&[("file.txt", "A\nb\nc\nf\nGGG\nh\nNEW\ni\nJ".into())],
&[("file.txt".into(), "A\nb\nc\nf\nGGG\nh\nNEW\ni\nJ".into())],
"0000003",
);
cx.run_until_parked();

View File

@@ -20,6 +20,7 @@ use std::{
cmp::Reverse,
collections::HashSet,
fmt::Write,
path::Path,
sync::Arc,
time::{Duration, Instant},
};
@@ -327,13 +328,17 @@ impl ActivityIndicator {
.flatten()
}
fn pending_environment_error<'a>(&'a self, cx: &'a App) -> Option<&'a EnvironmentErrorMessage> {
self.project.read(cx).peek_environment_error(cx)
fn pending_environment_errors<'a>(
&'a self,
cx: &'a App,
) -> impl Iterator<Item = (&'a Arc<Path>, &'a EnvironmentErrorMessage)> {
self.project.read(cx).shell_environment_errors(cx)
}
fn content_to_render(&mut self, cx: &mut Context<Self>) -> Option<Content> {
// Show if any direnv calls failed
if let Some(error) = self.pending_environment_error(cx) {
if let Some((abs_path, error)) = self.pending_environment_errors(cx).next() {
let abs_path = abs_path.clone();
return Some(Content {
icon: Some(
Icon::new(IconName::Warning)
@@ -343,7 +348,7 @@ impl ActivityIndicator {
message: error.0.clone(),
on_click: Some(Arc::new(move |this, window, cx| {
this.project.update(cx, |project, cx| {
project.pop_environment_error(cx);
project.remove_environment_error(&abs_path, cx);
});
window.dispatch_action(Box::new(workspace::OpenLog), cx);
})),

View File

@@ -39,6 +39,7 @@ heed.workspace = true
http_client.workspace = true
icons.workspace = true
indoc.workspace = true
itertools.workspace = true
language.workspace = true
language_model.workspace = true
log.workspace = true

View File

@@ -2,20 +2,19 @@ pub mod agent_profile;
pub mod context;
pub mod context_server_tool;
pub mod context_store;
pub mod history_store;
pub mod thread;
pub mod thread_store;
pub mod tool_use;
pub use context::{AgentContext, ContextId, ContextLoadResult};
pub use context_store::ContextStore;
use fs::Fs;
use std::sync::Arc;
pub use thread::{
LastRestoreCheckpoint, Message, MessageCrease, MessageId, MessageSegment, Thread, ThreadError,
ThreadEvent, ThreadFeedback, ThreadId, ThreadSummary, TokenUsageRatio,
};
pub use thread_store::{SerializedThread, TextThreadStore, ThreadStore};
pub fn init(fs: Arc<dyn Fs>, cx: &mut gpui::App) {
thread_store::init(fs, cx);
pub fn init(cx: &mut gpui::App) {
thread_store::init(cx);
}

View File

@@ -18,7 +18,6 @@ use std::path::PathBuf;
use std::{ops::Range, path::Path, sync::Arc};
use text::{Anchor, OffsetRangeExt as _};
use util::markdown::MarkdownCodeBlock;
use util::rel_path::RelPath;
use util::{ResultExt as _, post_inc};
pub const RULES_ICON: IconName = IconName::Reader;
@@ -159,7 +158,7 @@ pub struct FileContextHandle {
#[derive(Debug, Clone)]
pub struct FileContext {
pub handle: FileContextHandle,
pub full_path: String,
pub full_path: Arc<Path>,
pub text: SharedString,
pub is_outline: bool,
}
@@ -187,7 +186,7 @@ impl FileContextHandle {
log::error!("file context missing path");
return Task::ready(None);
};
let full_path = file.full_path(cx).to_string_lossy().into_owned();
let full_path: Arc<Path> = file.full_path(cx).into();
let rope = buffer_ref.as_rope().clone();
let buffer = self.buffer.clone();
@@ -236,14 +235,14 @@ pub struct DirectoryContextHandle {
#[derive(Debug, Clone)]
pub struct DirectoryContext {
pub handle: DirectoryContextHandle,
pub full_path: String,
pub full_path: Arc<Path>,
pub descendants: Vec<DirectoryContextDescendant>,
}
#[derive(Debug, Clone)]
pub struct DirectoryContextDescendant {
/// Path within the directory.
pub rel_path: Arc<RelPath>,
pub rel_path: Arc<Path>,
pub fenced_codeblock: SharedString,
}
@@ -274,16 +273,13 @@ impl DirectoryContextHandle {
}
let directory_path = entry.path.clone();
let directory_full_path = worktree_ref
.full_path(&directory_path)
.to_string_lossy()
.to_string();
let directory_full_path = worktree_ref.full_path(&directory_path).into();
let file_paths = collect_files_in_path(worktree_ref, &directory_path);
let descendants_future = future::join_all(file_paths.into_iter().map(|path| {
let worktree_ref = worktree.read(cx);
let worktree_id = worktree_ref.id();
let full_path = worktree_ref.full_path(&path).to_string_lossy().into_owned();
let full_path = worktree_ref.full_path(&path);
let rel_path = path
.strip_prefix(&directory_path)
@@ -364,7 +360,7 @@ pub struct SymbolContextHandle {
#[derive(Debug, Clone)]
pub struct SymbolContext {
pub handle: SymbolContextHandle,
pub full_path: String,
pub full_path: Arc<Path>,
pub line_range: Range<Point>,
pub text: SharedString,
}
@@ -403,7 +399,7 @@ impl SymbolContextHandle {
log::error!("symbol context's file has no path");
return Task::ready(None);
};
let full_path = file.full_path(cx).to_string_lossy().into_owned();
let full_path = file.full_path(cx).into();
let line_range = self.enclosing_range.to_point(&buffer_ref.snapshot());
let text = self.text(cx);
let buffer = self.buffer.clone();
@@ -437,7 +433,7 @@ pub struct SelectionContextHandle {
#[derive(Debug, Clone)]
pub struct SelectionContext {
pub handle: SelectionContextHandle,
pub full_path: String,
pub full_path: Arc<Path>,
pub line_range: Range<Point>,
pub text: SharedString,
}
@@ -476,7 +472,7 @@ impl SelectionContextHandle {
let text = self.text(cx);
let buffer = self.buffer.clone();
let context = AgentContext::Selection(SelectionContext {
full_path: full_path.to_string_lossy().into_owned(),
full_path: full_path.into(),
line_range: self.line_range(cx),
text,
handle: self,
@@ -706,7 +702,7 @@ impl Display for RulesContext {
#[derive(Debug, Clone)]
pub struct ImageContext {
pub project_path: Option<ProjectPath>,
pub full_path: Option<String>,
pub full_path: Option<Arc<Path>>,
pub original_image: Arc<gpui::Image>,
// TODO: handle this elsewhere and remove `ignore-interior-mutability` opt-out in clippy.toml
// needed due to a false positive of `clippy::mutable_key_type`.
@@ -972,7 +968,7 @@ pub fn load_context(
})
}
fn collect_files_in_path(worktree: &Worktree, path: &RelPath) -> Vec<Arc<RelPath>> {
fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<Arc<Path>> {
let mut files = Vec::new();
for entry in worktree.child_entries(path) {
@@ -986,17 +982,14 @@ fn collect_files_in_path(worktree: &Worktree, path: &RelPath) -> Vec<Arc<RelPath
files
}
fn codeblock_tag(full_path: &str, line_range: Option<Range<Point>>) -> String {
fn codeblock_tag(full_path: &Path, line_range: Option<Range<Point>>) -> String {
let mut result = String::new();
if let Some(extension) = Path::new(full_path)
.extension()
.and_then(|ext| ext.to_str())
{
if let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) {
let _ = write!(result, "{} ", extension);
}
let _ = write!(result, "{}", full_path);
let _ = write!(result, "{}", full_path.display());
if let Some(range) = line_range {
if range.start.row == range.end.row {

View File

@@ -14,10 +14,7 @@ use futures::{self, FutureExt};
use gpui::{App, Context, Entity, EventEmitter, Image, SharedString, Task, WeakEntity};
use language::{Buffer, File as _};
use language_model::LanguageModelImage;
use project::{
Project, ProjectItem, ProjectPath, Symbol, image_store::is_image_file,
lsp_store::SymbolLocation,
};
use project::{Project, ProjectItem, ProjectPath, Symbol, image_store::is_image_file};
use prompt_store::UserPromptId;
use ref_cast::RefCast as _;
use std::{
@@ -312,7 +309,7 @@ impl ContextStore {
let item = image_item.read(cx);
this.insert_image(
Some(item.project_path(cx)),
Some(item.file.full_path(cx).to_string_lossy().into_owned()),
Some(item.file.full_path(cx).into()),
item.image.clone(),
remove_if_exists,
cx,
@@ -328,7 +325,7 @@ impl ContextStore {
fn insert_image(
&mut self,
project_path: Option<ProjectPath>,
full_path: Option<String>,
full_path: Option<Arc<Path>>,
image: Arc<Image>,
remove_if_exists: bool,
cx: &mut Context<ContextStore>,
@@ -503,7 +500,7 @@ impl ContextStore {
let Some(context_path) = buffer.project_path(cx) else {
return false;
};
if symbol.path != SymbolLocation::InProject(context_path) {
if context_path != symbol.path {
return false;
}
let context_range = context.range.to_point_utf16(&buffer.snapshot());

View File

@@ -0,0 +1,253 @@
use crate::{ThreadId, thread_store::SerializedThreadMetadata};
use anyhow::{Context as _, Result};
use assistant_context::SavedContextMetadata;
use chrono::{DateTime, Utc};
use gpui::{App, AsyncApp, Entity, SharedString, Task, prelude::*};
use itertools::Itertools;
use paths::contexts_dir;
use serde::{Deserialize, Serialize};
use std::{collections::VecDeque, path::Path, sync::Arc, time::Duration};
use util::ResultExt as _;
const MAX_RECENTLY_OPENED_ENTRIES: usize = 6;
const NAVIGATION_HISTORY_PATH: &str = "agent-navigation-history.json";
const SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE: Duration = Duration::from_millis(50);
#[derive(Clone, Debug)]
pub enum HistoryEntry {
Thread(SerializedThreadMetadata),
Context(SavedContextMetadata),
}
impl HistoryEntry {
pub fn updated_at(&self) -> DateTime<Utc> {
match self {
HistoryEntry::Thread(thread) => thread.updated_at,
HistoryEntry::Context(context) => context.mtime.to_utc(),
}
}
pub fn id(&self) -> HistoryEntryId {
match self {
HistoryEntry::Thread(thread) => HistoryEntryId::Thread(thread.id.clone()),
HistoryEntry::Context(context) => HistoryEntryId::Context(context.path.clone()),
}
}
pub fn title(&self) -> &SharedString {
match self {
HistoryEntry::Thread(thread) => &thread.summary,
HistoryEntry::Context(context) => &context.title,
}
}
}
/// Generic identifier for a history entry.
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum HistoryEntryId {
Thread(ThreadId),
Context(Arc<Path>),
}
#[derive(Serialize, Deserialize)]
enum SerializedRecentOpen {
Thread(String),
ContextName(String),
/// Old format which stores the full path
Context(String),
}
pub struct HistoryStore {
context_store: Entity<assistant_context::ContextStore>,
recently_opened_entries: VecDeque<HistoryEntryId>,
_subscriptions: Vec<gpui::Subscription>,
_save_recently_opened_entries_task: Task<()>,
}
impl HistoryStore {
pub fn new(
context_store: Entity<assistant_context::ContextStore>,
initial_recent_entries: impl IntoIterator<Item = HistoryEntryId>,
cx: &mut Context<Self>,
) -> Self {
let subscriptions = vec![cx.observe(&context_store, |_, _, cx| cx.notify())];
cx.spawn(async move |this, cx| {
let entries = Self::load_recently_opened_entries(cx).await.log_err()?;
this.update(cx, |this, _| {
this.recently_opened_entries
.extend(
entries.into_iter().take(
MAX_RECENTLY_OPENED_ENTRIES
.saturating_sub(this.recently_opened_entries.len()),
),
);
})
.ok()
})
.detach();
Self {
context_store,
recently_opened_entries: initial_recent_entries.into_iter().collect(),
_subscriptions: subscriptions,
_save_recently_opened_entries_task: Task::ready(()),
}
}
pub fn entries(&self, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
let mut history_entries = Vec::new();
#[cfg(debug_assertions)]
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
return history_entries;
}
history_entries.extend(
self.context_store
.read(cx)
.unordered_contexts()
.cloned()
.map(HistoryEntry::Context),
);
history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at()));
history_entries
}
pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
self.entries(cx).into_iter().take(limit).collect()
}
pub fn recently_opened_entries(&self, cx: &App) -> Vec<HistoryEntry> {
#[cfg(debug_assertions)]
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
return Vec::new();
}
let context_entries =
self.context_store
.read(cx)
.unordered_contexts()
.flat_map(|context| {
self.recently_opened_entries
.iter()
.enumerate()
.flat_map(|(index, entry)| match entry {
HistoryEntryId::Context(path) if &context.path == path => {
Some((index, HistoryEntry::Context(context.clone())))
}
_ => None,
})
});
context_entries
// optimization to halt iteration early
.take(self.recently_opened_entries.len())
.sorted_unstable_by_key(|(index, _)| *index)
.map(|(_, entry)| entry)
.collect()
}
fn save_recently_opened_entries(&mut self, cx: &mut Context<Self>) {
let serialized_entries = self
.recently_opened_entries
.iter()
.filter_map(|entry| match entry {
HistoryEntryId::Context(path) => path.file_name().map(|file| {
SerializedRecentOpen::ContextName(file.to_string_lossy().to_string())
}),
HistoryEntryId::Thread(id) => Some(SerializedRecentOpen::Thread(id.to_string())),
})
.collect::<Vec<_>>();
self._save_recently_opened_entries_task = cx.spawn(async move |_, cx| {
cx.background_executor()
.timer(SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE)
.await;
cx.background_spawn(async move {
let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
let content = serde_json::to_string(&serialized_entries)?;
std::fs::write(path, content)?;
anyhow::Ok(())
})
.await
.log_err();
});
}
fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<Vec<HistoryEntryId>>> {
cx.background_spawn(async move {
let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
let contents = match smol::fs::read_to_string(path).await {
Ok(it) => it,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Ok(Vec::new());
}
Err(e) => {
return Err(e)
.context("deserializing persisted agent panel navigation history");
}
};
let entries = serde_json::from_str::<Vec<SerializedRecentOpen>>(&contents)
.context("deserializing persisted agent panel navigation history")?
.into_iter()
.take(MAX_RECENTLY_OPENED_ENTRIES)
.flat_map(|entry| match entry {
SerializedRecentOpen::Thread(id) => {
Some(HistoryEntryId::Thread(id.as_str().into()))
}
SerializedRecentOpen::ContextName(file_name) => Some(HistoryEntryId::Context(
contexts_dir().join(file_name).into(),
)),
SerializedRecentOpen::Context(path) => {
Path::new(&path).file_name().map(|file_name| {
HistoryEntryId::Context(contexts_dir().join(file_name).into())
})
}
})
.collect::<Vec<_>>();
Ok(entries)
})
}
pub fn push_recently_opened_entry(&mut self, entry: HistoryEntryId, cx: &mut Context<Self>) {
self.recently_opened_entries
.retain(|old_entry| old_entry != &entry);
self.recently_opened_entries.push_front(entry);
self.recently_opened_entries
.truncate(MAX_RECENTLY_OPENED_ENTRIES);
self.save_recently_opened_entries(cx);
}
pub fn remove_recently_opened_thread(&mut self, id: ThreadId, cx: &mut Context<Self>) {
self.recently_opened_entries.retain(
|entry| !matches!(entry, HistoryEntryId::Thread(thread_id) if thread_id == &id),
);
self.save_recently_opened_entries(cx);
}
pub fn replace_recently_opened_text_thread(
&mut self,
old_path: &Path,
new_path: &Arc<Path>,
cx: &mut Context<Self>,
) {
for entry in &mut self.recently_opened_entries {
match entry {
HistoryEntryId::Context(path) if path.as_ref() == old_path => {
*entry = HistoryEntryId::Context(new_path.clone());
break;
}
_ => {}
}
}
self.save_recently_opened_entries(cx);
}
pub fn remove_recently_opened_entry(&mut self, entry: &HistoryEntryId, cx: &mut Context<Self>) {
self.recently_opened_entries
.retain(|old_entry| old_entry != entry);
self.save_recently_opened_entries(cx);
}
}

View File

@@ -234,6 +234,7 @@ impl MessageSegment {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ProjectSnapshot {
pub worktree_snapshots: Vec<WorktreeSnapshot>,
pub unsaved_buffer_paths: Vec<String>,
pub timestamp: DateTime<Utc>,
}
@@ -1276,6 +1277,62 @@ impl Thread {
);
}
pub fn retry_last_completion(
&mut self,
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) {
// Clear any existing error state
self.retry_state = None;
// Use the last error context if available, otherwise fall back to configured model
let (model, intent) = if let Some((model, intent)) = self.last_error_context.take() {
(model, intent)
} else if let Some(configured_model) = self.configured_model.as_ref() {
let model = configured_model.model.clone();
let intent = if self.has_pending_tool_uses() {
CompletionIntent::ToolResults
} else {
CompletionIntent::UserPrompt
};
(model, intent)
} else if let Some(configured_model) = self.get_or_init_configured_model(cx) {
let model = configured_model.model.clone();
let intent = if self.has_pending_tool_uses() {
CompletionIntent::ToolResults
} else {
CompletionIntent::UserPrompt
};
(model, intent)
} else {
return;
};
self.send_to_model(model, intent, window, cx);
}
pub fn enable_burn_mode_and_retry(
&mut self,
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) {
self.completion_mode = CompletionMode::Burn;
cx.emit(ThreadEvent::ProfileChanged);
self.retry_last_completion(window, cx);
}
pub fn used_tools_since_last_user_message(&self) -> bool {
for message in self.messages.iter().rev() {
if self.tool_use.message_has_tool_results(message.id) {
return true;
} else if message.role == Role::User {
return false;
}
}
false
}
pub fn to_completion_request(
&self,
model: Arc<dyn LanguageModel>,
@@ -2800,11 +2857,27 @@ impl Thread {
.map(|worktree| Self::worktree_snapshot(worktree, git_store.clone(), cx))
.collect();
cx.spawn(async move |_, _| {
cx.spawn(async move |_, cx| {
let worktree_snapshots = futures::future::join_all(worktree_snapshots).await;
let mut unsaved_buffers = Vec::new();
cx.update(|app_cx| {
let buffer_store = project.read(app_cx).buffer_store();
for buffer_handle in buffer_store.read(app_cx).buffers() {
let buffer = buffer_handle.read(app_cx);
if buffer.is_dirty()
&& let Some(file) = buffer.file()
{
let path = file.path().to_string_lossy().to_string();
unsaved_buffers.push(path);
}
}
})
.ok();
Arc::new(ProjectSnapshot {
worktree_snapshots,
unsaved_buffer_paths: unsaved_buffers,
timestamp: Utc::now(),
})
})
@@ -2819,7 +2892,7 @@ impl Thread {
// Get worktree path and snapshot
let worktree_info = cx.update(|app_cx| {
let worktree = worktree.read(app_cx);
let path = worktree.abs_path().to_string_lossy().into_owned();
let path = worktree.abs_path().to_string_lossy().to_string();
let snapshot = worktree.snapshot();
(path, snapshot)
});
@@ -3202,7 +3275,6 @@ mod tests {
use agent_settings::{AgentProfileId, AgentSettings};
use assistant_tool::ToolRegistry;
use assistant_tools;
use fs::Fs;
use futures::StreamExt;
use futures::future::BoxFuture;
use futures::stream::BoxStream;
@@ -3220,15 +3292,15 @@ mod tests {
use settings::{LanguageModelParameters, Settings, SettingsStore};
use std::sync::Arc;
use std::time::Duration;
use theme::ThemeSettings;
use util::path;
use workspace::Workspace;
#[gpui::test]
async fn test_message_with_context(cx: &mut TestAppContext) {
let fs = init_test_settings(cx);
init_test_settings(cx);
let project = create_test_project(
&fs,
cx,
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
)
@@ -3303,10 +3375,9 @@ fn main() {{
#[gpui::test]
async fn test_only_include_new_contexts(cx: &mut TestAppContext) {
let fs = init_test_settings(cx);
init_test_settings(cx);
let project = create_test_project(
&fs,
cx,
json!({
"file1.rs": "fn function1() {}\n",
@@ -3460,10 +3531,9 @@ fn main() {{
#[gpui::test]
async fn test_message_without_files(cx: &mut TestAppContext) {
let fs = init_test_settings(cx);
init_test_settings(cx);
let project = create_test_project(
&fs,
cx,
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
)
@@ -3540,10 +3610,9 @@ fn main() {{
#[gpui::test]
#[ignore] // turn this test on when project_notifications tool is re-enabled
async fn test_stale_buffer_notification(cx: &mut TestAppContext) {
let fs = init_test_settings(cx);
init_test_settings(cx);
let project = create_test_project(
&fs,
cx,
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
)
@@ -3669,10 +3738,9 @@ fn main() {{
#[gpui::test]
async fn test_storing_profile_setting_per_thread(cx: &mut TestAppContext) {
let fs = init_test_settings(cx);
init_test_settings(cx);
let project = create_test_project(
&fs,
cx,
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
)
@@ -3692,10 +3760,9 @@ fn main() {{
#[gpui::test]
async fn test_serializing_thread_profile(cx: &mut TestAppContext) {
let fs = init_test_settings(cx);
init_test_settings(cx);
let project = create_test_project(
&fs,
cx,
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
)
@@ -3736,10 +3803,9 @@ fn main() {{
#[gpui::test]
async fn test_temperature_setting(cx: &mut TestAppContext) {
let fs = init_test_settings(cx);
init_test_settings(cx);
let project = create_test_project(
&fs,
cx,
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
)
@@ -3831,9 +3897,9 @@ fn main() {{
#[gpui::test]
async fn test_thread_summary(cx: &mut TestAppContext) {
let fs = init_test_settings(cx);
init_test_settings(cx);
let project = create_test_project(&fs, cx, json!({})).await;
let project = create_test_project(cx, json!({})).await;
let (_, _thread_store, thread, _context_store, model) =
setup_test_environment(cx, project.clone()).await;
@@ -3916,9 +3982,9 @@ fn main() {{
#[gpui::test]
async fn test_thread_summary_error_set_manually(cx: &mut TestAppContext) {
let fs = init_test_settings(cx);
init_test_settings(cx);
let project = create_test_project(&fs, cx, json!({})).await;
let project = create_test_project(cx, json!({})).await;
let (_, _thread_store, thread, _context_store, model) =
setup_test_environment(cx, project.clone()).await;
@@ -3938,9 +4004,9 @@ fn main() {{
#[gpui::test]
async fn test_thread_summary_error_retry(cx: &mut TestAppContext) {
let fs = init_test_settings(cx);
init_test_settings(cx);
let project = create_test_project(&fs, cx, json!({})).await;
let project = create_test_project(cx, json!({})).await;
let (_, _thread_store, thread, _context_store, model) =
setup_test_environment(cx, project.clone()).await;
@@ -4092,9 +4158,9 @@ fn main() {{
#[gpui::test]
async fn test_retry_on_overloaded_error(cx: &mut TestAppContext) {
let fs = init_test_settings(cx);
init_test_settings(cx);
let project = create_test_project(&fs, cx, json!({})).await;
let project = create_test_project(cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
@@ -4170,9 +4236,9 @@ fn main() {{
#[gpui::test]
async fn test_retry_on_internal_server_error(cx: &mut TestAppContext) {
let fs = init_test_settings(cx);
init_test_settings(cx);
let project = create_test_project(&fs, cx, json!({})).await;
let project = create_test_project(cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
@@ -4252,9 +4318,9 @@ fn main() {{
#[gpui::test]
async fn test_exponential_backoff_on_retries(cx: &mut TestAppContext) {
let fs = init_test_settings(cx);
init_test_settings(cx);
let project = create_test_project(&fs, cx, json!({})).await;
let project = create_test_project(cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
@@ -4372,9 +4438,9 @@ fn main() {{
#[gpui::test]
async fn test_max_retries_exceeded(cx: &mut TestAppContext) {
let fs = init_test_settings(cx);
init_test_settings(cx);
let project = create_test_project(&fs, cx, json!({})).await;
let project = create_test_project(cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
@@ -4463,9 +4529,9 @@ fn main() {{
#[gpui::test]
async fn test_retry_message_removed_on_retry(cx: &mut TestAppContext) {
let fs = init_test_settings(cx);
init_test_settings(cx);
let project = create_test_project(&fs, cx, json!({})).await;
let project = create_test_project(cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
@@ -4636,9 +4702,9 @@ fn main() {{
#[gpui::test]
async fn test_successful_completion_clears_retry_state(cx: &mut TestAppContext) {
let fs = init_test_settings(cx);
init_test_settings(cx);
let project = create_test_project(&fs, cx, json!({})).await;
let project = create_test_project(cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
@@ -4802,9 +4868,9 @@ fn main() {{
#[gpui::test]
async fn test_rate_limit_retry_single_attempt(cx: &mut TestAppContext) {
let fs = init_test_settings(cx);
init_test_settings(cx);
let project = create_test_project(&fs, cx, json!({})).await;
let project = create_test_project(cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
@@ -4987,9 +5053,9 @@ fn main() {{
#[gpui::test]
async fn test_ui_only_messages_not_sent_to_model(cx: &mut TestAppContext) {
let fs = init_test_settings(cx);
init_test_settings(cx);
let project = create_test_project(&fs, cx, json!({})).await;
let project = create_test_project(cx, json!({})).await;
let (_, _, thread, _, model) = setup_test_environment(cx, project.clone()).await;
// Insert a regular user message
@@ -5087,9 +5153,9 @@ fn main() {{
#[gpui::test]
async fn test_no_retry_without_burn_mode(cx: &mut TestAppContext) {
let fs = init_test_settings(cx);
init_test_settings(cx);
let project = create_test_project(&fs, cx, json!({})).await;
let project = create_test_project(cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Ensure we're in Normal mode (not Burn mode)
@@ -5160,9 +5226,9 @@ fn main() {{
#[gpui::test]
async fn test_retry_canceled_on_stop(cx: &mut TestAppContext) {
let fs = init_test_settings(cx);
init_test_settings(cx);
let project = create_test_project(&fs, cx, json!({})).await;
let project = create_test_project(cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
@@ -5268,8 +5334,7 @@ fn main() {{
cx.run_until_parked();
}
fn init_test_settings(cx: &mut TestAppContext) -> Arc<dyn Fs> {
let fs = FakeFs::new(cx.executor());
fn init_test_settings(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
@@ -5277,10 +5342,10 @@ fn main() {{
Project::init_settings(cx);
AgentSettings::register(cx);
prompt_store::init(cx);
thread_store::init(fs.clone(), cx);
thread_store::init(cx);
workspace::init_settings(cx);
language_model::init_settings(cx);
theme::init(theme::LoadThemes::JustBase, cx);
ThemeSettings::register(cx);
ToolRegistry::default_global(cx);
assistant_tool::init(cx);
@@ -5291,17 +5356,16 @@ fn main() {{
));
assistant_tools::init(http_client, cx);
});
fs
}
// Helper to create a test project with test files
async fn create_test_project(
fs: &Arc<dyn Fs>,
cx: &mut TestAppContext,
files: serde_json::Value,
) -> Entity<Project> {
fs.as_fake().insert_tree(path!("/test"), files).await;
Project::test(fs.clone(), [path!("/test").as_ref()], cx).await
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(

View File

@@ -10,7 +10,6 @@ use assistant_tool::{Tool, ToolId, ToolWorkingSet};
use chrono::{DateTime, Utc};
use collections::HashMap;
use context_server::ContextServerId;
use fs::{Fs, RemoveOptions};
use futures::{
FutureExt as _, StreamExt as _,
channel::{mpsc, oneshot},
@@ -38,9 +37,9 @@ use std::{
cell::{Ref, RefCell},
path::{Path, PathBuf},
rc::Rc,
sync::{Arc, LazyLock, Mutex},
sync::{Arc, Mutex},
};
use util::{ResultExt as _, rel_path::RelPath};
use util::ResultExt as _;
use zed_env_vars::ZED_STATELESS;
@@ -74,22 +73,20 @@ impl Column for DataType {
}
}
static RULES_FILE_NAMES: LazyLock<[&RelPath; 9]> = LazyLock::new(|| {
[
RelPath::unix(".rules").unwrap(),
RelPath::unix(".cursorrules").unwrap(),
RelPath::unix(".windsurfrules").unwrap(),
RelPath::unix(".clinerules").unwrap(),
RelPath::unix(".github/copilot-instructions.md").unwrap(),
RelPath::unix("CLAUDE.md").unwrap(),
RelPath::unix("AGENT.md").unwrap(),
RelPath::unix("AGENTS.md").unwrap(),
RelPath::unix("GEMINI.md").unwrap(),
]
});
const RULES_FILE_NAMES: [&str; 9] = [
".rules",
".cursorrules",
".windsurfrules",
".clinerules",
".github/copilot-instructions.md",
"CLAUDE.md",
"AGENT.md",
"AGENTS.md",
"GEMINI.md",
];
pub fn init(fs: Arc<dyn Fs>, cx: &mut App) {
ThreadsDatabase::init(fs, cx);
pub fn init(cx: &mut App) {
ThreadsDatabase::init(cx);
}
/// A system prompt shared by all threads created by this ThreadStore
@@ -234,10 +231,11 @@ impl ThreadStore {
self.enqueue_system_prompt_reload();
}
project::Event::WorktreeUpdatedEntries(_, items) => {
if items
.iter()
.any(|(path, _, _)| RULES_FILE_NAMES.iter().any(|name| path.as_ref() == *name))
{
if items.iter().any(|(path, _, _)| {
RULES_FILE_NAMES
.iter()
.any(|name| path.as_ref() == Path::new(name))
}) {
self.enqueue_system_prompt_reload();
}
}
@@ -329,7 +327,7 @@ impl ThreadStore {
cx: &mut App,
) -> Task<(WorktreeContext, Option<RulesLoadingError>)> {
let tree = worktree.read(cx);
let root_name = tree.root_name_str().into();
let root_name = tree.root_name().into();
let abs_path = tree.abs_path();
let mut context = WorktreeContext {
@@ -871,13 +869,13 @@ impl ThreadsDatabase {
GlobalThreadsDatabase::global(cx).0.clone()
}
fn init(fs: Arc<dyn Fs>, cx: &mut App) {
fn init(cx: &mut App) {
let executor = cx.background_executor().clone();
let database_future = executor
.spawn({
let executor = executor.clone();
let threads_dir = paths::data_dir().join("threads");
async move { ThreadsDatabase::new(fs, threads_dir, executor).await }
async move { ThreadsDatabase::new(threads_dir, executor) }
})
.then(|result| future::ready(result.map(Arc::new).map_err(Arc::new)))
.boxed()
@@ -886,17 +884,13 @@ impl ThreadsDatabase {
cx.set_global(GlobalThreadsDatabase(database_future));
}
pub async fn new(
fs: Arc<dyn Fs>,
threads_dir: PathBuf,
executor: BackgroundExecutor,
) -> Result<Self> {
fs.create_dir(&threads_dir).await?;
pub fn new(threads_dir: PathBuf, executor: BackgroundExecutor) -> Result<Self> {
std::fs::create_dir_all(&threads_dir)?;
let sqlite_path = threads_dir.join("threads.db");
let mdb_path = threads_dir.join("threads-db.1.mdb");
let needs_migration_from_heed = fs.is_file(&mdb_path).await;
let needs_migration_from_heed = mdb_path.exists();
let connection = if *ZED_STATELESS {
Connection::open_memory(Some("THREAD_FALLBACK_DB"))
@@ -938,14 +932,7 @@ impl ThreadsDatabase {
.spawn(async move {
log::info!("Starting threads.db migration");
Self::migrate_from_heed(&mdb_path, db_connection, executor_clone)?;
fs.remove_dir(
&mdb_path,
RemoveOptions {
recursive: true,
ignore_if_not_exists: true,
},
)
.await?;
std::fs::remove_dir_all(mdb_path)?;
log::info!("threads.db migrated to sqlite");
Ok::<(), anyhow::Error>(())
})

View File

@@ -27,7 +27,6 @@ use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::Arc;
use util::ResultExt;
use util::rel_path::RelPath;
const RULES_FILE_NAMES: [&str; 9] = [
".rules",
@@ -435,7 +434,7 @@ impl NativeAgent {
cx: &mut App,
) -> Task<(WorktreeContext, Option<RulesLoadingError>)> {
let tree = worktree.read(cx);
let root_name = tree.root_name_str().into();
let root_name = tree.root_name().into();
let abs_path = tree.abs_path();
let mut context = WorktreeContext {
@@ -475,7 +474,7 @@ impl NativeAgent {
.into_iter()
.filter_map(|name| {
worktree
.entry_for_path(RelPath::unix(name).unwrap())
.entry_for_path(name)
.filter(|entry| entry.is_file())
.map(|entry| entry.path.clone())
})
@@ -559,7 +558,7 @@ impl NativeAgent {
if items.iter().any(|(path, _, _)| {
RULES_FILE_NAMES
.iter()
.any(|name| path.as_ref() == RelPath::unix(name).unwrap())
.any(|name| path.as_ref() == Path::new(name))
}) {
self.project_context_needs_refresh.send(()).ok();
}
@@ -1205,11 +1204,11 @@ mod tests {
use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelInfo, MentionUri};
use fs::FakeFs;
use gpui::TestAppContext;
use indoc::formatdoc;
use indoc::indoc;
use language_model::fake_provider::FakeLanguageModel;
use serde_json::json;
use settings::SettingsStore;
use util::{path, rel_path::rel_path};
use util::path;
#[gpui::test]
async fn test_maintaining_project_context(cx: &mut TestAppContext) {
@@ -1259,17 +1258,14 @@ mod tests {
fs.insert_file("/a/.rules", Vec::new()).await;
cx.run_until_parked();
agent.read_with(cx, |agent, cx| {
let rules_entry = worktree
.read(cx)
.entry_for_path(rel_path(".rules"))
.unwrap();
let rules_entry = worktree.read(cx).entry_for_path(".rules").unwrap();
assert_eq!(
agent.project_context.read(cx).worktrees,
vec![WorktreeContext {
root_name: "a".into(),
abs_path: Path::new("/a").into(),
rules_file: Some(RulesFileContext {
path_in_worktree: rel_path(".rules").into(),
path_in_worktree: Path::new(".rules").into(),
text: "".into(),
project_entry_id: rules_entry.id.to_usize()
})
@@ -1418,6 +1414,7 @@ mod tests {
}
#[gpui::test]
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
async fn test_save_load_thread(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
@@ -1497,22 +1494,17 @@ mod tests {
model.send_last_completion_stream_text_chunk("Lorem.");
model.end_last_completion_stream();
cx.run_until_parked();
summary_model
.send_last_completion_stream_text_chunk(&format!("Explaining {}", path!("/a/b.md")));
summary_model.send_last_completion_stream_text_chunk("Explaining /a/b.md");
summary_model.end_last_completion_stream();
send.await.unwrap();
let uri = MentionUri::File {
abs_path: path!("/a/b.md").into(),
}
.to_uri();
acp_thread.read_with(cx, |thread, cx| {
assert_eq!(
thread.to_markdown(cx),
formatdoc! {"
indoc! {"
## User
What does [@b.md]({uri}) mean?
What does [@b.md](file:///a/b.md) mean?
## Assistant
@@ -1538,7 +1530,7 @@ mod tests {
history_entries(&history_store, cx),
vec![(
HistoryEntryId::AcpThread(session_id.clone()),
format!("Explaining {}", path!("/a/b.md"))
"Explaining /a/b.md".into()
)]
);
let acp_thread = agent
@@ -1548,10 +1540,10 @@ mod tests {
acp_thread.read_with(cx, |thread, cx| {
assert_eq!(
thread.to_markdown(cx),
formatdoc! {"
indoc! {"
## User
What does [@b.md]({uri}) mean?
What does [@b.md](file:///a/b.md) mean?
## Assistant

View File

@@ -422,15 +422,17 @@ mod tests {
use agent::MessageSegment;
use agent::context::LoadedContext;
use client::Client;
use fs::{FakeFs, Fs};
use fs::FakeFs;
use gpui::AppContext;
use gpui::TestAppContext;
use http_client::FakeHttpClient;
use language_model::Role;
use project::Project;
use serde_json::json;
use settings::SettingsStore;
use util::test::TempTree;
fn init_test(fs: Arc<dyn Fs>, cx: &mut TestAppContext) {
fn init_test(cx: &mut TestAppContext) {
env_logger::try_init().ok();
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
@@ -441,7 +443,7 @@ mod tests {
let http_client = FakeHttpClient::with_404_response();
let clock = Arc::new(clock::FakeSystemClock::new());
let client = Client::new(clock, http_client, cx);
agent::init(fs, cx);
agent::init(cx);
agent_settings::init(cx);
language_model::init(client, cx);
});
@@ -449,8 +451,10 @@ mod tests {
#[gpui::test]
async fn test_retrieving_old_thread(cx: &mut TestAppContext) {
let tree = TempTree::new(json!({}));
util::paths::set_home_dir(tree.path().into());
init_test(cx);
let fs = FakeFs::new(cx.executor());
init_test(fs.clone(), cx);
let project = Project::test(fs, [], cx).await;
// Save a thread using the old agent.

View File

@@ -262,7 +262,7 @@ impl HistoryStore {
.iter()
.filter_map(|entry| match entry {
HistoryEntryId::TextThread(path) => path.file_name().map(|file| {
SerializedRecentOpen::TextThread(file.to_string_lossy().into_owned())
SerializedRecentOpen::TextThread(file.to_string_lossy().to_string())
}),
HistoryEntryId::AcpThread(id) => {
Some(SerializedRecentOpen::AcpThread(id.to_string()))

View File

@@ -15,11 +15,10 @@ use agent_settings::{
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::adapt_schema_to_format;
use chrono::{DateTime, Utc};
use client::{ModelRequestUsage, RequestUsage, UserStore};
use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, Plan, UsageLimit};
use client::{ModelRequestUsage, RequestUsage};
use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
use collections::{HashMap, HashSet, IndexMap};
use fs::Fs;
use futures::stream;
use futures::{
FutureExt,
channel::{mpsc, oneshot},
@@ -35,7 +34,7 @@ use language_model::{
LanguageModelImage, LanguageModelProviderId, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse,
LanguageModelToolUseId, Role, SelectedModel, StopReason, TokenUsage, ZED_CLOUD_PROVIDER_ID,
LanguageModelToolUseId, Role, SelectedModel, StopReason, TokenUsage,
};
use project::{
Project,
@@ -586,7 +585,6 @@ pub struct Thread {
pending_title_generation: Option<Task<()>>,
summary: Option<SharedString>,
messages: Vec<Message>,
user_store: Entity<UserStore>,
completion_mode: CompletionMode,
/// Holds the task that handles agent interaction until the end of the turn.
/// Survives across multiple requests as the model performs tool calls and
@@ -643,7 +641,6 @@ impl Thread {
pending_title_generation: None,
summary: None,
messages: Vec::new(),
user_store: project.read(cx).user_store(),
completion_mode: AgentSettings::get_global(cx).preferred_completion_mode,
running_turn: None,
pending_message: None,
@@ -823,7 +820,6 @@ impl Thread {
pending_title_generation: None,
summary: db_thread.detailed_summary,
messages: db_thread.messages,
user_store: project.read(cx).user_store(),
completion_mode: db_thread.completion_mode.unwrap_or_default(),
running_turn: None,
pending_message: None,
@@ -883,11 +879,27 @@ impl Thread {
.map(|worktree| Self::worktree_snapshot(worktree, git_store.clone(), cx))
.collect();
cx.spawn(async move |_, _| {
cx.spawn(async move |_, cx| {
let worktree_snapshots = futures::future::join_all(worktree_snapshots).await;
let mut unsaved_buffers = Vec::new();
cx.update(|app_cx| {
let buffer_store = project.read(app_cx).buffer_store();
for buffer_handle in buffer_store.read(app_cx).buffers() {
let buffer = buffer_handle.read(app_cx);
if buffer.is_dirty()
&& let Some(file) = buffer.file()
{
let path = file.path().to_string_lossy().to_string();
unsaved_buffers.push(path);
}
}
})
.ok();
Arc::new(ProjectSnapshot {
worktree_snapshots,
unsaved_buffer_paths: unsaved_buffers,
timestamp: Utc::now(),
})
})
@@ -902,7 +914,7 @@ impl Thread {
// Get worktree path and snapshot
let worktree_info = cx.update(|app_cx| {
let worktree = worktree.read(app_cx);
let path = worktree.abs_path().to_string_lossy().into_owned();
let path = worktree.abs_path().to_string_lossy().to_string();
let snapshot = worktree.snapshot();
(path, snapshot)
});
@@ -1253,12 +1265,12 @@ impl Thread {
);
log::debug!("Calling model.stream_completion, attempt {}", attempt);
let (mut events, mut error) = match model.stream_completion(request, cx).await {
Ok(events) => (events, None),
Err(err) => (stream::empty().boxed(), Some(err)),
};
let mut events = model
.stream_completion(request, cx)
.await
.map_err(|error| anyhow!(error))?;
let mut tool_results = FuturesUnordered::new();
let mut error = None;
while let Some(event) = events.next().await {
log::trace!("Received completion event: {:?}", event);
match event {
@@ -1306,10 +1318,8 @@ impl Thread {
if let Some(error) = error {
attempt += 1;
let retry = this.update(cx, |this, cx| {
let user_store = this.user_store.read(cx);
this.handle_completion_error(error, attempt, user_store.plan())
})??;
let retry =
this.update(cx, |this, _| this.handle_completion_error(error, attempt))??;
let timer = cx.background_executor().timer(retry.duration);
event_stream.send_retry(retry);
timer.await;
@@ -1336,23 +1346,8 @@ impl Thread {
&mut self,
error: LanguageModelCompletionError,
attempt: u8,
plan: Option<Plan>,
) -> Result<acp_thread::RetryStatus> {
let Some(model) = self.model.as_ref() else {
return Err(anyhow!(error));
};
let auto_retry = if model.provider_id() == ZED_CLOUD_PROVIDER_ID {
match plan {
Some(Plan::V2(_)) => true,
Some(Plan::V1(_)) => self.completion_mode == CompletionMode::Burn,
None => false,
}
} else {
true
};
if !auto_retry {
if self.completion_mode == CompletionMode::Normal {
return Err(anyhow!(error));
}

View File

@@ -84,7 +84,9 @@ impl AgentTool for CopyPathTool {
.and_then(|project_path| project.entry_for_path(&project_path, cx))
{
Some(entity) => match project.find_project_path(&input.destination_path, cx) {
Some(project_path) => project.copy_entry(entity.id, project_path, cx),
Some(project_path) => {
project.copy_entry(entity.id, None, project_path.path, cx)
}
None => Task::ready(Err(anyhow!(
"Destination path {} was outside the project.",
input.destination_path

View File

@@ -6,7 +6,7 @@ use language::{DiagnosticSeverity, OffsetRangeExt};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{fmt::Write, sync::Arc};
use std::{fmt::Write, path::Path, sync::Arc};
use ui::SharedString;
use util::markdown::MarkdownInlineCode;
@@ -147,7 +147,9 @@ impl AgentTool for DiagnosticsTool {
has_diagnostics = true;
output.push_str(&format!(
"{}: {} error(s), {} warning(s)\n",
worktree.read(cx).absolutize(&project_path.path).display(),
Path::new(worktree.read(cx).root_name())
.join(project_path.path)
.display(),
summary.error_count,
summary.warning_count
));

View File

@@ -17,12 +17,10 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
use smol::stream::StreamExt as _;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use ui::SharedString;
use util::ResultExt;
use util::rel_path::RelPath;
const DEFAULT_UI_TEXT: &str = "Editing file";
@@ -150,11 +148,12 @@ impl EditFileTool {
// If any path component matches the local settings folder, then this could affect
// the editor in ways beyond the project source, so prompt.
let local_settings_folder = paths::local_settings_folder_name();
let local_settings_folder = paths::local_settings_folder_relative_path();
let path = Path::new(&input.path);
if path.components().any(|component| {
component.as_os_str() == <_ as AsRef<OsStr>>::as_ref(&local_settings_folder)
}) {
if path
.components()
.any(|component| component.as_os_str() == local_settings_folder.as_os_str())
{
return event_stream.authorize(
format!("{} (local settings)", input.display_description),
cx,
@@ -163,7 +162,6 @@ impl EditFileTool {
// It's also possible that the global config dir is configured to be inside the project,
// so check for that edge case too.
// TODO this is broken when remoting
if let Ok(canonical_path) = std::fs::canonicalize(&input.path)
&& canonical_path.starts_with(paths::config_dir())
{
@@ -218,7 +216,9 @@ impl AgentTool for EditFileTool {
.read(cx)
.short_full_path_for_project_path(&project_path, cx)
})
.unwrap_or(input.path.to_string_lossy().into_owned())
.unwrap_or(Path::new(&input.path).into())
.to_string_lossy()
.to_string()
.into(),
Err(raw_input) => {
if let Some(input) =
@@ -235,7 +235,9 @@ impl AgentTool for EditFileTool {
.read(cx)
.short_full_path_for_project_path(&project_path, cx)
})
.unwrap_or(input.path)
.unwrap_or(Path::new(&input.path).into())
.to_string_lossy()
.to_string()
.into();
}
@@ -476,7 +478,7 @@ impl AgentTool for EditFileTool {
) -> Result<()> {
event_stream.update_diff(cx.new(|cx| {
Diff::finalized(
output.input_path.to_string_lossy().into_owned(),
output.input_path,
Some(output.old_text.to_string()),
output.new_text,
self.language_registry.clone(),
@@ -540,12 +542,10 @@ fn resolve_path(
let file_name = input
.path
.file_name()
.and_then(|file_name| file_name.to_str())
.and_then(|file_name| RelPath::unix(file_name).ok())
.context("Can't create file: invalid filename")?;
let new_file_path = parent_project_path.map(|parent| ProjectPath {
path: parent.path.join(file_name),
path: Arc::from(parent.path.join(file_name)),
..parent
});
@@ -565,7 +565,7 @@ mod tests {
use prompt_store::ProjectContext;
use serde_json::json;
use settings::SettingsStore;
use util::{path, rel_path::rel_path};
use util::path;
#[gpui::test]
async fn test_edit_nonexistent_file(cx: &mut TestAppContext) {
@@ -614,13 +614,13 @@ mod tests {
let mode = &EditFileMode::Create;
let result = test_resolve_path(mode, "root/new.txt", cx);
assert_resolved_path_eq(result.await, rel_path("new.txt"));
assert_resolved_path_eq(result.await, "new.txt");
let result = test_resolve_path(mode, "new.txt", cx);
assert_resolved_path_eq(result.await, rel_path("new.txt"));
assert_resolved_path_eq(result.await, "new.txt");
let result = test_resolve_path(mode, "dir/new.txt", cx);
assert_resolved_path_eq(result.await, rel_path("dir/new.txt"));
assert_resolved_path_eq(result.await, "dir/new.txt");
let result = test_resolve_path(mode, "root/dir/subdir/existing.txt", cx);
assert_eq!(
@@ -642,10 +642,10 @@ mod tests {
let path_with_root = "root/dir/subdir/existing.txt";
let path_without_root = "dir/subdir/existing.txt";
let result = test_resolve_path(mode, path_with_root, cx);
assert_resolved_path_eq(result.await, rel_path(path_without_root));
assert_resolved_path_eq(result.await, path_without_root);
let result = test_resolve_path(mode, path_without_root, cx);
assert_resolved_path_eq(result.await, rel_path(path_without_root));
assert_resolved_path_eq(result.await, path_without_root);
let result = test_resolve_path(mode, "root/nonexistent.txt", cx);
assert_eq!(
@@ -690,10 +690,14 @@ mod tests {
cx.update(|cx| resolve_path(&input, project, cx))
}
#[track_caller]
fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &RelPath) {
let actual = path.expect("Should return valid path").path;
assert_eq!(actual.as_ref(), expected);
fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &str) {
let actual = path
.expect("Should return valid path")
.path
.to_str()
.unwrap()
.replace("\\", "/"); // Naive Windows paths normalization
assert_eq!(actual, expected);
}
#[gpui::test]
@@ -790,7 +794,7 @@ mod tests {
store.update_user_settings(cx, |settings| {
settings.project.all_languages.defaults.format_on_save = Some(FormatOnSave::On);
settings.project.all_languages.defaults.formatter =
Some(language::language_settings::FormatterList::default());
Some(language::language_settings::SelectedFormatter::Auto);
});
});
});
@@ -1404,8 +1408,8 @@ mod tests {
// Parent directory references - find_project_path resolves these
(
"project/../other",
true,
"Path with .. that goes outside of root directory",
false,
"Path with .. is resolved by find_project_path",
),
(
"project/./src/file.rs",
@@ -1433,18 +1437,16 @@ mod tests {
)
});
cx.run_until_parked();
if should_confirm {
stream_rx.expect_authorization().await;
} else {
auth.await.unwrap();
assert!(
stream_rx.try_next().is_err(),
"Failed for case: {} - path: {} - expected no confirmation but got one",
description,
path
);
auth.await.unwrap();
}
}
}

View File

@@ -156,14 +156,10 @@ impl AgentTool for FindPathTool {
}
fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Result<Vec<PathBuf>>> {
let path_style = project.read(cx).path_style(cx);
let path_matcher = match PathMatcher::new(
[
// Sometimes models try to search for "". In this case, return all paths in the project.
if glob.is_empty() { "*" } else { glob },
],
path_style,
) {
let path_matcher = match PathMatcher::new([
// Sometimes models try to search for "". In this case, return all paths in the project.
if glob.is_empty() { "*" } else { glob },
]) {
Ok(matcher) => matcher,
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))),
};
@@ -177,8 +173,9 @@ fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Resu
let mut results = Vec::new();
for snapshot in snapshots {
for entry in snapshot.entries(false, 0) {
if path_matcher.is_match(snapshot.root_name().join(&entry.path).as_std_path()) {
results.push(snapshot.absolutize(&entry.path));
let root_name = PathBuf::from(snapshot.root_name());
if path_matcher.is_match(root_name.join(&entry.path)) {
results.push(snapshot.abs_path().join(entry.path.as_ref()));
}
}
}

View File

@@ -110,15 +110,12 @@ impl AgentTool for GrepTool {
const CONTEXT_LINES: u32 = 2;
const MAX_ANCESTOR_LINES: u32 = 10;
let path_style = self.project.read(cx).path_style(cx);
let include_matcher = match PathMatcher::new(
input
.include_pattern
.as_ref()
.into_iter()
.collect::<Vec<_>>(),
path_style,
) {
Ok(matcher) => matcher,
Err(error) => {
@@ -135,7 +132,7 @@ impl AgentTool for GrepTool {
.iter()
.chain(global_settings.private_files.sources().iter());
match PathMatcher::new(exclude_patterns, path_style) {
match PathMatcher::new(exclude_patterns) {
Ok(matcher) => matcher,
Err(error) => {
return Task::ready(Err(anyhow!("invalid exclude pattern: {error}")));

View File

@@ -2,12 +2,12 @@ use crate::{AgentTool, ToolCallEventStream};
use agent_client_protocol::ToolKind;
use anyhow::{Result, anyhow};
use gpui::{App, Entity, SharedString, Task};
use project::{Project, ProjectPath, WorktreeSettings};
use project::{Project, WorktreeSettings};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
use std::fmt::Write;
use std::sync::Arc;
use std::{path::Path, sync::Arc};
use util::markdown::MarkdownInlineCode;
/// Lists files and directories in a given path. Prefer the `grep` or `find_path` tools when searching the codebase.
@@ -86,13 +86,13 @@ impl AgentTool for ListDirectoryTool {
.read(cx)
.worktrees(cx)
.filter_map(|worktree| {
let worktree = worktree.read(cx);
let root_entry = worktree.root_entry()?;
if root_entry.is_dir() {
Some(root_entry.path.display(worktree.path_style()))
} else {
None
}
worktree.read(cx).root_entry().and_then(|entry| {
if entry.is_dir() {
entry.path.to_str()
} else {
None
}
})
})
.collect::<Vec<_>>()
.join("\n");
@@ -143,7 +143,7 @@ impl AgentTool for ListDirectoryTool {
}
let worktree_snapshot = worktree.read(cx).snapshot();
let worktree_root_name = worktree.read(cx).root_name();
let worktree_root_name = worktree.read(cx).root_name().to_string();
let Some(entry) = worktree_snapshot.entry_for_path(&project_path.path) else {
return Task::ready(Err(anyhow!("Path not found: {}", input.path)));
@@ -165,17 +165,25 @@ impl AgentTool for ListDirectoryTool {
continue;
}
let project_path: ProjectPath = (worktree_snapshot.id(), entry.path.clone()).into();
if worktree_settings.is_path_excluded(&project_path.path)
|| worktree_settings.is_path_private(&project_path.path)
if self
.project
.read(cx)
.find_project_path(&entry.path, cx)
.map(|project_path| {
let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx);
worktree_settings.is_path_excluded(&project_path.path)
|| worktree_settings.is_path_private(&project_path.path)
})
.unwrap_or(false)
{
continue;
}
let full_path = worktree_root_name
let full_path = Path::new(&worktree_root_name)
.join(&entry.path)
.display(worktree_snapshot.path_style())
.into_owned();
.display()
.to_string();
if entry.is_dir() {
folders.push(full_path);
} else {

View File

@@ -98,7 +98,7 @@ impl AgentTool for MovePathTool {
.and_then(|project_path| project.entry_for_path(&project_path, cx))
{
Some(entity) => match project.find_project_path(&input.destination_path, cx) {
Some(project_path) => project.rename_entry(entity.id, project_path, cx),
Some(project_path) => project.rename_entry(entity.id, project_path.path, cx),
None => Task::ready(Err(anyhow!(
"Destination path {} was outside the project.",
input.destination_path

View File

@@ -104,7 +104,7 @@ mod tests {
async fn test_to_absolute_path(cx: &mut TestAppContext) {
init_test(cx);
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let temp_path = temp_dir.path().to_string_lossy().into_owned();
let temp_path = temp_dir.path().to_string_lossy().to_string();
let fs = FakeFs::new(cx.executor());
fs.insert_tree(

View File

@@ -82,12 +82,12 @@ impl AgentTool for ReadFileTool {
{
match (input.start_line, input.end_line) {
(Some(start), Some(end)) => {
format!("Read file `{path}` (lines {}-{})", start, end,)
format!("Read file `{}` (lines {}-{})", path.display(), start, end,)
}
(Some(start), None) => {
format!("Read file `{path}` (from line {})", start)
format!("Read file `{}` (from line {})", path.display(), start)
}
_ => format!("Read file `{path}`"),
_ => format!("Read file `{}`", path.display()),
}
.into()
} else {
@@ -225,12 +225,9 @@ impl AgentTool for ReadFileTool {
Ok(result.into())
} else {
// No line ranges specified, so check file size to see if it's too big.
let buffer_content = outline::get_buffer_content_or_outline(
buffer.clone(),
Some(&abs_path.to_string_lossy()),
cx,
)
.await?;
let buffer_content =
outline::get_buffer_content_or_outline(buffer.clone(), Some(&abs_path), cx)
.await?;
action_log.update(cx, |log, cx| {
log.buffer_read(buffer.clone(), cx);

View File

@@ -82,7 +82,7 @@ impl AgentTool for TerminalTool {
.into(),
}
} else {
"".into()
"Run terminal command".into()
}
}

View File

@@ -47,8 +47,6 @@ task.workspace = true
tempfile.workspace = true
thiserror.workspace = true
ui.workspace = true
terminal.workspace = true
uuid.workspace = true
util.workspace = true
watch.workspace = true
workspace-hack.workspace = true

View File

@@ -9,8 +9,7 @@ use futures::io::BufReader;
use project::Project;
use project::agent_server_store::AgentServerCommand;
use serde::Deserialize;
use task::Shell;
use util::{ResultExt as _, get_default_system_shell_preferring_bash};
use util::ResultExt as _;
use std::path::PathBuf;
use std::{any::Any, cell::RefCell};
@@ -20,9 +19,7 @@ use thiserror::Error;
use anyhow::{Context as _, Result};
use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString, Task, WeakEntity};
use acp_thread::{AcpThread, AuthRequired, LoadError, TerminalProviderEvent};
use terminal::TerminalBuilder;
use terminal::terminal_settings::{AlternateScroll, CursorShape};
use acp_thread::{AcpThread, AuthRequired, LoadError};
#[derive(Debug, Error)]
#[error("Unsupported version")]
@@ -82,7 +79,7 @@ impl AcpConnection {
is_remote: bool,
cx: &mut AsyncApp,
) -> Result<Self> {
let mut child = util::command::new_smol_command(&command.path);
let mut child = util::command::new_smol_command(command.path);
child
.args(command.args.iter().map(|arg| arg.as_str()))
.envs(command.env.iter().flatten())
@@ -97,11 +94,6 @@ impl AcpConnection {
let stdout = child.stdout.take().context("Failed to take stdout")?;
let stdin = child.stdin.take().context("Failed to take stdin")?;
let stderr = child.stderr.take().context("Failed to take stderr")?;
log::info!(
"Spawning external agent server: {:?}, {:?}",
command.path,
command.args
);
log::trace!("Spawned (pid: {})", child.id());
let sessions = Rc::new(RefCell::new(HashMap::default()));
@@ -168,10 +160,7 @@ impl AcpConnection {
meta: None,
},
terminal: true,
meta: Some(serde_json::json!({
// Experimental: Allow for rendering terminal output from the agents
"terminal_output": true,
})),
meta: None,
},
meta: None,
})
@@ -391,10 +380,6 @@ impl AgentConnection for AcpConnection {
match result {
Ok(response) => Ok(response),
Err(err) => {
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
return Err(anyhow!(acp::Error::auth_required()));
}
if err.code != ErrorCode::INTERNAL_ERROR.code {
anyhow::bail!(err)
}
@@ -711,100 +696,10 @@ impl acp::Client for ClientDelegate {
}
}
// Clone so we can inspect meta both before and after handing off to the thread
let update_clone = notification.update.clone();
// Pre-handle: if a ToolCall carries terminal_info, create/register a display-only terminal.
if let acp::SessionUpdate::ToolCall(tc) = &update_clone {
if let Some(meta) = &tc.meta {
if let Some(terminal_info) = meta.get("terminal_info") {
if let Some(id_str) = terminal_info.get("terminal_id").and_then(|v| v.as_str())
{
let terminal_id = acp::TerminalId(id_str.into());
let cwd = terminal_info
.get("cwd")
.and_then(|v| v.as_str().map(PathBuf::from));
// Create a minimal display-only lower-level terminal and register it.
let _ = session.thread.update(&mut self.cx.clone(), |thread, cx| {
let builder = TerminalBuilder::new_display_only(
CursorShape::default(),
AlternateScroll::On,
None,
0,
)?;
let lower = cx.new(|cx| builder.subscribe(cx));
thread.on_terminal_provider_event(
TerminalProviderEvent::Created {
terminal_id: terminal_id.clone(),
label: tc.title.clone(),
cwd,
output_byte_limit: None,
terminal: lower,
},
cx,
);
anyhow::Ok(())
});
}
}
}
}
// Forward the update to the acp_thread as usual.
session.thread.update(&mut self.cx.clone(), |thread, cx| {
thread.handle_session_update(notification.update.clone(), cx)
thread.handle_session_update(notification.update, cx)
})??;
// Post-handle: stream terminal output/exit if present on ToolCallUpdate meta.
if let acp::SessionUpdate::ToolCallUpdate(tcu) = &update_clone {
if let Some(meta) = &tcu.meta {
if let Some(term_out) = meta.get("terminal_output") {
if let Some(id_str) = term_out.get("terminal_id").and_then(|v| v.as_str()) {
let terminal_id = acp::TerminalId(id_str.into());
if let Some(s) = term_out.get("data").and_then(|v| v.as_str()) {
let data = s.as_bytes().to_vec();
let _ = session.thread.update(&mut self.cx.clone(), |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Output {
terminal_id: terminal_id.clone(),
data,
},
cx,
);
});
}
}
}
// terminal_exit
if let Some(term_exit) = meta.get("terminal_exit") {
if let Some(id_str) = term_exit.get("terminal_id").and_then(|v| v.as_str()) {
let terminal_id = acp::TerminalId(id_str.into());
let status = acp::TerminalExitStatus {
exit_code: term_exit
.get("exit_code")
.and_then(|v| v.as_u64())
.map(|i| i as u32),
signal: term_exit
.get("signal")
.and_then(|v| v.as_str().map(|s| s.to_string())),
meta: None,
};
let _ = session.thread.update(&mut self.cx.clone(), |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Exit {
terminal_id: terminal_id.clone(),
status,
},
cx,
);
});
}
}
}
}
Ok(())
}
@@ -812,71 +707,25 @@ impl acp::Client for ClientDelegate {
&self,
args: acp::CreateTerminalRequest,
) -> Result<acp::CreateTerminalResponse, acp::Error> {
let thread = self.session_thread(&args.session_id)?;
let project = thread.read_with(&self.cx, |thread, _cx| thread.project().clone())?;
let mut env = if let Some(dir) = &args.cwd {
project
.update(&mut self.cx.clone(), |project, cx| {
project.directory_environment(&task::Shell::System, dir.clone().into(), cx)
})?
.await
.unwrap_or_default()
} else {
Default::default()
};
for var in args.env {
env.insert(var.name, var.value);
}
// Use remote shell or default system shell, as appropriate
let shell = project
.update(&mut self.cx.clone(), |project, cx| {
project
.remote_client()
.and_then(|r| r.read(cx).default_system_shell())
.map(Shell::Program)
})?
.unwrap_or_else(|| Shell::Program(get_default_system_shell_preferring_bash()));
let is_windows = project
.read_with(&self.cx, |project, cx| project.path_style(cx).is_windows())
.unwrap_or(cfg!(windows));
let (task_command, task_args) = task::ShellBuilder::new(&shell, is_windows)
.redirect_stdin_to_dev_null()
.build(Some(args.command.clone()), &args.args);
let terminal_entity = project
.update(&mut self.cx.clone(), |project, cx| {
project.create_terminal_task(
task::SpawnInTerminal {
command: Some(task_command),
args: task_args,
cwd: args.cwd.clone(),
env,
..Default::default()
},
let terminal = self
.session_thread(&args.session_id)?
.update(&mut self.cx.clone(), |thread, cx| {
thread.create_terminal(
args.command,
args.args,
args.env,
args.cwd,
args.output_byte_limit,
cx,
)
})?
.await?;
// Register with renderer
let terminal_entity = thread.update(&mut self.cx.clone(), |thread, cx| {
thread.register_terminal_created(
acp::TerminalId(uuid::Uuid::new_v4().to_string().into()),
format!("{} {}", args.command, args.args.join(" ")),
args.cwd.clone(),
args.output_byte_limit,
terminal_entity,
cx,
)
})?;
let terminal_id =
terminal_entity.read_with(&self.cx, |terminal, _| terminal.id().clone())?;
Ok(acp::CreateTerminalResponse {
terminal_id,
meta: None,
})
Ok(
terminal.read_with(&self.cx, |terminal, _| acp::CreateTerminalResponse {
terminal_id: terminal.id().clone(),
meta: None,
})?,
)
}
async fn kill_terminal_command(

View File

@@ -1,6 +1,5 @@
mod acp;
mod claude;
mod codex;
mod custom;
mod gemini;
@@ -9,7 +8,6 @@ pub mod e2e_tests;
pub use claude::*;
use client::ProxySettings;
pub use codex::*;
use collections::HashMap;
pub use custom::*;
use fs::Fs;

View File

@@ -62,7 +62,7 @@ impl AgentServer for ClaudeCode {
cx: &mut App,
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
let name = self.name();
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
let is_remote = delegate.project.read(cx).is_via_remote_server();
let store = delegate.store.downgrade();
let extra_env = load_proxy_env(cx);

View File

@@ -1,106 +0,0 @@
use std::rc::Rc;
use std::sync::Arc;
use std::{any::Any, path::Path};
use acp_thread::AgentConnection;
use agent_client_protocol as acp;
use anyhow::{Context as _, Result};
use fs::Fs;
use gpui::{App, AppContext as _, SharedString, Task};
use project::agent_server_store::{AllAgentServersSettings, CODEX_NAME};
use settings::{SettingsStore, update_settings_file};
use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
#[derive(Clone)]
pub struct Codex;
#[cfg(test)]
pub(crate) mod tests {
use super::*;
crate::common_e2e_tests!(async |_, _, _| Codex, allow_option_id = "proceed_once");
}
impl AgentServer for Codex {
fn telemetry_id(&self) -> &'static str {
"codex"
}
fn name(&self) -> SharedString {
"Codex".into()
}
fn logo(&self) -> ui::IconName {
ui::IconName::AiOpenAi
}
fn default_mode(&self, cx: &mut App) -> Option<acp::SessionModeId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).codex.clone()
});
settings
.as_ref()
.and_then(|s| s.default_mode.clone().map(|m| acp::SessionModeId(m.into())))
}
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
update_settings_file(fs, cx, |settings, _| {
settings
.agent_servers
.get_or_insert_default()
.codex
.get_or_insert_default()
.default_mode = mode_id.map(|m| m.to_string())
});
}
fn connect(
&self,
root_dir: Option<&Path>,
delegate: AgentServerDelegate,
cx: &mut App,
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
let name = self.name();
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
let is_remote = delegate.project.read(cx).is_via_remote_server();
let store = delegate.store.downgrade();
let extra_env = load_proxy_env(cx);
let default_mode = self.default_mode(cx);
cx.spawn(async move |cx| {
let (command, root_dir, login) = store
.update(cx, |store, cx| {
let agent = store
.get_external_agent(&CODEX_NAME.into())
.context("Codex is not registered")?;
anyhow::Ok(agent.get_command(
root_dir.as_deref(),
extra_env,
delegate.status_tx,
// For now, report that there are no updates.
// (A future PR will use the GitHub Releases API to fetch them.)
delegate.new_version_available,
&mut cx.to_async(),
))
})??
.await?;
let connection = crate::acp::connect(
name,
command,
root_dir.as_ref(),
default_mode,
is_remote,
cx,
)
.await?;
Ok((connection, login))
})
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
}

View File

@@ -67,7 +67,7 @@ impl crate::AgentServer for CustomAgentServer {
cx: &mut App,
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
let name = self.name();
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
let is_remote = delegate.project.read(cx).is_via_remote_server();
let default_mode = self.default_mode(cx);
let store = delegate.store.downgrade();

View File

@@ -483,13 +483,6 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
default_mode: None,
}),
gemini: Some(crate::gemini::tests::local_command().into()),
codex: Some(BuiltinAgentServerSettings {
path: Some("codex-acp".into()),
args: None,
env: None,
ignore_system_version: None,
default_mode: None,
}),
custom: collections::HashMap::default(),
},
cx,

View File

@@ -31,7 +31,7 @@ impl AgentServer for Gemini {
cx: &mut App,
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
let name = self.name();
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
let is_remote = delegate.project.read(cx).is_via_remote_server();
let store = delegate.store.downgrade();
let mut extra_env = load_proxy_env(cx);

View File

@@ -151,7 +151,7 @@ impl Default for AgentProfileId {
}
impl Settings for AgentSettings {
fn from_settings(content: &settings::SettingsContent) -> Self {
fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
let agent = content.agent.clone().unwrap();
Self {
enabled: agent.enabled.unwrap(),

View File

@@ -80,6 +80,7 @@ serde.workspace = true
serde_json.workspace = true
serde_json_lenient.workspace = true
settings.workspace = true
shlex.workspace = true
smol.workspace = true
streaming_diff.workspace = true
task.workspace = true

View File

@@ -1,6 +1,5 @@
use std::cell::RefCell;
use std::ops::Range;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
@@ -14,7 +13,7 @@ use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{App, Entity, Task, WeakEntity};
use language::{Buffer, CodeLabel, HighlightId};
use lsp::CompletionContext;
use project::lsp_store::{CompletionDocumentation, SymbolLocation};
use project::lsp_store::CompletionDocumentation;
use project::{
Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, Project,
ProjectPath, Symbol, WorktreeId,
@@ -23,11 +22,10 @@ use prompt_store::PromptStore;
use rope::Point;
use text::{Anchor, ToPoint as _};
use ui::prelude::*;
use util::rel_path::RelPath;
use workspace::Workspace;
use crate::AgentPanel;
use crate::acp::message_editor::MessageEditor;
use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
use crate::context_picker::file_context_picker::{FileMatch, search_files};
use crate::context_picker::rules_context_picker::{RulesContextEntry, search_rules};
use crate::context_picker::symbol_context_picker::SymbolMatch;
@@ -189,7 +187,7 @@ impl ContextPickerCompletionProvider {
pub(crate) fn completion_for_path(
project_path: ProjectPath,
path_prefix: &RelPath,
path_prefix: &str,
is_recent: bool,
is_directory: bool,
source_range: Range<Anchor>,
@@ -197,12 +195,10 @@ impl ContextPickerCompletionProvider {
project: Entity<Project>,
cx: &mut App,
) -> Option<Completion> {
let path_style = project.read(cx).path_style(cx);
let (file_name, directory) =
crate::context_picker::file_context_picker::extract_file_name_and_directory(
&project_path.path,
path_prefix,
path_style,
);
let label =
@@ -254,15 +250,7 @@ impl ContextPickerCompletionProvider {
let label = CodeLabel::plain(symbol.name.clone(), None);
let abs_path = match &symbol.path {
SymbolLocation::InProject(project_path) => {
project.read(cx).absolute_path(&project_path, cx)?
}
SymbolLocation::OutsideProject {
abs_path,
signature: _,
} => PathBuf::from(abs_path.as_ref()),
};
let abs_path = project.read(cx).absolute_path(&symbol.path, cx)?;
let uri = MentionUri::Symbol {
abs_path,
name: symbol.name.clone(),
@@ -759,13 +747,13 @@ impl CompletionProvider for ContextPickerCompletionProvider {
let editor = editor.clone();
move |cx| {
editor
.update(cx, |editor, cx| {
.update(cx, |_editor, cx| {
match intent {
CompletionIntent::Complete
| CompletionIntent::CompleteWithInsert
| CompletionIntent::CompleteWithReplace => {
if !is_missing_argument {
editor.send(cx);
cx.emit(MessageEditorEvent::Send);
}
}
CompletionIntent::Compose => {}
@@ -775,7 +763,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
}
});
}
false
is_missing_argument
}
})),
}
@@ -910,17 +898,6 @@ impl CompletionProvider for ContextPickerCompletionProvider {
offset_to_line,
self.prompt_capabilities.borrow().embedded_context,
)
.filter(|completion| {
// Right now we don't support completing arguments of slash commands
let is_slash_command_with_argument = matches!(
completion,
ContextCompletion::SlashCommand(SlashCommandCompletion {
argument: Some(_),
..
})
);
!is_slash_command_with_argument
})
.map(|completion| {
completion.source_range().start <= offset_to_line + position.column as usize
&& completion.source_range().end >= offset_to_line + position.column as usize

View File

@@ -203,7 +203,7 @@ impl EntryViewState {
self.entries.drain(range);
}
pub fn agent_ui_font_size_changed(&mut self, cx: &mut App) {
pub fn agent_font_size_changed(&mut self, cx: &mut App) {
for entry in self.entries.iter() {
match entry {
Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}
@@ -387,7 +387,7 @@ fn diff_editor_text_style_refinement(cx: &mut App) -> TextStyleRefinement {
font_size: Some(
TextSize::Small
.rems(cx)
.to_pixels(ThemeSettings::get_global(cx).agent_ui_font_size(cx))
.to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
.into(),
),
..Default::default()
@@ -414,6 +414,7 @@ mod tests {
use project::Project;
use serde_json::json;
use settings::{Settings as _, SettingsStore};
use theme::ThemeSettings;
use util::path;
use workspace::Workspace;
@@ -543,7 +544,7 @@ mod tests {
Project::init_settings(cx);
AgentSettings::register(cx);
workspace::init_settings(cx);
theme::init(theme::LoadThemes::JustBase, cx);
ThemeSettings::register(cx);
release_channel::init(SemanticVersion::default(), cx);
EditorSettings::register(cx);
});

View File

@@ -48,7 +48,7 @@ use std::{
use text::OffsetRangeExt;
use theme::ThemeSettings;
use ui::{ButtonLike, TintColor, Toggleable, prelude::*};
use util::{ResultExt, debug_panic, rel_path::RelPath};
use util::{ResultExt, debug_panic};
use workspace::{Workspace, notifications::NotifyResultExt as _};
use zed_actions::agent::Chat;
@@ -76,7 +76,7 @@ pub enum MessageEditorEvent {
impl EventEmitter<MessageEditorEvent> for MessageEditor {}
const COMMAND_HINT_INLAY_ID: u32 = 0;
const COMMAND_HINT_INLAY_ID: usize = 0;
impl MessageEditor {
pub fn new(
@@ -141,9 +141,7 @@ impl MessageEditor {
subscriptions.push(cx.subscribe_in(&editor, window, {
move |this, editor, event, window, cx| {
if let EditorEvent::Edited { .. } = event
&& !editor.read(cx).read_only(cx)
{
if let EditorEvent::Edited { .. } = event {
let snapshot = editor.update(cx, |editor, cx| {
let new_hints = this
.command_hint(editor.buffer(), cx)
@@ -292,18 +290,18 @@ impl MessageEditor {
let snapshot = self
.editor
.update(cx, |editor, cx| editor.snapshot(window, cx));
let Some((excerpt_id, _, _)) = snapshot.buffer_snapshot().as_singleton() else {
let Some((excerpt_id, _, _)) = snapshot.buffer_snapshot.as_singleton() else {
return Task::ready(());
};
let Some(start_anchor) = snapshot
.buffer_snapshot()
.buffer_snapshot
.anchor_in_excerpt(*excerpt_id, start)
else {
return Task::ready(());
};
let end_anchor = snapshot
.buffer_snapshot()
.anchor_before(start_anchor.to_offset(&snapshot.buffer_snapshot()) + content_len + 1);
.buffer_snapshot
.anchor_before(start_anchor.to_offset(&snapshot.buffer_snapshot) + content_len + 1);
let crease = if let MentionUri::File { abs_path } = &mention_uri
&& let Some(extension) = abs_path.extension()
@@ -454,12 +452,9 @@ impl MessageEditor {
.update(cx, |project, cx| project.open_buffer(project_path, cx));
cx.spawn(async move |_, cx| {
let buffer = buffer.await?;
let buffer_content = outline::get_buffer_content_or_outline(
buffer.clone(),
Some(&abs_path.to_string_lossy()),
&cx,
)
.await?;
let buffer_content =
outline::get_buffer_content_or_outline(buffer.clone(), Some(&abs_path), &cx)
.await?;
Ok(Mention::Text {
content: buffer_content.text,
@@ -720,7 +715,7 @@ impl MessageEditor {
continue;
};
let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot());
let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
if crease_range.start > ix {
//todo(): Custom slash command ContentBlock?
// let chunk = if prevent_slash_commands
@@ -825,20 +820,13 @@ impl MessageEditor {
});
}
pub fn send(&mut self, cx: &mut Context<Self>) {
fn send(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
if self.is_empty(cx) {
return;
}
self.editor.update(cx, |editor, cx| {
editor.clear_inlay_hints(cx);
});
cx.emit(MessageEditorEvent::Send)
}
fn chat(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
self.send(cx);
}
fn cancel(&mut self, _: &editor::actions::Cancel, _: &mut Window, cx: &mut Context<Self>) {
cx.emit(MessageEditorEvent::Cancel)
}
@@ -874,11 +862,11 @@ impl MessageEditor {
self.editor.update(cx, |message_editor, cx| {
let snapshot = message_editor.snapshot(window, cx);
let (excerpt_id, _, buffer_snapshot) =
snapshot.buffer_snapshot().as_singleton().unwrap();
snapshot.buffer_snapshot.as_singleton().unwrap();
let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
let multibuffer_anchor = snapshot
.buffer_snapshot()
.buffer_snapshot
.anchor_in_excerpt(*excerpt_id, text_anchor);
message_editor.edit(
[(
@@ -959,7 +947,6 @@ impl MessageEditor {
window: &mut Window,
cx: &mut Context<Self>,
) {
let path_style = self.project.read(cx).path_style(cx);
let buffer = self.editor.read(cx).buffer().clone();
let Some(buffer) = buffer.read(cx).as_singleton() else {
return;
@@ -969,15 +956,18 @@ impl MessageEditor {
let Some(entry) = self.project.read(cx).entry_for_path(&path, cx) else {
continue;
};
let Some(worktree) = self.project.read(cx).worktree_for_id(path.worktree_id, cx) else {
let Some(abs_path) = self.project.read(cx).absolute_path(&path, cx) else {
continue;
};
let abs_path = worktree.read(cx).absolutize(&path.path);
let path_prefix = abs_path
.file_name()
.unwrap_or(path.path.as_os_str())
.display()
.to_string();
let (file_name, _) =
crate::context_picker::file_context_picker::extract_file_name_and_directory(
&path.path,
worktree.read(cx).root_name(),
path_style,
&path_prefix,
);
let uri = if entry.is_dir() {
@@ -1039,7 +1029,6 @@ impl MessageEditor {
) else {
return;
};
self.editor.update(cx, |message_editor, cx| {
message_editor.edit([(cursor_anchor..cursor_anchor, completion.new_text)], cx);
});
@@ -1187,20 +1176,14 @@ fn full_mention_for_directory(
abs_path: &Path,
cx: &mut App,
) -> Task<Result<Mention>> {
fn collect_files_in_path(worktree: &Worktree, path: &RelPath) -> Vec<(Arc<RelPath>, String)> {
fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<(Arc<Path>, PathBuf)> {
let mut files = Vec::new();
for entry in worktree.child_entries(path) {
if entry.is_dir() {
files.extend(collect_files_in_path(worktree, &entry.path));
} else if entry.is_file() {
files.push((
entry.path.clone(),
worktree
.full_path(&entry.path)
.to_string_lossy()
.to_string(),
));
files.push((entry.path.clone(), worktree.full_path(&entry.path)));
}
}
@@ -1278,7 +1261,7 @@ fn full_mention_for_directory(
})
}
fn render_directory_contents(entries: Vec<(Arc<RelPath>, String, String)>) -> String {
fn render_directory_contents(entries: Vec<(Arc<Path>, PathBuf, String)>) -> String {
let mut output = String::new();
for (_relative_path, full_path, content) in entries {
let fence = codeblock_fence_for_path(Some(&full_path), None);
@@ -1297,7 +1280,7 @@ impl Render for MessageEditor {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.key_context("MessageEditor")
.on_action(cx.listener(Self::chat))
.on_action(cx.listener(Self::send))
.on_action(cx.listener(Self::cancel))
.capture_action(cx.listener(Self::paste))
.flex_1()
@@ -1309,7 +1292,7 @@ impl Render for MessageEditor {
font_family: settings.buffer_font.family.clone(),
font_fallbacks: settings.buffer_font.fallbacks.clone(),
font_features: settings.buffer_font.features.clone(),
font_size: settings.agent_buffer_font_size(cx).into(),
font_size: settings.buffer_font_size(cx).into(),
line_height: relative(settings.buffer_line_height.value()),
..Default::default()
};
@@ -1560,7 +1543,7 @@ impl MentionSet {
fn remove_invalid(&mut self, snapshot: EditorSnapshot) {
for (crease_id, crease) in snapshot.crease_snapshot.creases() {
if !crease.range().start.is_valid(&snapshot.buffer_snapshot()) {
if !crease.range().start.is_valid(&snapshot.buffer_snapshot) {
self.mentions.remove(&crease_id);
}
}
@@ -1612,7 +1595,7 @@ mod tests {
use serde_json::json;
use text::Point;
use ui::{App, Context, IntoElement, Render, SharedString, Window};
use util::{path, paths::PathStyle, rel_path::rel_path};
use util::{path, uri};
use workspace::{AppState, Item, Workspace};
use crate::acp::{
@@ -2021,11 +2004,21 @@ mod tests {
editor.update_in(&mut cx, |editor, _window, cx| {
assert_eq!(editor.text(cx), "/say-hello ");
assert_eq!(editor.display_text(cx), "/say-hello <name>");
assert!(!editor.has_visible_completions_menu());
assert!(editor.has_visible_completions_menu());
assert_eq!(
current_completion_labels_with_documentation(editor),
&[("say-hello".into(), "Say hello to whoever you want".into())]
);
});
cx.simulate_input("GPT5");
editor.update_in(&mut cx, |editor, window, cx| {
assert!(editor.has_visible_completions_menu());
editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
});
cx.run_until_parked();
editor.update_in(&mut cx, |editor, window, cx| {
@@ -2034,7 +2027,7 @@ mod tests {
assert!(!editor.has_visible_completions_menu());
// Delete argument
for _ in 0..5 {
for _ in 0..4 {
editor.backspace(&editor::actions::Backspace, window, cx);
}
});
@@ -2042,12 +2035,13 @@ mod tests {
cx.run_until_parked();
editor.update_in(&mut cx, |editor, window, cx| {
assert_eq!(editor.text(cx), "/say-hello");
assert_eq!(editor.text(cx), "/say-hello ");
// Hint is visible because argument was deleted
assert_eq!(editor.display_text(cx), "/say-hello <name>");
// Delete last command letter
editor.backspace(&editor::actions::Backspace, window, cx);
editor.backspace(&editor::actions::Backspace, window, cx);
});
cx.run_until_parked();
@@ -2111,18 +2105,16 @@ mod tests {
let mut cx = VisualTestContext::from_window(*window, cx);
let paths = vec![
rel_path("a/one.txt"),
rel_path("a/two.txt"),
rel_path("a/three.txt"),
rel_path("a/four.txt"),
rel_path("b/five.txt"),
rel_path("b/six.txt"),
rel_path("b/seven.txt"),
rel_path("b/eight.txt"),
path!("a/one.txt"),
path!("a/two.txt"),
path!("a/three.txt"),
path!("a/four.txt"),
path!("b/five.txt"),
path!("b/six.txt"),
path!("b/seven.txt"),
path!("b/eight.txt"),
];
let slash = PathStyle::local().separator();
let mut opened_editors = Vec::new();
for path in paths {
let buffer = workspace
@@ -2130,7 +2122,7 @@ mod tests {
workspace.open_path(
ProjectPath {
worktree_id,
path: path.into(),
path: Path::new(path).into(),
},
None,
false,
@@ -2191,10 +2183,10 @@ mod tests {
assert_eq!(
current_completion_labels(editor),
&[
format!("eight.txt dir{slash}b{slash}"),
format!("seven.txt dir{slash}b{slash}"),
format!("six.txt dir{slash}b{slash}"),
format!("five.txt dir{slash}b{slash}"),
"eight.txt dir/b/",
"seven.txt dir/b/",
"six.txt dir/b/",
"five.txt dir/b/",
]
);
editor.set_text("", window, cx);
@@ -2222,14 +2214,14 @@ mod tests {
assert_eq!(
current_completion_labels(editor),
&[
format!("eight.txt dir{slash}b{slash}"),
format!("seven.txt dir{slash}b{slash}"),
format!("six.txt dir{slash}b{slash}"),
format!("five.txt dir{slash}b{slash}"),
"Files & Directories".into(),
"Symbols".into(),
"Threads".into(),
"Fetch".into()
"eight.txt dir/b/",
"seven.txt dir/b/",
"six.txt dir/b/",
"five.txt dir/b/",
"Files & Directories",
"Symbols",
"Threads",
"Fetch"
]
);
});
@@ -2256,10 +2248,7 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(editor.text(cx), "Lorem @file one");
assert!(editor.has_visible_completions_menu());
assert_eq!(
current_completion_labels(editor),
vec![format!("one.txt dir{slash}a{slash}")]
);
assert_eq!(current_completion_labels(editor), vec!["one.txt dir/a/"]);
});
editor.update_in(&mut cx, |editor, window, cx| {
@@ -2267,11 +2256,7 @@ mod tests {
editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
});
let url_one = MentionUri::File {
abs_path: path!("/dir/a/one.txt").into(),
}
.to_uri()
.to_string();
let url_one = uri!("file:///dir/a/one.txt");
editor.update(&mut cx, |editor, cx| {
let text = editor.text(cx);
assert_eq!(text, format!("Lorem [@one.txt]({url_one}) "));
@@ -2376,11 +2361,7 @@ mod tests {
.into_values()
.collect::<Vec<_>>();
let url_eight = MentionUri::File {
abs_path: path!("/dir/b/eight.txt").into(),
}
.to_uri()
.to_string();
let url_eight = uri!("file:///dir/b/eight.txt");
{
let [_, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
@@ -2479,12 +2460,6 @@ mod tests {
editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
});
let symbol = MentionUri::Symbol {
abs_path: path!("/dir/a/one.txt").into(),
name: "MySymbol".into(),
line_range: 0..=0,
};
let contents = message_editor
.update(&mut cx, |message_editor, cx| {
message_editor.mention_set().contents(
@@ -2504,7 +2479,12 @@ mod tests {
panic!("Unexpected mentions");
};
pretty_assertions::assert_eq!(content, "1");
pretty_assertions::assert_eq!(uri, &symbol);
pretty_assertions::assert_eq!(
uri,
&format!("{url_one}?symbol=MySymbol#L1:1")
.parse::<MentionUri>()
.unwrap()
);
}
cx.run_until_parked();
@@ -2512,10 +2492,7 @@ mod tests {
editor.read_with(&cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!(
"Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
symbol.to_uri(),
)
format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ")
);
});
@@ -2525,10 +2502,10 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) @file x.png", symbol.to_uri())
format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) @file x.png")
);
assert!(editor.has_visible_completions_menu());
assert_eq!(current_completion_labels(editor), &[format!("x.png dir{slash}")]);
assert_eq!(current_completion_labels(editor), &["x.png dir/"]);
});
editor.update_in(&mut cx, |editor, window, cx| {
@@ -2554,10 +2531,7 @@ mod tests {
editor.read_with(&cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!(
"Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
symbol.to_uri()
)
format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ")
);
});
@@ -2567,10 +2541,10 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) @file x.png", symbol.to_uri())
format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) @file x.png")
);
assert!(editor.has_visible_completions_menu());
assert_eq!(current_completion_labels(editor), &[format!("x.png dir{slash}")]);
assert_eq!(current_completion_labels(editor), &["x.png dir/"]);
});
editor.update_in(&mut cx, |editor, window, cx| {
@@ -2582,14 +2556,11 @@ mod tests {
// Mention was removed
editor.read_with(&cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!(
"Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
symbol.to_uri()
)
);
});
assert_eq!(
editor.text(cx),
format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ")
);
});
// Now getting the contents succeeds, because the invalid mention was removed
let contents = message_editor

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ mod tool_picker;
use std::{ops::Range, sync::Arc};
use agent_settings::AgentSettings;
use anyhow::Result;
use assistant_tool::{ToolSource, ToolWorkingSet};
use cloud_llm_client::{Plan, PlanV1, PlanV2};
@@ -25,13 +26,13 @@ use language_model::{
};
use notifications::status_toast::{StatusToast, ToastIcon};
use project::{
agent_server_store::{AgentServerStore, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME},
agent_server_store::{AgentServerStore, CLAUDE_CODE_NAME, GEMINI_NAME},
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
};
use settings::{SettingsStore, update_settings_file};
use settings::{Settings, SettingsStore, update_settings_file};
use ui::{
Chip, CommonAnimationExt, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex,
Indicator, PopoverMenu, Switch, SwitchColor, Tooltip, WithScrollbar, prelude::*,
Indicator, PopoverMenu, Switch, SwitchColor, SwitchField, Tooltip, WithScrollbar, prelude::*,
};
use util::ResultExt as _;
use workspace::{Workspace, create_and_open_local_file};
@@ -401,6 +402,101 @@ 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 fs = self.fs.clone();
SwitchField::new(
"always-allow-tool-actions-switch",
"Allow running commands without asking for confirmation",
Some(
"The agent can perform potentially destructive actions without asking for your confirmation.".into(),
),
always_allow_tool_actions,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
update_settings_file(fs.clone(), cx, move |settings, _| {
settings.agent.get_or_insert_default().set_always_allow_tool_actions(allow);
});
},
)
}
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 fs = self.fs.clone();
SwitchField::new(
"single-file-review",
"Enable single-file agent reviews",
Some("Agent edits are also displayed in single-file editors for review.".into()),
single_file_review,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
update_settings_file(fs.clone(), cx, move |settings, _| {
settings
.agent
.get_or_insert_default()
.set_single_file_review(allow);
});
},
)
}
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;
let fs = self.fs.clone();
SwitchField::new(
"sound-notification",
"Play sound when finished generating",
Some(
"Hear a notification sound when the agent is done generating changes or needs your input.".into(),
),
play_sound_when_agent_done,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
update_settings_file(fs.clone(), cx, move |settings, _| {
settings.agent.get_or_insert_default().set_play_sound_when_agent_done(allow);
});
},
)
}
fn render_modifier_to_send(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let use_modifier_to_send = AgentSettings::get_global(cx).use_modifier_to_send;
let fs = self.fs.clone();
SwitchField::new(
"modifier-send",
"Use modifier to submit a message",
Some(
"Make a modifier (cmd-enter on macOS, ctrl-enter on Linux or Windows) required to send messages.".into(),
),
use_modifier_to_send,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
update_settings_file(fs.clone(), cx, move |settings, _| {
settings.agent.get_or_insert_default().set_use_modifier_to_send(allow);
});
},
)
}
fn render_general_settings_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.pr(DynamicSpacing::Base20.rems(cx))
.gap_2p5()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(Headline::new("General Settings"))
.child(self.render_command_permission(cx))
.child(self.render_single_file_review(cx))
.child(self.render_sound_notification(cx))
.child(self.render_modifier_to_send(cx))
}
fn render_zed_plan_info(&self, plan: Option<Plan>, cx: &mut Context<Self>) -> impl IntoElement {
if let Some(plan) = plan {
let free_chip_bg = cx
@@ -918,9 +1014,7 @@ impl AgentConfiguration {
.agent_server_store
.read(cx)
.external_agents()
.filter(|name| {
name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME && name.0 != CODEX_NAME
})
.filter(|name| name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME)
.cloned()
.collect::<Vec<_>>();
@@ -983,20 +1077,15 @@ impl AgentConfiguration {
.color(Color::Muted),
),
)
.child(self.render_agent_server(
IconName::AiClaude,
"Claude Code",
))
.child(Divider::horizontal().color(DividerColor::BorderFaded))
.child(self.render_agent_server(
IconName::AiOpenAi,
"Codex",
))
.child(Divider::horizontal().color(DividerColor::BorderFaded))
.child(self.render_agent_server(
IconName::AiGemini,
"Gemini CLI",
))
.child(Divider::horizontal().color(DividerColor::BorderFaded))
.child(self.render_agent_server(
IconName::AiClaude,
"Claude Code",
))
.map(|mut parent| {
for agent in user_defined_agents {
parent = parent.child(Divider::horizontal().color(DividerColor::BorderFaded))
@@ -1045,6 +1134,7 @@ impl Render for AgentConfiguration {
.track_scroll(&self.scroll_handle)
.size_full()
.overflow_y_scroll()
.child(self.render_general_settings_section(cx))
.child(self.render_agent_servers_section(cx))
.child(self.render_context_servers_section(window, cx))
.child(self.render_provider_configuration_section(cx)),

View File

@@ -619,10 +619,10 @@ mod tests {
cx.update(|_window, cx| {
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
registry.register_provider(
Arc::new(FakeLanguageModelProvider::new(
FakeLanguageModelProvider::new(
LanguageModelProviderId::new("someprovider"),
LanguageModelProviderName::new("Some Provider"),
)),
),
cx,
);
});

View File

@@ -317,8 +317,6 @@ impl ManageProfilesModal {
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement + use<> {
let is_focused = profile.navigation.focus_handle.contains_focused(window, cx);
div()
.id(SharedString::from(format!("profile-{}", profile.id)))
.track_focus(&profile.navigation.focus_handle)
@@ -330,27 +328,25 @@ impl ManageProfilesModal {
})
.child(
ListItem::new(SharedString::from(format!("profile-{}", profile.id)))
.toggle_state(is_focused)
.toggle_state(profile.navigation.focus_handle.contains_focused(window, cx))
.inset(true)
.spacing(ListItemSpacing::Sparse)
.child(Label::new(profile.name.clone()))
.when(is_focused, |this| {
this.end_slot(
h_flex()
.gap_1()
.child(
Label::new("Customize")
.size(LabelSize::Small)
.color(Color::Muted),
)
.children(KeyBinding::for_action_in(
&menu::Confirm,
&self.focus_handle,
window,
cx,
)),
)
})
.end_slot(
h_flex()
.gap_1()
.child(
Label::new("Customize")
.size(LabelSize::Small)
.color(Color::Muted),
)
.children(KeyBinding::for_action_in(
&menu::Confirm,
&self.focus_handle,
window,
cx,
)),
)
.on_click({
let profile_id = profile.id.clone();
cx.listener(move |this, _, window, cx| {

View File

@@ -562,6 +562,10 @@ impl Item for AgentDiffPane {
self.editor.for_each_project_item(cx, f)
}
fn is_singleton(&self, _: &App) -> bool {
false
}
fn set_nav_history(
&mut self,
nav_history: ItemNavHistory,
@@ -846,7 +850,7 @@ fn render_diff_hunk_controls(
editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx);
let position =
hunk_range.end.to_point(&snapshot.buffer_snapshot());
hunk_range.end.to_point(&snapshot.buffer_snapshot);
editor.go_to_hunk_before_or_after_position(
&snapshot,
position,
@@ -882,7 +886,7 @@ fn render_diff_hunk_controls(
editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx);
let point =
hunk_range.start.to_point(&snapshot.buffer_snapshot());
hunk_range.start.to_point(&snapshot.buffer_snapshot);
editor.go_to_hunk_before_or_after_position(
&snapshot,
point,
@@ -1814,6 +1818,7 @@ mod tests {
use serde_json::json;
use settings::{Settings, SettingsStore};
use std::{path::Path, rc::Rc};
use theme::ThemeSettings;
use util::path;
#[gpui::test]
@@ -1826,7 +1831,7 @@ mod tests {
AgentSettings::register(cx);
prompt_store::init(cx);
workspace::init_settings(cx);
theme::init(theme::LoadThemes::JustBase, cx);
ThemeSettings::register(cx);
EditorSettings::register(cx);
language_model::init_settings(cx);
});
@@ -1978,7 +1983,7 @@ mod tests {
AgentSettings::register(cx);
prompt_store::init(cx);
workspace::init_settings(cx);
theme::init(theme::LoadThemes::JustBase, cx);
ThemeSettings::register(cx);
EditorSettings::register(cx);
language_model::init_settings(cx);
workspace::register_project_item::<Editor>(cx);

View File

@@ -7,7 +7,7 @@ use acp_thread::AcpThread;
use agent2::{DbThreadMetadata, HistoryEntry};
use db::kvp::{Dismissable, KEY_VALUE_STORE};
use project::agent_server_store::{
AgentServerCommand, AllAgentServersSettings, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME,
AgentServerCommand, AllAgentServersSettings, CLAUDE_CODE_NAME, GEMINI_NAME,
};
use serde::{Deserialize, Serialize};
use settings::{
@@ -19,10 +19,9 @@ use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
use crate::{
AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, Follow, InlineAssistant,
NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory,
ResetTrialEndUpsell, ResetTrialUpsell, ToggleNavigationMenu, ToggleNewThreadMenu,
ToggleOptionsMenu,
AddContextServer, DeleteRecentlyOpenThread, Follow, InlineAssistant, NewTextThread, NewThread,
OpenActiveThreadAsMarkdown, OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell,
ToggleNavigationMenu, ToggleNewThreadMenu, ToggleOptionsMenu,
acp::AcpThreadView,
agent_configuration::{AgentConfiguration, AssistantConfigurationEvent},
slash_command::SlashCommandCompletionProvider,
@@ -34,6 +33,7 @@ use crate::{
};
use agent::{
context_store::ContextStore,
history_store::{HistoryEntryId, HistoryStore},
thread_store::{TextThreadStore, ThreadStore},
};
use agent_settings::AgentSettings;
@@ -53,7 +53,7 @@ use gpui::{
};
use language::LanguageRegistry;
use language_model::{ConfigurationError, LanguageModelRegistry};
use project::{Project, ProjectPath, Worktree};
use project::{DisableAiSettings, Project, ProjectPath, Worktree};
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
use rules_library::{RulesLibrary, open_rules_library};
use search::{BufferSearchBar, buffer_search};
@@ -140,16 +140,6 @@ pub fn init(cx: &mut App) {
.register_action(|workspace, _: &Follow, window, cx| {
workspace.follow(CollaboratorId::Agent, window, cx);
})
.register_action(|workspace, _: &OpenAgentDiff, window, cx| {
let thread = workspace
.panel::<AgentPanel>(cx)
.and_then(|panel| panel.read(cx).active_thread_view().cloned())
.and_then(|thread_view| thread_view.read(cx).thread().cloned());
if let Some(thread) = thread {
AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
}
})
.register_action(|workspace, _: &ToggleNavigationMenu, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
@@ -222,11 +212,11 @@ enum WhichFontSize {
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub enum AgentType {
#[default]
NativeAgent,
Zed,
TextThread,
Gemini,
ClaudeCode,
Codex,
NativeAgent,
Custom {
name: SharedString,
command: AgentServerCommand,
@@ -236,20 +226,19 @@ pub enum AgentType {
impl AgentType {
fn label(&self) -> SharedString {
match self {
Self::NativeAgent | Self::TextThread => "Zed Agent".into(),
Self::Zed | Self::TextThread => "Zed Agent".into(),
Self::NativeAgent => "Agent 2".into(),
Self::Gemini => "Gemini CLI".into(),
Self::ClaudeCode => "Claude Code".into(),
Self::Codex => "Codex".into(),
Self::Custom { name, .. } => name.into(),
}
}
fn icon(&self) -> Option<IconName> {
match self {
Self::NativeAgent | Self::TextThread => None,
Self::Zed | Self::NativeAgent | Self::TextThread => None,
Self::Gemini => Some(IconName::AiGemini),
Self::ClaudeCode => Some(IconName::AiClaude),
Self::Codex => Some(IconName::AiOpenAi),
Self::Custom { .. } => Some(IconName::Terminal),
}
}
@@ -260,7 +249,6 @@ impl From<ExternalAgent> for AgentType {
match value {
ExternalAgent::Gemini => Self::Gemini,
ExternalAgent::ClaudeCode => Self::ClaudeCode,
ExternalAgent::Codex => Self::Codex,
ExternalAgent::Custom { name, command } => Self::Custom { name, command },
ExternalAgent::NativeAgent => Self::NativeAgent,
}
@@ -306,6 +294,7 @@ impl ActiveView {
pub fn prompt_editor(
context_editor: Entity<TextThreadEditor>,
history_store: Entity<HistoryStore>,
acp_history_store: Entity<agent2::HistoryStore>,
language_registry: Arc<LanguageRegistry>,
window: &mut Window,
@@ -373,6 +362,18 @@ impl ActiveView {
})
}
ContextEvent::PathChanged { old_path, new_path } => {
history_store.update(cx, |history_store, cx| {
if let Some(old_path) = old_path {
history_store
.replace_recently_opened_text_thread(old_path, new_path, cx);
} else {
history_store.push_recently_opened_entry(
HistoryEntryId::Context(new_path.clone()),
cx,
);
}
});
acp_history_store.update(cx, |history_store, cx| {
if let Some(old_path) = old_path {
history_store
@@ -414,7 +415,7 @@ pub struct AgentPanel {
language_registry: Arc<LanguageRegistry>,
thread_store: Entity<ThreadStore>,
acp_history: Entity<AcpThreadHistory>,
history_store: Entity<agent2::HistoryStore>,
acp_history_store: Entity<agent2::HistoryStore>,
context_store: Entity<TextThreadStore>,
prompt_store: Option<Entity<PromptStore>>,
inline_assist_context_store: Entity<ContextStore>,
@@ -422,6 +423,7 @@ pub struct AgentPanel {
configuration_subscription: Option<Subscription>,
active_view: ActiveView,
previous_view: Option<ActiveView>,
history_store: Entity<HistoryStore>,
new_thread_menu_handle: PopoverMenuHandle<ContextMenu>,
agent_panel_menu_handle: PopoverMenuHandle<ContextMenu>,
assistant_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
@@ -512,7 +514,6 @@ impl AgentPanel {
cx,
)
});
panel.as_mut(cx).loading = true;
if let Some(serialized_panel) = serialized_panel {
panel.update(cx, |panel, cx| {
@@ -554,8 +555,10 @@ impl AgentPanel {
let inline_assist_context_store =
cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade())));
let history_store = cx.new(|cx| agent2::HistoryStore::new(context_store.clone(), cx));
let acp_history = cx.new(|cx| AcpThreadHistory::new(history_store.clone(), window, cx));
let history_store = cx.new(|cx| HistoryStore::new(context_store.clone(), [], cx));
let acp_history_store = cx.new(|cx| agent2::HistoryStore::new(context_store.clone(), cx));
let acp_history = cx.new(|cx| AcpThreadHistory::new(acp_history_store.clone(), window, cx));
cx.subscribe_in(
&acp_history,
window,
@@ -577,12 +580,14 @@ impl AgentPanel {
)
.detach();
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::native_agent(
fs.clone(),
prompt_store.clone(),
history_store.clone(),
acp_history_store.clone(),
project.clone(),
workspace.clone(),
window,
@@ -608,6 +613,7 @@ impl AgentPanel {
ActiveView::prompt_editor(
context_editor,
history_store.clone(),
acp_history_store.clone(),
language_registry.clone(),
window,
cx,
@@ -659,6 +665,43 @@ impl AgentPanel {
)
});
let mut old_disable_ai = false;
cx.observe_global_in::<SettingsStore>(window, move |panel, window, cx| {
let disable_ai = DisableAiSettings::get_global(cx).disable_ai;
if old_disable_ai != disable_ai {
let agent_panel_id = cx.entity_id();
let agent_panel_visible = panel
.workspace
.update(cx, |workspace, cx| {
let agent_dock_position = panel.position(window, cx);
let agent_dock = workspace.dock_at_position(agent_dock_position);
let agent_panel_focused = agent_dock
.read(cx)
.active_panel()
.is_some_and(|panel| panel.panel_id() == agent_panel_id);
let active_panel_visible = agent_dock
.read(cx)
.visible_panel()
.is_some_and(|panel| panel.panel_id() == agent_panel_id);
if agent_panel_focused {
cx.dispatch_action(&ToggleFocus);
}
active_panel_visible
})
.unwrap_or_default();
if agent_panel_visible {
cx.emit(PanelEvent::Close);
}
old_disable_ai = disable_ai;
}
})
.detach();
Self {
active_view,
workspace,
@@ -673,6 +716,7 @@ impl AgentPanel {
configuration_subscription: None,
inline_assist_context_store,
previous_view: None,
history_store: history_store.clone(),
new_thread_menu_handle: PopoverMenuHandle::default(),
agent_panel_menu_handle: PopoverMenuHandle::default(),
assistant_navigation_menu_handle: PopoverMenuHandle::default(),
@@ -683,7 +727,7 @@ impl AgentPanel {
pending_serialization: None,
onboarding,
acp_history,
history_store,
acp_history_store,
selected_agent: AgentType::default(),
loading: false,
}
@@ -737,7 +781,7 @@ impl AgentPanel {
cx: &mut Context<Self>,
) {
let Some(thread) = self
.history_store
.acp_history_store
.read(cx)
.thread_from_session_id(&action.from_session_id)
else {
@@ -786,6 +830,7 @@ impl AgentPanel {
ActiveView::prompt_editor(
context_editor.clone(),
self.history_store.clone(),
self.acp_history_store.clone(),
self.language_registry.clone(),
window,
cx,
@@ -811,13 +856,13 @@ impl AgentPanel {
const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
#[derive(Serialize, Deserialize)]
#[derive(Default, Serialize, Deserialize)]
struct LastUsedExternalAgent {
agent: crate::ExternalAgent,
}
let loading = self.loading;
let history = self.history_store.clone();
let history = self.acp_history_store.clone();
cx.spawn_in(window, async move |this, cx| {
let ext_agent = match agent_choice {
@@ -852,18 +897,18 @@ impl AgentPanel {
.and_then(|value| {
serde_json::from_str::<LastUsedExternalAgent>(&value).log_err()
})
.map(|agent| agent.agent)
.unwrap_or(ExternalAgent::NativeAgent)
.unwrap_or_default()
.agent
}
}
};
let server = ext_agent.server(fs, history);
if !loading {
telemetry::event!("Agent Thread Started", agent = server.telemetry_id());
telemetry::event!("Agent Thread Started", agent = ext_agent.name());
}
let server = ext_agent.server(fs, history);
this.update_in(cx, |this, window, cx| {
let selected_agent = ext_agent.into();
if this.selected_agent != selected_agent {
@@ -878,7 +923,7 @@ impl AgentPanel {
summarize_thread,
workspace.clone(),
project,
this.history_store.clone(),
this.acp_history_store.clone(),
this.prompt_store.clone(),
window,
cx,
@@ -976,6 +1021,7 @@ impl AgentPanel {
ActiveView::prompt_editor(
editor,
self.history_store.clone(),
self.acp_history_store.clone(),
self.language_registry.clone(),
window,
cx,
@@ -1057,15 +1103,15 @@ impl AgentPanel {
WhichFontSize::AgentFont => {
if persist {
update_settings_file(self.fs.clone(), cx, move |settings, cx| {
let agent_ui_font_size =
ThemeSettings::get_global(cx).agent_ui_font_size(cx) + delta;
let agent_font_size =
ThemeSettings::get_global(cx).agent_font_size(cx) + delta;
let _ = settings
.theme
.agent_ui_font_size
.insert(theme::clamp_font_size(agent_ui_font_size).into());
.agent_font_size
.insert(theme::clamp_font_size(agent_font_size).into());
});
} else {
theme::adjust_agent_ui_font_size(cx, |size| size + delta);
theme::adjust_agent_font_size(cx, |size| size + delta);
}
}
WhichFontSize::BufferFont => {
@@ -1085,10 +1131,10 @@ impl AgentPanel {
) {
if action.persist {
update_settings_file(self.fs.clone(), cx, move |settings, _| {
settings.theme.agent_ui_font_size = None;
settings.theme.agent_font_size = None;
});
} else {
theme::reset_agent_ui_font_size(cx);
theme::reset_agent_font_size(cx);
}
}
@@ -1239,6 +1285,11 @@ impl AgentPanel {
match &new_view {
ActiveView::TextThread { context_editor, .. } => {
self.history_store.update(cx, |store, cx| {
if let Some(path) = context_editor.read(cx).context().read(cx).path() {
store.push_recently_opened_entry(HistoryEntryId::Context(path.clone()), cx)
}
});
self.acp_history_store.update(cx, |store, cx| {
if let Some(path) = context_editor.read(cx).context().read(cx).path() {
store.push_recently_opened_entry(
agent2::HistoryEntryId::TextThread(path.clone()),
@@ -1272,7 +1323,7 @@ impl AgentPanel {
) -> ContextMenu {
let entries = panel
.read(cx)
.history_store
.acp_history_store
.read(cx)
.recently_opened_entries(cx);
@@ -1317,7 +1368,7 @@ impl AgentPanel {
move |_window, cx| {
panel
.update(cx, |this, cx| {
this.history_store.update(cx, |history_store, cx| {
this.acp_history_store.update(cx, |history_store, cx| {
history_store.remove_recently_opened_entry(&id, cx);
});
})
@@ -1343,6 +1394,15 @@ impl AgentPanel {
cx: &mut Context<Self>,
) {
match agent {
AgentType::Zed => {
window.dispatch_action(
NewThread {
from_thread_id: None,
}
.boxed_clone(),
cx,
);
}
AgentType::TextThread => {
window.dispatch_action(NewTextThread.boxed_clone(), cx);
}
@@ -1367,11 +1427,6 @@ impl AgentPanel {
cx,
)
}
AgentType::Codex => {
self.selected_agent = AgentType::Codex;
self.serialize(cx);
self.external_thread(Some(crate::ExternalAgent::Codex), None, None, window, cx)
}
AgentType::Custom { name, command } => self.external_thread(
Some(crate::ExternalAgent::Custom { name, command }),
None,
@@ -1884,58 +1939,6 @@ impl AgentPanel {
)
.separator()
.header("External Agents")
.item(
ContextMenuEntry::new("New Claude Code Thread")
.icon(IconName::AiClaude)
.disabled(is_via_collab)
.icon_color(Color::Muted)
.handler({
let workspace = workspace.clone();
move |window, cx| {
if let Some(workspace) = workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
if let Some(panel) =
workspace.panel::<AgentPanel>(cx)
{
panel.update(cx, |panel, cx| {
panel.new_agent_thread(
AgentType::ClaudeCode,
window,
cx,
);
});
}
});
}
}
}),
)
.item(
ContextMenuEntry::new("New Codex Thread")
.icon(IconName::AiOpenAi)
.disabled(is_via_collab)
.icon_color(Color::Muted)
.handler({
let workspace = workspace.clone();
move |window, cx| {
if let Some(workspace) = workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
if let Some(panel) =
workspace.panel::<AgentPanel>(cx)
{
panel.update(cx, |panel, cx| {
panel.new_agent_thread(
AgentType::Codex,
window,
cx,
);
});
}
});
}
}
}),
)
.item(
ContextMenuEntry::new("New Gemini CLI Thread")
.icon(IconName::AiGemini)
@@ -1962,12 +1965,38 @@ impl AgentPanel {
}
}),
)
.item(
ContextMenuEntry::new("New Claude Code Thread")
.icon(IconName::AiClaude)
.disabled(is_via_collab)
.icon_color(Color::Muted)
.handler({
let workspace = workspace.clone();
move |window, cx| {
if let Some(workspace) = workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
if let Some(panel) =
workspace.panel::<AgentPanel>(cx)
{
panel.update(cx, |panel, cx| {
panel.new_agent_thread(
AgentType::ClaudeCode,
window,
cx,
);
});
}
});
}
}
}),
)
.map(|mut menu| {
let agent_names = agent_server_store
.read(cx)
.external_agents()
.filter(|name| {
name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME && name.0 != CODEX_NAME
name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME
})
.cloned()
.collect::<Vec<_>>();
@@ -2139,7 +2168,10 @@ impl AgentPanel {
false
}
_ => {
let history_is_empty = self.history_store.read(cx).is_empty(cx);
let history_is_empty = self.acp_history_store.read(cx).is_empty(cx)
&& self
.history_store
.update(cx, |store, cx| store.recent_entries(1, cx).is_empty());
let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
.providers()
@@ -2500,7 +2532,7 @@ impl Render for AgentPanel {
match self.active_view.which_font_size_used() {
WhichFontSize::AgentFont => {
WithRemSize::new(ThemeSettings::get_global(cx).agent_ui_font_size(cx))
WithRemSize::new(ThemeSettings::get_global(cx).agent_font_size(cx))
.size_full()
.child(content)
.into_any()

View File

@@ -161,12 +161,12 @@ pub struct NewNativeAgentThreadFromSummary {
}
// TODO unify this with AgentType
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExternalAgent {
enum ExternalAgent {
#[default]
Gemini,
ClaudeCode,
Codex,
NativeAgent,
Custom {
name: SharedString,
@@ -183,13 +183,12 @@ fn placeholder_command() -> AgentServerCommand {
}
impl ExternalAgent {
pub fn parse_built_in(server: &dyn agent_servers::AgentServer) -> Option<Self> {
match server.telemetry_id() {
"gemini-cli" => Some(Self::Gemini),
"claude-code" => Some(Self::ClaudeCode),
"codex" => Some(Self::Codex),
"zed" => Some(Self::NativeAgent),
_ => None,
fn name(&self) -> &'static str {
match self {
Self::NativeAgent => "zed",
Self::Gemini => "gemini-cli",
Self::ClaudeCode => "claude-code",
Self::Custom { .. } => "custom",
}
}
@@ -201,7 +200,6 @@ impl ExternalAgent {
match self {
Self::Gemini => Rc::new(agent_servers::Gemini),
Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
Self::Codex => Rc::new(agent_servers::Codex),
Self::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)),
Self::Custom { name, command: _ } => {
Rc::new(agent_servers::CustomAgentServer::new(name.clone()))
@@ -266,7 +264,7 @@ pub fn init(
init_language_model_settings(cx);
}
assistant_slash_command::init(cx);
agent::init(fs.clone(), cx);
agent::init(cx);
agent_panel::init(cx);
context_server_configuration::init(language_registry.clone(), fs.clone(), cx);
TextThreadEditor::init(cx);

View File

@@ -33,8 +33,6 @@ use thread_context_picker::{
use ui::{
ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*,
};
use util::paths::PathStyle;
use util::rel_path::RelPath;
use workspace::{Workspace, notifications::NotifyResultExt};
use agent::{
@@ -230,19 +228,12 @@ impl ContextPicker {
let context_picker = cx.entity();
let menu = ContextMenu::build(window, cx, move |menu, _window, cx| {
let Some(workspace) = self.workspace.upgrade() else {
return menu;
};
let path_style = workspace.read(cx).path_style(cx);
let recent = self.recent_entries(cx);
let has_recent = !recent.is_empty();
let recent_entries = recent
.into_iter()
.enumerate()
.map(|(ix, entry)| {
self.recent_menu_item(context_picker.clone(), ix, entry, path_style)
})
.collect::<Vec<_>>();
.map(|(ix, entry)| self.recent_menu_item(context_picker.clone(), ix, entry));
let entries = self
.workspace
@@ -404,7 +395,6 @@ impl ContextPicker {
context_picker: Entity<ContextPicker>,
ix: usize,
entry: RecentEntry,
path_style: PathStyle,
) -> ContextMenuItem {
match entry {
RecentEntry::File {
@@ -423,7 +413,6 @@ impl ContextPicker {
&path,
&path_prefix,
false,
path_style,
context_store.clone(),
cx,
)
@@ -597,7 +586,7 @@ impl Render for ContextPicker {
pub(crate) enum RecentEntry {
File {
project_path: ProjectPath,
path_prefix: Arc<RelPath>,
path_prefix: Arc<str>,
},
Thread(ThreadContextEntry),
}

View File

@@ -13,7 +13,6 @@ use http_client::HttpClientWithUrl;
use itertools::Itertools;
use language::{Buffer, CodeLabel, HighlightId};
use lsp::CompletionContext;
use project::lsp_store::SymbolLocation;
use project::{
Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, ProjectPath,
Symbol, WorktreeId,
@@ -23,8 +22,6 @@ use rope::Point;
use text::{Anchor, OffsetRangeExt, ToPoint};
use ui::prelude::*;
use util::ResultExt as _;
use util::paths::PathStyle;
use util::rel_path::RelPath;
use workspace::Workspace;
use agent::{
@@ -577,12 +574,11 @@ impl ContextPickerCompletionProvider {
fn completion_for_path(
project_path: ProjectPath,
path_prefix: &RelPath,
path_prefix: &str,
is_recent: bool,
is_directory: bool,
excerpt_id: ExcerptId,
source_range: Range<Anchor>,
path_style: PathStyle,
editor: Entity<Editor>,
context_store: Entity<ContextStore>,
cx: &App,
@@ -590,7 +586,6 @@ impl ContextPickerCompletionProvider {
let (file_name, directory) = super::file_context_picker::extract_file_name_and_directory(
&project_path.path,
path_prefix,
path_style,
);
let label =
@@ -662,22 +657,17 @@ impl ContextPickerCompletionProvider {
workspace: Entity<Workspace>,
cx: &mut App,
) -> Option<Completion> {
let path_style = workspace.read(cx).path_style(cx);
let SymbolLocation::InProject(symbol_path) = &symbol.path else {
return None;
};
let path_prefix = workspace
.read(cx)
.project()
.read(cx)
.worktree_for_id(symbol_path.worktree_id, cx)?
.worktree_for_id(symbol.path.worktree_id, cx)?
.read(cx)
.root_name();
let (file_name, directory) = super::file_context_picker::extract_file_name_and_directory(
&symbol_path.path,
&symbol.path.path,
path_prefix,
path_style,
);
let full_path = if let Some(directory) = directory {
format!("{}{}", directory, file_name)
@@ -778,7 +768,6 @@ impl CompletionProvider for ContextPickerCompletionProvider {
let text_thread_store = self.text_thread_store.clone();
let editor = self.editor.clone();
let http_client = workspace.read(cx).client().http_client();
let path_style = workspace.read(cx).path_style(cx);
let MentionCompletion { mode, argument, .. } = state;
let query = argument.unwrap_or_else(|| "".to_string());
@@ -845,7 +834,6 @@ impl CompletionProvider for ContextPickerCompletionProvider {
mat.is_dir,
excerpt_id,
source_range.clone(),
path_style,
editor.clone(),
context_store.clone(),
cx,
@@ -1076,7 +1064,7 @@ mod tests {
use serde_json::json;
use settings::SettingsStore;
use std::{ops::Deref, rc::Rc};
use util::{path, rel_path::rel_path};
use util::path;
use workspace::{AppState, Item};
#[test]
@@ -1227,18 +1215,16 @@ mod tests {
let mut cx = VisualTestContext::from_window(*window.deref(), cx);
let paths = vec![
rel_path("a/one.txt"),
rel_path("a/two.txt"),
rel_path("a/three.txt"),
rel_path("a/four.txt"),
rel_path("b/five.txt"),
rel_path("b/six.txt"),
rel_path("b/seven.txt"),
rel_path("b/eight.txt"),
path!("a/one.txt"),
path!("a/two.txt"),
path!("a/three.txt"),
path!("a/four.txt"),
path!("b/five.txt"),
path!("b/six.txt"),
path!("b/seven.txt"),
path!("b/eight.txt"),
];
let slash = PathStyle::local().separator();
let mut opened_editors = Vec::new();
for path in paths {
let buffer = workspace
@@ -1246,7 +1232,7 @@ mod tests {
workspace.open_path(
ProjectPath {
worktree_id,
path: path.into(),
path: Path::new(path).into(),
},
None,
false,
@@ -1322,13 +1308,13 @@ mod tests {
assert_eq!(
current_completion_labels(editor),
&[
format!("seven.txt dir{slash}b{slash}"),
format!("six.txt dir{slash}b{slash}"),
format!("five.txt dir{slash}b{slash}"),
format!("four.txt dir{slash}a{slash}"),
"Files & Directories".into(),
"Symbols".into(),
"Fetch".into()
"seven.txt dir/b/",
"six.txt dir/b/",
"five.txt dir/b/",
"four.txt dir/a/",
"Files & Directories",
"Symbols",
"Fetch"
]
);
});
@@ -1355,10 +1341,7 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(editor.text(cx), "Lorem @file one");
assert!(editor.has_visible_completions_menu());
assert_eq!(
current_completion_labels(editor),
vec![format!("one.txt dir{slash}a{slash}")]
);
assert_eq!(current_completion_labels(editor), vec!["one.txt dir/a/"]);
});
editor.update_in(&mut cx, |editor, window, cx| {
@@ -1367,10 +1350,7 @@ mod tests {
});
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!("Lorem [@one.txt](@file:dir{slash}a{slash}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),
@@ -1381,10 +1361,7 @@ mod tests {
cx.simulate_input(" ");
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!("Lorem [@one.txt](@file:dir{slash}a{slash}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),
@@ -1397,7 +1374,7 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) Ipsum "),
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum ",
);
assert!(!editor.has_visible_completions_menu());
assert_eq!(
@@ -1411,7 +1388,7 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) Ipsum @file "),
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum @file ",
);
assert!(editor.has_visible_completions_menu());
assert_eq!(
@@ -1429,7 +1406,7 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) Ipsum [@seven.txt](@file:dir{slash}b{slash}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!(
@@ -1446,7 +1423,7 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) Ipsum [@seven.txt](@file:dir{slash}b{slash}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!(
@@ -1467,7 +1444,7 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) Ipsum [@seven.txt](@file:dir{slash}b{slash}seven.txt) \n[@six.txt](@file:dir{slash}b{slash}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!(

View File

@@ -1,3 +1,4 @@
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
@@ -9,7 +10,7 @@ use gpui::{
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
use ui::{ListItem, Tooltip, prelude::*};
use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath};
use util::ResultExt as _;
use workspace::Workspace;
use crate::context_picker::ContextPicker;
@@ -160,8 +161,6 @@ impl PickerDelegate for FileContextPickerDelegate {
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let FileMatch { mat, .. } = &self.matches.get(ix)?;
let workspace = self.workspace.upgrade()?;
let path_style = workspace.read(cx).path_style(cx);
Some(
ListItem::new(ix)
@@ -173,7 +172,6 @@ impl PickerDelegate for FileContextPickerDelegate {
&mat.path,
&mat.path_prefix,
mat.is_dir,
path_style,
self.context_store.clone(),
cx,
)),
@@ -216,13 +214,14 @@ pub(crate) fn search_files(
let file_matches = project.worktrees(cx).flat_map(|worktree| {
let worktree = worktree.read(cx);
let path_prefix: Arc<str> = worktree.root_name().into();
worktree.entries(false, 0).map(move |entry| FileMatch {
mat: PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: worktree.id().to_usize(),
path: entry.path.clone(),
path_prefix: worktree.root_name().into(),
path_prefix: path_prefix.clone(),
distance_to_relative_ancestor: 0,
is_dir: entry.is_dir(),
},
@@ -270,31 +269,51 @@ pub(crate) fn search_files(
}
pub fn extract_file_name_and_directory(
path: &RelPath,
path_prefix: &RelPath,
path_style: PathStyle,
path: &Path,
path_prefix: &str,
) -> (SharedString, Option<SharedString>) {
let full_path = path_prefix.join(path);
let file_name = full_path.file_name().unwrap_or_default();
let display_path = full_path.display(path_style);
let (directory, file_name) = display_path.split_at(display_path.len() - file_name.len());
(
file_name.to_string().into(),
Some(SharedString::new(directory)).filter(|dir| !dir.is_empty()),
)
if path == Path::new("") {
(
SharedString::from(
path_prefix
.trim_end_matches(std::path::MAIN_SEPARATOR)
.to_string(),
),
None,
)
} else {
let file_name = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string()
.into();
let mut directory = path_prefix
.trim_end_matches(std::path::MAIN_SEPARATOR)
.to_string();
if !directory.ends_with('/') {
directory.push('/');
}
if let Some(parent) = path.parent().filter(|parent| parent != &Path::new("")) {
directory.push_str(&parent.to_string_lossy());
directory.push('/');
}
(file_name, Some(directory.into()))
}
}
pub fn render_file_context_entry(
id: ElementId,
worktree_id: WorktreeId,
path: &Arc<RelPath>,
path_prefix: &Arc<RelPath>,
path: &Arc<Path>,
path_prefix: &Arc<str>,
is_directory: bool,
path_style: PathStyle,
context_store: WeakEntity<ContextStore>,
cx: &App,
) -> Stateful<Div> {
let (file_name, directory) = extract_file_name_and_directory(path, path_prefix, path_style);
let (file_name, directory) = extract_file_name_and_directory(path, path_prefix);
let added = context_store.upgrade().and_then(|context_store| {
let project_path = ProjectPath {
@@ -311,9 +330,9 @@ pub fn render_file_context_entry(
});
let file_icon = if is_directory {
FileIcons::get_folder_icon(false, path.as_std_path(), cx)
FileIcons::get_folder_icon(false, path, cx)
} else {
FileIcons::get_icon(path.as_std_path(), cx)
FileIcons::get_icon(path, cx)
}
.map(Icon::from_path)
.unwrap_or_else(|| Icon::new(IconName::File));

View File

@@ -2,14 +2,13 @@ use std::cmp::Reverse;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use anyhow::{Result, anyhow};
use anyhow::Result;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
};
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
use project::lsp_store::SymbolLocation;
use project::{DocumentSymbol, Symbol};
use ui::{ListItem, prelude::*};
use util::ResultExt as _;
@@ -192,10 +191,7 @@ pub(crate) fn add_symbol(
) -> Task<Result<(Option<AgentContextHandle>, bool)>> {
let project = workspace.read(cx).project().clone();
let open_buffer_task = project.update(cx, |project, cx| {
let SymbolLocation::InProject(symbol_path) = &symbol.path else {
return Task::ready(Err(anyhow!("can't add symbol from outside of project")));
};
project.open_buffer(symbol_path.clone(), cx)
project.open_buffer(symbol.path.clone(), cx)
});
cx.spawn(async move |cx| {
let buffer = open_buffer_task.await?;
@@ -295,11 +291,10 @@ pub(crate) fn search_symbols(
.map(|(id, symbol)| {
StringMatchCandidate::new(id, symbol.label.filter_text())
})
.partition(|candidate| match &symbols[candidate.id].path {
SymbolLocation::InProject(project_path) => project
.entry_for_path(project_path, cx)
.is_some_and(|e| !e.is_ignored),
SymbolLocation::OutsideProject { .. } => false,
.partition(|candidate| {
project
.entry_for_path(&symbols[candidate.id].path, cx)
.is_some_and(|e| !e.is_ignored)
})
})
.log_err()
@@ -365,18 +360,13 @@ fn compute_symbol_entries(
}
pub fn render_symbol_context_entry(id: ElementId, entry: &SymbolEntry) -> Stateful<Div> {
let path = match &entry.symbol.path {
SymbolLocation::InProject(project_path) => {
project_path.path.file_name().unwrap_or_default().into()
}
SymbolLocation::OutsideProject {
abs_path,
signature: _,
} => abs_path
.file_name()
.map(|f| f.to_string_lossy())
.unwrap_or_default(),
};
let path = entry
.symbol
.path
.path
.file_name()
.map(|s| s.to_string_lossy())
.unwrap_or_default();
let symbol_location = format!("{} L{}", path, entry.symbol.range.start.0.row + 1);
h_flex()

View File

@@ -18,9 +18,7 @@ use agent_settings::AgentSettings;
use anyhow::{Context as _, Result};
use client::telemetry::Telemetry;
use collections::{HashMap, HashSet, VecDeque, hash_map};
use editor::RowExt;
use editor::SelectionEffects;
use editor::scroll::ScrollOffset;
use editor::{
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorEvent, ExcerptId, ExcerptRange,
MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint,
@@ -382,7 +380,7 @@ impl InlineAssistant {
if let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) {
for assist_id in &editor_assists.assist_ids {
let assist = &self.assists[assist_id];
let range = assist.range.to_point(&snapshot.buffer_snapshot());
let range = assist.range.to_point(&snapshot.buffer_snapshot);
if range.start.row <= newest_selection.start.row
&& newest_selection.end.row <= range.end.row
{
@@ -402,16 +400,16 @@ impl InlineAssistant {
selection.end.row -= 1;
}
selection.end.column = snapshot
.buffer_snapshot()
.buffer_snapshot
.line_len(MultiBufferRow(selection.end.row));
} else if let Some(fold) =
snapshot.crease_for_buffer_row(MultiBufferRow(selection.end.row))
{
selection.start = fold.range().start;
selection.end = fold.range().end;
if MultiBufferRow(selection.end.row) < snapshot.buffer_snapshot().max_row() {
if MultiBufferRow(selection.end.row) < snapshot.buffer_snapshot.max_row() {
let chars = snapshot
.buffer_snapshot()
.buffer_snapshot
.chars_at(Point::new(selection.end.row + 1, 0));
for c in chars {
@@ -427,7 +425,7 @@ impl InlineAssistant {
{
selection.end.row += 1;
selection.end.column = snapshot
.buffer_snapshot()
.buffer_snapshot
.line_len(MultiBufferRow(selection.end.row));
}
}
@@ -447,7 +445,7 @@ impl InlineAssistant {
}
selections.push(selection);
}
let snapshot = &snapshot.buffer_snapshot();
let snapshot = &snapshot.buffer_snapshot;
let newest_selection = newest_selection.unwrap();
let mut codegen_ranges = Vec::new();
@@ -746,7 +744,7 @@ impl InlineAssistant {
let scroll_bottom = scroll_top + editor.visible_line_count().unwrap_or(0.);
editor_assists.scroll_lock = editor
.row_for_block(decorations.prompt_block_id, cx)
.map(|row| row.as_f64())
.map(|row| row.0 as f32)
.filter(|prompt_row| (scroll_top..scroll_bottom).contains(&prompt_row))
.map(|prompt_row| InlineAssistScrollLock {
assist_id,
@@ -912,9 +910,7 @@ impl InlineAssistant {
editor.update(cx, |editor, cx| {
let scroll_position = editor.scroll_position(cx);
let target_scroll_top = editor
.row_for_block(decorations.prompt_block_id, cx)?
.as_f64()
let target_scroll_top = editor.row_for_block(decorations.prompt_block_id, cx)?.0 as f32
- scroll_lock.distance_from_top;
if target_scroll_top != scroll_position.y {
editor.set_scroll_position(point(scroll_position.x, target_scroll_top), window, cx);
@@ -963,9 +959,8 @@ impl InlineAssistant {
if let Some(decorations) = assist.decorations.as_ref() {
let distance_from_top = editor.update(cx, |editor, cx| {
let scroll_top = editor.scroll_position(cx).y;
let prompt_row = editor
.row_for_block(decorations.prompt_block_id, cx)?
.0 as ScrollOffset;
let prompt_row =
editor.row_for_block(decorations.prompt_block_id, cx)?.0 as f32;
Some(prompt_row - scroll_top)
});
@@ -1197,8 +1192,8 @@ impl InlineAssistant {
let mut scroll_target_range = None;
if let Some(decorations) = assist.decorations.as_ref() {
scroll_target_range = maybe!({
let top = editor.row_for_block(decorations.prompt_block_id, cx)?.0 as f64;
let bottom = editor.row_for_block(decorations.end_block_id, cx)?.0 as f64;
let top = editor.row_for_block(decorations.prompt_block_id, cx)?.0 as f32;
let bottom = editor.row_for_block(decorations.end_block_id, cx)?.0 as f32;
Some((top, bottom))
});
if scroll_target_range.is_none() {
@@ -1212,15 +1207,15 @@ impl InlineAssistant {
.start
.to_display_point(&snapshot.display_snapshot)
.row();
let top = start_row.0 as ScrollOffset;
let top = start_row.0 as f32;
let bottom = top + 1.0;
(top, bottom)
});
let mut scroll_target_top = scroll_target_range.0;
let mut scroll_target_bottom = scroll_target_range.1;
scroll_target_top -= editor.vertical_scroll_margin() as ScrollOffset;
scroll_target_bottom += editor.vertical_scroll_margin() as ScrollOffset;
scroll_target_top -= editor.vertical_scroll_margin() as f32;
scroll_target_bottom += editor.vertical_scroll_margin() as f32;
let height_in_lines = editor.visible_line_count().unwrap_or(0.);
let scroll_top = editor.scroll_position(cx).y;
@@ -1548,7 +1543,7 @@ struct EditorInlineAssists {
struct InlineAssistScrollLock {
assist_id: InlineAssistId,
distance_from_top: ScrollOffset,
distance_from_top: f32,
}
impl EditorInlineAssists {

View File

@@ -3,20 +3,12 @@ use agent_settings::{
AgentProfile, AgentProfileId, AgentSettings, AvailableProfiles, builtin_profiles,
};
use fs::Fs;
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
use gpui::{
Action, AnyElement, App, BackgroundExecutor, Context, DismissEvent, Entity, FocusHandle,
Focusable, SharedString, Subscription, Task, Window,
};
use picker::{Picker, PickerDelegate, popover_menu::PickerPopoverMenu};
use settings::{Settings as _, SettingsStore, update_settings_file};
use std::{
sync::atomic::Ordering,
sync::{Arc, atomic::AtomicBool},
};
use gpui::{Action, Entity, FocusHandle, Subscription, prelude::*};
use settings::{DockPosition, Settings as _, SettingsStore, update_settings_file};
use std::sync::Arc;
use ui::{
DocumentationAside, DocumentationEdge, DocumentationSide, HighlightedLabel, LabelSize,
ListItem, ListItemSpacing, PopoverMenuHandle, TintColor, Tooltip, prelude::*,
ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, PopoverMenu,
PopoverMenuHandle, TintColor, Tooltip, prelude::*,
};
/// Trait for types that can provide and manage agent profiles
@@ -33,11 +25,9 @@ pub trait ProfileProvider {
pub struct ProfileSelector {
profiles: AvailableProfiles,
pending_refresh: bool,
fs: Arc<dyn Fs>,
provider: Arc<dyn ProfileProvider>,
picker: Option<Entity<Picker<ProfilePickerDelegate>>>,
picker_handle: PopoverMenuHandle<Picker<ProfilePickerDelegate>>,
menu_handle: PopoverMenuHandle<ContextMenu>,
focus_handle: FocusHandle,
_subscriptions: Vec<Subscription>,
}
@@ -50,91 +40,125 @@ impl ProfileSelector {
cx: &mut Context<Self>,
) -> Self {
let settings_subscription = cx.observe_global::<SettingsStore>(move |this, cx| {
this.pending_refresh = true;
cx.notify();
this.refresh_profiles(cx);
});
Self {
profiles: AgentProfile::available_profiles(cx),
pending_refresh: false,
fs,
provider,
picker: None,
picker_handle: PopoverMenuHandle::default(),
menu_handle: PopoverMenuHandle::default(),
focus_handle,
_subscriptions: vec![settings_subscription],
}
}
pub fn menu_handle(&self) -> PopoverMenuHandle<Picker<ProfilePickerDelegate>> {
self.picker_handle.clone()
pub fn menu_handle(&self) -> PopoverMenuHandle<ContextMenu> {
self.menu_handle.clone()
}
fn ensure_picker(
&mut self,
fn refresh_profiles(&mut self, cx: &mut Context<Self>) {
self.profiles = AgentProfile::available_profiles(cx);
}
fn build_context_menu(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Entity<Picker<ProfilePickerDelegate>> {
if self.picker.is_none() {
let delegate = ProfilePickerDelegate::new(
self.fs.clone(),
self.provider.clone(),
self.profiles.clone(),
cx.background_executor().clone(),
cx,
);
) -> Entity<ContextMenu> {
ContextMenu::build(window, cx, |mut menu, _window, cx| {
let settings = AgentSettings::get_global(cx);
let picker = cx.new(|cx| {
Picker::list(delegate, window, cx)
.show_scrollbar(true)
.width(rems(18.))
.max_height(Some(rems(20.).into()))
});
self.picker = Some(picker);
}
if self.pending_refresh {
if let Some(picker) = &self.picker {
let profiles = AgentProfile::available_profiles(cx);
self.profiles = profiles.clone();
picker.update(cx, |picker, cx| {
let query = picker.query(cx);
picker
.delegate
.refresh_profiles(profiles.clone(), query, cx);
});
let mut found_non_builtin = false;
for (profile_id, profile_name) in self.profiles.iter() {
if !builtin_profiles::is_builtin(profile_id) {
found_non_builtin = true;
continue;
}
menu = menu.item(self.menu_entry_for_profile(
profile_id.clone(),
profile_name,
settings,
cx,
));
}
self.pending_refresh = false;
}
self.picker.as_ref().unwrap().clone()
if found_non_builtin {
menu = menu.separator().header("Custom Profiles");
for (profile_id, profile_name) in self.profiles.iter() {
if builtin_profiles::is_builtin(profile_id) {
continue;
}
menu = menu.item(self.menu_entry_for_profile(
profile_id.clone(),
profile_name,
settings,
cx,
));
}
}
menu = menu.separator();
menu = menu.item(ContextMenuEntry::new("Configure Profiles…").handler(
move |window, cx| {
window.dispatch_action(ManageProfiles::default().boxed_clone(), cx);
},
));
menu
})
}
}
impl Focusable for ProfileSelector {
fn focus_handle(&self, cx: &App) -> FocusHandle {
if let Some(picker) = &self.picker {
picker.focus_handle(cx)
fn menu_entry_for_profile(
&self,
profile_id: AgentProfileId,
profile_name: &SharedString,
settings: &AgentSettings,
cx: &App,
) -> ContextMenuEntry {
let documentation = match profile_name.to_lowercase().as_str() {
builtin_profiles::WRITE => Some("Get help to write anything."),
builtin_profiles::ASK => Some("Chat about your codebase."),
builtin_profiles::MINIMAL => Some("Chat about anything with no tools."),
_ => None,
};
let thread_profile_id = self.provider.profile_id(cx);
let entry = ContextMenuEntry::new(profile_name.clone())
.toggleable(IconPosition::End, profile_id == thread_profile_id);
let entry = if let Some(doc_text) = documentation {
entry.documentation_aside(
documentation_side(settings.dock),
DocumentationEdge::Top,
move |_| Label::new(doc_text).into_any_element(),
)
} else {
self.focus_handle.clone()
}
entry
};
entry.handler({
let fs = self.fs.clone();
let provider = self.provider.clone();
move |_window, cx| {
update_settings_file(fs.clone(), cx, {
let profile_id = profile_id.clone();
move |settings, _cx| {
settings
.agent
.get_or_insert_default()
.set_profile(profile_id.0);
}
});
provider.set_profile(profile_id.clone(), cx);
}
})
}
}
impl Render for ProfileSelector {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if !self.provider.profiles_supported(cx) {
return Button::new("tools-not-supported-button", "Tools Unsupported")
.disabled(true)
.label_size(LabelSize::Small)
.color(Color::Muted)
.tooltip(Tooltip::text("This model does not support tools."))
.into_any_element();
}
let picker = self.ensure_picker(window, cx);
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let settings = AgentSettings::get_global(cx);
let profile_id = self.provider.profile_id(cx);
let profile = settings.profiles.get(&profile_id);
@@ -142,594 +166,62 @@ impl Render for ProfileSelector {
let selected_profile = profile
.map(|profile| profile.name.clone())
.unwrap_or_else(|| "Unknown".into());
let focus_handle = self.focus_handle.clone();
let trigger_button = Button::new("profile-selector", selected_profile)
.label_size(LabelSize::Small)
.color(Color::Muted)
.icon(IconName::ChevronDown)
.icon_size(IconSize::XSmall)
.icon_position(IconPosition::End)
.icon_color(Color::Muted)
.selected_style(ButtonStyle::Tinted(TintColor::Accent));
if self.provider.profiles_supported(cx) {
let this = cx.entity();
let focus_handle = self.focus_handle.clone();
let trigger_button = Button::new("profile-selector-model", selected_profile)
.label_size(LabelSize::Small)
.color(Color::Muted)
.icon(IconName::ChevronDown)
.icon_size(IconSize::XSmall)
.icon_position(IconPosition::End)
.icon_color(Color::Muted)
.selected_style(ButtonStyle::Tinted(TintColor::Accent));
PickerPopoverMenu::new(
picker,
trigger_button,
move |window, cx| {
Tooltip::for_action_in(
"Toggle Profile Menu",
&ToggleProfileSelector,
&focus_handle,
window,
cx,
)
},
gpui::Corner::BottomRight,
cx,
)
.with_handle(self.picker_handle.clone())
.render(window, cx)
.into_any_element()
}
}
#[derive(Clone)]
struct ProfileCandidate {
id: AgentProfileId,
name: SharedString,
is_builtin: bool,
}
#[derive(Clone)]
struct ProfileMatchEntry {
candidate_index: usize,
positions: Vec<usize>,
}
enum ProfilePickerEntry {
Header(SharedString),
Profile(ProfileMatchEntry),
}
pub(crate) struct ProfilePickerDelegate {
fs: Arc<dyn Fs>,
provider: Arc<dyn ProfileProvider>,
background: BackgroundExecutor,
candidates: Vec<ProfileCandidate>,
string_candidates: Arc<Vec<StringMatchCandidate>>,
filtered_entries: Vec<ProfilePickerEntry>,
selected_index: usize,
query: String,
cancel: Option<Arc<AtomicBool>>,
}
impl ProfilePickerDelegate {
fn new(
fs: Arc<dyn Fs>,
provider: Arc<dyn ProfileProvider>,
profiles: AvailableProfiles,
background: BackgroundExecutor,
cx: &mut Context<ProfileSelector>,
) -> Self {
let candidates = Self::candidates_from(profiles);
let string_candidates = Arc::new(Self::string_candidates(&candidates));
let filtered_entries = Self::entries_from_candidates(&candidates);
let mut this = Self {
fs,
provider,
background,
candidates,
string_candidates,
filtered_entries,
selected_index: 0,
query: String::new(),
cancel: None,
};
this.selected_index = this
.index_of_profile(&this.provider.profile_id(cx))
.unwrap_or_else(|| this.first_selectable_index().unwrap_or(0));
this
}
fn refresh_profiles(
&mut self,
profiles: AvailableProfiles,
query: String,
cx: &mut Context<Picker<Self>>,
) {
self.candidates = Self::candidates_from(profiles);
self.string_candidates = Arc::new(Self::string_candidates(&self.candidates));
self.query = query;
if self.query.is_empty() {
self.filtered_entries = Self::entries_from_candidates(&self.candidates);
} else {
let matches = self.search_blocking(&self.query);
self.filtered_entries = self.entries_from_matches(matches);
}
self.selected_index = self
.index_of_profile(&self.provider.profile_id(cx))
.unwrap_or_else(|| self.first_selectable_index().unwrap_or(0));
cx.notify();
}
fn candidates_from(profiles: AvailableProfiles) -> Vec<ProfileCandidate> {
profiles
.into_iter()
.map(|(id, name)| ProfileCandidate {
is_builtin: builtin_profiles::is_builtin(&id),
id,
name,
})
.collect()
}
fn string_candidates(candidates: &[ProfileCandidate]) -> Vec<StringMatchCandidate> {
candidates
.iter()
.enumerate()
.map(|(index, candidate)| StringMatchCandidate::new(index, candidate.name.as_ref()))
.collect()
}
fn documentation(candidate: &ProfileCandidate) -> Option<&'static str> {
match candidate.id.as_str() {
builtin_profiles::WRITE => Some("Get help to write anything."),
builtin_profiles::ASK => Some("Chat about your codebase."),
builtin_profiles::MINIMAL => Some("Chat about anything with no tools."),
_ => None,
}
}
fn entries_from_candidates(candidates: &[ProfileCandidate]) -> Vec<ProfilePickerEntry> {
let mut entries = Vec::new();
let mut inserted_custom_header = false;
for (idx, candidate) in candidates.iter().enumerate() {
if !candidate.is_builtin && !inserted_custom_header {
if !entries.is_empty() {
entries.push(ProfilePickerEntry::Header("Custom Profiles".into()));
}
inserted_custom_header = true;
}
entries.push(ProfilePickerEntry::Profile(ProfileMatchEntry {
candidate_index: idx,
positions: Vec::new(),
}));
}
entries
}
fn entries_from_matches(&self, matches: Vec<StringMatch>) -> Vec<ProfilePickerEntry> {
let mut entries = Vec::new();
for mat in matches {
if self.candidates.get(mat.candidate_id).is_some() {
entries.push(ProfilePickerEntry::Profile(ProfileMatchEntry {
candidate_index: mat.candidate_id,
positions: mat.positions,
}));
}
}
entries
}
fn first_selectable_index(&self) -> Option<usize> {
self.filtered_entries
.iter()
.position(|entry| matches!(entry, ProfilePickerEntry::Profile(_)))
}
fn index_of_profile(&self, profile_id: &AgentProfileId) -> Option<usize> {
self.filtered_entries.iter().position(|entry| {
matches!(entry, ProfilePickerEntry::Profile(profile) if self
.candidates
.get(profile.candidate_index)
.map(|candidate| &candidate.id == profile_id)
.unwrap_or(false))
})
}
fn search_blocking(&self, query: &str) -> Vec<StringMatch> {
if query.is_empty() {
return self
.string_candidates
.iter()
.map(|candidate| StringMatch {
candidate_id: candidate.id,
score: 0.0,
positions: Vec::new(),
string: candidate.string.clone(),
PopoverMenu::new("profile-selector")
.trigger_with_tooltip(trigger_button, {
move |window, cx| {
Tooltip::for_action_in(
"Toggle Profile Menu",
&ToggleProfileSelector,
&focus_handle,
window,
cx,
)
}
})
.collect();
}
let cancel_flag = AtomicBool::new(false);
self.background.block(match_strings(
self.string_candidates.as_ref(),
query,
false,
true,
100,
&cancel_flag,
self.background.clone(),
))
}
}
impl PickerDelegate for ProfilePickerDelegate {
type ListItem = AnyElement;
fn placeholder_text(&self, _: &mut Window, _: &mut App) -> Arc<str> {
"Search profiles…".into()
}
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
let text = if self.candidates.is_empty() {
"No profiles.".into()
.anchor(
if documentation_side(settings.dock) == DocumentationSide::Left {
gpui::Corner::BottomRight
} else {
gpui::Corner::BottomLeft
},
)
.with_handle(self.menu_handle.clone())
.menu(move |window, cx| {
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
})
.offset(gpui::Point {
x: px(0.0),
y: px(-2.0),
})
.into_any_element()
} else {
"No profiles match your search.".into()
};
Some(text)
}
fn match_count(&self) -> usize {
self.filtered_entries.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
self.selected_index = ix.min(self.filtered_entries.len().saturating_sub(1));
cx.notify();
}
fn can_select(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> bool {
match self.filtered_entries.get(ix) {
Some(ProfilePickerEntry::Profile(_)) => true,
Some(ProfilePickerEntry::Header(_)) | None => false,
}
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
if query.is_empty() {
self.query.clear();
self.filtered_entries = Self::entries_from_candidates(&self.candidates);
self.selected_index = self
.index_of_profile(&self.provider.profile_id(cx))
.unwrap_or_else(|| self.first_selectable_index().unwrap_or(0));
cx.notify();
return Task::ready(());
}
if let Some(prev) = &self.cancel {
prev.store(true, Ordering::Relaxed);
}
let cancel = Arc::new(AtomicBool::new(false));
self.cancel = Some(cancel.clone());
let string_candidates = self.string_candidates.clone();
let background = self.background.clone();
let provider = self.provider.clone();
self.query = query.clone();
let cancel_for_future = cancel;
cx.spawn_in(window, async move |this, cx| {
let matches = match_strings(
string_candidates.as_ref(),
&query,
false,
true,
100,
cancel_for_future.as_ref(),
background,
)
.await;
this.update_in(cx, |this, _, cx| {
if this.delegate.query != query {
return;
}
this.delegate.filtered_entries = this.delegate.entries_from_matches(matches);
this.delegate.selected_index = this
.delegate
.index_of_profile(&provider.profile_id(cx))
.unwrap_or_else(|| this.delegate.first_selectable_index().unwrap_or(0));
cx.notify();
})
.ok();
})
}
fn confirm(&mut self, _: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
match self.filtered_entries.get(self.selected_index) {
Some(ProfilePickerEntry::Profile(entry)) => {
if let Some(candidate) = self.candidates.get(entry.candidate_index) {
let profile_id = candidate.id.clone();
let fs = self.fs.clone();
let provider = self.provider.clone();
update_settings_file(fs, cx, {
let profile_id = profile_id.clone();
move |settings, _cx| {
settings
.agent
.get_or_insert_default()
.set_profile(profile_id.0);
}
});
provider.set_profile(profile_id.clone(), cx);
telemetry::event!(
"agent_profile_switched",
profile_id = profile_id.as_str(),
source = "picker"
);
}
cx.emit(DismissEvent);
}
_ => {}
}
}
fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
cx.defer_in(window, |picker, window, cx| {
picker.set_query("", window, cx);
});
cx.emit(DismissEvent);
}
fn render_match(
&self,
ix: usize,
selected: bool,
_: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
match self.filtered_entries.get(ix)? {
ProfilePickerEntry::Header(label) => Some(
div()
.px_2p5()
.pb_0p5()
.when(ix > 0, |this| {
this.mt_1p5()
.pt_2()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
})
.child(
Label::new(label.clone())
.size(LabelSize::XSmall)
.color(Color::Muted),
)
.into_any_element(),
),
ProfilePickerEntry::Profile(entry) => {
let candidate = self.candidates.get(entry.candidate_index)?;
let active_id = self.provider.profile_id(cx);
let is_active = active_id == candidate.id;
Some(
ListItem::new(SharedString::from(candidate.id.0.clone()))
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(HighlightedLabel::new(
candidate.name.clone(),
entry.positions.clone(),
))
.when(is_active, |this| {
this.end_slot(
div()
.pr_2()
.child(Icon::new(IconName::Check).color(Color::Accent)),
)
})
.into_any_element(),
)
}
}
}
fn documentation_aside(
&self,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<DocumentationAside> {
use std::rc::Rc;
let entry = match self.filtered_entries.get(self.selected_index)? {
ProfilePickerEntry::Profile(entry) => entry,
ProfilePickerEntry::Header(_) => return None,
};
let candidate = self.candidates.get(entry.candidate_index)?;
let docs_aside = Self::documentation(candidate)?.to_string();
let settings = AgentSettings::get_global(cx);
let side = match settings.dock {
settings::DockPosition::Left => DocumentationSide::Right,
settings::DockPosition::Bottom | settings::DockPosition::Right => {
DocumentationSide::Left
}
};
Some(DocumentationAside {
side,
edge: DocumentationEdge::Top,
render: Rc::new(move |_| Label::new(docs_aside.clone()).into_any_element()),
})
}
fn render_footer(
&self,
_: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<gpui::AnyElement> {
Some(
h_flex()
.w_full()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.p_1()
.gap_4()
.justify_between()
.child(
Button::new("configure", "Configure")
.icon(IconName::Settings)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(|_, window, cx| {
window.dispatch_action(ManageProfiles::default().boxed_clone(), cx);
}),
)
.into_any(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use fs::FakeFs;
use gpui::TestAppContext;
#[gpui::test]
fn entries_include_custom_profiles(_cx: &mut TestAppContext) {
let candidates = vec![
ProfileCandidate {
id: AgentProfileId("write".into()),
name: SharedString::from("Write"),
is_builtin: true,
},
ProfileCandidate {
id: AgentProfileId("my-custom".into()),
name: SharedString::from("My Custom"),
is_builtin: false,
},
];
let entries = ProfilePickerDelegate::entries_from_candidates(&candidates);
assert!(entries.iter().any(|entry| matches!(
entry,
ProfilePickerEntry::Profile(profile)
if candidates[profile.candidate_index].id.as_str() == "my-custom"
)));
assert!(entries.iter().any(|entry| matches!(
entry,
ProfilePickerEntry::Header(label) if label.as_ref() == "Custom Profiles"
)));
}
#[gpui::test]
fn fuzzy_filter_returns_no_results_and_keeps_configure(cx: &mut TestAppContext) {
let candidates = vec![ProfileCandidate {
id: AgentProfileId("write".into()),
name: SharedString::from("Write"),
is_builtin: true,
}];
let delegate = ProfilePickerDelegate {
fs: FakeFs::new(cx.executor()),
provider: Arc::new(TestProfileProvider::new(AgentProfileId("write".into()))),
background: cx.executor(),
candidates,
string_candidates: Arc::new(Vec::new()),
filtered_entries: Vec::new(),
selected_index: 0,
query: String::new(),
cancel: None,
};
let matches = Vec::new(); // No matches
let _entries = delegate.entries_from_matches(matches);
}
#[gpui::test]
fn active_profile_selection_logic_works(cx: &mut TestAppContext) {
let candidates = vec![
ProfileCandidate {
id: AgentProfileId("write".into()),
name: SharedString::from("Write"),
is_builtin: true,
},
ProfileCandidate {
id: AgentProfileId("ask".into()),
name: SharedString::from("Ask"),
is_builtin: true,
},
];
let delegate = ProfilePickerDelegate {
fs: FakeFs::new(cx.executor()),
provider: Arc::new(TestProfileProvider::new(AgentProfileId("write".into()))),
background: cx.executor(),
candidates,
string_candidates: Arc::new(Vec::new()),
filtered_entries: vec![
ProfilePickerEntry::Profile(ProfileMatchEntry {
candidate_index: 0,
positions: Vec::new(),
}),
ProfilePickerEntry::Profile(ProfileMatchEntry {
candidate_index: 1,
positions: Vec::new(),
}),
],
selected_index: 0,
query: String::new(),
cancel: None,
};
// Active profile should be found at index 0
let active_index = delegate.index_of_profile(&AgentProfileId("write".into()));
assert_eq!(active_index, Some(0));
}
struct TestProfileProvider {
profile_id: AgentProfileId,
}
impl TestProfileProvider {
fn new(profile_id: AgentProfileId) -> Self {
Self { profile_id }
}
}
impl ProfileProvider for TestProfileProvider {
fn profile_id(&self, _cx: &App) -> AgentProfileId {
self.profile_id.clone()
}
fn set_profile(&self, _profile_id: AgentProfileId, _cx: &mut App) {}
fn profiles_supported(&self, _cx: &App) -> bool {
true
Button::new("tools-not-supported-button", "Tools Unsupported")
.disabled(true)
.label_size(LabelSize::Small)
.color(Color::Muted)
.tooltip(Tooltip::text("This model does not support tools."))
.into_any_element()
}
}
}
fn documentation_side(position: DockPosition) -> DocumentationSide {
match position {
DockPosition::Left => DocumentationSide::Right,
DockPosition::Bottom => DocumentationSide::Left,
DockPosition::Right => DocumentationSide::Left,
}
}

View File

@@ -238,7 +238,7 @@ impl TerminalInlineAssistant {
let latest_output = terminal.last_n_non_empty_lines(DEFAULT_CONTEXT_LINES);
let working_directory = terminal
.working_directory()
.map(|path| path.to_string_lossy().into_owned());
.map(|path| path.to_string_lossy().to_string());
(latest_output, working_directory)
})
.ok()

View File

@@ -17,7 +17,6 @@ use editor::{
BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata, CustomBlockId, FoldId,
RenderBlock, ToDisplayPoint,
},
scroll::ScrollOffset,
};
use editor::{FoldPlaceholder, display_map::CreaseId};
use fs::Fs;
@@ -109,7 +108,7 @@ pub enum InsertDraggedFiles {
#[derive(Copy, Clone, Debug, PartialEq)]
struct ScrollPosition {
offset_before_cursor: gpui::Point<ScrollOffset>,
offset_before_cursor: gpui::Point<f32>,
cursor: Anchor,
}
@@ -632,7 +631,7 @@ impl TextThreadEditor {
let snapshot = editor.snapshot(window, cx);
let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
let scroll_top =
cursor_point.row().as_f64() - scroll_position.offset_before_cursor.y;
cursor_point.row().as_f32() - scroll_position.offset_before_cursor.y;
editor.set_scroll_position(
point(scroll_position.offset_before_cursor.x, scroll_top),
window,
@@ -980,7 +979,7 @@ impl TextThreadEditor {
let cursor_row = cursor
.to_display_point(&snapshot.display_snapshot)
.row()
.as_f64();
.as_f32();
let scroll_position = editor
.scroll_manager
.anchor()
@@ -1432,14 +1431,10 @@ impl TextThreadEditor {
else {
continue;
};
let path_style = worktree.read(cx).path_style();
let full_path = worktree
.read(cx)
.root_name()
.join(&project_path.path)
.display(path_style)
.into_owned();
file_slash_command_args.push(full_path);
let worktree_root_name = worktree.read(cx).root_name().to_string();
let mut full_path = PathBuf::from(worktree_root_name.clone());
full_path.push(&project_path.path);
file_slash_command_args.push(full_path.to_string_lossy().to_string());
}
let cmd_name = FileSlashCommand.name();

View File

@@ -48,7 +48,7 @@ impl Render for BurnModeTooltip {
let keybinding = KeyBinding::for_action(&ToggleBurnMode, window, cx)
.map(|kb| kb.size(rems_from_px(12.)));
tooltip_container(cx, |this, _| {
tooltip_container(window, cx, |this, _, _| {
this
.child(
h_flex()

View File

@@ -17,7 +17,6 @@ use agent::context::{
FileContextHandle, ImageContext, ImageStatus, RulesContextHandle, SelectionContextHandle,
SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle,
};
use util::paths::PathStyle;
#[derive(IntoElement)]
pub enum ContextPill {
@@ -304,54 +303,33 @@ impl AddedContext {
cx: &App,
) -> Option<AddedContext> {
match handle {
AgentContextHandle::File(handle) => {
Self::pending_file(handle, project.path_style(cx), cx)
}
AgentContextHandle::File(handle) => Self::pending_file(handle, cx),
AgentContextHandle::Directory(handle) => Self::pending_directory(handle, project, cx),
AgentContextHandle::Symbol(handle) => {
Self::pending_symbol(handle, project.path_style(cx), cx)
}
AgentContextHandle::Selection(handle) => {
Self::pending_selection(handle, project.path_style(cx), cx)
}
AgentContextHandle::Symbol(handle) => Self::pending_symbol(handle, cx),
AgentContextHandle::Selection(handle) => Self::pending_selection(handle, cx),
AgentContextHandle::FetchedUrl(handle) => Some(Self::fetched_url(handle)),
AgentContextHandle::Thread(handle) => Some(Self::pending_thread(handle, cx)),
AgentContextHandle::TextThread(handle) => Some(Self::pending_text_thread(handle, cx)),
AgentContextHandle::Rules(handle) => Self::pending_rules(handle, prompt_store, cx),
AgentContextHandle::Image(handle) => {
Some(Self::image(handle, model, project.path_style(cx), cx))
}
AgentContextHandle::Image(handle) => Some(Self::image(handle, model, cx)),
}
}
fn pending_file(
handle: FileContextHandle,
path_style: PathStyle,
cx: &App,
) -> Option<AddedContext> {
let full_path = handle
.buffer
.read(cx)
.file()?
.full_path(cx)
.to_string_lossy()
.to_string();
Some(Self::file(handle, &full_path, path_style, cx))
fn pending_file(handle: FileContextHandle, cx: &App) -> Option<AddedContext> {
let full_path = handle.buffer.read(cx).file()?.full_path(cx);
Some(Self::file(handle, &full_path, cx))
}
fn file(
handle: FileContextHandle,
full_path: &str,
path_style: PathStyle,
cx: &App,
) -> AddedContext {
let (name, parent) = extract_file_name_and_directory_from_full_path(full_path, path_style);
fn file(handle: FileContextHandle, full_path: &Path, cx: &App) -> AddedContext {
let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
let (name, parent) =
extract_file_name_and_directory_from_full_path(full_path, &full_path_string);
AddedContext {
kind: ContextKind::File,
name,
parent,
tooltip: Some(SharedString::new(full_path)),
icon_path: FileIcons::get_icon(Path::new(full_path), cx),
tooltip: Some(full_path_string),
icon_path: FileIcons::get_icon(full_path, cx),
status: ContextStatus::Ready,
render_hover: None,
handle: AgentContextHandle::File(handle),
@@ -365,24 +343,19 @@ impl AddedContext {
) -> Option<AddedContext> {
let worktree = project.worktree_for_entry(handle.entry_id, cx)?.read(cx);
let entry = worktree.entry_for_id(handle.entry_id)?;
let full_path = worktree
.full_path(&entry.path)
.to_string_lossy()
.to_string();
Some(Self::directory(handle, &full_path, project.path_style(cx)))
let full_path = worktree.full_path(&entry.path);
Some(Self::directory(handle, &full_path))
}
fn directory(
handle: DirectoryContextHandle,
full_path: &str,
path_style: PathStyle,
) -> AddedContext {
let (name, parent) = extract_file_name_and_directory_from_full_path(full_path, path_style);
fn directory(handle: DirectoryContextHandle, full_path: &Path) -> AddedContext {
let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
let (name, parent) =
extract_file_name_and_directory_from_full_path(full_path, &full_path_string);
AddedContext {
kind: ContextKind::Directory,
name,
parent,
tooltip: Some(SharedString::new(full_path)),
tooltip: Some(full_path_string),
icon_path: None,
status: ContextStatus::Ready,
render_hover: None,
@@ -390,17 +363,9 @@ impl AddedContext {
}
}
fn pending_symbol(
handle: SymbolContextHandle,
path_style: PathStyle,
cx: &App,
) -> Option<AddedContext> {
let excerpt = ContextFileExcerpt::new(
&handle.full_path(cx)?.to_string_lossy(),
handle.enclosing_line_range(cx),
path_style,
cx,
);
fn pending_symbol(handle: SymbolContextHandle, cx: &App) -> Option<AddedContext> {
let excerpt =
ContextFileExcerpt::new(&handle.full_path(cx)?, handle.enclosing_line_range(cx), cx);
Some(AddedContext {
kind: ContextKind::Symbol,
name: handle.symbol.clone(),
@@ -418,17 +383,8 @@ impl AddedContext {
})
}
fn pending_selection(
handle: SelectionContextHandle,
path_style: PathStyle,
cx: &App,
) -> Option<AddedContext> {
let excerpt = ContextFileExcerpt::new(
&handle.full_path(cx)?.to_string_lossy(),
handle.line_range(cx),
path_style,
cx,
);
fn pending_selection(handle: SelectionContextHandle, cx: &App) -> Option<AddedContext> {
let excerpt = ContextFileExcerpt::new(&handle.full_path(cx)?, handle.line_range(cx), cx);
Some(AddedContext {
kind: ContextKind::Selection,
name: excerpt.file_name_and_range.clone(),
@@ -529,13 +485,13 @@ impl AddedContext {
fn image(
context: ImageContext,
model: Option<&Arc<dyn language_model::LanguageModel>>,
path_style: PathStyle,
cx: &App,
) -> AddedContext {
let (name, parent, icon_path) = if let Some(full_path) = context.full_path.as_ref() {
let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
let (name, parent) =
extract_file_name_and_directory_from_full_path(full_path, path_style);
let icon_path = FileIcons::get_icon(Path::new(full_path), cx);
extract_file_name_and_directory_from_full_path(full_path, &full_path_string);
let icon_path = FileIcons::get_icon(full_path, cx);
(name, parent, icon_path)
} else {
("Image".into(), None, None)
@@ -584,20 +540,19 @@ impl AddedContext {
}
fn extract_file_name_and_directory_from_full_path(
path: &str,
path_style: PathStyle,
path: &Path,
name_fallback: &SharedString,
) -> (SharedString, Option<SharedString>) {
let (parent, file_name) = path_style.split(path);
let parent = parent.and_then(|parent| {
let parent = parent.trim_end_matches(path_style.separator());
let (_, parent) = path_style.split(parent);
if parent.is_empty() {
None
} else {
Some(SharedString::new(parent))
}
});
(SharedString::new(file_name), parent)
let name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned().into())
.unwrap_or_else(|| name_fallback.clone());
let parent = path
.parent()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().into_owned().into());
(name, parent)
}
#[derive(Debug, Clone)]
@@ -609,25 +564,25 @@ struct ContextFileExcerpt {
}
impl ContextFileExcerpt {
pub fn new(full_path: &str, line_range: Range<Point>, path_style: PathStyle, cx: &App) -> Self {
let (parent, file_name) = path_style.split(full_path);
pub fn new(full_path: &Path, line_range: Range<Point>, cx: &App) -> Self {
let full_path_string = full_path.to_string_lossy().into_owned();
let file_name = full_path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| full_path_string.clone());
let line_range_text = format!(" ({}-{})", line_range.start.row + 1, line_range.end.row + 1);
let mut full_path_and_range = full_path.to_owned();
let mut full_path_and_range = full_path_string;
full_path_and_range.push_str(&line_range_text);
let mut file_name_and_range = file_name.to_owned();
let mut file_name_and_range = file_name;
file_name_and_range.push_str(&line_range_text);
let parent_name = parent.and_then(|parent| {
let parent = parent.trim_end_matches(path_style.separator());
let (_, parent) = path_style.split(parent);
if parent.is_empty() {
None
} else {
Some(SharedString::new(parent))
}
});
let parent_name = full_path
.parent()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().into_owned().into());
let icon_path = FileIcons::get_icon(Path::new(full_path), cx);
let icon_path = FileIcons::get_icon(full_path, cx);
ContextFileExcerpt {
file_name_and_range: file_name_and_range.into(),
@@ -704,7 +659,7 @@ impl ContextPillHover {
impl Render for ContextPillHover {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
tooltip_container(cx, move |this, cx| {
tooltip_container(window, cx, move |this, window, cx| {
this.occlude()
.on_mouse_move(|_, _, cx| cx.stop_propagation())
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
@@ -735,7 +690,6 @@ impl Component for AddedContext {
image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
},
None,
PathStyle::local(),
cx,
),
);
@@ -756,7 +710,6 @@ impl Component for AddedContext {
.shared(),
},
None,
PathStyle::local(),
cx,
),
);
@@ -772,7 +725,6 @@ impl Component for AddedContext {
image_task: Task::ready(None).shared(),
},
None,
PathStyle::local(),
cx,
),
);
@@ -815,8 +767,7 @@ mod tests {
full_path: None,
};
let added_context =
AddedContext::image(image_context, Some(&model), PathStyle::local(), cx);
let added_context = AddedContext::image(image_context, Some(&model), cx);
assert!(matches!(
added_context.status,
@@ -839,7 +790,7 @@ mod tests {
full_path: None,
};
let added_context = AddedContext::image(image_context, None, PathStyle::local(), cx);
let added_context = AddedContext::image(image_context, None, cx);
assert!(
matches!(added_context.status, ContextStatus::Ready),

View File

@@ -40,7 +40,7 @@ impl AgentOnboardingModal {
}
fn view_blog(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
cx.open_url("https://zed.dev/blog/fastest-ai-code-editor");
cx.open_url("http://zed.dev/blog/fastest-ai-code-editor");
cx.notify();
agent_onboarding_event!("Blog Link Clicked");

View File

@@ -12,8 +12,8 @@ impl UnavailableEditingTooltip {
}
impl Render for UnavailableEditingTooltip {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
tooltip_container(cx, |this, _| {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
tooltip_container(window, cx, |this, _, _| {
this.child(Label::new("Unavailable Editing")).child(
div().max_w_64().child(
Label::new(format!(

View File

@@ -18,6 +18,7 @@ default = []
client.workspace = true
cloud_llm_client.workspace = true
component.workspace = true
feature_flags.workspace = true
gpui.workspace = true
language_model.workspace = true
serde.workspace = true

View File

@@ -18,6 +18,7 @@ pub use young_account_banner::YoungAccountBanner;
use std::sync::Arc;
use client::{Client, UserStore, zed_urls};
use feature_flags::{BillingV2FeatureFlag, FeatureFlagAppExt as _};
use gpui::{AnyElement, Entity, IntoElement, ParentElement};
use ui::{Divider, RegisterComponent, Tooltip, prelude::*};
@@ -84,32 +85,10 @@ impl ZedAiOnboarding {
self
}
fn render_dismiss_button(&self) -> Option<AnyElement> {
self.dismiss_onboarding.as_ref().map(|dismiss_callback| {
let callback = dismiss_callback.clone();
h_flex()
.absolute()
.top_0()
.right_0()
.child(
IconButton::new("dismiss_onboarding", IconName::Close)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Dismiss"))
.on_click(move |_, window, cx| {
telemetry::event!("Banner Dismissed", source = "AI Onboarding",);
callback(window, cx)
}),
)
.into_any_element()
})
}
fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement {
fn render_sign_in_disclaimer(&self, cx: &mut App) -> AnyElement {
let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
v_flex()
.relative()
.gap_1()
.child(Headline::new("Welcome to Zed AI"))
.child(
@@ -117,7 +96,7 @@ impl ZedAiOnboarding {
.color(Color::Muted)
.mb_2(),
)
.child(PlanDefinitions.pro_plan(true, false))
.child(PlanDefinitions.pro_plan(cx.has_flag::<BillingV2FeatureFlag>(), false))
.child(
Button::new("sign_in", "Try Zed Pro for Free")
.disabled(signing_in)
@@ -131,7 +110,6 @@ impl ZedAiOnboarding {
}
}),
)
.children(self.render_dismiss_button())
.into_any_element()
}
@@ -142,7 +120,7 @@ impl ZedAiOnboarding {
.max_w_full()
.gap_1()
.child(Headline::new("Welcome to Zed AI"))
.child(YoungAccountBanner)
.child(YoungAccountBanner::new(is_v2))
.child(
v_flex()
.mt_2()
@@ -203,7 +181,27 @@ impl ZedAiOnboarding {
)
.child(PlanDefinitions.free_plan(is_v2)),
)
.children(self.render_dismiss_button())
.when_some(
self.dismiss_onboarding.as_ref(),
|this, dismiss_callback| {
let callback = dismiss_callback.clone();
this.child(
h_flex().absolute().top_0().right_0().child(
IconButton::new("dismiss_onboarding", IconName::Close)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Dismiss"))
.on_click(move |_, window, cx| {
telemetry::event!(
"Banner Dismissed",
source = "AI Onboarding",
);
callback(window, cx)
}),
),
)
},
)
.child(
v_flex()
.mt_2()
@@ -248,7 +246,26 @@ impl ZedAiOnboarding {
.mb_2(),
)
.child(PlanDefinitions.pro_trial(is_v2, false))
.children(self.render_dismiss_button())
.when_some(
self.dismiss_onboarding.as_ref(),
|this, dismiss_callback| {
let callback = dismiss_callback.clone();
this.child(
h_flex().absolute().top_0().right_0().child(
IconButton::new("dismiss_onboarding", IconName::Close)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Dismiss"))
.on_click(move |_, window, cx| {
telemetry::event!(
"Banner Dismissed",
source = "AI Onboarding",
);
callback(window, cx)
}),
),
)
},
)
.into_any_element()
}
@@ -262,7 +279,26 @@ impl ZedAiOnboarding {
.mb_2(),
)
.child(PlanDefinitions.pro_plan(is_v2, false))
.children(self.render_dismiss_button())
.when_some(
self.dismiss_onboarding.as_ref(),
|this, dismiss_callback| {
let callback = dismiss_callback.clone();
this.child(
h_flex().absolute().top_0().right_0().child(
IconButton::new("dismiss_onboarding", IconName::Close)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Dismiss"))
.on_click(move |_, window, cx| {
telemetry::event!(
"Banner Dismissed",
source = "AI Onboarding",
);
callback(window, cx)
}),
),
)
},
)
.into_any_element()
}
}
@@ -271,7 +307,7 @@ impl RenderOnce for ZedAiOnboarding {
fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
if matches!(self.sign_in_status, SignInStatus::SignedIn) {
match self.plan {
None => self.render_free_plan_state(true, cx),
None => self.render_free_plan_state(cx.has_flag::<BillingV2FeatureFlag>(), cx),
Some(plan @ (Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree))) => {
self.render_free_plan_state(plan.is_v2(), cx)
}

View File

@@ -2,6 +2,7 @@ use std::sync::Arc;
use client::{Client, UserStore, zed_urls};
use cloud_llm_client::{Plan, PlanV1, PlanV2};
use feature_flags::{BillingV2FeatureFlag, FeatureFlagAppExt};
use gpui::{AnyElement, App, Entity, IntoElement, RenderOnce, Window};
use ui::{CommonAnimationExt, Divider, Vector, VectorName, prelude::*};
@@ -49,7 +50,9 @@ impl AiUpsellCard {
impl RenderOnce for AiUpsellCard {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let is_v2_plan = self.user_plan.map_or(true, |plan| plan.is_v2());
let is_v2_plan = self
.user_plan
.map_or(cx.has_flag::<BillingV2FeatureFlag>(), |plan| plan.is_v2());
let pro_section = v_flex()
.flex_grow()
@@ -172,7 +175,7 @@ impl RenderOnce for AiUpsellCard {
.child(Label::new("Try Zed AI").size(LabelSize::Large))
.map(|this| {
if self.account_too_young {
this.child(YoungAccountBanner).child(
this.child(YoungAccountBanner::new(is_v2_plan)).child(
v_flex()
.mt_2()
.gap_1()

View File

@@ -2,17 +2,30 @@ use gpui::{IntoElement, ParentElement};
use ui::{Banner, prelude::*};
#[derive(IntoElement)]
pub struct YoungAccountBanner;
pub struct YoungAccountBanner {
is_v2: bool,
}
impl YoungAccountBanner {
pub fn new(is_v2: bool) -> Self {
Self { is_v2 }
}
}
impl RenderOnce for YoungAccountBanner {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
const YOUNG_ACCOUNT_DISCLAIMER: &str = "To prevent abuse of our service, GitHub accounts created fewer than 30 days ago are not eligible for the Pro trial. You can request an exception by reaching out to billing-support@zed.dev";
const YOUNG_ACCOUNT_DISCLAIMER: &str = "To prevent abuse of our service, GitHub accounts created fewer than 30 days ago are not eligible for free plan usage or Pro plan free trial. You can request an exception by reaching out to billing-support@zed.dev";
const YOUNG_ACCOUNT_DISCLAIMER_V2: &str = "To prevent abuse of our service, GitHub accounts created fewer than 30 days ago are not eligible for the Pro trial. You can request an exception by reaching out to billing-support@zed.dev";
let label = div()
.w_full()
.text_sm()
.text_color(cx.theme().colors().text_muted)
.child(YOUNG_ACCOUNT_DISCLAIMER);
.child(if self.is_v2 {
YOUNG_ACCOUNT_DISCLAIMER_V2
} else {
YOUNG_ACCOUNT_DISCLAIMER
});
div()
.max_w_full()

View File

@@ -16,8 +16,8 @@ anyhow.workspace = true
futures.workspace = true
gpui.workspace = true
net.workspace = true
proto.workspace = true
smol.workspace = true
log.workspace = true
tempfile.workspace = true
util.workspace = true
workspace-hack.workspace = true
@@ -25,6 +25,3 @@ zeroize.workspace = true
[target.'cfg(target_os = "windows")'.dependencies]
windows.workspace = true
[package.metadata.cargo-machete]
ignored = ["log"]

View File

@@ -1,16 +1,10 @@
mod encrypted_password;
pub use encrypted_password::{EncryptedPassword, IKnowWhatIAmDoingAndIHaveReadTheDocs};
pub use encrypted_password::{EncryptedPassword, ProcessExt};
use net::async_net::UnixListener;
use smol::lock::Mutex;
use util::fs::make_file_executable;
use std::ffi::OsStr;
use std::ops::ControlFlow;
use std::sync::Arc;
#[cfg(target_os = "windows")]
use std::sync::OnceLock;
use std::time::Duration;
use std::{ffi::OsStr, time::Duration};
use anyhow::{Context as _, Result};
use futures::channel::{mpsc, oneshot};
@@ -20,13 +14,9 @@ use futures::{
};
use gpui::{AsyncApp, BackgroundExecutor, Task};
use smol::fs;
use util::{ResultExt as _, debug_panic, maybe, paths::PathExt};
use util::ResultExt as _;
/// Path to the program used for askpass
///
/// On Unix and remote servers, this defaults to the current executable
/// On Windows, this is set to the CLI variant of zed
static ASKPASS_PROGRAM: OnceLock<std::path::PathBuf> = OnceLock::new();
use crate::encrypted_password::decrypt;
#[derive(PartialEq, Eq)]
pub enum AskPassResult {
@@ -36,7 +26,6 @@ pub enum AskPassResult {
pub struct AskPassDelegate {
tx: mpsc::UnboundedSender<(String, oneshot::Sender<EncryptedPassword>)>,
executor: BackgroundExecutor,
_task: Task<()>,
}
@@ -54,27 +43,24 @@ impl AskPassDelegate {
password_prompt(prompt, channel, cx);
}
});
Self {
tx,
_task: task,
executor: cx.background_executor().clone(),
}
Self { tx, _task: task }
}
pub fn ask_password(&mut self, prompt: String) -> Task<Option<EncryptedPassword>> {
let mut this_tx = self.tx.clone();
self.executor.spawn(async move {
let (tx, rx) = oneshot::channel();
this_tx.send((prompt, tx)).await.ok()?;
rx.await.ok()
})
pub async fn ask_password(&mut self, prompt: String) -> Result<EncryptedPassword> {
let (tx, rx) = oneshot::channel();
self.tx.send((prompt, tx)).await?;
Ok(rx.await?)
}
}
pub struct AskPassSession {
#[cfg(not(target_os = "windows"))]
script_path: std::path::PathBuf,
#[cfg(target_os = "windows")]
askpass_helper: String,
#[cfg(target_os = "windows")]
secret: std::sync::Arc<OnceLock<EncryptedPassword>>,
askpass_task: PasswordProxy,
_askpass_task: Task<()>,
askpass_opened_rx: Option<oneshot::Receiver<()>>,
askpass_kill_master_rx: Option<oneshot::Receiver<()>>,
}
@@ -89,57 +75,104 @@ impl AskPassSession {
/// You must retain this session until the master process exits.
#[must_use]
pub async fn new(executor: &BackgroundExecutor, mut delegate: AskPassDelegate) -> Result<Self> {
use net::async_net::UnixListener;
use util::fs::make_file_executable;
#[cfg(target_os = "windows")]
let secret = std::sync::Arc::new(OnceLock::new());
let temp_dir = tempfile::Builder::new().prefix("zed-askpass").tempdir()?;
let askpass_socket = temp_dir.path().join("askpass.sock");
let askpass_script_path = temp_dir.path().join(ASKPASS_SCRIPT_NAME);
let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>();
let askpass_opened_tx = Arc::new(Mutex::new(Some(askpass_opened_tx)));
let listener = UnixListener::bind(&askpass_socket).context("creating askpass socket")?;
#[cfg(not(target_os = "windows"))]
let zed_path = util::get_shell_safe_zed_path()?;
#[cfg(target_os = "windows")]
let zed_path = std::env::current_exe()
.context("finding current executable path for use in askpass")?;
let (askpass_kill_master_tx, askpass_kill_master_rx) = oneshot::channel::<()>();
let kill_tx = Arc::new(Mutex::new(Some(askpass_kill_master_tx)));
let mut kill_tx = Some(askpass_kill_master_tx);
#[cfg(target_os = "windows")]
let askpass_secret = secret.clone();
let get_password = {
let executor = executor.clone();
let askpass_task = executor.spawn(async move {
let mut askpass_opened_tx = Some(askpass_opened_tx);
move |prompt| {
let prompt = delegate.ask_password(prompt);
let kill_tx = kill_tx.clone();
let askpass_opened_tx = askpass_opened_tx.clone();
#[cfg(target_os = "windows")]
let askpass_secret = askpass_secret.clone();
executor.spawn(async move {
if let Some(askpass_opened_tx) = askpass_opened_tx.lock().await.take() {
askpass_opened_tx.send(()).ok();
while let Ok((mut stream, _)) = listener.accept().await {
if let Some(askpass_opened_tx) = askpass_opened_tx.take() {
askpass_opened_tx.send(()).ok();
}
let mut buffer = Vec::new();
let mut reader = BufReader::new(&mut stream);
if reader.read_until(b'\0', &mut buffer).await.is_err() {
buffer.clear();
}
let prompt = String::from_utf8_lossy(&buffer);
if let Some(password) = delegate
.ask_password(prompt.to_string())
.await
.context("getting askpass password")
.log_err()
{
#[cfg(target_os = "windows")]
{
askpass_secret.get_or_init(|| password.clone());
}
if let Some(password) = prompt.await {
#[cfg(target_os = "windows")]
{
_ = askpass_secret.set(password.clone());
}
ControlFlow::Continue(Ok(password))
} else {
if let Some(kill_tx) = kill_tx.lock().await.take() {
kill_tx.send(()).log_err();
}
ControlFlow::Break(())
if let Ok(decrypted) = decrypt(password) {
stream.write_all(decrypted.as_bytes()).await.log_err();
}
})
} else {
if let Some(kill_tx) = kill_tx.take() {
kill_tx.send(()).log_err();
}
// note: we expect the caller to drop this task when it's done.
// We need to keep the stream open until the caller is done to avoid
// spurious errors from ssh.
std::future::pending::<()>().await;
drop(stream);
}
}
};
let askpass_task = PasswordProxy::new(get_password, executor.clone()).await?;
drop(temp_dir)
});
// Create an askpass script that communicates back to this process.
let askpass_script = generate_askpass_script(&zed_path, &askpass_socket);
fs::write(&askpass_script_path, askpass_script)
.await
.with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?;
make_file_executable(&askpass_script_path).await?;
#[cfg(target_os = "windows")]
let askpass_helper = format!(
"powershell.exe -ExecutionPolicy Bypass -File {}",
askpass_script_path.display()
);
Ok(Self {
#[cfg(not(target_os = "windows"))]
script_path: askpass_script_path,
#[cfg(target_os = "windows")]
secret,
#[cfg(target_os = "windows")]
askpass_helper,
askpass_task,
_askpass_task: askpass_task,
askpass_kill_master_rx: Some(askpass_kill_master_rx),
askpass_opened_rx: Some(askpass_opened_rx),
})
}
#[cfg(not(target_os = "windows"))]
pub fn script_path(&self) -> impl AsRef<OsStr> {
&self.script_path
}
#[cfg(target_os = "windows")]
pub fn script_path(&self) -> impl AsRef<OsStr> {
&self.askpass_helper
}
// This will run the askpass task forever, resolving as many authentication requests as needed.
// The caller is responsible for examining the result of their own commands and cancelling this
// future when this is no longer needed. Note that this can only be called once, but due to the
@@ -171,109 +204,8 @@ impl AskPassSession {
pub fn get_password(&self) -> Option<EncryptedPassword> {
self.secret.get().cloned()
}
pub fn script_path(&self) -> impl AsRef<OsStr> {
self.askpass_task.script_path()
}
}
pub struct PasswordProxy {
_task: Task<()>,
#[cfg(not(target_os = "windows"))]
askpass_script_path: std::path::PathBuf,
#[cfg(target_os = "windows")]
askpass_helper: String,
}
impl PasswordProxy {
pub async fn new(
mut get_password: impl FnMut(String) -> Task<ControlFlow<(), Result<EncryptedPassword>>>
+ 'static
+ Send
+ Sync,
executor: BackgroundExecutor,
) -> Result<Self> {
let temp_dir = tempfile::Builder::new().prefix("zed-askpass").tempdir()?;
let askpass_socket = temp_dir.path().join("askpass.sock");
let askpass_script_path = temp_dir.path().join(ASKPASS_SCRIPT_NAME);
let current_exec =
std::env::current_exe().context("Failed to determine current zed executable path.")?;
let askpass_program = ASKPASS_PROGRAM
.get_or_init(|| current_exec)
.try_shell_safe()
.context("Failed to shell-escape Askpass program path.")?
.to_string();
// Create an askpass script that communicates back to this process.
let askpass_script = generate_askpass_script(&askpass_program, &askpass_socket);
let _task = executor.spawn(async move {
maybe!(async move {
let listener =
UnixListener::bind(&askpass_socket).context("creating askpass socket")?;
while let Ok((mut stream, _)) = listener.accept().await {
let mut buffer = Vec::new();
let mut reader = BufReader::new(&mut stream);
if reader.read_until(b'\0', &mut buffer).await.is_err() {
buffer.clear();
}
let prompt = String::from_utf8_lossy(&buffer).into_owned();
let password = get_password(prompt).await;
match password {
ControlFlow::Continue(password) => {
if let Ok(password) = password
&& let Ok(decrypted) =
password.decrypt(IKnowWhatIAmDoingAndIHaveReadTheDocs)
{
stream.write_all(decrypted.as_bytes()).await.log_err();
}
}
ControlFlow::Break(()) => {
// note: we expect the caller to drop this task when it's done.
// We need to keep the stream open until the caller is done to avoid
// spurious errors from ssh.
std::future::pending::<()>().await;
drop(stream);
}
}
}
drop(temp_dir);
Result::<_, anyhow::Error>::Ok(())
})
.await
.log_err();
});
fs::write(&askpass_script_path, askpass_script)
.await
.with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?;
make_file_executable(&askpass_script_path).await?;
#[cfg(target_os = "windows")]
let askpass_helper = format!(
"powershell.exe -ExecutionPolicy Bypass -File {}",
askpass_script_path.display()
);
Ok(Self {
_task,
#[cfg(not(target_os = "windows"))]
askpass_script_path,
#[cfg(target_os = "windows")]
askpass_helper,
})
}
pub fn script_path(&self) -> impl AsRef<OsStr> {
#[cfg(not(target_os = "windows"))]
{
&self.askpass_script_path
}
#[cfg(target_os = "windows")]
{
&self.askpass_helper
}
}
}
/// The main function for when Zed is running in netcat mode for use in askpass.
/// Called from both the remote server binary and the zed binary in their respective main functions.
pub fn main(socket: &str) {
@@ -320,17 +252,12 @@ pub fn main(socket: &str) {
}
}
pub fn set_askpass_program(path: std::path::PathBuf) {
if ASKPASS_PROGRAM.set(path).is_err() {
debug_panic!("askpass program has already been set");
}
}
#[inline]
#[cfg(not(target_os = "windows"))]
fn generate_askpass_script(askpass_program: &str, askpass_socket: &std::path::Path) -> String {
fn generate_askpass_script(zed_path: &str, askpass_socket: &std::path::Path) -> String {
format!(
"{shebang}\n{print_args} | {askpass_program} --askpass={askpass_socket} 2> /dev/null \n",
"{shebang}\n{print_args} | {zed_exe} --askpass={askpass_socket} 2> /dev/null \n",
zed_exe = zed_path,
askpass_socket = askpass_socket.display(),
print_args = "printf '%s\\0' \"$@\"",
shebang = "#!/bin/sh",
@@ -339,12 +266,13 @@ fn generate_askpass_script(askpass_program: &str, askpass_socket: &std::path::Pa
#[inline]
#[cfg(target_os = "windows")]
fn generate_askpass_script(askpass_program: &str, askpass_socket: &std::path::Path) -> String {
fn generate_askpass_script(zed_path: &std::path::Path, askpass_socket: &std::path::Path) -> String {
format!(
r#"
$ErrorActionPreference = 'Stop';
($args -join [char]0) | & "{askpass_program}" --askpass={askpass_socket} 2> $null
($args -join [char]0) | & "{zed_exe}" --askpass={askpass_socket} 2> $null
"#,
zed_exe = zed_path.display(),
askpass_socket = askpass_socket.display(),
)
}

View File

@@ -21,6 +21,27 @@ type LengthWithoutPadding = u32;
#[derive(Clone)]
pub struct EncryptedPassword(Vec<u8>, LengthWithoutPadding);
pub trait ProcessExt {
fn encrypted_env(&mut self, name: &str, value: EncryptedPassword) -> &mut Self;
}
impl ProcessExt for smol::process::Command {
fn encrypted_env(&mut self, name: &str, value: EncryptedPassword) -> &mut Self {
if let Ok(password) = decrypt(value) {
self.env(name, password);
}
self
}
}
impl TryFrom<EncryptedPassword> for proto::AskPassResponse {
type Error = anyhow::Error;
fn try_from(pw: EncryptedPassword) -> Result<Self, Self::Error> {
let pw = decrypt(pw)?;
Ok(Self { response: pw })
}
}
impl Drop for EncryptedPassword {
fn drop(&mut self) {
self.0.zeroize();
@@ -42,14 +63,12 @@ impl TryFrom<&str> for EncryptedPassword {
if padded_length != len {
value.resize(padded_length as usize, 0);
}
if len != 0 {
unsafe {
CryptProtectMemory(
value.as_mut_ptr() as _,
padded_length,
CRYPTPROTECTMEMORY_SAME_PROCESS,
)?;
}
unsafe {
CryptProtectMemory(
value.as_mut_ptr() as _,
len,
CRYPTPROTECTMEMORY_SAME_PROCESS,
)?;
}
Ok(Self(value, len))
}
@@ -58,45 +77,35 @@ impl TryFrom<&str> for EncryptedPassword {
}
}
/// Read the docs for [EncryptedPassword]; please take care of not storing the plaintext string in memory for extended
/// periods of time.
pub struct IKnowWhatIAmDoingAndIHaveReadTheDocs;
pub(crate) fn decrypt(mut password: EncryptedPassword) -> Result<String> {
#[cfg(windows)]
{
use anyhow::Context;
use windows::Win32::Security::Cryptography::{
CRYPTPROTECTMEMORY_BLOCK_SIZE, CRYPTPROTECTMEMORY_SAME_PROCESS, CryptUnprotectMemory,
};
assert_eq!(
password.0.len() % CRYPTPROTECTMEMORY_BLOCK_SIZE as usize,
0,
"Violated pre-condition (buffer size <{}> must be a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE <{}>) for CryptUnprotectMemory.",
password.0.len(),
CRYPTPROTECTMEMORY_BLOCK_SIZE
);
unsafe {
CryptUnprotectMemory(
password.0.as_mut_ptr() as _,
password.1,
CRYPTPROTECTMEMORY_SAME_PROCESS,
)
.context("while decrypting a SSH password")?
};
impl EncryptedPassword {
pub fn decrypt(mut self, _: IKnowWhatIAmDoingAndIHaveReadTheDocs) -> Result<String> {
#[cfg(windows)]
{
use anyhow::Context;
use windows::Win32::Security::Cryptography::{
CRYPTPROTECTMEMORY_BLOCK_SIZE, CRYPTPROTECTMEMORY_SAME_PROCESS,
CryptUnprotectMemory,
};
assert_eq!(
self.0.len() % CRYPTPROTECTMEMORY_BLOCK_SIZE as usize,
0,
"Violated pre-condition (buffer size <{}> must be a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE <{}>) for CryptUnprotectMemory.",
self.0.len(),
CRYPTPROTECTMEMORY_BLOCK_SIZE
);
if self.1 != 0 {
unsafe {
CryptUnprotectMemory(
self.0.as_mut_ptr() as _,
self.0.len().try_into()?,
CRYPTPROTECTMEMORY_SAME_PROCESS,
)
.context("while decrypting a SSH password")?
};
{
// Remove padding
_ = self.0.drain(self.1 as usize..);
}
}
Ok(String::from_utf8(std::mem::take(&mut self.0))?)
// Remove padding
_ = password.0.drain(password.1 as usize..);
}
#[cfg(not(windows))]
Ok(String::from_utf8(std::mem::take(&mut self.0))?)
Ok(String::from_utf8(std::mem::take(&mut password.0))?)
}
#[cfg(not(windows))]
Ok(String::from_utf8(std::mem::take(&mut password.0))?)
}

View File

@@ -2669,7 +2669,7 @@ impl AssistantContext {
}
pub fn summarize(&mut self, mut replace_old: bool, cx: &mut Context<Self>) {
let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model() else {
let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
return;
};

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