Compare commits
1 Commits
performanc
...
inline-ass
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7cf3c306a |
59
.github/ISSUE_TEMPLATE/01_bug_ai.yml
vendored
Normal file
59
.github/ISSUE_TEMPLATE/01_bug_ai.yml
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
name: Bug Report (AI)
|
||||||
|
description: Zed Agent Panel Bugs
|
||||||
|
type: "Bug"
|
||||||
|
labels: ["ai"]
|
||||||
|
title: "AI: <a short description of the AI Related 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**:
|
||||||
|
|
||||||
|
### Model Provider Details
|
||||||
|
- Provider: (Anthropic via ZedPro, Anthropic via API key, Copilot Chat, Mistral, OpenAI, etc)
|
||||||
|
- Model Name:
|
||||||
|
- Mode: (Agent Panel, Inline Assistant, Terminal Assistant or Text Threads)
|
||||||
|
- Other Details (MCPs, other settings, etc):
|
||||||
|
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
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: If applicable, attach your `Zed.log` file to this issue.
|
||||||
|
description: |
|
||||||
|
From the command palette, run `zed: open log` to see the last 1000 lines.
|
||||||
|
Or run `zed: reveal log in file manager` to reveal the log file itself.
|
||||||
|
value: |
|
||||||
|
<details><summary>Zed.log</summary>
|
||||||
|
|
||||||
|
<!-- Paste your log inside the code block. -->
|
||||||
|
```log
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
53
.github/ISSUE_TEMPLATE/04_bug_debugger.yml
vendored
Normal file
53
.github/ISSUE_TEMPLATE/04_bug_debugger.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
name: Bug Report (Debugger)
|
||||||
|
description: Zed Debugger-Related Bugs
|
||||||
|
type: "Bug"
|
||||||
|
labels: ["debugger"]
|
||||||
|
title: "Debugger: <a short description of the Debugger 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
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: If applicable, attach your `Zed.log` file to this issue.
|
||||||
|
description: |
|
||||||
|
From the command palette, run `zed: open log` to see the last 1000 lines.
|
||||||
|
Or run `zed: reveal log in file manager` to reveal the log file itself.
|
||||||
|
value: |
|
||||||
|
<details><summary>Zed.log</summary>
|
||||||
|
|
||||||
|
<!-- Paste your log inside the code block. -->
|
||||||
|
```log
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
53
.github/ISSUE_TEMPLATE/06_bug_git.yml
vendored
Normal file
53
.github/ISSUE_TEMPLATE/06_bug_git.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
name: Bug Report (Git)
|
||||||
|
description: Zed Git Related Bugs
|
||||||
|
type: "Bug"
|
||||||
|
labels: ["git"]
|
||||||
|
title: "Git: <a short description of the Git bug>"
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Summary
|
||||||
|
description: Describe the bug with a one-line summary, and provide detailed reproduction steps
|
||||||
|
value: |
|
||||||
|
<!-- Please insert a one-line summary of the issue below -->
|
||||||
|
SUMMARY_SENTENCE_HERE
|
||||||
|
|
||||||
|
### Description
|
||||||
|
<!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
|
||||||
|
Steps to trigger the problem:
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
|
||||||
|
**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
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: If applicable, attach your `Zed.log` file to this issue.
|
||||||
|
description: |
|
||||||
|
From the command palette, run `zed: open log` to see the last 1000 lines.
|
||||||
|
Or run `zed: reveal log in file manager` to reveal the log file itself.
|
||||||
|
value: |
|
||||||
|
<details><summary>Zed.log</summary>
|
||||||
|
|
||||||
|
<!-- Paste your log inside the code block. -->
|
||||||
|
```log
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
53
.github/ISSUE_TEMPLATE/07_bug_windows.yml
vendored
Normal file
53
.github/ISSUE_TEMPLATE/07_bug_windows.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: If applicable, attach your `Zed.log` file to this issue.
|
||||||
|
description: |
|
||||||
|
From the command palette, run `zed: open log` to see the last 1000 lines.
|
||||||
|
Or run `zed: reveal log in file manager` to reveal the log file itself.
|
||||||
|
value: |
|
||||||
|
<details><summary>Zed.log</summary>
|
||||||
|
|
||||||
|
<!-- Paste your log inside the code block. -->
|
||||||
|
```log
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
70
.github/ISSUE_TEMPLATE/1.bug-report.yml
vendored
70
.github/ISSUE_TEMPLATE/1.bug-report.yml
vendored
@@ -1,70 +0,0 @@
|
|||||||
name: Report an issue
|
|
||||||
description: Report an issue with Zed.
|
|
||||||
type: Bug
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Feature requests should be opened in [discussions](https://github.com/zed-industries/zed/discussions/new/choose).
|
|
||||||
|
|
||||||
Before opening a new issue, please do a [search](https://github.com/zed-industries/zed/issues) of existing issues and :+1: upvote the existing issue instead. This will help us maintain a proper signal-to-noise ratio.
|
|
||||||
|
|
||||||
If you need help with your own project, you can ask a question in our [Discord Support Forums](https://discord.com/invite/zedindustries).
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Reproduction steps
|
|
||||||
description: A step-by-step description of how to reproduce the issue from a **clean Zed install**. Any code must be sufficient to reproduce (make sure to include context!). Include code as text, not just as a screenshot. **Issues with insufficient detail may be summarily closed**.
|
|
||||||
placeholder: |
|
|
||||||
1. Start Zed
|
|
||||||
2. Click X
|
|
||||||
3. Y will happen
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Current vs. Expected behavior
|
|
||||||
description: |
|
|
||||||
A clear and concise description of what is the current behavior (screenshots, videos), vs. what you expected the behavior to be.
|
|
||||||
|
|
||||||
**Skipping this/failure to provide complete information will result in the issue being closed.**
|
|
||||||
placeholder: "Based on my reproduction steps above, when I click X, I expect this to happen, but instead Y happens."
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: If applicable, attach your Zed log file to this issue.
|
|
||||||
description: |
|
|
||||||
Open the command palette in Zed, then type `zed: open log` to see the last 1000 lines. Or type `zed: reveal log in file manager` in the command palette to reveal the log file itself.
|
|
||||||
value: |
|
|
||||||
<details><summary>Zed.log</summary>
|
|
||||||
|
|
||||||
<!-- Paste your log inside the code block. -->
|
|
||||||
```log
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: If applicable, provide details about your model provider
|
|
||||||
placeholder: |
|
|
||||||
- Provider: (Anthropic via ZedPro, Anthropic via API key, Copilot Chat, Mistral, OpenAI, etc.)
|
|
||||||
- Model Name: (Claude Sonnet 4.5, Gemini 3 Pro, GPT-5)
|
|
||||||
- Mode: (Agent Panel, Inline Assistant, Terminal Assistant or Text Threads)
|
|
||||||
- Other details (ACPs, MCPs, other settings, etc.):
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Zed version and system specs
|
|
||||||
description: |
|
|
||||||
Open the command palette in Zed, then type “zed: copy system specs into clipboard”. **Skipping this/failure to provide complete information will result in the issue being closed**.
|
|
||||||
placeholder: |
|
|
||||||
Zed: v0.215.0 (Zed Nightly bfe141ea79aa4984028934067ba75c48d99136ae)
|
|
||||||
OS: macOS 15.1
|
|
||||||
Memory: 36 GiB
|
|
||||||
Architecture: aarch64
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
75
.github/ISSUE_TEMPLATE/10_bug_report.yml
vendored
Normal file
75
.github/ISSUE_TEMPLATE/10_bug_report.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
name: Bug Report (Other)
|
||||||
|
description: |
|
||||||
|
Something else is broken in Zed (exclude crashing).
|
||||||
|
type: "Bug"
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Summary
|
||||||
|
description: Provide a one sentence summary and detailed reproduction steps
|
||||||
|
value: |
|
||||||
|
<!-- Begin your issue with a one sentence summary -->
|
||||||
|
SUMMARY_SENTENCE_HERE
|
||||||
|
|
||||||
|
### Description
|
||||||
|
<!-- Describe with sufficient detail to reproduce from a clean Zed install.
|
||||||
|
- Any code must be sufficient to reproduce (include context!)
|
||||||
|
- Include code as text, not just as a screenshot.
|
||||||
|
- Issues with insufficient detail may be summarily closed.
|
||||||
|
-->
|
||||||
|
|
||||||
|
DESCRIPTION_HERE
|
||||||
|
|
||||||
|
Steps to reproduce:
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
4.
|
||||||
|
|
||||||
|
**Expected Behavior**:
|
||||||
|
**Actual Behavior**:
|
||||||
|
|
||||||
|
<!-- Before Submitting, did you:
|
||||||
|
1. Include settings.json, keymap.json, .editorconfig if relevant?
|
||||||
|
2. Check your Zed.log for relevant errors? (please include!)
|
||||||
|
3. Click Preview to ensure everything looks right?
|
||||||
|
4. Hide videos, large images and logs in ``` inside collapsible blocks:
|
||||||
|
|
||||||
|
<details><summary>click to expand</summary>
|
||||||
|
|
||||||
|
```json
|
||||||
|
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
-->
|
||||||
|
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: environment
|
||||||
|
attributes:
|
||||||
|
label: Zed Version and System Specs
|
||||||
|
description: |
|
||||||
|
Open Zed, from the command palette select "zed: copy system specs into clipboard"
|
||||||
|
placeholder: |
|
||||||
|
Output of "zed: copy system specs into clipboard"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: If applicable, attach your `Zed.log` file to this issue.
|
||||||
|
description: |
|
||||||
|
From the command palette, run `zed: open log` to see the last 1000 lines.
|
||||||
|
Or run `zed: reveal log in file manager` to reveal the log file itself.
|
||||||
|
value: |
|
||||||
|
<details><summary>Zed.log</summary>
|
||||||
|
|
||||||
|
<!-- Paste your log inside the code block. -->
|
||||||
|
```log
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
50
.github/ISSUE_TEMPLATE/11_crash_report.yml
vendored
Normal file
50
.github/ISSUE_TEMPLATE/11_crash_report.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
name: Crash Report
|
||||||
|
description: Zed is Crashing or Hanging
|
||||||
|
type: "Crash"
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Summary
|
||||||
|
description: Summarize the issue with detailed reproduction steps
|
||||||
|
value: |
|
||||||
|
<!-- Begin your issue with a one sentence summary -->
|
||||||
|
SUMMARY_SENTENCE_HERE
|
||||||
|
|
||||||
|
### Description
|
||||||
|
<!-- Include all steps necessary to reproduce from a clean Zed installation. Be verbose -->
|
||||||
|
Steps to trigger the problem:
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
|
||||||
|
Actual Behavior:
|
||||||
|
Expected Behavior:
|
||||||
|
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: environment
|
||||||
|
attributes:
|
||||||
|
label: Zed Version and System Specs
|
||||||
|
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
|
||||||
|
placeholder: |
|
||||||
|
Output of "zed: copy system specs into clipboard"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: If applicable, attach your `Zed.log` file to this issue.
|
||||||
|
description: |
|
||||||
|
From the command palette, run `zed: open log` to see the last 1000 lines.
|
||||||
|
Or run `zed: reveal log in file manager` to reveal the log file itself.
|
||||||
|
value: |
|
||||||
|
<details><summary>Zed.log</summary>
|
||||||
|
|
||||||
|
<!-- Paste your log inside the code block. -->
|
||||||
|
```log
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
52
.github/ISSUE_TEMPLATE/2.crash-report.yml
vendored
52
.github/ISSUE_TEMPLATE/2.crash-report.yml
vendored
@@ -1,52 +0,0 @@
|
|||||||
name: Report a crash
|
|
||||||
description: Zed is crashing or freezing or hanging.
|
|
||||||
type: Crash
|
|
||||||
body:
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Reproduction steps
|
|
||||||
description: A step-by-step description of how to reproduce the crash from a **clean Zed install**. **Be verbose**. **Issues with insufficient detail may be summarily closed**.
|
|
||||||
placeholder: |
|
|
||||||
1. Start Zed
|
|
||||||
2. Perform an action
|
|
||||||
3. Zed crashes
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Current vs. Expected behavior
|
|
||||||
description: |
|
|
||||||
Go into depth about what actions you’re performing in Zed to trigger the crash. If Zed crashes before it loads any windows, make sure to mention that. Again, **be verbose**.
|
|
||||||
|
|
||||||
**Skipping this/failure to provide complete information will result in the issue being closed.**
|
|
||||||
placeholder: "Based on my reproduction steps above, when I perform said action, I expect this to happen, but instead Zed crashes."
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Zed version and system specs
|
|
||||||
description: |
|
|
||||||
Open the command palette in Zed, then type “zed: copy system specs into clipboard”. **Skipping this/failure to provide complete information will result in the issue being closed**.
|
|
||||||
placeholder: |
|
|
||||||
Zed: v0.215.0 (Zed Nightly bfe141ea79aa4984028934067ba75c48d99136ae)
|
|
||||||
OS: macOS 15.1
|
|
||||||
Memory: 36 GiB
|
|
||||||
Architecture: aarch64
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: If applicable, attach your Zed log file to this issue
|
|
||||||
description: |
|
|
||||||
Open the command palette in Zed, then type `zed: open log` to see the last 1000 lines. Or type `zed: reveal log in file manager` in the command palette to reveal the log file itself.
|
|
||||||
value: |
|
|
||||||
<details><summary>Zed.log</summary>
|
|
||||||
|
|
||||||
<!-- Paste your log inside the code block. -->
|
|
||||||
```log
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
10
.github/ISSUE_TEMPLATE/config.yml
vendored
10
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,9 +1,9 @@
|
|||||||
# yaml-language-server: $schema=https://www.schemastore.org/github-issue-config.json
|
# yaml-language-server: $schema=https://www.schemastore.org/github-issue-config.json
|
||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Feature request
|
- name: Feature Request
|
||||||
url: https://github.com/zed-industries/zed/discussions/new/choose
|
url: https://github.com/zed-industries/zed/discussions/new/choose
|
||||||
about: To request a feature, open a new discussion under one of the appropriate categories.
|
about: To request a feature, open a new Discussion in one of the appropriate Discussion categories
|
||||||
- name: Our Discord community
|
- name: "Zed Discord"
|
||||||
url: https://discord.com/invite/zedindustries
|
url: https://zed.dev/community-links
|
||||||
about: Join our Discord server for real-time discussion and user support.
|
about: Real-time discussion and user support
|
||||||
|
|||||||
@@ -13,65 +13,13 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Check if author is a community champion and apply label
|
- name: Check if author is a community champion and apply label
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v7
|
||||||
env:
|
|
||||||
COMMUNITY_CHAMPIONS: |
|
|
||||||
0x2CA
|
|
||||||
5brian
|
|
||||||
5herlocked
|
|
||||||
abdelq
|
|
||||||
afgomez
|
|
||||||
AidanV
|
|
||||||
akbxr
|
|
||||||
AlvaroParker
|
|
||||||
artemevsevev
|
|
||||||
bajrangCoder
|
|
||||||
bcomnes
|
|
||||||
Be-ing
|
|
||||||
blopker
|
|
||||||
bobbymannino
|
|
||||||
CharlesChen0823
|
|
||||||
chbk
|
|
||||||
cppcoffee
|
|
||||||
davewa
|
|
||||||
ddoemonn
|
|
||||||
djsauble
|
|
||||||
fantacell
|
|
||||||
findrakecil
|
|
||||||
gko
|
|
||||||
huacnlee
|
|
||||||
imumesh18
|
|
||||||
jacobtread
|
|
||||||
jansol
|
|
||||||
jeffreyguenther
|
|
||||||
jenslys
|
|
||||||
jongretar
|
|
||||||
lemorage
|
|
||||||
lnay
|
|
||||||
marcocondrache
|
|
||||||
marius851000
|
|
||||||
mikebronner
|
|
||||||
ognevny
|
|
||||||
RemcoSmitsDev
|
|
||||||
romaninsh
|
|
||||||
Simek
|
|
||||||
someone13574
|
|
||||||
sourcefrog
|
|
||||||
suxiaoshao
|
|
||||||
Takk8IS
|
|
||||||
tidely
|
|
||||||
timvermeulen
|
|
||||||
valentinegb
|
|
||||||
versecafe
|
|
||||||
vitallium
|
|
||||||
warrenjokinen
|
|
||||||
ya7010
|
|
||||||
Zertsov
|
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const communityChampions = process.env.COMMUNITY_CHAMPIONS
|
const communityChampionBody = `${{ secrets.COMMUNITY_CHAMPIONS }}`;
|
||||||
|
|
||||||
|
const communityChampions = communityChampionBody
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map(handle => handle.trim().toLowerCase())
|
.map(handle => handle.trim().toLowerCase());
|
||||||
.filter(handle => handle.length > 0);
|
|
||||||
|
|
||||||
let author;
|
let author;
|
||||||
if (context.eventName === 'issues') {
|
if (context.eventName === 'issues') {
|
||||||
|
|||||||
2
.github/workflows/extension_tests.yml
vendored
2
.github/workflows/extension_tests.yml
vendored
@@ -77,7 +77,7 @@ jobs:
|
|||||||
uses: taiki-e/install-action@nextest
|
uses: taiki-e/install-action@nextest
|
||||||
- name: steps::cargo_nextest
|
- name: steps::cargo_nextest
|
||||||
if: inputs.run_tests
|
if: inputs.run_tests
|
||||||
run: cargo nextest run --workspace --no-fail-fast
|
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
timeout-minutes: 3
|
timeout-minutes: 3
|
||||||
check_extension:
|
check_extension:
|
||||||
|
|||||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
|||||||
run: ./script/clear-target-dir-if-larger-than 300
|
run: ./script/clear-target-dir-if-larger-than 300
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
- name: steps::cargo_nextest
|
- name: steps::cargo_nextest
|
||||||
run: cargo nextest run --workspace --no-fail-fast
|
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
- name: steps::cleanup_cargo_config
|
- name: steps::cleanup_cargo_config
|
||||||
if: always()
|
if: always()
|
||||||
@@ -80,7 +80,7 @@ jobs:
|
|||||||
run: ./script/clear-target-dir-if-larger-than 250
|
run: ./script/clear-target-dir-if-larger-than 250
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
- name: steps::cargo_nextest
|
- name: steps::cargo_nextest
|
||||||
run: cargo nextest run --workspace --no-fail-fast
|
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
- name: steps::cleanup_cargo_config
|
- name: steps::cleanup_cargo_config
|
||||||
if: always()
|
if: always()
|
||||||
@@ -112,7 +112,7 @@ jobs:
|
|||||||
run: ./script/clear-target-dir-if-larger-than.ps1 250
|
run: ./script/clear-target-dir-if-larger-than.ps1 250
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
- name: steps::cargo_nextest
|
- name: steps::cargo_nextest
|
||||||
run: cargo nextest run --workspace --no-fail-fast
|
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
- name: steps::cleanup_cargo_config
|
- name: steps::cleanup_cargo_config
|
||||||
if: always()
|
if: always()
|
||||||
|
|||||||
2
.github/workflows/release_nightly.yml
vendored
2
.github/workflows/release_nightly.yml
vendored
@@ -51,7 +51,7 @@ jobs:
|
|||||||
run: ./script/clear-target-dir-if-larger-than.ps1 250
|
run: ./script/clear-target-dir-if-larger-than.ps1 250
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
- name: steps::cargo_nextest
|
- name: steps::cargo_nextest
|
||||||
run: cargo nextest run --workspace --no-fail-fast
|
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
- name: steps::cleanup_cargo_config
|
- name: steps::cleanup_cargo_config
|
||||||
if: always()
|
if: always()
|
||||||
|
|||||||
13
.github/workflows/run_tests.yml
vendored
13
.github/workflows/run_tests.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
|||||||
}
|
}
|
||||||
|
|
||||||
check_pattern "run_action_checks" '^\.github/(workflows/|actions/|actionlint.yml)|tooling/xtask|script/' -qP
|
check_pattern "run_action_checks" '^\.github/(workflows/|actions/|actionlint.yml)|tooling/xtask|script/' -qP
|
||||||
check_pattern "run_docs" '^(docs/|crates/.*\.rs)' -qP
|
check_pattern "run_docs" '^docs/' -qP
|
||||||
check_pattern "run_licenses" '^(Cargo.lock|script/.*licenses)' -qP
|
check_pattern "run_licenses" '^(Cargo.lock|script/.*licenses)' -qP
|
||||||
check_pattern "run_nix" '^(nix/|flake\.|Cargo\.|rust-toolchain.toml|\.cargo/config.toml)' -qP
|
check_pattern "run_nix" '^(nix/|flake\.|Cargo\.|rust-toolchain.toml|\.cargo/config.toml)' -qP
|
||||||
check_pattern "run_tests" '^(docs/|script/update_top_ranking_issues/|\.github/(ISSUE_TEMPLATE|workflows/(?!run_tests)))' -qvP
|
check_pattern "run_tests" '^(docs/|script/update_top_ranking_issues/|\.github/(ISSUE_TEMPLATE|workflows/(?!run_tests)))' -qvP
|
||||||
@@ -117,7 +117,7 @@ jobs:
|
|||||||
run: ./script/clear-target-dir-if-larger-than.ps1 250
|
run: ./script/clear-target-dir-if-larger-than.ps1 250
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
- name: steps::cargo_nextest
|
- name: steps::cargo_nextest
|
||||||
run: cargo nextest run --workspace --no-fail-fast
|
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
- name: steps::cleanup_cargo_config
|
- name: steps::cleanup_cargo_config
|
||||||
if: always()
|
if: always()
|
||||||
@@ -166,7 +166,7 @@ jobs:
|
|||||||
run: ./script/clear-target-dir-if-larger-than 250
|
run: ./script/clear-target-dir-if-larger-than 250
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
- name: steps::cargo_nextest
|
- name: steps::cargo_nextest
|
||||||
run: cargo nextest run --workspace --no-fail-fast
|
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
- name: steps::cleanup_cargo_config
|
- name: steps::cleanup_cargo_config
|
||||||
if: always()
|
if: always()
|
||||||
@@ -200,7 +200,7 @@ jobs:
|
|||||||
run: ./script/clear-target-dir-if-larger-than 300
|
run: ./script/clear-target-dir-if-larger-than 300
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
- name: steps::cargo_nextest
|
- name: steps::cargo_nextest
|
||||||
run: cargo nextest run --workspace --no-fail-fast
|
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
- name: steps::cleanup_cargo_config
|
- name: steps::cleanup_cargo_config
|
||||||
if: always()
|
if: always()
|
||||||
@@ -493,10 +493,7 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- orchestrate
|
- orchestrate
|
||||||
if: needs.orchestrate.outputs.run_tests == 'true'
|
if: needs.orchestrate.outputs.run_tests == 'true'
|
||||||
runs-on: namespace-profile-16x32-ubuntu-2204
|
runs-on: self-mini-macos
|
||||||
env:
|
|
||||||
GIT_AUTHOR_NAME: Protobuf Action
|
|
||||||
GIT_AUTHOR_EMAIL: ci@zed.dev
|
|
||||||
steps:
|
steps:
|
||||||
- name: steps::checkout_repo
|
- name: steps::checkout_repo
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||||
|
|||||||
495
Cargo.lock
generated
495
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
17
Cargo.toml
17
Cargo.toml
@@ -147,6 +147,7 @@ members = [
|
|||||||
"crates/rules_library",
|
"crates/rules_library",
|
||||||
"crates/schema_generator",
|
"crates/schema_generator",
|
||||||
"crates/search",
|
"crates/search",
|
||||||
|
"crates/semantic_version",
|
||||||
"crates/session",
|
"crates/session",
|
||||||
"crates/settings",
|
"crates/settings",
|
||||||
"crates/settings_json",
|
"crates/settings_json",
|
||||||
@@ -164,6 +165,7 @@ members = [
|
|||||||
"crates/sum_tree",
|
"crates/sum_tree",
|
||||||
"crates/supermaven",
|
"crates/supermaven",
|
||||||
"crates/supermaven_api",
|
"crates/supermaven_api",
|
||||||
|
"crates/sweep_ai",
|
||||||
"crates/codestral",
|
"crates/codestral",
|
||||||
"crates/svg_preview",
|
"crates/svg_preview",
|
||||||
"crates/system_specs",
|
"crates/system_specs",
|
||||||
@@ -201,6 +203,7 @@ members = [
|
|||||||
"crates/zed_actions",
|
"crates/zed_actions",
|
||||||
"crates/zed_env_vars",
|
"crates/zed_env_vars",
|
||||||
"crates/zeta",
|
"crates/zeta",
|
||||||
|
"crates/zeta2",
|
||||||
"crates/zeta_cli",
|
"crates/zeta_cli",
|
||||||
"crates/zlog",
|
"crates/zlog",
|
||||||
"crates/zlog_settings",
|
"crates/zlog_settings",
|
||||||
@@ -379,6 +382,7 @@ rope = { path = "crates/rope" }
|
|||||||
rpc = { path = "crates/rpc" }
|
rpc = { path = "crates/rpc" }
|
||||||
rules_library = { path = "crates/rules_library" }
|
rules_library = { path = "crates/rules_library" }
|
||||||
search = { path = "crates/search" }
|
search = { path = "crates/search" }
|
||||||
|
semantic_version = { path = "crates/semantic_version" }
|
||||||
session = { path = "crates/session" }
|
session = { path = "crates/session" }
|
||||||
settings = { path = "crates/settings" }
|
settings = { path = "crates/settings" }
|
||||||
settings_json = { path = "crates/settings_json" }
|
settings_json = { path = "crates/settings_json" }
|
||||||
@@ -395,6 +399,7 @@ streaming_diff = { path = "crates/streaming_diff" }
|
|||||||
sum_tree = { path = "crates/sum_tree" }
|
sum_tree = { path = "crates/sum_tree" }
|
||||||
supermaven = { path = "crates/supermaven" }
|
supermaven = { path = "crates/supermaven" }
|
||||||
supermaven_api = { path = "crates/supermaven_api" }
|
supermaven_api = { path = "crates/supermaven_api" }
|
||||||
|
sweep_ai = { path = "crates/sweep_ai" }
|
||||||
codestral = { path = "crates/codestral" }
|
codestral = { path = "crates/codestral" }
|
||||||
system_specs = { path = "crates/system_specs" }
|
system_specs = { path = "crates/system_specs" }
|
||||||
tab_switcher = { path = "crates/tab_switcher" }
|
tab_switcher = { path = "crates/tab_switcher" }
|
||||||
@@ -432,6 +437,7 @@ zed = { path = "crates/zed" }
|
|||||||
zed_actions = { path = "crates/zed_actions" }
|
zed_actions = { path = "crates/zed_actions" }
|
||||||
zed_env_vars = { path = "crates/zed_env_vars" }
|
zed_env_vars = { path = "crates/zed_env_vars" }
|
||||||
zeta = { path = "crates/zeta" }
|
zeta = { path = "crates/zeta" }
|
||||||
|
zeta2 = { path = "crates/zeta2" }
|
||||||
zlog = { path = "crates/zlog" }
|
zlog = { path = "crates/zlog" }
|
||||||
zlog_settings = { path = "crates/zlog_settings" }
|
zlog_settings = { path = "crates/zlog_settings" }
|
||||||
|
|
||||||
@@ -603,6 +609,7 @@ pulldown-cmark = { version = "0.12.0", default-features = false }
|
|||||||
quote = "1.0.9"
|
quote = "1.0.9"
|
||||||
rand = "0.9"
|
rand = "0.9"
|
||||||
rayon = "1.8"
|
rayon = "1.8"
|
||||||
|
ref-cast = "1.0.24"
|
||||||
regex = "1.5"
|
regex = "1.5"
|
||||||
# WARNING: If you change this, you must also publish a new version of zed-reqwest to crates.io
|
# 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 = "c15662463bda39148ba154100dd44d3fba5873a4", default-features = false, features = [
|
||||||
@@ -625,7 +632,7 @@ rustls-platform-verifier = "0.5.0"
|
|||||||
# WARNING: If you change this, you must also publish a new version of zed-scap to crates.io
|
# 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 = "4afea48c3b002197176fb19cd0f9b180dd36eaac", default-features = false, package = "zed-scap", version = "0.0.8-zed" }
|
||||||
schemars = { version = "1.0", features = ["indexmap2"] }
|
schemars = { version = "1.0", features = ["indexmap2"] }
|
||||||
semver = { version = "1.0", features = ["serde"] }
|
semver = "1.0"
|
||||||
serde = { version = "1.0.221", features = ["derive", "rc"] }
|
serde = { version = "1.0.221", features = ["derive", "rc"] }
|
||||||
serde_derive = "1.0.221"
|
serde_derive = "1.0.221"
|
||||||
serde_json = { version = "1.0.144", features = ["preserve_order", "raw_value"] }
|
serde_json = { version = "1.0.144", features = ["preserve_order", "raw_value"] }
|
||||||
@@ -636,6 +643,7 @@ serde_json_lenient = { version = "0.2", features = [
|
|||||||
serde_path_to_error = "0.1.17"
|
serde_path_to_error = "0.1.17"
|
||||||
serde_repr = "0.1"
|
serde_repr = "0.1"
|
||||||
serde_urlencoded = "0.7"
|
serde_urlencoded = "0.7"
|
||||||
|
serde_with = "3.4.0"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
shellexpand = "2.1.0"
|
shellexpand = "2.1.0"
|
||||||
shlex = "1.3.0"
|
shlex = "1.3.0"
|
||||||
@@ -713,7 +721,6 @@ wasmtime = { version = "29", default-features = false, features = [
|
|||||||
"parallel-compilation",
|
"parallel-compilation",
|
||||||
] }
|
] }
|
||||||
wasmtime-wasi = "29"
|
wasmtime-wasi = "29"
|
||||||
wax = "0.6"
|
|
||||||
which = "6.0.0"
|
which = "6.0.0"
|
||||||
windows-core = "0.61"
|
windows-core = "0.61"
|
||||||
wit-component = "0.221"
|
wit-component = "0.221"
|
||||||
@@ -777,7 +784,6 @@ features = [
|
|||||||
notify = { git = "https://github.com/zed-industries/notify.git", rev = "b4588b2e5aee68f4c0e100f140e808cbce7b1419" }
|
notify = { git = "https://github.com/zed-industries/notify.git", rev = "b4588b2e5aee68f4c0e100f140e808cbce7b1419" }
|
||||||
notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "b4588b2e5aee68f4c0e100f140e808cbce7b1419" }
|
notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "b4588b2e5aee68f4c0e100f140e808cbce7b1419" }
|
||||||
windows-capture = { git = "https://github.com/zed-industries/windows-capture.git", rev = "f0d6c1b6691db75461b732f6d5ff56eed002eeb9" }
|
windows-capture = { git = "https://github.com/zed-industries/windows-capture.git", rev = "f0d6c1b6691db75461b732f6d5ff56eed002eeb9" }
|
||||||
calloop = { git = "https://github.com/zed-industries/calloop" }
|
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
split-debuginfo = "unpacked"
|
split-debuginfo = "unpacked"
|
||||||
@@ -841,6 +847,7 @@ refineable = { codegen-units = 1 }
|
|||||||
release_channel = { codegen-units = 1 }
|
release_channel = { codegen-units = 1 }
|
||||||
reqwest_client = { codegen-units = 1 }
|
reqwest_client = { codegen-units = 1 }
|
||||||
rich_text = { codegen-units = 1 }
|
rich_text = { codegen-units = 1 }
|
||||||
|
semantic_version = { codegen-units = 1 }
|
||||||
session = { codegen-units = 1 }
|
session = { codegen-units = 1 }
|
||||||
snippet = { codegen-units = 1 }
|
snippet = { codegen-units = 1 }
|
||||||
snippets_ui = { codegen-units = 1 }
|
snippets_ui = { codegen-units = 1 }
|
||||||
@@ -866,10 +873,6 @@ debug = "full"
|
|||||||
lto = false
|
lto = false
|
||||||
codegen-units = 16
|
codegen-units = 16
|
||||||
|
|
||||||
[profile.profiling]
|
|
||||||
inherits = "release"
|
|
||||||
debug = "full"
|
|
||||||
|
|
||||||
[workspace.lints.rust]
|
[workspace.lints.rust]
|
||||||
unexpected_cfgs = { level = "allow" }
|
unexpected_cfgs = { level = "allow" }
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,8 @@
|
|||||||
"f11": "zed::ToggleFullScreen",
|
"f11": "zed::ToggleFullScreen",
|
||||||
"ctrl-alt-z": "edit_prediction::RateCompletions",
|
"ctrl-alt-z": "edit_prediction::RateCompletions",
|
||||||
"ctrl-alt-shift-i": "edit_prediction::ToggleMenu",
|
"ctrl-alt-shift-i": "edit_prediction::ToggleMenu",
|
||||||
"ctrl-alt-l": "lsp_tool::ToggleMenu"
|
"ctrl-alt-l": "lsp_tool::ToggleMenu",
|
||||||
|
"ctrl-alt-.": "project_panel::ToggleHideHidden"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -239,11 +240,13 @@
|
|||||||
"ctrl-alt-l": "agent::OpenRulesLibrary",
|
"ctrl-alt-l": "agent::OpenRulesLibrary",
|
||||||
"ctrl-i": "agent::ToggleProfileSelector",
|
"ctrl-i": "agent::ToggleProfileSelector",
|
||||||
"ctrl-alt-/": "agent::ToggleModelSelector",
|
"ctrl-alt-/": "agent::ToggleModelSelector",
|
||||||
|
"ctrl-shift-a": "agent::ToggleContextPicker",
|
||||||
"ctrl-shift-j": "agent::ToggleNavigationMenu",
|
"ctrl-shift-j": "agent::ToggleNavigationMenu",
|
||||||
"ctrl-alt-i": "agent::ToggleOptionsMenu",
|
"ctrl-alt-i": "agent::ToggleOptionsMenu",
|
||||||
"ctrl-alt-shift-n": "agent::ToggleNewThreadMenu",
|
"ctrl-alt-shift-n": "agent::ToggleNewThreadMenu",
|
||||||
"shift-alt-escape": "agent::ExpandMessageEditor",
|
"shift-alt-escape": "agent::ExpandMessageEditor",
|
||||||
"ctrl->": "agent::AddSelectionToThread",
|
"ctrl->": "agent::AddSelectionToThread",
|
||||||
|
"ctrl-alt-e": "agent::RemoveAllContext",
|
||||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||||
"ctrl-shift-enter": "agent::ContinueThread",
|
"ctrl-shift-enter": "agent::ContinueThread",
|
||||||
"super-ctrl-b": "agent::ToggleBurnMode",
|
"super-ctrl-b": "agent::ToggleBurnMode",
|
||||||
@@ -320,6 +323,17 @@
|
|||||||
"alt-enter": "editor::Newline"
|
"alt-enter": "editor::Newline"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "ContextStrip",
|
||||||
|
"bindings": {
|
||||||
|
"up": "agent::FocusUp",
|
||||||
|
"right": "agent::FocusRight",
|
||||||
|
"left": "agent::FocusLeft",
|
||||||
|
"down": "agent::FocusDown",
|
||||||
|
"backspace": "agent::RemoveFocusedContext",
|
||||||
|
"enter": "agent::AcceptSuggestedContext"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "AcpThread > ModeSelector",
|
"context": "AcpThread > ModeSelector",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
@@ -811,7 +825,8 @@
|
|||||||
"context": "PromptEditor",
|
"context": "PromptEditor",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-[": "agent::CyclePreviousInlineAssist",
|
"ctrl-[": "agent::CyclePreviousInlineAssist",
|
||||||
"ctrl-]": "agent::CycleNextInlineAssist"
|
"ctrl-]": "agent::CycleNextInlineAssist",
|
||||||
|
"ctrl-alt-e": "agent::RemoveAllContext"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -851,7 +866,6 @@
|
|||||||
"context": "ProjectPanel",
|
"context": "ProjectPanel",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"left": "project_panel::CollapseSelectedEntry",
|
"left": "project_panel::CollapseSelectedEntry",
|
||||||
"ctrl-left": "project_panel::CollapseAllEntries",
|
|
||||||
"right": "project_panel::ExpandSelectedEntry",
|
"right": "project_panel::ExpandSelectedEntry",
|
||||||
"new": "project_panel::NewFile",
|
"new": "project_panel::NewFile",
|
||||||
"ctrl-n": "project_panel::NewFile",
|
"ctrl-n": "project_panel::NewFile",
|
||||||
@@ -1237,25 +1251,11 @@
|
|||||||
"context": "Onboarding",
|
"context": "Onboarding",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-=": ["zed::IncreaseUiFontSize", { "persist": false }],
|
|
||||||
"ctrl-+": ["zed::IncreaseUiFontSize", { "persist": false }],
|
|
||||||
"ctrl--": ["zed::DecreaseUiFontSize", { "persist": false }],
|
|
||||||
"ctrl-0": ["zed::ResetUiFontSize", { "persist": false }],
|
|
||||||
"ctrl-enter": "onboarding::Finish",
|
"ctrl-enter": "onboarding::Finish",
|
||||||
"alt-shift-l": "onboarding::SignIn",
|
"alt-shift-l": "onboarding::SignIn",
|
||||||
"alt-shift-a": "onboarding::OpenAccount"
|
"alt-shift-a": "onboarding::OpenAccount"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"context": "Welcome",
|
|
||||||
"use_key_equivalents": true,
|
|
||||||
"bindings": {
|
|
||||||
"ctrl-=": ["zed::IncreaseUiFontSize", { "persist": false }],
|
|
||||||
"ctrl-+": ["zed::IncreaseUiFontSize", { "persist": false }],
|
|
||||||
"ctrl--": ["zed::DecreaseUiFontSize", { "persist": false }],
|
|
||||||
"ctrl-0": ["zed::ResetUiFontSize", { "persist": false }]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"context": "InvalidBuffer",
|
"context": "InvalidBuffer",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
|
|||||||
@@ -49,7 +49,8 @@
|
|||||||
"ctrl-cmd-f": "zed::ToggleFullScreen",
|
"ctrl-cmd-f": "zed::ToggleFullScreen",
|
||||||
"ctrl-cmd-z": "edit_prediction::RateCompletions",
|
"ctrl-cmd-z": "edit_prediction::RateCompletions",
|
||||||
"ctrl-cmd-i": "edit_prediction::ToggleMenu",
|
"ctrl-cmd-i": "edit_prediction::ToggleMenu",
|
||||||
"ctrl-cmd-l": "lsp_tool::ToggleMenu"
|
"ctrl-cmd-l": "lsp_tool::ToggleMenu",
|
||||||
|
"cmd-alt-.": "project_panel::ToggleHideHidden"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -278,11 +279,13 @@
|
|||||||
"cmd-alt-p": "agent::ManageProfiles",
|
"cmd-alt-p": "agent::ManageProfiles",
|
||||||
"cmd-i": "agent::ToggleProfileSelector",
|
"cmd-i": "agent::ToggleProfileSelector",
|
||||||
"cmd-alt-/": "agent::ToggleModelSelector",
|
"cmd-alt-/": "agent::ToggleModelSelector",
|
||||||
|
"cmd-shift-a": "agent::ToggleContextPicker",
|
||||||
"cmd-shift-j": "agent::ToggleNavigationMenu",
|
"cmd-shift-j": "agent::ToggleNavigationMenu",
|
||||||
"cmd-alt-m": "agent::ToggleOptionsMenu",
|
"cmd-alt-m": "agent::ToggleOptionsMenu",
|
||||||
"cmd-alt-shift-n": "agent::ToggleNewThreadMenu",
|
"cmd-alt-shift-n": "agent::ToggleNewThreadMenu",
|
||||||
"shift-alt-escape": "agent::ExpandMessageEditor",
|
"shift-alt-escape": "agent::ExpandMessageEditor",
|
||||||
"cmd->": "agent::AddSelectionToThread",
|
"cmd->": "agent::AddSelectionToThread",
|
||||||
|
"cmd-alt-e": "agent::RemoveAllContext",
|
||||||
"cmd-shift-e": "project_panel::ToggleFocus",
|
"cmd-shift-e": "project_panel::ToggleFocus",
|
||||||
"cmd-ctrl-b": "agent::ToggleBurnMode",
|
"cmd-ctrl-b": "agent::ToggleBurnMode",
|
||||||
"cmd-shift-enter": "agent::ContinueThread",
|
"cmd-shift-enter": "agent::ContinueThread",
|
||||||
@@ -363,6 +366,18 @@
|
|||||||
"alt-enter": "editor::Newline"
|
"alt-enter": "editor::Newline"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "ContextStrip",
|
||||||
|
"use_key_equivalents": true,
|
||||||
|
"bindings": {
|
||||||
|
"up": "agent::FocusUp",
|
||||||
|
"right": "agent::FocusRight",
|
||||||
|
"left": "agent::FocusLeft",
|
||||||
|
"down": "agent::FocusDown",
|
||||||
|
"backspace": "agent::RemoveFocusedContext",
|
||||||
|
"enter": "agent::AcceptSuggestedContext"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "AgentConfiguration",
|
"context": "AgentConfiguration",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
@@ -875,7 +890,9 @@
|
|||||||
"context": "PromptEditor",
|
"context": "PromptEditor",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
"cmd-shift-a": "agent::ToggleContextPicker",
|
||||||
"cmd-alt-/": "agent::ToggleModelSelector",
|
"cmd-alt-/": "agent::ToggleModelSelector",
|
||||||
|
"cmd-alt-e": "agent::RemoveAllContext",
|
||||||
"ctrl-[": "agent::CyclePreviousInlineAssist",
|
"ctrl-[": "agent::CyclePreviousInlineAssist",
|
||||||
"ctrl-]": "agent::CycleNextInlineAssist"
|
"ctrl-]": "agent::CycleNextInlineAssist"
|
||||||
}
|
}
|
||||||
@@ -919,7 +936,6 @@
|
|||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"left": "project_panel::CollapseSelectedEntry",
|
"left": "project_panel::CollapseSelectedEntry",
|
||||||
"cmd-left": "project_panel::CollapseAllEntries",
|
|
||||||
"right": "project_panel::ExpandSelectedEntry",
|
"right": "project_panel::ExpandSelectedEntry",
|
||||||
"cmd-n": "project_panel::NewFile",
|
"cmd-n": "project_panel::NewFile",
|
||||||
"cmd-d": "project_panel::Duplicate",
|
"cmd-d": "project_panel::Duplicate",
|
||||||
@@ -1218,23 +1234,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "RatePredictionsModal",
|
"context": "RateCompletionModal",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"cmd-shift-enter": "zeta::ThumbsUpActivePrediction",
|
"cmd-shift-enter": "zeta::ThumbsUpActiveCompletion",
|
||||||
"cmd-shift-backspace": "zeta::ThumbsDownActivePrediction",
|
"cmd-shift-backspace": "zeta::ThumbsDownActiveCompletion",
|
||||||
"shift-down": "zeta::NextEdit",
|
"shift-down": "zeta::NextEdit",
|
||||||
"shift-up": "zeta::PreviousEdit",
|
"shift-up": "zeta::PreviousEdit",
|
||||||
"right": "zeta::PreviewPrediction"
|
"right": "zeta::PreviewCompletion"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "RatePredictionsModal > Editor",
|
"context": "RateCompletionModal > Editor",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"escape": "zeta::FocusPredictions",
|
"escape": "zeta::FocusCompletions",
|
||||||
"cmd-shift-enter": "zeta::ThumbsUpActivePrediction",
|
"cmd-shift-enter": "zeta::ThumbsUpActiveCompletion",
|
||||||
"cmd-shift-backspace": "zeta::ThumbsDownActivePrediction"
|
"cmd-shift-backspace": "zeta::ThumbsDownActiveCompletion"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1340,25 +1356,11 @@
|
|||||||
"context": "Onboarding",
|
"context": "Onboarding",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"cmd-=": ["zed::IncreaseUiFontSize", { "persist": false }],
|
|
||||||
"cmd-+": ["zed::IncreaseUiFontSize", { "persist": false }],
|
|
||||||
"cmd--": ["zed::DecreaseUiFontSize", { "persist": false }],
|
|
||||||
"cmd-0": ["zed::ResetUiFontSize", { "persist": false }],
|
|
||||||
"cmd-enter": "onboarding::Finish",
|
"cmd-enter": "onboarding::Finish",
|
||||||
"alt-tab": "onboarding::SignIn",
|
"alt-tab": "onboarding::SignIn",
|
||||||
"alt-shift-a": "onboarding::OpenAccount"
|
"alt-shift-a": "onboarding::OpenAccount"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"context": "Welcome",
|
|
||||||
"use_key_equivalents": true,
|
|
||||||
"bindings": {
|
|
||||||
"cmd-=": ["zed::IncreaseUiFontSize", { "persist": false }],
|
|
||||||
"cmd-+": ["zed::IncreaseUiFontSize", { "persist": false }],
|
|
||||||
"cmd--": ["zed::DecreaseUiFontSize", { "persist": false }],
|
|
||||||
"cmd-0": ["zed::ResetUiFontSize", { "persist": false }]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"context": "InvalidBuffer",
|
"context": "InvalidBuffer",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
|
|||||||
@@ -41,7 +41,8 @@
|
|||||||
"shift-f11": "debugger::StepOut",
|
"shift-f11": "debugger::StepOut",
|
||||||
"f11": "zed::ToggleFullScreen",
|
"f11": "zed::ToggleFullScreen",
|
||||||
"ctrl-shift-i": "edit_prediction::ToggleMenu",
|
"ctrl-shift-i": "edit_prediction::ToggleMenu",
|
||||||
"shift-alt-l": "lsp_tool::ToggleMenu"
|
"shift-alt-l": "lsp_tool::ToggleMenu",
|
||||||
|
"ctrl-alt-.": "project_panel::ToggleHideHidden"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -240,11 +241,13 @@
|
|||||||
"shift-alt-p": "agent::ManageProfiles",
|
"shift-alt-p": "agent::ManageProfiles",
|
||||||
"ctrl-i": "agent::ToggleProfileSelector",
|
"ctrl-i": "agent::ToggleProfileSelector",
|
||||||
"shift-alt-/": "agent::ToggleModelSelector",
|
"shift-alt-/": "agent::ToggleModelSelector",
|
||||||
|
"ctrl-shift-a": "agent::ToggleContextPicker",
|
||||||
"ctrl-shift-j": "agent::ToggleNavigationMenu",
|
"ctrl-shift-j": "agent::ToggleNavigationMenu",
|
||||||
"ctrl-alt-i": "agent::ToggleOptionsMenu",
|
"ctrl-alt-i": "agent::ToggleOptionsMenu",
|
||||||
// "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu",
|
// "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu",
|
||||||
"shift-alt-escape": "agent::ExpandMessageEditor",
|
"shift-alt-escape": "agent::ExpandMessageEditor",
|
||||||
"ctrl-shift-.": "agent::AddSelectionToThread",
|
"ctrl-shift-.": "agent::AddSelectionToThread",
|
||||||
|
"shift-alt-e": "agent::RemoveAllContext",
|
||||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||||
"ctrl-shift-enter": "agent::ContinueThread",
|
"ctrl-shift-enter": "agent::ContinueThread",
|
||||||
"super-ctrl-b": "agent::ToggleBurnMode",
|
"super-ctrl-b": "agent::ToggleBurnMode",
|
||||||
@@ -326,6 +329,18 @@
|
|||||||
"alt-enter": "editor::Newline"
|
"alt-enter": "editor::Newline"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "ContextStrip",
|
||||||
|
"use_key_equivalents": true,
|
||||||
|
"bindings": {
|
||||||
|
"up": "agent::FocusUp",
|
||||||
|
"right": "agent::FocusRight",
|
||||||
|
"left": "agent::FocusLeft",
|
||||||
|
"down": "agent::FocusDown",
|
||||||
|
"backspace": "agent::RemoveFocusedContext",
|
||||||
|
"enter": "agent::AcceptSuggestedContext"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "AcpThread > ModeSelector",
|
"context": "AcpThread > ModeSelector",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
@@ -823,7 +838,8 @@
|
|||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-[": "agent::CyclePreviousInlineAssist",
|
"ctrl-[": "agent::CyclePreviousInlineAssist",
|
||||||
"ctrl-]": "agent::CycleNextInlineAssist"
|
"ctrl-]": "agent::CycleNextInlineAssist",
|
||||||
|
"shift-alt-e": "agent::RemoveAllContext"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -864,7 +880,6 @@
|
|||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"left": "project_panel::CollapseSelectedEntry",
|
"left": "project_panel::CollapseSelectedEntry",
|
||||||
"ctrl-left": "project_panel::CollapseAllEntries",
|
|
||||||
"right": "project_panel::ExpandSelectedEntry",
|
"right": "project_panel::ExpandSelectedEntry",
|
||||||
"ctrl-n": "project_panel::NewFile",
|
"ctrl-n": "project_panel::NewFile",
|
||||||
"alt-n": "project_panel::NewDirectory",
|
"alt-n": "project_panel::NewDirectory",
|
||||||
@@ -1270,25 +1285,11 @@
|
|||||||
"context": "Onboarding",
|
"context": "Onboarding",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-=": ["zed::IncreaseUiFontSize", { "persist": false }],
|
|
||||||
"ctrl-+": ["zed::IncreaseUiFontSize", { "persist": false }],
|
|
||||||
"ctrl--": ["zed::DecreaseUiFontSize", { "persist": false }],
|
|
||||||
"ctrl-0": ["zed::ResetUiFontSize", { "persist": false }],
|
|
||||||
"ctrl-enter": "onboarding::Finish",
|
"ctrl-enter": "onboarding::Finish",
|
||||||
"alt-shift-l": "onboarding::SignIn",
|
"alt-shift-l": "onboarding::SignIn",
|
||||||
"shift-alt-a": "onboarding::OpenAccount"
|
"shift-alt-a": "onboarding::OpenAccount"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"context": "Welcome",
|
|
||||||
"use_key_equivalents": true,
|
|
||||||
"bindings": {
|
|
||||||
"ctrl-=": ["zed::IncreaseUiFontSize", { "persist": false }],
|
|
||||||
"ctrl-+": ["zed::IncreaseUiFontSize", { "persist": false }],
|
|
||||||
"ctrl--": ["zed::DecreaseUiFontSize", { "persist": false }],
|
|
||||||
"ctrl-0": ["zed::ResetUiFontSize", { "persist": false }]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"context": "GitWorktreeSelector || (GitWorktreeSelector > Picker > Editor)",
|
"context": "GitWorktreeSelector || (GitWorktreeSelector > Picker > Editor)",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-alt-s": "zed::OpenSettings",
|
"ctrl-alt-s": "zed::OpenSettingsFile",
|
||||||
"ctrl-{": "pane::ActivatePreviousItem",
|
"ctrl-{": "pane::ActivatePreviousItem",
|
||||||
"ctrl-}": "pane::ActivateNextItem",
|
"ctrl-}": "pane::ActivateNextItem",
|
||||||
"shift-escape": null, // Unmap workspace::zoom
|
"shift-escape": null, // Unmap workspace::zoom
|
||||||
"ctrl-~": "git::Branch",
|
|
||||||
"ctrl-f2": "debugger::Stop",
|
"ctrl-f2": "debugger::Stop",
|
||||||
"f6": "debugger::Pause",
|
"f6": "debugger::Pause",
|
||||||
"f7": "debugger::StepInto",
|
"f7": "debugger::StepInto",
|
||||||
"f8": "debugger::StepOver",
|
"f8": "debugger::StepOver",
|
||||||
"shift-f8": "debugger::StepOut",
|
"shift-f8": "debugger::StepOut",
|
||||||
"f9": "debugger::Continue",
|
"f9": "debugger::Continue",
|
||||||
"shift-f9": "debugger::Start",
|
|
||||||
"alt-shift-f9": "debugger::Start"
|
"alt-shift-f9": "debugger::Start"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -48,7 +46,7 @@
|
|||||||
"alt-f7": "editor::FindAllReferences",
|
"alt-f7": "editor::FindAllReferences",
|
||||||
"ctrl-alt-f7": "editor::FindAllReferences",
|
"ctrl-alt-f7": "editor::FindAllReferences",
|
||||||
"ctrl-b": "editor::GoToDefinition", // Conflicts with workspace::ToggleLeftDock
|
"ctrl-b": "editor::GoToDefinition", // Conflicts with workspace::ToggleLeftDock
|
||||||
"ctrl-alt-b": "editor::GoToImplementation", // Conflicts with workspace::ToggleRightDock
|
"ctrl-alt-b": "editor::GoToDefinitionSplit", // Conflicts with workspace::ToggleRightDock
|
||||||
"ctrl-shift-b": "editor::GoToTypeDefinition",
|
"ctrl-shift-b": "editor::GoToTypeDefinition",
|
||||||
"ctrl-alt-shift-b": "editor::GoToTypeDefinitionSplit",
|
"ctrl-alt-shift-b": "editor::GoToTypeDefinitionSplit",
|
||||||
"f2": "editor::GoToDiagnostic",
|
"f2": "editor::GoToDiagnostic",
|
||||||
@@ -72,11 +70,7 @@
|
|||||||
"ctrl-r": ["buffer_search::Deploy", { "replace_enabled": true }],
|
"ctrl-r": ["buffer_search::Deploy", { "replace_enabled": true }],
|
||||||
"ctrl-shift-n": "file_finder::Toggle",
|
"ctrl-shift-n": "file_finder::Toggle",
|
||||||
"ctrl-g": "go_to_line::Toggle",
|
"ctrl-g": "go_to_line::Toggle",
|
||||||
"alt-enter": "editor::ToggleCodeActions",
|
"alt-enter": "editor::ToggleCodeActions"
|
||||||
"ctrl-space": "editor::ShowCompletions",
|
|
||||||
"ctrl-q": "editor::Hover",
|
|
||||||
"ctrl-p": "editor::ShowSignatureHelp",
|
|
||||||
"ctrl-\\": "assistant::InlineAssist"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -100,13 +94,9 @@
|
|||||||
"ctrl-shift-f12": "workspace::ToggleAllDocks",
|
"ctrl-shift-f12": "workspace::ToggleAllDocks",
|
||||||
"ctrl-shift-r": ["pane::DeploySearch", { "replace_enabled": true }],
|
"ctrl-shift-r": ["pane::DeploySearch", { "replace_enabled": true }],
|
||||||
"alt-shift-f10": "task::Spawn",
|
"alt-shift-f10": "task::Spawn",
|
||||||
"shift-f10": "task::Spawn",
|
|
||||||
"ctrl-f5": "task::Rerun",
|
|
||||||
"ctrl-e": "file_finder::Toggle",
|
"ctrl-e": "file_finder::Toggle",
|
||||||
"ctrl-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
|
// "ctrl-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
|
||||||
"ctrl-shift-n": "file_finder::Toggle",
|
"ctrl-shift-n": "file_finder::Toggle",
|
||||||
"ctrl-n": "project_symbols::Toggle",
|
|
||||||
"ctrl-alt-n": "file_finder::Toggle",
|
|
||||||
"ctrl-shift-a": "command_palette::Toggle",
|
"ctrl-shift-a": "command_palette::Toggle",
|
||||||
"shift shift": "command_palette::Toggle",
|
"shift shift": "command_palette::Toggle",
|
||||||
"ctrl-alt-shift-n": "project_symbols::Toggle",
|
"ctrl-alt-shift-n": "project_symbols::Toggle",
|
||||||
@@ -143,9 +133,7 @@
|
|||||||
"context": "Pane",
|
"context": "Pane",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-alt-left": "pane::GoBack",
|
"ctrl-alt-left": "pane::GoBack",
|
||||||
"ctrl-alt-right": "pane::GoForward",
|
"ctrl-alt-right": "pane::GoForward"
|
||||||
"alt-left": "pane::ActivatePreviousItem",
|
|
||||||
"alt-right": "pane::ActivateNextItem"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -164,6 +152,8 @@
|
|||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-shift-t": "workspace::NewTerminal",
|
"ctrl-shift-t": "workspace::NewTerminal",
|
||||||
"alt-f12": "workspace::CloseActiveDock",
|
"alt-f12": "workspace::CloseActiveDock",
|
||||||
|
"alt-left": "pane::ActivatePreviousItem",
|
||||||
|
"alt-right": "pane::ActivateNextItem",
|
||||||
"ctrl-up": "terminal::ScrollLineUp",
|
"ctrl-up": "terminal::ScrollLineUp",
|
||||||
"ctrl-down": "terminal::ScrollLineDown",
|
"ctrl-down": "terminal::ScrollLineDown",
|
||||||
"shift-pageup": "terminal::ScrollPageUp",
|
"shift-pageup": "terminal::ScrollPageUp",
|
||||||
|
|||||||
@@ -5,14 +5,12 @@
|
|||||||
"cmd-}": "pane::ActivateNextItem",
|
"cmd-}": "pane::ActivateNextItem",
|
||||||
"cmd-0": "git_panel::ToggleFocus", // overrides `cmd-0` zoom reset
|
"cmd-0": "git_panel::ToggleFocus", // overrides `cmd-0` zoom reset
|
||||||
"shift-escape": null, // Unmap workspace::zoom
|
"shift-escape": null, // Unmap workspace::zoom
|
||||||
"cmd-~": "git::Branch",
|
|
||||||
"ctrl-f2": "debugger::Stop",
|
"ctrl-f2": "debugger::Stop",
|
||||||
"f6": "debugger::Pause",
|
"f6": "debugger::Pause",
|
||||||
"f7": "debugger::StepInto",
|
"f7": "debugger::StepInto",
|
||||||
"f8": "debugger::StepOver",
|
"f8": "debugger::StepOver",
|
||||||
"shift-f8": "debugger::StepOut",
|
"shift-f8": "debugger::StepOut",
|
||||||
"f9": "debugger::Continue",
|
"f9": "debugger::Continue",
|
||||||
"shift-f9": "debugger::Start",
|
|
||||||
"alt-shift-f9": "debugger::Start"
|
"alt-shift-f9": "debugger::Start"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -47,7 +45,7 @@
|
|||||||
"alt-f7": "editor::FindAllReferences",
|
"alt-f7": "editor::FindAllReferences",
|
||||||
"cmd-alt-f7": "editor::FindAllReferences",
|
"cmd-alt-f7": "editor::FindAllReferences",
|
||||||
"cmd-b": "editor::GoToDefinition", // Conflicts with workspace::ToggleLeftDock
|
"cmd-b": "editor::GoToDefinition", // Conflicts with workspace::ToggleLeftDock
|
||||||
"cmd-alt-b": "editor::GoToImplementation",
|
"cmd-alt-b": "editor::GoToDefinitionSplit",
|
||||||
"cmd-shift-b": "editor::GoToTypeDefinition",
|
"cmd-shift-b": "editor::GoToTypeDefinition",
|
||||||
"cmd-alt-shift-b": "editor::GoToTypeDefinitionSplit",
|
"cmd-alt-shift-b": "editor::GoToTypeDefinitionSplit",
|
||||||
"f2": "editor::GoToDiagnostic",
|
"f2": "editor::GoToDiagnostic",
|
||||||
@@ -70,11 +68,7 @@
|
|||||||
"cmd-r": ["buffer_search::Deploy", { "replace_enabled": true }],
|
"cmd-r": ["buffer_search::Deploy", { "replace_enabled": true }],
|
||||||
"cmd-shift-o": "file_finder::Toggle",
|
"cmd-shift-o": "file_finder::Toggle",
|
||||||
"cmd-l": "go_to_line::Toggle",
|
"cmd-l": "go_to_line::Toggle",
|
||||||
"alt-enter": "editor::ToggleCodeActions",
|
"alt-enter": "editor::ToggleCodeActions"
|
||||||
"ctrl-space": "editor::ShowCompletions",
|
|
||||||
"cmd-j": "editor::Hover",
|
|
||||||
"cmd-p": "editor::ShowSignatureHelp",
|
|
||||||
"cmd-\\": "assistant::InlineAssist"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -102,13 +96,9 @@
|
|||||||
"cmd-shift-f12": "workspace::ToggleAllDocks",
|
"cmd-shift-f12": "workspace::ToggleAllDocks",
|
||||||
"cmd-shift-r": ["pane::DeploySearch", { "replace_enabled": true }],
|
"cmd-shift-r": ["pane::DeploySearch", { "replace_enabled": true }],
|
||||||
"ctrl-alt-r": "task::Spawn",
|
"ctrl-alt-r": "task::Spawn",
|
||||||
"shift-f10": "task::Spawn",
|
|
||||||
"cmd-f5": "task::Rerun",
|
|
||||||
"cmd-e": "file_finder::Toggle",
|
"cmd-e": "file_finder::Toggle",
|
||||||
"cmd-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
|
// "cmd-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
|
||||||
"cmd-shift-o": "file_finder::Toggle",
|
"cmd-shift-o": "file_finder::Toggle",
|
||||||
"cmd-shift-n": "file_finder::Toggle",
|
|
||||||
"cmd-n": "project_symbols::Toggle",
|
|
||||||
"cmd-shift-a": "command_palette::Toggle",
|
"cmd-shift-a": "command_palette::Toggle",
|
||||||
"shift shift": "command_palette::Toggle",
|
"shift shift": "command_palette::Toggle",
|
||||||
"cmd-alt-o": "project_symbols::Toggle", // JetBrains: Go to Symbol
|
"cmd-alt-o": "project_symbols::Toggle", // JetBrains: Go to Symbol
|
||||||
@@ -145,9 +135,7 @@
|
|||||||
"context": "Pane",
|
"context": "Pane",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"cmd-alt-left": "pane::GoBack",
|
"cmd-alt-left": "pane::GoBack",
|
||||||
"cmd-alt-right": "pane::GoForward",
|
"cmd-alt-right": "pane::GoForward"
|
||||||
"alt-left": "pane::ActivatePreviousItem",
|
|
||||||
"alt-right": "pane::ActivateNextItem"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -414,20 +414,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "VimControl && vim_mode == helix_normal && !menu",
|
"context": "vim_mode == helix_normal && !menu",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"escape": "vim::SwitchToHelixNormalMode",
|
|
||||||
"i": "vim::HelixInsert",
|
"i": "vim::HelixInsert",
|
||||||
"a": "vim::HelixAppend",
|
"a": "vim::HelixAppend",
|
||||||
"ctrl-[": "editor::Cancel"
|
"ctrl-[": "editor::Cancel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"context": "vim_mode == helix_select && !menu",
|
|
||||||
"bindings": {
|
|
||||||
"escape": "vim::SwitchToHelixNormalMode"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"context": "(vim_mode == helix_normal || vim_mode == helix_select) && !menu",
|
"context": "(vim_mode == helix_normal || vim_mode == helix_select) && !menu",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
@@ -477,9 +470,6 @@
|
|||||||
"alt-p": "editor::SelectPreviousSyntaxNode",
|
"alt-p": "editor::SelectPreviousSyntaxNode",
|
||||||
"alt-n": "editor::SelectNextSyntaxNode",
|
"alt-n": "editor::SelectNextSyntaxNode",
|
||||||
|
|
||||||
"n": "vim::HelixSelectNext",
|
|
||||||
"shift-n": "vim::HelixSelectPrevious",
|
|
||||||
|
|
||||||
// Goto mode
|
// Goto mode
|
||||||
"g e": "vim::EndOfDocument",
|
"g e": "vim::EndOfDocument",
|
||||||
"g h": "vim::StartOfLine",
|
"g h": "vim::StartOfLine",
|
||||||
|
|||||||
@@ -175,16 +175,6 @@
|
|||||||
//
|
//
|
||||||
// Default: true
|
// Default: true
|
||||||
"zoomed_padding": true,
|
"zoomed_padding": true,
|
||||||
// What draws Zed's window decorations (titlebar):
|
|
||||||
// 1. Client application (Zed) draws its own window decorations
|
|
||||||
// "client"
|
|
||||||
// 2. Display server draws the window decorations. Not supported by GNOME Wayland.
|
|
||||||
// "server"
|
|
||||||
//
|
|
||||||
// This requires restarting Zed for changes to take effect.
|
|
||||||
//
|
|
||||||
// Default: "client"
|
|
||||||
"window_decorations": "client",
|
|
||||||
// Whether to use the system provided dialogs for Open and Save As.
|
// Whether to use the system provided dialogs for Open and Save As.
|
||||||
// When set to false, Zed will use the built-in keyboard-first pickers.
|
// When set to false, Zed will use the built-in keyboard-first pickers.
|
||||||
"use_system_path_prompts": true,
|
"use_system_path_prompts": true,
|
||||||
@@ -265,12 +255,6 @@
|
|||||||
// Whether to display inline and alongside documentation for items in the
|
// Whether to display inline and alongside documentation for items in the
|
||||||
// completions menu
|
// completions menu
|
||||||
"show_completion_documentation": true,
|
"show_completion_documentation": true,
|
||||||
// Whether to colorize brackets in the editor.
|
|
||||||
// (also known as "rainbow brackets")
|
|
||||||
//
|
|
||||||
// The colors that are used for different indentation levels are defined in the theme (theme key: `accents`).
|
|
||||||
// They can be customized by using theme overrides.
|
|
||||||
"colorize_brackets": false,
|
|
||||||
// When to show the scrollbar in the completion menu.
|
// When to show the scrollbar in the completion menu.
|
||||||
// This setting can take four values:
|
// This setting can take four values:
|
||||||
//
|
//
|
||||||
@@ -1332,10 +1316,7 @@
|
|||||||
// "hunk_style": "staged_hollow"
|
// "hunk_style": "staged_hollow"
|
||||||
// 2. Show unstaged hunks hollow and staged hunks filled:
|
// 2. Show unstaged hunks hollow and staged hunks filled:
|
||||||
// "hunk_style": "unstaged_hollow"
|
// "hunk_style": "unstaged_hollow"
|
||||||
"hunk_style": "staged_hollow",
|
"hunk_style": "staged_hollow"
|
||||||
// Should the name or path be displayed first in the git view.
|
|
||||||
// "path_style": "file_name_first" or "file_path_first"
|
|
||||||
"path_style": "file_name_first"
|
|
||||||
},
|
},
|
||||||
// The list of custom Git hosting providers.
|
// The list of custom Git hosting providers.
|
||||||
"git_hosting_providers": [
|
"git_hosting_providers": [
|
||||||
@@ -1441,7 +1422,7 @@
|
|||||||
"default_height": 320,
|
"default_height": 320,
|
||||||
// What working directory to use when launching the terminal.
|
// What working directory to use when launching the terminal.
|
||||||
// May take 4 values:
|
// May take 4 values:
|
||||||
// 1. Use the current file's project directory. Fallback to the
|
// 1. Use the current file's project directory. Will Fallback to the
|
||||||
// first project directory strategy if unsuccessful
|
// first project directory strategy if unsuccessful
|
||||||
// "working_directory": "current_project_directory"
|
// "working_directory": "current_project_directory"
|
||||||
// 2. Use the first project in this workspace's directory
|
// 2. Use the first project in this workspace's directory
|
||||||
@@ -1595,59 +1576,7 @@
|
|||||||
//
|
//
|
||||||
// Most terminal themes have APCA values of 40-70.
|
// Most terminal themes have APCA values of 40-70.
|
||||||
// A value of 45 preserves colorful themes while ensuring legibility.
|
// A value of 45 preserves colorful themes while ensuring legibility.
|
||||||
"minimum_contrast": 45,
|
"minimum_contrast": 45
|
||||||
// Regexes used to identify paths for hyperlink navigation. Supports optional named capture
|
|
||||||
// groups `path`, `line`, `column`, and `link`. If none of these are present, the entire match
|
|
||||||
// is the hyperlink target. If `path` is present, it is the hyperlink target, along with `line`
|
|
||||||
// and `column` if present. `link` may be used to customize what text in terminal is part of the
|
|
||||||
// hyperlink. If `link` is not present, the text of the entire match is used. If `line` and
|
|
||||||
// `column` are not present, the default built-in line and column suffix processing is used
|
|
||||||
// which parses `line:column` and `(line,column)` variants. The default value handles Python
|
|
||||||
// diagnostics and common path, line, column syntaxes. This can be extended or replaced to
|
|
||||||
// handle specific scenarios. For example, to enable support for hyperlinking paths which
|
|
||||||
// contain spaces in rust output,
|
|
||||||
//
|
|
||||||
// [
|
|
||||||
// "\\s+(-->|:::|at) (?<link>(?<path>.+?))(:$|$)",
|
|
||||||
// "\\s+(Compiling|Checking|Documenting) [^(]+\\((?<link>(?<path>.+))\\)"
|
|
||||||
// ],
|
|
||||||
//
|
|
||||||
// could be used. Processing stops at the first regex with a match, even if no link is
|
|
||||||
// produced which is the case when the cursor is not over the hyperlinked text. For best
|
|
||||||
// performance it is recommended to order regexes from most common to least common. For
|
|
||||||
// readability and documentation, each regex may be an array of strings which are collected
|
|
||||||
// into one multi-line regex string for use in terminal path hyperlink detection.
|
|
||||||
"path_hyperlink_regexes": [
|
|
||||||
// Python-style diagnostics
|
|
||||||
"File \"(?<path>[^\"]+)\", line (?<line>[0-9]+)",
|
|
||||||
// Common path syntax with optional line, column, description, trailing punctuation, or
|
|
||||||
// surrounding symbols or quotes
|
|
||||||
[
|
|
||||||
"(?x)",
|
|
||||||
"# optionally starts with 0-2 opening prefix symbols",
|
|
||||||
"[({\\[<]{0,2}",
|
|
||||||
"# which may be followed by an opening quote",
|
|
||||||
"(?<quote>[\"'`])?",
|
|
||||||
"# `path` is the shortest sequence of any non-space character",
|
|
||||||
"(?<link>(?<path>[^ ]+?",
|
|
||||||
" # which may end with a line and optionally a column,",
|
|
||||||
" (?<line_column>:+[0-9]+(:[0-9]+)?|:?\\([0-9]+([,:][0-9]+)?\\))?",
|
|
||||||
"))",
|
|
||||||
"# which must be followed by a matching quote",
|
|
||||||
"(?(<quote>)\\k<quote>)",
|
|
||||||
"# and optionally a single closing symbol",
|
|
||||||
"[)}\\]>]?",
|
|
||||||
"# if line/column matched, may be followed by a description",
|
|
||||||
"(?(<line_column>):[^ 0-9][^ ]*)?",
|
|
||||||
"# which may be followed by trailing punctuation",
|
|
||||||
"[.,:)}\\]>]*",
|
|
||||||
"# and always includes trailing whitespace or end of line",
|
|
||||||
"([ ]+|$)"
|
|
||||||
]
|
|
||||||
],
|
|
||||||
// Timeout for hover and Cmd-click path hyperlink discovery in milliseconds. Specifying a
|
|
||||||
// timeout of `0` will disable path hyperlinking in terminal.
|
|
||||||
"path_hyperlink_timeout_ms": 1
|
|
||||||
},
|
},
|
||||||
"code_actions_on_format": {},
|
"code_actions_on_format": {},
|
||||||
// Settings related to running tasks.
|
// Settings related to running tasks.
|
||||||
@@ -1889,7 +1818,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"PHP": {
|
"PHP": {
|
||||||
"language_servers": ["phpactor", "!intelephense", "!phptools", "..."],
|
"language_servers": ["phpactor", "!intelephense", "..."],
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"allowed": true,
|
"allowed": true,
|
||||||
"plugins": ["@prettier/plugin-php"],
|
"plugins": ["@prettier/plugin-php"],
|
||||||
@@ -2130,15 +2059,15 @@
|
|||||||
"dev": {
|
"dev": {
|
||||||
// "theme": "Andromeda"
|
// "theme": "Andromeda"
|
||||||
},
|
},
|
||||||
// Settings overrides to use when using Linux.
|
// Settings overrides to use when using linux
|
||||||
"linux": {},
|
"linux": {},
|
||||||
// Settings overrides to use when using macOS.
|
// Settings overrides to use when using macos
|
||||||
"macos": {},
|
"macos": {},
|
||||||
// Settings overrides to use when using Windows.
|
// Settings overrides to use when using windows
|
||||||
"windows": {
|
"windows": {
|
||||||
"languages": {
|
"languages": {
|
||||||
"PHP": {
|
"PHP": {
|
||||||
"language_servers": ["intelephense", "!phpactor", "!phptools", "..."]
|
"language_servers": ["intelephense", "!phpactor", "..."]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -197,11 +197,6 @@ pub trait AgentModelSelector: 'static {
|
|||||||
fn watch(&self, _cx: &mut App) -> Option<watch::Receiver<()>> {
|
fn watch(&self, _cx: &mut App) -> Option<watch::Receiver<()>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether the model picker should render a footer.
|
|
||||||
fn should_render_footer(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ gpui.workspace = true
|
|||||||
language.workspace = true
|
language.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
proto.workspace = true
|
proto.workspace = true
|
||||||
semver.workspace = true
|
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
|
|||||||
@@ -925,15 +925,15 @@ impl StatusItemView for ActivityIndicator {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use gpui::SemanticVersion;
|
||||||
use release_channel::AppCommitSha;
|
use release_channel::AppCommitSha;
|
||||||
use semver::Version;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_version_tooltip_message() {
|
fn test_version_tooltip_message() {
|
||||||
let message = ActivityIndicator::version_tooltip_message(&VersionCheckType::Semantic(
|
let message = ActivityIndicator::version_tooltip_message(&VersionCheckType::Semantic(
|
||||||
Version::new(1, 0, 0),
|
SemanticVersion::new(1, 0, 0),
|
||||||
));
|
));
|
||||||
|
|
||||||
assert_eq!(message, "Version: 1.0.0");
|
assert_eq!(message, "Version: 1.0.0");
|
||||||
|
|||||||
@@ -961,10 +961,6 @@ impl acp_thread::AgentModelSelector for NativeAgentModelSelector {
|
|||||||
fn watch(&self, cx: &mut App) -> Option<watch::Receiver<()>> {
|
fn watch(&self, cx: &mut App) -> Option<watch::Receiver<()>> {
|
||||||
Some(self.connection.0.read(cx).models.watch())
|
Some(self.connection.0.read(cx).models.watch())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_render_footer(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl acp_thread::AgentConnection for NativeAgentConnection {
|
impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||||
|
|||||||
@@ -182,7 +182,6 @@ impl DbThread {
|
|||||||
crate::Message::Agent(AgentMessage {
|
crate::Message::Agent(AgentMessage {
|
||||||
content,
|
content,
|
||||||
tool_results,
|
tool_results,
|
||||||
reasoning_details: None,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
language_model::Role::System => {
|
language_model::Role::System => {
|
||||||
|
|||||||
@@ -703,7 +703,6 @@ impl EditAgent {
|
|||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec![MessageContent::Text(prompt)],
|
content: vec![MessageContent::Text(prompt)],
|
||||||
cache: false,
|
cache: false,
|
||||||
reasoning_details: None,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Include tools in the request so that we can take advantage of
|
// Include tools in the request so that we can take advantage of
|
||||||
|
|||||||
@@ -1081,7 +1081,6 @@ fn message(
|
|||||||
role,
|
role,
|
||||||
content: contents.into_iter().collect(),
|
content: contents.into_iter().collect(),
|
||||||
cache: false,
|
cache: false,
|
||||||
reasoning_details: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1269,7 +1268,6 @@ impl EvalAssertion {
|
|||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec![prompt.into()],
|
content: vec![prompt.into()],
|
||||||
cache: false,
|
cache: false,
|
||||||
reasoning_details: None,
|
|
||||||
}],
|
}],
|
||||||
thinking_allowed: true,
|
thinking_allowed: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -1596,7 +1594,6 @@ impl EditAgentTest {
|
|||||||
role: Role::System,
|
role: Role::System,
|
||||||
content: vec![MessageContent::Text(system_prompt)],
|
content: vec![MessageContent::Text(system_prompt)],
|
||||||
cache: true,
|
cache: true,
|
||||||
reasoning_details: None,
|
|
||||||
}]
|
}]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.chain(eval.conversation)
|
.chain(eval.conversation)
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ pub async fn get_buffer_content_or_outline(
|
|||||||
if outline_items.is_empty() {
|
if outline_items.is_empty() {
|
||||||
let text = buffer.read_with(cx, |buffer, _| {
|
let text = buffer.read_with(cx, |buffer, _| {
|
||||||
let snapshot = buffer.snapshot();
|
let snapshot = buffer.snapshot();
|
||||||
let len = snapshot.len().min(snapshot.as_rope().floor_char_boundary(1024));
|
let len = snapshot.len().min(1024);
|
||||||
let content = snapshot.text_for_range(0..len).collect::<String>();
|
let content = snapshot.text_for_range(0..len).collect::<String>();
|
||||||
if let Some(path) = path {
|
if let Some(path) = path {
|
||||||
format!("# First 1KB of {path} (file too large to show full content, and no outline available)\n\n{content}")
|
format!("# First 1KB of {path} (file too large to show full content, and no outline available)\n\n{content}")
|
||||||
@@ -178,7 +178,7 @@ mod tests {
|
|||||||
let fs = FakeFs::new(cx.executor());
|
let fs = FakeFs::new(cx.executor());
|
||||||
let project = Project::test(fs, [], cx).await;
|
let project = Project::test(fs, [], cx).await;
|
||||||
|
|
||||||
let content = "⚡".repeat(100 * 1024); // 100KB
|
let content = "A".repeat(100 * 1024); // 100KB
|
||||||
let content_len = content.len();
|
let content_len = content.len();
|
||||||
let buffer = project
|
let buffer = project
|
||||||
.update(cx, |project, cx| project.create_buffer(true, cx))
|
.update(cx, |project, cx| project.create_buffer(true, cx))
|
||||||
@@ -194,7 +194,7 @@ mod tests {
|
|||||||
|
|
||||||
// Should contain some of the actual file content
|
// Should contain some of the actual file content
|
||||||
assert!(
|
assert!(
|
||||||
result.text.contains("⚡⚡⚡⚡⚡⚡⚡"),
|
result.text.contains("AAAAAAAAAA"),
|
||||||
"Result did not contain content subset"
|
"Result did not contain content subset"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -215,8 +215,7 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
|
|||||||
vec![LanguageModelRequestMessage {
|
vec![LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec!["Message 1".into()],
|
content: vec!["Message 1".into()],
|
||||||
cache: true,
|
cache: true
|
||||||
reasoning_details: None,
|
|
||||||
}]
|
}]
|
||||||
);
|
);
|
||||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Text(
|
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Text(
|
||||||
@@ -240,20 +239,17 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
|
|||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec!["Message 1".into()],
|
content: vec!["Message 1".into()],
|
||||||
cache: false,
|
cache: false
|
||||||
reasoning_details: None,
|
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::Assistant,
|
role: Role::Assistant,
|
||||||
content: vec!["Response to Message 1".into()],
|
content: vec!["Response to Message 1".into()],
|
||||||
cache: false,
|
cache: false
|
||||||
reasoning_details: None,
|
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec!["Message 2".into()],
|
content: vec!["Message 2".into()],
|
||||||
cache: true,
|
cache: true
|
||||||
reasoning_details: None,
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -299,44 +295,37 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
|
|||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec!["Message 1".into()],
|
content: vec!["Message 1".into()],
|
||||||
cache: false,
|
cache: false
|
||||||
reasoning_details: None,
|
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::Assistant,
|
role: Role::Assistant,
|
||||||
content: vec!["Response to Message 1".into()],
|
content: vec!["Response to Message 1".into()],
|
||||||
cache: false,
|
cache: false
|
||||||
reasoning_details: None,
|
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec!["Message 2".into()],
|
content: vec!["Message 2".into()],
|
||||||
cache: false,
|
cache: false
|
||||||
reasoning_details: None,
|
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::Assistant,
|
role: Role::Assistant,
|
||||||
content: vec!["Response to Message 2".into()],
|
content: vec!["Response to Message 2".into()],
|
||||||
cache: false,
|
cache: false
|
||||||
reasoning_details: None,
|
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec!["Use the echo tool".into()],
|
content: vec!["Use the echo tool".into()],
|
||||||
cache: false,
|
cache: false
|
||||||
reasoning_details: None,
|
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::Assistant,
|
role: Role::Assistant,
|
||||||
content: vec![MessageContent::ToolUse(tool_use)],
|
content: vec![MessageContent::ToolUse(tool_use)],
|
||||||
cache: false,
|
cache: false
|
||||||
reasoning_details: None,
|
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec![MessageContent::ToolResult(tool_result)],
|
content: vec![MessageContent::ToolResult(tool_result)],
|
||||||
cache: true,
|
cache: true
|
||||||
reasoning_details: None,
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -659,26 +648,25 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) {
|
|||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec!["abc".into()],
|
content: vec!["abc".into()],
|
||||||
cache: false,
|
cache: false
|
||||||
reasoning_details: None,
|
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::Assistant,
|
role: Role::Assistant,
|
||||||
content: vec![MessageContent::ToolUse(tool_use.clone())],
|
content: vec![MessageContent::ToolUse(tool_use.clone())],
|
||||||
cache: false,
|
cache: false
|
||||||
reasoning_details: None,
|
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec![MessageContent::ToolResult(tool_result.clone())],
|
content: vec![MessageContent::ToolResult(tool_result.clone())],
|
||||||
cache: true,
|
cache: true
|
||||||
reasoning_details: None,
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Simulate reaching tool use limit.
|
// Simulate reaching tool use limit.
|
||||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUseLimitReached);
|
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::StatusUpdate(
|
||||||
|
cloud_llm_client::CompletionRequestStatus::ToolUseLimitReached,
|
||||||
|
));
|
||||||
fake_model.end_last_completion_stream();
|
fake_model.end_last_completion_stream();
|
||||||
let last_event = events.collect::<Vec<_>>().await.pop().unwrap();
|
let last_event = events.collect::<Vec<_>>().await.pop().unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
@@ -696,26 +684,22 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) {
|
|||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec!["abc".into()],
|
content: vec!["abc".into()],
|
||||||
cache: false,
|
cache: false
|
||||||
reasoning_details: None,
|
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::Assistant,
|
role: Role::Assistant,
|
||||||
content: vec![MessageContent::ToolUse(tool_use)],
|
content: vec![MessageContent::ToolUse(tool_use)],
|
||||||
cache: false,
|
cache: false
|
||||||
reasoning_details: None,
|
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec![MessageContent::ToolResult(tool_result)],
|
content: vec![MessageContent::ToolResult(tool_result)],
|
||||||
cache: false,
|
cache: false
|
||||||
reasoning_details: None,
|
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec!["Continue where you left off".into()],
|
content: vec!["Continue where you left off".into()],
|
||||||
cache: true,
|
cache: true
|
||||||
reasoning_details: None,
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -765,7 +749,9 @@ async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) {
|
|||||||
};
|
};
|
||||||
fake_model
|
fake_model
|
||||||
.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone()));
|
.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone()));
|
||||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUseLimitReached);
|
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::StatusUpdate(
|
||||||
|
cloud_llm_client::CompletionRequestStatus::ToolUseLimitReached,
|
||||||
|
));
|
||||||
fake_model.end_last_completion_stream();
|
fake_model.end_last_completion_stream();
|
||||||
let last_event = events.collect::<Vec<_>>().await.pop().unwrap();
|
let last_event = events.collect::<Vec<_>>().await.pop().unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
@@ -787,26 +773,22 @@ async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) {
|
|||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec!["abc".into()],
|
content: vec!["abc".into()],
|
||||||
cache: false,
|
cache: false
|
||||||
reasoning_details: None,
|
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::Assistant,
|
role: Role::Assistant,
|
||||||
content: vec![MessageContent::ToolUse(tool_use)],
|
content: vec![MessageContent::ToolUse(tool_use)],
|
||||||
cache: false,
|
cache: false
|
||||||
reasoning_details: None,
|
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec![MessageContent::ToolResult(tool_result)],
|
content: vec![MessageContent::ToolResult(tool_result)],
|
||||||
cache: false,
|
cache: false
|
||||||
reasoning_details: None,
|
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec!["ghi".into()],
|
content: vec!["ghi".into()],
|
||||||
cache: true,
|
cache: true
|
||||||
reasoning_details: None,
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -1849,8 +1831,7 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
|
|||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec!["Hey!".into()],
|
content: vec!["Hey!".into()],
|
||||||
cache: true,
|
cache: true
|
||||||
reasoning_details: None,
|
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::Assistant,
|
role: Role::Assistant,
|
||||||
@@ -1858,8 +1839,7 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
|
|||||||
MessageContent::Text("Hi!".into()),
|
MessageContent::Text("Hi!".into()),
|
||||||
MessageContent::ToolUse(echo_tool_use.clone())
|
MessageContent::ToolUse(echo_tool_use.clone())
|
||||||
],
|
],
|
||||||
cache: false,
|
cache: false
|
||||||
reasoning_details: None,
|
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
@@ -1870,8 +1850,7 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
|
|||||||
content: "test".into(),
|
content: "test".into(),
|
||||||
output: Some("test".into())
|
output: Some("test".into())
|
||||||
})],
|
})],
|
||||||
cache: false,
|
cache: false
|
||||||
reasoning_details: None,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -2269,14 +2248,12 @@ async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
|
|||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec!["Call the echo tool!".into()],
|
content: vec!["Call the echo tool!".into()],
|
||||||
cache: false,
|
cache: false
|
||||||
reasoning_details: None,
|
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::Assistant,
|
role: Role::Assistant,
|
||||||
content: vec![language_model::MessageContent::ToolUse(tool_use_1.clone())],
|
content: vec![language_model::MessageContent::ToolUse(tool_use_1.clone())],
|
||||||
cache: false,
|
cache: false
|
||||||
reasoning_details: None,
|
|
||||||
},
|
},
|
||||||
LanguageModelRequestMessage {
|
LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
@@ -2289,8 +2266,7 @@ async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
|
|||||||
output: Some("test".into())
|
output: Some("test".into())
|
||||||
}
|
}
|
||||||
)],
|
)],
|
||||||
cache: true,
|
cache: true
|
||||||
reasoning_details: None,
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -2304,8 +2280,7 @@ async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
|
|||||||
thread.last_message(),
|
thread.last_message(),
|
||||||
Some(Message::Agent(AgentMessage {
|
Some(Message::Agent(AgentMessage {
|
||||||
content: vec![AgentMessageContent::Text("Done".into())],
|
content: vec![AgentMessageContent::Text("Done".into())],
|
||||||
tool_results: IndexMap::default(),
|
tool_results: IndexMap::default()
|
||||||
reasoning_details: None,
|
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use agent_settings::{
|
|||||||
use anyhow::{Context as _, Result, anyhow};
|
use anyhow::{Context as _, Result, anyhow};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use client::{ModelRequestUsage, RequestUsage, UserStore};
|
use client::{ModelRequestUsage, RequestUsage, UserStore};
|
||||||
use cloud_llm_client::{CompletionIntent, Plan, UsageLimit};
|
use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, Plan, UsageLimit};
|
||||||
use collections::{HashMap, HashSet, IndexMap};
|
use collections::{HashMap, HashSet, IndexMap};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use futures::stream;
|
use futures::stream;
|
||||||
@@ -113,7 +113,6 @@ impl Message {
|
|||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec!["Continue where you left off".into()],
|
content: vec!["Continue where you left off".into()],
|
||||||
cache: false,
|
cache: false,
|
||||||
reasoning_details: None,
|
|
||||||
}],
|
}],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,7 +177,6 @@ impl UserMessage {
|
|||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: Vec::with_capacity(self.content.len()),
|
content: Vec::with_capacity(self.content.len()),
|
||||||
cache: false,
|
cache: false,
|
||||||
reasoning_details: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const OPEN_CONTEXT: &str = "<context>\n\
|
const OPEN_CONTEXT: &str = "<context>\n\
|
||||||
@@ -446,7 +444,6 @@ impl AgentMessage {
|
|||||||
role: Role::Assistant,
|
role: Role::Assistant,
|
||||||
content: Vec::with_capacity(self.content.len()),
|
content: Vec::with_capacity(self.content.len()),
|
||||||
cache: false,
|
cache: false,
|
||||||
reasoning_details: self.reasoning_details.clone(),
|
|
||||||
};
|
};
|
||||||
for chunk in &self.content {
|
for chunk in &self.content {
|
||||||
match chunk {
|
match chunk {
|
||||||
@@ -482,7 +479,6 @@ impl AgentMessage {
|
|||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: Vec::new(),
|
content: Vec::new(),
|
||||||
cache: false,
|
cache: false,
|
||||||
reasoning_details: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for tool_result in self.tool_results.values() {
|
for tool_result in self.tool_results.values() {
|
||||||
@@ -512,7 +508,6 @@ impl AgentMessage {
|
|||||||
pub struct AgentMessage {
|
pub struct AgentMessage {
|
||||||
pub content: Vec<AgentMessageContent>,
|
pub content: Vec<AgentMessageContent>,
|
||||||
pub tool_results: IndexMap<LanguageModelToolUseId, LanguageModelToolResult>,
|
pub tool_results: IndexMap<LanguageModelToolUseId, LanguageModelToolResult>,
|
||||||
pub reasoning_details: Option<serde_json::Value>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
@@ -1403,18 +1398,6 @@ impl Thread {
|
|||||||
self.handle_thinking_event(text, signature, event_stream, cx)
|
self.handle_thinking_event(text, signature, event_stream, cx)
|
||||||
}
|
}
|
||||||
RedactedThinking { data } => self.handle_redacted_thinking_event(data, cx),
|
RedactedThinking { data } => self.handle_redacted_thinking_event(data, cx),
|
||||||
ReasoningDetails(details) => {
|
|
||||||
let last_message = self.pending_message();
|
|
||||||
// Store the last non-empty reasoning_details (overwrites earlier ones)
|
|
||||||
// This ensures we keep the encrypted reasoning with signatures, not the early text reasoning
|
|
||||||
if let serde_json::Value::Array(ref arr) = details {
|
|
||||||
if !arr.is_empty() {
|
|
||||||
last_message.reasoning_details = Some(details);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
last_message.reasoning_details = Some(details);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ToolUse(tool_use) => {
|
ToolUse(tool_use) => {
|
||||||
return Ok(self.handle_tool_use_event(tool_use, event_stream, cx));
|
return Ok(self.handle_tool_use_event(tool_use, event_stream, cx));
|
||||||
}
|
}
|
||||||
@@ -1447,16 +1430,20 @@ impl Thread {
|
|||||||
);
|
);
|
||||||
self.update_token_usage(usage, cx);
|
self.update_token_usage(usage, cx);
|
||||||
}
|
}
|
||||||
UsageUpdated { amount, limit } => {
|
StatusUpdate(CompletionRequestStatus::UsageUpdated { amount, limit }) => {
|
||||||
self.update_model_request_usage(amount, limit, cx);
|
self.update_model_request_usage(amount, limit, cx);
|
||||||
}
|
}
|
||||||
ToolUseLimitReached => {
|
StatusUpdate(
|
||||||
|
CompletionRequestStatus::Started
|
||||||
|
| CompletionRequestStatus::Queued { .. }
|
||||||
|
| CompletionRequestStatus::Failed { .. },
|
||||||
|
) => {}
|
||||||
|
StatusUpdate(CompletionRequestStatus::ToolUseLimitReached) => {
|
||||||
self.tool_use_limit_reached = true;
|
self.tool_use_limit_reached = true;
|
||||||
}
|
}
|
||||||
Stop(StopReason::Refusal) => return Err(CompletionError::Refusal.into()),
|
Stop(StopReason::Refusal) => return Err(CompletionError::Refusal.into()),
|
||||||
Stop(StopReason::MaxTokens) => return Err(CompletionError::MaxTokens.into()),
|
Stop(StopReason::MaxTokens) => return Err(CompletionError::MaxTokens.into()),
|
||||||
Stop(StopReason::ToolUse | StopReason::EndTurn) => {}
|
Stop(StopReason::ToolUse | StopReason::EndTurn) => {}
|
||||||
Started | Queued { .. } => {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
@@ -1690,7 +1677,6 @@ impl Thread {
|
|||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec![SUMMARIZE_THREAD_DETAILED_PROMPT.into()],
|
content: vec![SUMMARIZE_THREAD_DETAILED_PROMPT.into()],
|
||||||
cache: false,
|
cache: false,
|
||||||
reasoning_details: None,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let task = cx
|
let task = cx
|
||||||
@@ -1701,7 +1687,9 @@ impl Thread {
|
|||||||
let event = event.log_err()?;
|
let event = event.log_err()?;
|
||||||
let text = match event {
|
let text = match event {
|
||||||
LanguageModelCompletionEvent::Text(text) => text,
|
LanguageModelCompletionEvent::Text(text) => text,
|
||||||
LanguageModelCompletionEvent::UsageUpdated { amount, limit } => {
|
LanguageModelCompletionEvent::StatusUpdate(
|
||||||
|
CompletionRequestStatus::UsageUpdated { amount, limit },
|
||||||
|
) => {
|
||||||
this.update(cx, |thread, cx| {
|
this.update(cx, |thread, cx| {
|
||||||
thread.update_model_request_usage(amount, limit, cx);
|
thread.update_model_request_usage(amount, limit, cx);
|
||||||
})
|
})
|
||||||
@@ -1755,7 +1743,6 @@ impl Thread {
|
|||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec![SUMMARIZE_THREAD_PROMPT.into()],
|
content: vec![SUMMARIZE_THREAD_PROMPT.into()],
|
||||||
cache: false,
|
cache: false,
|
||||||
reasoning_details: None,
|
|
||||||
});
|
});
|
||||||
self.pending_title_generation = Some(cx.spawn(async move |this, cx| {
|
self.pending_title_generation = Some(cx.spawn(async move |this, cx| {
|
||||||
let mut title = String::new();
|
let mut title = String::new();
|
||||||
@@ -1766,7 +1753,9 @@ impl Thread {
|
|||||||
let event = event?;
|
let event = event?;
|
||||||
let text = match event {
|
let text = match event {
|
||||||
LanguageModelCompletionEvent::Text(text) => text,
|
LanguageModelCompletionEvent::Text(text) => text,
|
||||||
LanguageModelCompletionEvent::UsageUpdated { amount, limit } => {
|
LanguageModelCompletionEvent::StatusUpdate(
|
||||||
|
CompletionRequestStatus::UsageUpdated { amount, limit },
|
||||||
|
) => {
|
||||||
this.update(cx, |thread, cx| {
|
this.update(cx, |thread, cx| {
|
||||||
thread.update_model_request_usage(amount, limit, cx);
|
thread.update_model_request_usage(amount, limit, cx);
|
||||||
})?;
|
})?;
|
||||||
@@ -2003,7 +1992,6 @@ impl Thread {
|
|||||||
role: Role::System,
|
role: Role::System,
|
||||||
content: vec![system_prompt.into()],
|
content: vec![system_prompt.into()],
|
||||||
cache: false,
|
cache: false,
|
||||||
reasoning_details: None,
|
|
||||||
}];
|
}];
|
||||||
for message in &self.messages {
|
for message in &self.messages {
|
||||||
messages.extend(message.to_request());
|
messages.extend(message.to_request());
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ pub struct AcpConnection {
|
|||||||
auth_methods: Vec<acp::AuthMethod>,
|
auth_methods: Vec<acp::AuthMethod>,
|
||||||
agent_capabilities: acp::AgentCapabilities,
|
agent_capabilities: acp::AgentCapabilities,
|
||||||
default_mode: Option<acp::SessionModeId>,
|
default_mode: Option<acp::SessionModeId>,
|
||||||
default_model: Option<acp::ModelId>,
|
|
||||||
root_dir: PathBuf,
|
root_dir: PathBuf,
|
||||||
// NB: Don't move this into the wait_task, since we need to ensure the process is
|
// NB: Don't move this into the wait_task, since we need to ensure the process is
|
||||||
// killed on drop (setting kill_on_drop on the command seems to not always work).
|
// killed on drop (setting kill_on_drop on the command seems to not always work).
|
||||||
@@ -58,7 +57,6 @@ pub async fn connect(
|
|||||||
command: AgentServerCommand,
|
command: AgentServerCommand,
|
||||||
root_dir: &Path,
|
root_dir: &Path,
|
||||||
default_mode: Option<acp::SessionModeId>,
|
default_mode: Option<acp::SessionModeId>,
|
||||||
default_model: Option<acp::ModelId>,
|
|
||||||
is_remote: bool,
|
is_remote: bool,
|
||||||
cx: &mut AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
) -> Result<Rc<dyn AgentConnection>> {
|
) -> Result<Rc<dyn AgentConnection>> {
|
||||||
@@ -68,7 +66,6 @@ pub async fn connect(
|
|||||||
command.clone(),
|
command.clone(),
|
||||||
root_dir,
|
root_dir,
|
||||||
default_mode,
|
default_mode,
|
||||||
default_model,
|
|
||||||
is_remote,
|
is_remote,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@@ -85,7 +82,6 @@ impl AcpConnection {
|
|||||||
command: AgentServerCommand,
|
command: AgentServerCommand,
|
||||||
root_dir: &Path,
|
root_dir: &Path,
|
||||||
default_mode: Option<acp::SessionModeId>,
|
default_mode: Option<acp::SessionModeId>,
|
||||||
default_model: Option<acp::ModelId>,
|
|
||||||
is_remote: bool,
|
is_remote: bool,
|
||||||
cx: &mut AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
@@ -211,7 +207,6 @@ impl AcpConnection {
|
|||||||
sessions,
|
sessions,
|
||||||
agent_capabilities: response.agent_capabilities,
|
agent_capabilities: response.agent_capabilities,
|
||||||
default_mode,
|
default_mode,
|
||||||
default_model,
|
|
||||||
_io_task: io_task,
|
_io_task: io_task,
|
||||||
_wait_task: wait_task,
|
_wait_task: wait_task,
|
||||||
_stderr_task: stderr_task,
|
_stderr_task: stderr_task,
|
||||||
@@ -250,7 +245,6 @@ impl AgentConnection for AcpConnection {
|
|||||||
let conn = self.connection.clone();
|
let conn = self.connection.clone();
|
||||||
let sessions = self.sessions.clone();
|
let sessions = self.sessions.clone();
|
||||||
let default_mode = self.default_mode.clone();
|
let default_mode = self.default_mode.clone();
|
||||||
let default_model = self.default_model.clone();
|
|
||||||
let cwd = cwd.to_path_buf();
|
let cwd = cwd.to_path_buf();
|
||||||
let context_server_store = project.read(cx).context_server_store().read(cx);
|
let context_server_store = project.read(cx).context_server_store().read(cx);
|
||||||
let mcp_servers =
|
let mcp_servers =
|
||||||
@@ -339,7 +333,6 @@ impl AgentConnection for AcpConnection {
|
|||||||
let default_mode = default_mode.clone();
|
let default_mode = default_mode.clone();
|
||||||
let session_id = response.session_id.clone();
|
let session_id = response.session_id.clone();
|
||||||
let modes = modes.clone();
|
let modes = modes.clone();
|
||||||
let conn = conn.clone();
|
|
||||||
async move |_| {
|
async move |_| {
|
||||||
let result = conn.set_session_mode(acp::SetSessionModeRequest {
|
let result = conn.set_session_mode(acp::SetSessionModeRequest {
|
||||||
session_id,
|
session_id,
|
||||||
@@ -374,53 +367,6 @@ impl AgentConnection for AcpConnection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(default_model) = default_model {
|
|
||||||
if let Some(models) = models.as_ref() {
|
|
||||||
let mut models_ref = models.borrow_mut();
|
|
||||||
let has_model = models_ref.available_models.iter().any(|model| model.model_id == default_model);
|
|
||||||
|
|
||||||
if has_model {
|
|
||||||
let initial_model_id = models_ref.current_model_id.clone();
|
|
||||||
|
|
||||||
cx.spawn({
|
|
||||||
let default_model = default_model.clone();
|
|
||||||
let session_id = response.session_id.clone();
|
|
||||||
let models = models.clone();
|
|
||||||
let conn = conn.clone();
|
|
||||||
async move |_| {
|
|
||||||
let result = conn.set_session_model(acp::SetSessionModelRequest {
|
|
||||||
session_id,
|
|
||||||
model_id: default_model,
|
|
||||||
meta: None,
|
|
||||||
})
|
|
||||||
.await.log_err();
|
|
||||||
|
|
||||||
if result.is_none() {
|
|
||||||
models.borrow_mut().current_model_id = initial_model_id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).detach();
|
|
||||||
|
|
||||||
models_ref.current_model_id = default_model;
|
|
||||||
} else {
|
|
||||||
let available_models = models_ref
|
|
||||||
.available_models
|
|
||||||
.iter()
|
|
||||||
.map(|model| format!("- `{}`: {}", model.model_id, model.name))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
log::warn!(
|
|
||||||
"`{default_model}` is not a valid {name} model. Available options:\n{available_models}",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log::warn!(
|
|
||||||
"`{name}` does not support model selection, but `default_model` was set in settings.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let session_id = response.session_id;
|
let session_id = response.session_id;
|
||||||
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
|
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
|
||||||
let thread = cx.new(|cx| {
|
let thread = cx.new(|cx| {
|
||||||
|
|||||||
@@ -68,18 +68,6 @@ pub trait AgentServer: Send {
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_model(&self, _cx: &mut App) -> Option<agent_client_protocol::ModelId> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_default_model(
|
|
||||||
&self,
|
|
||||||
_model_id: Option<agent_client_protocol::ModelId>,
|
|
||||||
_fs: Arc<dyn Fs>,
|
|
||||||
_cx: &mut App,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
fn connect(
|
fn connect(
|
||||||
&self,
|
&self,
|
||||||
root_dir: Option<&Path>,
|
root_dir: Option<&Path>,
|
||||||
|
|||||||
@@ -55,27 +55,6 @@ impl AgentServer for ClaudeCode {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_model(&self, cx: &mut App) -> Option<acp::ModelId> {
|
|
||||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
|
||||||
settings.get::<AllAgentServersSettings>(None).claude.clone()
|
|
||||||
});
|
|
||||||
|
|
||||||
settings
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|s| s.default_model.clone().map(|m| acp::ModelId(m.into())))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {
|
|
||||||
update_settings_file(fs, cx, |settings, _| {
|
|
||||||
settings
|
|
||||||
.agent_servers
|
|
||||||
.get_or_insert_default()
|
|
||||||
.claude
|
|
||||||
.get_or_insert_default()
|
|
||||||
.default_model = model_id.map(|m| m.to_string())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn connect(
|
fn connect(
|
||||||
&self,
|
&self,
|
||||||
root_dir: Option<&Path>,
|
root_dir: Option<&Path>,
|
||||||
@@ -89,7 +68,6 @@ impl AgentServer for ClaudeCode {
|
|||||||
let store = delegate.store.downgrade();
|
let store = delegate.store.downgrade();
|
||||||
let extra_env = load_proxy_env(cx);
|
let extra_env = load_proxy_env(cx);
|
||||||
let default_mode = self.default_mode(cx);
|
let default_mode = self.default_mode(cx);
|
||||||
let default_model = self.default_model(cx);
|
|
||||||
|
|
||||||
cx.spawn(async move |cx| {
|
cx.spawn(async move |cx| {
|
||||||
let (command, root_dir, login) = store
|
let (command, root_dir, login) = store
|
||||||
@@ -112,7 +90,6 @@ impl AgentServer for ClaudeCode {
|
|||||||
command,
|
command,
|
||||||
root_dir.as_ref(),
|
root_dir.as_ref(),
|
||||||
default_mode,
|
default_mode,
|
||||||
default_model,
|
|
||||||
is_remote,
|
is_remote,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -56,27 +56,6 @@ impl AgentServer for Codex {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_model(&self, cx: &mut App) -> Option<acp::ModelId> {
|
|
||||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
|
||||||
settings.get::<AllAgentServersSettings>(None).codex.clone()
|
|
||||||
});
|
|
||||||
|
|
||||||
settings
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|s| s.default_model.clone().map(|m| acp::ModelId(m.into())))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_default_model(&self, model_id: Option<acp::ModelId>, 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_model = model_id.map(|m| m.to_string())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn connect(
|
fn connect(
|
||||||
&self,
|
&self,
|
||||||
root_dir: Option<&Path>,
|
root_dir: Option<&Path>,
|
||||||
@@ -90,7 +69,6 @@ impl AgentServer for Codex {
|
|||||||
let store = delegate.store.downgrade();
|
let store = delegate.store.downgrade();
|
||||||
let extra_env = load_proxy_env(cx);
|
let extra_env = load_proxy_env(cx);
|
||||||
let default_mode = self.default_mode(cx);
|
let default_mode = self.default_mode(cx);
|
||||||
let default_model = self.default_model(cx);
|
|
||||||
|
|
||||||
cx.spawn(async move |cx| {
|
cx.spawn(async move |cx| {
|
||||||
let (command, root_dir, login) = store
|
let (command, root_dir, login) = store
|
||||||
@@ -114,7 +92,6 @@ impl AgentServer for Codex {
|
|||||||
command,
|
command,
|
||||||
root_dir.as_ref(),
|
root_dir.as_ref(),
|
||||||
default_mode,
|
default_mode,
|
||||||
default_model,
|
|
||||||
is_remote,
|
is_remote,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -44,63 +44,19 @@ impl crate::AgentServer for CustomAgentServer {
|
|||||||
|
|
||||||
settings
|
settings
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|s| s.default_mode().map(|m| acp::SessionModeId(m.into())))
|
.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) {
|
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
|
||||||
let name = self.name();
|
let name = self.name();
|
||||||
update_settings_file(fs, cx, move |settings, _| {
|
update_settings_file(fs, cx, move |settings, _| {
|
||||||
let settings = settings
|
if let Some(settings) = settings
|
||||||
.agent_servers
|
.agent_servers
|
||||||
.get_or_insert_default()
|
.get_or_insert_default()
|
||||||
.custom
|
.custom
|
||||||
.entry(name.clone())
|
.get_mut(&name)
|
||||||
.or_insert_with(|| settings::CustomAgentServerSettings::Extension {
|
{
|
||||||
default_model: None,
|
settings.default_mode = mode_id.map(|m| m.to_string())
|
||||||
default_mode: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
match settings {
|
|
||||||
settings::CustomAgentServerSettings::Custom { default_mode, .. }
|
|
||||||
| settings::CustomAgentServerSettings::Extension { default_mode, .. } => {
|
|
||||||
*default_mode = mode_id.map(|m| m.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_model(&self, cx: &mut App) -> Option<acp::ModelId> {
|
|
||||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
|
||||||
settings
|
|
||||||
.get::<AllAgentServersSettings>(None)
|
|
||||||
.custom
|
|
||||||
.get(&self.name())
|
|
||||||
.cloned()
|
|
||||||
});
|
|
||||||
|
|
||||||
settings
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|s| s.default_model().map(|m| acp::ModelId(m.into())))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {
|
|
||||||
let name = self.name();
|
|
||||||
update_settings_file(fs, cx, move |settings, _| {
|
|
||||||
let settings = settings
|
|
||||||
.agent_servers
|
|
||||||
.get_or_insert_default()
|
|
||||||
.custom
|
|
||||||
.entry(name.clone())
|
|
||||||
.or_insert_with(|| settings::CustomAgentServerSettings::Extension {
|
|
||||||
default_model: None,
|
|
||||||
default_mode: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
match settings {
|
|
||||||
settings::CustomAgentServerSettings::Custom { default_model, .. }
|
|
||||||
| settings::CustomAgentServerSettings::Extension { default_model, .. } => {
|
|
||||||
*default_model = model_id.map(|m| m.to_string());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -116,7 +72,6 @@ impl crate::AgentServer for CustomAgentServer {
|
|||||||
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().into_owned());
|
||||||
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
||||||
let default_mode = self.default_mode(cx);
|
let default_mode = self.default_mode(cx);
|
||||||
let default_model = self.default_model(cx);
|
|
||||||
let store = delegate.store.downgrade();
|
let store = delegate.store.downgrade();
|
||||||
let extra_env = load_proxy_env(cx);
|
let extra_env = load_proxy_env(cx);
|
||||||
|
|
||||||
@@ -143,7 +98,6 @@ impl crate::AgentServer for CustomAgentServer {
|
|||||||
command,
|
command,
|
||||||
root_dir.as_ref(),
|
root_dir.as_ref(),
|
||||||
default_mode,
|
default_mode,
|
||||||
default_model,
|
|
||||||
is_remote,
|
is_remote,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -476,7 +476,6 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
|
|||||||
env: None,
|
env: None,
|
||||||
ignore_system_version: None,
|
ignore_system_version: None,
|
||||||
default_mode: None,
|
default_mode: None,
|
||||||
default_model: None,
|
|
||||||
}),
|
}),
|
||||||
gemini: Some(crate::gemini::tests::local_command().into()),
|
gemini: Some(crate::gemini::tests::local_command().into()),
|
||||||
codex: Some(BuiltinAgentServerSettings {
|
codex: Some(BuiltinAgentServerSettings {
|
||||||
@@ -485,7 +484,6 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
|
|||||||
env: None,
|
env: None,
|
||||||
ignore_system_version: None,
|
ignore_system_version: None,
|
||||||
default_mode: None,
|
default_mode: None,
|
||||||
default_model: None,
|
|
||||||
}),
|
}),
|
||||||
custom: collections::HashMap::default(),
|
custom: collections::HashMap::default(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ impl AgentServer for Gemini {
|
|||||||
let store = delegate.store.downgrade();
|
let store = delegate.store.downgrade();
|
||||||
let mut extra_env = load_proxy_env(cx);
|
let mut extra_env = load_proxy_env(cx);
|
||||||
let default_mode = self.default_mode(cx);
|
let default_mode = self.default_mode(cx);
|
||||||
let default_model = self.default_model(cx);
|
|
||||||
|
|
||||||
cx.spawn(async move |cx| {
|
cx.spawn(async move |cx| {
|
||||||
extra_env.insert("SURFACE".to_owned(), "zed".to_owned());
|
extra_env.insert("SURFACE".to_owned(), "zed".to_owned());
|
||||||
@@ -70,7 +69,6 @@ impl AgentServer for Gemini {
|
|||||||
command,
|
command,
|
||||||
root_dir.as_ref(),
|
root_dir.as_ref(),
|
||||||
default_mode,
|
default_mode,
|
||||||
default_model,
|
|
||||||
is_remote,
|
is_remote,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ doctest = false
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
test-support = ["gpui/test-support", "language/test-support"]
|
test-support = ["gpui/test-support", "language/test-support"]
|
||||||
|
unit-eval = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
acp_thread.workspace = true
|
acp_thread.workspace = true
|
||||||
@@ -47,6 +48,7 @@ fs.workspace = true
|
|||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
fuzzy.workspace = true
|
fuzzy.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
gpui_tokio.workspace = true
|
||||||
html_to_markdown.workspace = true
|
html_to_markdown.workspace = true
|
||||||
http_client.workspace = true
|
http_client.workspace = true
|
||||||
indoc.workspace = true
|
indoc.workspace = true
|
||||||
@@ -69,6 +71,7 @@ postage.workspace = true
|
|||||||
project.workspace = true
|
project.workspace = true
|
||||||
prompt_store.workspace = true
|
prompt_store.workspace = true
|
||||||
proto.workspace = true
|
proto.workspace = true
|
||||||
|
ref-cast.workspace = true
|
||||||
release_channel.workspace = true
|
release_channel.workspace = true
|
||||||
rope.workspace = true
|
rope.workspace = true
|
||||||
rules_library.workspace = true
|
rules_library.workspace = true
|
||||||
@@ -92,6 +95,7 @@ time_format.workspace = true
|
|||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
ui_input.workspace = true
|
ui_input.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
|
urlencoding.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
watch.workspace = true
|
watch.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
@@ -104,6 +108,7 @@ acp_thread = { workspace = true, features = ["test-support"] }
|
|||||||
agent = { workspace = true, features = ["test-support"] }
|
agent = { workspace = true, features = ["test-support"] }
|
||||||
assistant_text_thread = { workspace = true, features = ["test-support"] }
|
assistant_text_thread = { workspace = true, features = ["test-support"] }
|
||||||
buffer_diff = { workspace = true, features = ["test-support"] }
|
buffer_diff = { workspace = true, features = ["test-support"] }
|
||||||
|
clock.workspace = true
|
||||||
db = { workspace = true, features = ["test-support"] }
|
db = { workspace = true, features = ["test-support"] }
|
||||||
editor = { workspace = true, features = ["test-support"] }
|
editor = { workspace = true, features = ["test-support"] }
|
||||||
gpui = { workspace = true, "features" = ["test-support"] }
|
gpui = { workspace = true, "features" = ["test-support"] }
|
||||||
@@ -113,7 +118,6 @@ languages = { workspace = true, features = ["test-support"] }
|
|||||||
language_model = { workspace = true, "features" = ["test-support"] }
|
language_model = { workspace = true, "features" = ["test-support"] }
|
||||||
pretty_assertions.workspace = true
|
pretty_assertions.workspace = true
|
||||||
project = { workspace = true, features = ["test-support"] }
|
project = { workspace = true, features = ["test-support"] }
|
||||||
semver.workspace = true
|
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
tree-sitter-md.workspace = true
|
tree-sitter-md.workspace = true
|
||||||
unindent.workspace = true
|
unindent.workspace = true
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
mod completion_provider;
|
||||||
mod entry_view_state;
|
mod entry_view_state;
|
||||||
mod message_editor;
|
mod message_editor;
|
||||||
mod mode_selector;
|
mod mode_selector;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -405,7 +405,7 @@ mod tests {
|
|||||||
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
|
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
|
||||||
use editor::RowInfo;
|
use editor::RowInfo;
|
||||||
use fs::FakeFs;
|
use fs::FakeFs;
|
||||||
use gpui::{AppContext as _, TestAppContext};
|
use gpui::{AppContext as _, SemanticVersion, TestAppContext};
|
||||||
|
|
||||||
use crate::acp::entry_view_state::EntryViewState;
|
use crate::acp::entry_view_state::EntryViewState;
|
||||||
use multi_buffer::MultiBufferRow;
|
use multi_buffer::MultiBufferRow;
|
||||||
@@ -539,7 +539,7 @@ mod tests {
|
|||||||
let settings_store = SettingsStore::test(cx);
|
let settings_store = SettingsStore::test(cx);
|
||||||
cx.set_global(settings_store);
|
cx.set_global(settings_store);
|
||||||
theme::init(theme::LoadThemes::JustBase, cx);
|
theme::init(theme::LoadThemes::JustBase, cx);
|
||||||
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
release_channel::init(SemanticVersion::default(), cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@ use ui::{
|
|||||||
PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*,
|
PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{CycleModeSelector, ToggleProfileSelector, ui::HoldForDefault};
|
use crate::{CycleModeSelector, ToggleProfileSelector};
|
||||||
|
|
||||||
pub struct ModeSelector {
|
pub struct ModeSelector {
|
||||||
connection: Rc<dyn AgentSessionModes>,
|
connection: Rc<dyn AgentSessionModes>,
|
||||||
@@ -108,11 +108,36 @@ impl ModeSelector {
|
|||||||
entry.documentation_aside(side, DocumentationEdge::Bottom, {
|
entry.documentation_aside(side, DocumentationEdge::Bottom, {
|
||||||
let description = description.clone();
|
let description = description.clone();
|
||||||
|
|
||||||
move |_| {
|
move |cx| {
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(Label::new(description.clone()))
|
.child(Label::new(description.clone()))
|
||||||
.child(HoldForDefault::new(is_default))
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.pt_1()
|
||||||
|
.border_t_1()
|
||||||
|
.border_color(cx.theme().colors().border_variant)
|
||||||
|
.gap_0p5()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(Color::Muted.color(cx))
|
||||||
|
.child("Hold")
|
||||||
|
.child(h_flex().flex_shrink_0().children(
|
||||||
|
ui::render_modifiers(
|
||||||
|
&gpui::Modifiers::secondary_key(),
|
||||||
|
PlatformStyle::platform(),
|
||||||
|
None,
|
||||||
|
Some(ui::TextSize::Default.rems(cx).into()),
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.child(div().map(|this| {
|
||||||
|
if is_default {
|
||||||
|
this.child("to also unset as default")
|
||||||
|
} else {
|
||||||
|
this.child("to also set as default")
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,38 +1,27 @@
|
|||||||
use std::{cmp::Reverse, rc::Rc, sync::Arc};
|
use std::{cmp::Reverse, rc::Rc, sync::Arc};
|
||||||
|
|
||||||
use acp_thread::{AgentModelInfo, AgentModelList, AgentModelSelector};
|
use acp_thread::{AgentModelInfo, AgentModelList, AgentModelSelector};
|
||||||
use agent_servers::AgentServer;
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use collections::IndexMap;
|
use collections::IndexMap;
|
||||||
use fs::Fs;
|
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use fuzzy::{StringMatchCandidate, match_strings};
|
use fuzzy::{StringMatchCandidate, match_strings};
|
||||||
use gpui::{
|
use gpui::{AsyncWindowContext, BackgroundExecutor, DismissEvent, Task, WeakEntity};
|
||||||
Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, FocusHandle, Task, WeakEntity,
|
|
||||||
};
|
|
||||||
use ordered_float::OrderedFloat;
|
use ordered_float::OrderedFloat;
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use ui::{
|
use ui::{
|
||||||
DocumentationAside, DocumentationEdge, DocumentationSide, IntoElement, KeyBinding, ListItem,
|
DocumentationAside, DocumentationEdge, DocumentationSide, IntoElement, ListItem,
|
||||||
ListItemSpacing, prelude::*,
|
ListItemSpacing, prelude::*,
|
||||||
};
|
};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use zed_actions::agent::OpenSettings;
|
|
||||||
|
|
||||||
use crate::ui::HoldForDefault;
|
|
||||||
|
|
||||||
pub type AcpModelSelector = Picker<AcpModelPickerDelegate>;
|
pub type AcpModelSelector = Picker<AcpModelPickerDelegate>;
|
||||||
|
|
||||||
pub fn acp_model_selector(
|
pub fn acp_model_selector(
|
||||||
selector: Rc<dyn AgentModelSelector>,
|
selector: Rc<dyn AgentModelSelector>,
|
||||||
agent_server: Rc<dyn AgentServer>,
|
|
||||||
fs: Arc<dyn Fs>,
|
|
||||||
focus_handle: FocusHandle,
|
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<AcpModelSelector>,
|
cx: &mut Context<AcpModelSelector>,
|
||||||
) -> AcpModelSelector {
|
) -> AcpModelSelector {
|
||||||
let delegate =
|
let delegate = AcpModelPickerDelegate::new(selector, window, cx);
|
||||||
AcpModelPickerDelegate::new(selector, agent_server, fs, focus_handle, window, cx);
|
|
||||||
Picker::list(delegate, window, cx)
|
Picker::list(delegate, window, cx)
|
||||||
.show_scrollbar(true)
|
.show_scrollbar(true)
|
||||||
.width(rems(20.))
|
.width(rems(20.))
|
||||||
@@ -46,23 +35,17 @@ enum AcpModelPickerEntry {
|
|||||||
|
|
||||||
pub struct AcpModelPickerDelegate {
|
pub struct AcpModelPickerDelegate {
|
||||||
selector: Rc<dyn AgentModelSelector>,
|
selector: Rc<dyn AgentModelSelector>,
|
||||||
agent_server: Rc<dyn AgentServer>,
|
|
||||||
fs: Arc<dyn Fs>,
|
|
||||||
filtered_entries: Vec<AcpModelPickerEntry>,
|
filtered_entries: Vec<AcpModelPickerEntry>,
|
||||||
models: Option<AgentModelList>,
|
models: Option<AgentModelList>,
|
||||||
selected_index: usize,
|
selected_index: usize,
|
||||||
selected_description: Option<(usize, SharedString, bool)>,
|
selected_description: Option<(usize, SharedString)>,
|
||||||
selected_model: Option<AgentModelInfo>,
|
selected_model: Option<AgentModelInfo>,
|
||||||
_refresh_models_task: Task<()>,
|
_refresh_models_task: Task<()>,
|
||||||
focus_handle: FocusHandle,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AcpModelPickerDelegate {
|
impl AcpModelPickerDelegate {
|
||||||
fn new(
|
fn new(
|
||||||
selector: Rc<dyn AgentModelSelector>,
|
selector: Rc<dyn AgentModelSelector>,
|
||||||
agent_server: Rc<dyn AgentServer>,
|
|
||||||
fs: Arc<dyn Fs>,
|
|
||||||
focus_handle: FocusHandle,
|
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<AcpModelSelector>,
|
cx: &mut Context<AcpModelSelector>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@@ -103,15 +86,12 @@ impl AcpModelPickerDelegate {
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
selector,
|
selector,
|
||||||
agent_server,
|
|
||||||
fs,
|
|
||||||
filtered_entries: Vec::new(),
|
filtered_entries: Vec::new(),
|
||||||
models: None,
|
models: None,
|
||||||
selected_model: None,
|
selected_model: None,
|
||||||
selected_index: 0,
|
selected_index: 0,
|
||||||
selected_description: None,
|
selected_description: None,
|
||||||
_refresh_models_task: refresh_models_task,
|
_refresh_models_task: refresh_models_task,
|
||||||
focus_handle,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,21 +181,6 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
|||||||
if let Some(AcpModelPickerEntry::Model(model_info)) =
|
if let Some(AcpModelPickerEntry::Model(model_info)) =
|
||||||
self.filtered_entries.get(self.selected_index)
|
self.filtered_entries.get(self.selected_index)
|
||||||
{
|
{
|
||||||
if window.modifiers().secondary() {
|
|
||||||
let default_model = self.agent_server.default_model(cx);
|
|
||||||
let is_default = default_model.as_ref() == Some(&model_info.id);
|
|
||||||
|
|
||||||
self.agent_server.set_default_model(
|
|
||||||
if is_default {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(model_info.id.clone())
|
|
||||||
},
|
|
||||||
self.fs.clone(),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.selector
|
self.selector
|
||||||
.select_model(model_info.id.clone(), cx)
|
.select_model(model_info.id.clone(), cx)
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
@@ -260,8 +225,6 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
|||||||
),
|
),
|
||||||
AcpModelPickerEntry::Model(model_info) => {
|
AcpModelPickerEntry::Model(model_info) => {
|
||||||
let is_selected = Some(model_info) == self.selected_model.as_ref();
|
let is_selected = Some(model_info) == self.selected_model.as_ref();
|
||||||
let default_model = self.agent_server.default_model(cx);
|
|
||||||
let is_default = default_model.as_ref() == Some(&model_info.id);
|
|
||||||
|
|
||||||
let model_icon_color = if is_selected {
|
let model_icon_color = if is_selected {
|
||||||
Color::Accent
|
Color::Accent
|
||||||
@@ -276,8 +239,8 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
|||||||
this
|
this
|
||||||
.on_hover(cx.listener(move |menu, hovered, _, cx| {
|
.on_hover(cx.listener(move |menu, hovered, _, cx| {
|
||||||
if *hovered {
|
if *hovered {
|
||||||
menu.delegate.selected_description = Some((ix, description.clone(), is_default));
|
menu.delegate.selected_description = Some((ix, description.clone()));
|
||||||
} else if matches!(menu.delegate.selected_description, Some((id, _, _)) if id == ix) {
|
} else if matches!(menu.delegate.selected_description, Some((id, _)) if id == ix) {
|
||||||
menu.delegate.selected_description = None;
|
menu.delegate.selected_description = None;
|
||||||
}
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -320,57 +283,14 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
|||||||
_window: &mut Window,
|
_window: &mut Window,
|
||||||
_cx: &mut Context<Picker<Self>>,
|
_cx: &mut Context<Picker<Self>>,
|
||||||
) -> Option<ui::DocumentationAside> {
|
) -> Option<ui::DocumentationAside> {
|
||||||
self.selected_description
|
self.selected_description.as_ref().map(|(_, description)| {
|
||||||
.as_ref()
|
let description = description.clone();
|
||||||
.map(|(_, description, is_default)| {
|
DocumentationAside::new(
|
||||||
let description = description.clone();
|
DocumentationSide::Left,
|
||||||
let is_default = *is_default;
|
DocumentationEdge::Top,
|
||||||
|
Rc::new(move |_| Label::new(description.clone()).into_any_element()),
|
||||||
DocumentationAside::new(
|
)
|
||||||
DocumentationSide::Left,
|
})
|
||||||
DocumentationEdge::Top,
|
|
||||||
Rc::new(move |_| {
|
|
||||||
v_flex()
|
|
||||||
.gap_1()
|
|
||||||
.child(Label::new(description.clone()))
|
|
||||||
.child(HoldForDefault::new(is_default))
|
|
||||||
.into_any_element()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_footer(
|
|
||||||
&self,
|
|
||||||
_window: &mut Window,
|
|
||||||
cx: &mut Context<Picker<Self>>,
|
|
||||||
) -> Option<AnyElement> {
|
|
||||||
let focus_handle = self.focus_handle.clone();
|
|
||||||
|
|
||||||
if !self.selector.should_render_footer() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(
|
|
||||||
h_flex()
|
|
||||||
.w_full()
|
|
||||||
.p_1p5()
|
|
||||||
.border_t_1()
|
|
||||||
.border_color(cx.theme().colors().border_variant)
|
|
||||||
.child(
|
|
||||||
Button::new("configure", "Configure")
|
|
||||||
.full_width()
|
|
||||||
.style(ButtonStyle::Outlined)
|
|
||||||
.key_binding(
|
|
||||||
KeyBinding::for_action_in(&OpenSettings, &focus_handle, cx)
|
|
||||||
.map(|kb| kb.size(rems_from_px(12.))),
|
|
||||||
)
|
|
||||||
.on_click(|_, window, cx| {
|
|
||||||
window.dispatch_action(OpenSettings.boxed_clone(), cx);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.into_any(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use acp_thread::{AgentModelInfo, AgentModelSelector};
|
use acp_thread::{AgentModelInfo, AgentModelSelector};
|
||||||
use agent_servers::AgentServer;
|
|
||||||
use fs::Fs;
|
|
||||||
use gpui::{Entity, FocusHandle};
|
use gpui::{Entity, FocusHandle};
|
||||||
use picker::popover_menu::PickerPopoverMenu;
|
use picker::popover_menu::PickerPopoverMenu;
|
||||||
use ui::{
|
use ui::{
|
||||||
@@ -23,25 +20,13 @@ pub struct AcpModelSelectorPopover {
|
|||||||
impl AcpModelSelectorPopover {
|
impl AcpModelSelectorPopover {
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
selector: Rc<dyn AgentModelSelector>,
|
selector: Rc<dyn AgentModelSelector>,
|
||||||
agent_server: Rc<dyn AgentServer>,
|
|
||||||
fs: Arc<dyn Fs>,
|
|
||||||
menu_handle: PopoverMenuHandle<AcpModelSelector>,
|
menu_handle: PopoverMenuHandle<AcpModelSelector>,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let focus_handle_clone = focus_handle.clone();
|
|
||||||
Self {
|
Self {
|
||||||
selector: cx.new(move |cx| {
|
selector: cx.new(move |cx| acp_model_selector(selector, window, cx)),
|
||||||
acp_model_selector(
|
|
||||||
selector,
|
|
||||||
agent_server,
|
|
||||||
fs,
|
|
||||||
focus_handle_clone.clone(),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
menu_handle,
|
menu_handle,
|
||||||
focus_handle,
|
focus_handle,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -297,7 +297,6 @@ pub struct AcpThreadView {
|
|||||||
_cancel_task: Option<Task<()>>,
|
_cancel_task: Option<Task<()>>,
|
||||||
_subscriptions: [Subscription; 5],
|
_subscriptions: [Subscription; 5],
|
||||||
show_codex_windows_warning: bool,
|
show_codex_windows_warning: bool,
|
||||||
in_flight_prompt: Option<Vec<acp::ContentBlock>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ThreadState {
|
enum ThreadState {
|
||||||
@@ -438,7 +437,6 @@ impl AcpThreadView {
|
|||||||
new_server_version_available: None,
|
new_server_version_available: None,
|
||||||
resume_thread_metadata: resume_thread,
|
resume_thread_metadata: resume_thread,
|
||||||
show_codex_windows_warning,
|
show_codex_windows_warning,
|
||||||
in_flight_prompt: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -593,13 +591,9 @@ impl AcpThreadView {
|
|||||||
.connection()
|
.connection()
|
||||||
.model_selector(thread.read(cx).session_id())
|
.model_selector(thread.read(cx).session_id())
|
||||||
.map(|selector| {
|
.map(|selector| {
|
||||||
let agent_server = this.agent.clone();
|
|
||||||
let fs = this.project.read(cx).fs().clone();
|
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
AcpModelSelectorPopover::new(
|
AcpModelSelectorPopover::new(
|
||||||
selector,
|
selector,
|
||||||
agent_server,
|
|
||||||
fs,
|
|
||||||
PopoverMenuHandle::default(),
|
PopoverMenuHandle::default(),
|
||||||
this.focus_handle(cx),
|
this.focus_handle(cx),
|
||||||
window,
|
window,
|
||||||
@@ -653,6 +647,7 @@ impl AcpThreadView {
|
|||||||
mode_selector,
|
mode_selector,
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
};
|
};
|
||||||
|
this.message_editor.focus_handle(cx).focus(window);
|
||||||
|
|
||||||
this.profile_selector = this.as_native_thread(cx).map(|thread| {
|
this.profile_selector = this.as_native_thread(cx).map(|thread| {
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
@@ -1156,7 +1151,6 @@ impl AcpThreadView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
this.in_flight_prompt = Some(contents.clone());
|
|
||||||
this.set_editor_is_expanded(false, cx);
|
this.set_editor_is_expanded(false, cx);
|
||||||
this.scroll_to_bottom(cx);
|
this.scroll_to_bottom(cx);
|
||||||
this.message_editor.update(cx, |message_editor, cx| {
|
this.message_editor.update(cx, |message_editor, cx| {
|
||||||
@@ -1184,12 +1178,7 @@ impl AcpThreadView {
|
|||||||
})?;
|
})?;
|
||||||
let res = send.await;
|
let res = send.await;
|
||||||
let turn_time_ms = turn_start_time.elapsed().as_millis();
|
let turn_time_ms = turn_start_time.elapsed().as_millis();
|
||||||
let status = if res.is_ok() {
|
let status = if res.is_ok() { "success" } else { "failure" };
|
||||||
this.update(cx, |this, _| this.in_flight_prompt.take()).ok();
|
|
||||||
"success"
|
|
||||||
} else {
|
|
||||||
"failure"
|
|
||||||
};
|
|
||||||
telemetry::event!(
|
telemetry::event!(
|
||||||
"Agent Turn Completed",
|
"Agent Turn Completed",
|
||||||
agent = agent_telemetry_id,
|
agent = agent_telemetry_id,
|
||||||
@@ -1272,28 +1261,6 @@ impl AcpThreadView {
|
|||||||
};
|
};
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
// Check if there are any edits from prompts before the one being regenerated.
|
|
||||||
//
|
|
||||||
// If there are, we keep/accept them since we're not regenerating the prompt that created them.
|
|
||||||
//
|
|
||||||
// If editing the prompt that generated the edits, they are auto-rejected
|
|
||||||
// through the `rewind` function in the `acp_thread`.
|
|
||||||
let has_earlier_edits = thread.read_with(cx, |thread, _| {
|
|
||||||
thread
|
|
||||||
.entries()
|
|
||||||
.iter()
|
|
||||||
.take(entry_ix)
|
|
||||||
.any(|entry| entry.diffs().next().is_some())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if has_earlier_edits {
|
|
||||||
thread.update(cx, |thread, cx| {
|
|
||||||
thread.action_log().update(cx, |action_log, cx| {
|
|
||||||
action_log.keep_all_edits(None, cx);
|
|
||||||
});
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
thread
|
thread
|
||||||
.update(cx, |thread, cx| thread.rewind(user_message_id, cx))?
|
.update(cx, |thread, cx| thread.rewind(user_message_id, cx))?
|
||||||
.await?;
|
.await?;
|
||||||
@@ -4796,7 +4763,7 @@ impl AcpThreadView {
|
|||||||
buffer.update(cx, |buffer, cx| {
|
buffer.update(cx, |buffer, cx| {
|
||||||
buffer.set_text(markdown, cx);
|
buffer.set_text(markdown, cx);
|
||||||
buffer.set_language(Some(markdown_language), cx);
|
buffer.set_language(Some(markdown_language), cx);
|
||||||
buffer.set_capability(language::Capability::ReadWrite, cx);
|
buffer.set_capability(language::Capability::ReadOnly, cx);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
workspace.update_in(cx, |workspace, window, cx| {
|
workspace.update_in(cx, |workspace, window, cx| {
|
||||||
@@ -5053,12 +5020,15 @@ impl AcpThreadView {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
let mut container = h_flex()
|
let mut container = h_flex()
|
||||||
|
.id("thread-controls-container")
|
||||||
|
.group("thread-controls-container")
|
||||||
.w_full()
|
.w_full()
|
||||||
.py_2()
|
.py_2()
|
||||||
.px_5()
|
.px_5()
|
||||||
.gap_px()
|
.gap_px()
|
||||||
.opacity(0.6)
|
.opacity(0.6)
|
||||||
.hover(|s| s.opacity(1.))
|
.hover(|style| style.opacity(1.))
|
||||||
|
.flex_wrap()
|
||||||
.justify_end();
|
.justify_end();
|
||||||
|
|
||||||
if AgentSettings::get_global(cx).enable_feedback
|
if AgentSettings::get_global(cx).enable_feedback
|
||||||
@@ -5068,13 +5038,23 @@ impl AcpThreadView {
|
|||||||
{
|
{
|
||||||
let feedback = self.thread_feedback.feedback;
|
let feedback = self.thread_feedback.feedback;
|
||||||
|
|
||||||
let tooltip_meta = || {
|
|
||||||
SharedString::new(
|
|
||||||
"Rating the thread sends all of your current conversation to the Zed team.",
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
container = container
|
container = container
|
||||||
|
.child(
|
||||||
|
div().visible_on_hover("thread-controls-container").child(
|
||||||
|
Label::new(match feedback {
|
||||||
|
Some(ThreadFeedback::Positive) => "Thanks for your feedback!",
|
||||||
|
Some(ThreadFeedback::Negative) => {
|
||||||
|
"We appreciate your feedback and will use it to improve."
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
"Rating the thread sends all of your current conversation to the Zed team."
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.color(Color::Muted)
|
||||||
|
.size(LabelSize::XSmall)
|
||||||
|
.truncate(),
|
||||||
|
),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
|
IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
|
||||||
.shape(ui::IconButtonShape::Square)
|
.shape(ui::IconButtonShape::Square)
|
||||||
@@ -5083,12 +5063,7 @@ impl AcpThreadView {
|
|||||||
Some(ThreadFeedback::Positive) => Color::Accent,
|
Some(ThreadFeedback::Positive) => Color::Accent,
|
||||||
_ => Color::Ignored,
|
_ => Color::Ignored,
|
||||||
})
|
})
|
||||||
.tooltip(move |window, cx| match feedback {
|
.tooltip(Tooltip::text("Helpful Response"))
|
||||||
Some(ThreadFeedback::Positive) => {
|
|
||||||
Tooltip::text("Thanks for your feedback!")(window, cx)
|
|
||||||
}
|
|
||||||
_ => Tooltip::with_meta("Helpful Response", None, tooltip_meta(), cx),
|
|
||||||
})
|
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
this.handle_feedback_click(ThreadFeedback::Positive, window, cx);
|
this.handle_feedback_click(ThreadFeedback::Positive, window, cx);
|
||||||
})),
|
})),
|
||||||
@@ -5101,16 +5076,7 @@ impl AcpThreadView {
|
|||||||
Some(ThreadFeedback::Negative) => Color::Accent,
|
Some(ThreadFeedback::Negative) => Color::Accent,
|
||||||
_ => Color::Ignored,
|
_ => Color::Ignored,
|
||||||
})
|
})
|
||||||
.tooltip(move |window, cx| match feedback {
|
.tooltip(Tooltip::text("Not Helpful"))
|
||||||
Some(ThreadFeedback::Negative) => {
|
|
||||||
Tooltip::text(
|
|
||||||
"We appreciate your feedback and will use it to improve in the future.",
|
|
||||||
)(window, cx)
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
Tooltip::with_meta("Not Helpful Response", None, tooltip_meta(), cx)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
this.handle_feedback_click(ThreadFeedback::Negative, window, cx);
|
this.handle_feedback_click(ThreadFeedback::Negative, window, cx);
|
||||||
})),
|
})),
|
||||||
@@ -5723,11 +5689,6 @@ impl AcpThreadView {
|
|||||||
provider_id: None,
|
provider_id: None,
|
||||||
};
|
};
|
||||||
this.clear_thread_error(cx);
|
this.clear_thread_error(cx);
|
||||||
if let Some(message) = this.in_flight_prompt.take() {
|
|
||||||
this.message_editor.update(cx, |editor, cx| {
|
|
||||||
editor.set_message(message, window, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let this = cx.weak_entity();
|
let this = cx.weak_entity();
|
||||||
window.defer(cx, |window, cx| {
|
window.defer(cx, |window, cx| {
|
||||||
Self::handle_auth_required(this, err, agent, connection, window, cx);
|
Self::handle_auth_required(this, err, agent, connection, window, cx);
|
||||||
@@ -6105,9 +6066,8 @@ pub(crate) mod tests {
|
|||||||
use acp_thread::StubAgentConnection;
|
use acp_thread::StubAgentConnection;
|
||||||
use agent_client_protocol::SessionId;
|
use agent_client_protocol::SessionId;
|
||||||
use assistant_text_thread::TextThreadStore;
|
use assistant_text_thread::TextThreadStore;
|
||||||
use editor::MultiBufferOffset;
|
|
||||||
use fs::FakeFs;
|
use fs::FakeFs;
|
||||||
use gpui::{EventEmitter, TestAppContext, VisualTestContext};
|
use gpui::{EventEmitter, SemanticVersion, TestAppContext, VisualTestContext};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
@@ -6624,7 +6584,7 @@ pub(crate) mod tests {
|
|||||||
let settings_store = SettingsStore::test(cx);
|
let settings_store = SettingsStore::test(cx);
|
||||||
cx.set_global(settings_store);
|
cx.set_global(settings_store);
|
||||||
theme::init(theme::LoadThemes::JustBase, cx);
|
theme::init(theme::LoadThemes::JustBase, cx);
|
||||||
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
release_channel::init(SemanticVersion::default(), cx);
|
||||||
prompt_store::init(cx)
|
prompt_store::init(cx)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -7274,7 +7234,7 @@ pub(crate) mod tests {
|
|||||||
Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx);
|
Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx);
|
||||||
|
|
||||||
editor.change_selections(Default::default(), window, cx, |selections| {
|
editor.change_selections(Default::default(), window, cx, |selections| {
|
||||||
selections.select_ranges([MultiBufferOffset(8)..MultiBufferOffset(15)]);
|
selections.select_ranges([8..15]);
|
||||||
});
|
});
|
||||||
|
|
||||||
editor
|
editor
|
||||||
@@ -7336,7 +7296,7 @@ pub(crate) mod tests {
|
|||||||
Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx);
|
Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx);
|
||||||
|
|
||||||
editor.change_selections(Default::default(), window, cx, |selections| {
|
editor.change_selections(Default::default(), window, cx, |selections| {
|
||||||
selections.select_ranges([MultiBufferOffset(8)..MultiBufferOffset(15)]);
|
selections.select_ranges([8..15]);
|
||||||
});
|
});
|
||||||
|
|
||||||
editor
|
editor
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use client::zed_urls;
|
|||||||
use cloud_llm_client::{Plan, PlanV1, PlanV2};
|
use cloud_llm_client::{Plan, PlanV1, PlanV2};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use context_server::ContextServerId;
|
use context_server::ContextServerId;
|
||||||
use editor::{Editor, MultiBufferOffset, SelectionEffects, scroll::Autoscroll};
|
use editor::{Editor, SelectionEffects, scroll::Autoscroll};
|
||||||
use extension::ExtensionManifest;
|
use extension::ExtensionManifest;
|
||||||
use extension_host::ExtensionStore;
|
use extension_host::ExtensionStore;
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
@@ -1343,12 +1343,11 @@ async fn open_new_agent_servers_entry_in_settings_editor(
|
|||||||
.custom
|
.custom
|
||||||
.insert(
|
.insert(
|
||||||
server_name,
|
server_name,
|
||||||
settings::CustomAgentServerSettings::Custom {
|
settings::CustomAgentServerSettings {
|
||||||
path: "path_to_executable".into(),
|
path: "path_to_executable".into(),
|
||||||
args: vec![],
|
args: vec![],
|
||||||
env: Some(HashMap::default()),
|
env: Some(HashMap::default()),
|
||||||
default_mode: None,
|
default_mode: None,
|
||||||
default_model: None,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1363,15 +1362,7 @@ async fn open_new_agent_servers_entry_in_settings_editor(
|
|||||||
.map(|(range, _)| range.clone())
|
.map(|(range, _)| range.clone())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
item.edit(
|
item.edit(edits, cx);
|
||||||
edits.into_iter().map(|(range, s)| {
|
|
||||||
(
|
|
||||||
MultiBufferOffset(range.start)..MultiBufferOffset(range.end),
|
|
||||||
s,
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
if let Some((unique_server_name, buffer)) =
|
if let Some((unique_server_name, buffer)) =
|
||||||
unique_server_name.zip(item.buffer().read(cx).as_singleton())
|
unique_server_name.zip(item.buffer().read(cx).as_singleton())
|
||||||
{
|
{
|
||||||
@@ -1384,9 +1375,7 @@ async fn open_new_agent_servers_entry_in_settings_editor(
|
|||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|selections| {
|
|selections| {
|
||||||
selections.select_ranges(vec![
|
selections.select_ranges(vec![range]);
|
||||||
MultiBufferOffset(range.start)..MultiBufferOffset(range.end),
|
|
||||||
]);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -253,7 +253,6 @@ impl ManageProfilesModal {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
false, // Do not use popover styles for the model picker
|
false, // Do not use popover styles for the model picker
|
||||||
self.focus_handle.clone(),
|
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ use editor::{
|
|||||||
scroll::Autoscroll,
|
scroll::Autoscroll,
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, AnyElement, App, AppContext, Empty, Entity, EventEmitter, FocusHandle, Focusable,
|
Action, AnyElement, AnyView, App, AppContext, Empty, Entity, EventEmitter, FocusHandle,
|
||||||
Global, SharedString, Subscription, Task, WeakEntity, Window, prelude::*,
|
Focusable, Global, SharedString, Subscription, Task, WeakEntity, Window, prelude::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
use language::{Buffer, Capability, DiskState, OffsetRangeExt, Point};
|
use language::{Buffer, Capability, DiskState, OffsetRangeExt, Point};
|
||||||
@@ -580,11 +580,11 @@ impl Item for AgentDiffPane {
|
|||||||
type_id: TypeId,
|
type_id: TypeId,
|
||||||
self_handle: &'a Entity<Self>,
|
self_handle: &'a Entity<Self>,
|
||||||
_: &'a App,
|
_: &'a App,
|
||||||
) -> Option<gpui::AnyEntity> {
|
) -> Option<AnyView> {
|
||||||
if type_id == TypeId::of::<Self>() {
|
if type_id == TypeId::of::<Self>() {
|
||||||
Some(self_handle.clone().into())
|
Some(self_handle.to_any())
|
||||||
} else if type_id == TypeId::of::<Editor>() {
|
} else if type_id == TypeId::of::<Editor>() {
|
||||||
Some(self.editor.clone().into())
|
Some(self.editor.to_any())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ impl AgentModelSelector {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let focus_handle_clone = focus_handle.clone();
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
selector: cx.new(move |cx| {
|
selector: cx.new(move |cx| {
|
||||||
let fs = fs.clone();
|
let fs = fs.clone();
|
||||||
@@ -50,7 +48,6 @@ impl AgentModelSelector {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
true, // Use popover styles for picker
|
true, // Use popover styles for picker
|
||||||
focus_handle_clone,
|
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ use agent::{ContextServerRegistry, DbThreadMetadata, HistoryEntry, HistoryStore}
|
|||||||
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
||||||
use project::{
|
use project::{
|
||||||
ExternalAgentServerName,
|
ExternalAgentServerName,
|
||||||
agent_server_store::{CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME},
|
agent_server_store::{
|
||||||
|
AgentServerCommand, AllAgentServersSettings, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{
|
use settings::{
|
||||||
@@ -17,8 +19,6 @@ use settings::{
|
|||||||
|
|
||||||
use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
|
use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
|
||||||
|
|
||||||
use crate::ManageProfiles;
|
|
||||||
use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, Follow, InlineAssistant,
|
AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, Follow, InlineAssistant,
|
||||||
NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory,
|
NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory,
|
||||||
@@ -34,7 +34,14 @@ use crate::{
|
|||||||
ExpandMessageEditor,
|
ExpandMessageEditor,
|
||||||
acp::{AcpThreadHistory, ThreadHistoryEvent},
|
acp::{AcpThreadHistory, ThreadHistoryEvent},
|
||||||
};
|
};
|
||||||
use crate::{ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary};
|
use crate::{
|
||||||
|
ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary, placeholder_command,
|
||||||
|
};
|
||||||
|
use crate::{ManageProfiles, context_store::ContextStore};
|
||||||
|
use crate::{
|
||||||
|
inline_assistant::ContextProviders,
|
||||||
|
ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal},
|
||||||
|
};
|
||||||
use agent_settings::AgentSettings;
|
use agent_settings::AgentSettings;
|
||||||
use ai_onboarding::AgentPanelOnboarding;
|
use ai_onboarding::AgentPanelOnboarding;
|
||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
@@ -57,7 +64,7 @@ use project::{Project, ProjectPath, Worktree};
|
|||||||
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
|
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
|
||||||
use rules_library::{RulesLibrary, open_rules_library};
|
use rules_library::{RulesLibrary, open_rules_library};
|
||||||
use search::{BufferSearchBar, buffer_search};
|
use search::{BufferSearchBar, buffer_search};
|
||||||
use settings::{Settings, update_settings_file};
|
use settings::{Settings, SettingsStore, update_settings_file};
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
use ui::utils::WithRemSize;
|
use ui::utils::WithRemSize;
|
||||||
use ui::{
|
use ui::{
|
||||||
@@ -244,6 +251,7 @@ pub enum AgentType {
|
|||||||
Codex,
|
Codex,
|
||||||
Custom {
|
Custom {
|
||||||
name: SharedString,
|
name: SharedString,
|
||||||
|
command: AgentServerCommand,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,7 +283,7 @@ impl From<ExternalAgent> for AgentType {
|
|||||||
ExternalAgent::Gemini => Self::Gemini,
|
ExternalAgent::Gemini => Self::Gemini,
|
||||||
ExternalAgent::ClaudeCode => Self::ClaudeCode,
|
ExternalAgent::ClaudeCode => Self::ClaudeCode,
|
||||||
ExternalAgent::Codex => Self::Codex,
|
ExternalAgent::Codex => Self::Codex,
|
||||||
ExternalAgent::Custom { name } => Self::Custom { name },
|
ExternalAgent::Custom { name, command } => Self::Custom { name, command },
|
||||||
ExternalAgent::NativeAgent => Self::NativeAgent,
|
ExternalAgent::NativeAgent => Self::NativeAgent,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -431,6 +439,7 @@ pub struct AgentPanel {
|
|||||||
text_thread_store: Entity<assistant_text_thread::TextThreadStore>,
|
text_thread_store: Entity<assistant_text_thread::TextThreadStore>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
context_server_registry: Entity<ContextServerRegistry>,
|
context_server_registry: Entity<ContextServerRegistry>,
|
||||||
|
inline_assist_context_store: Entity<ContextStore>,
|
||||||
configuration: Option<Entity<AgentConfiguration>>,
|
configuration: Option<Entity<AgentConfiguration>>,
|
||||||
configuration_subscription: Option<Subscription>,
|
configuration_subscription: Option<Subscription>,
|
||||||
active_view: ActiveView,
|
active_view: ActiveView,
|
||||||
@@ -449,6 +458,10 @@ pub struct AgentPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AgentPanel {
|
impl AgentPanel {
|
||||||
|
pub(crate) fn workspace(&self) -> WeakEntity<Workspace> {
|
||||||
|
self.workspace.clone()
|
||||||
|
}
|
||||||
|
|
||||||
fn serialize(&mut self, cx: &mut Context<Self>) {
|
fn serialize(&mut self, cx: &mut Context<Self>) {
|
||||||
let width = self.width;
|
let width = self.width;
|
||||||
let selected_agent = self.selected_agent.clone();
|
let selected_agent = self.selected_agent.clone();
|
||||||
@@ -542,6 +555,7 @@ impl AgentPanel {
|
|||||||
let client = workspace.client().clone();
|
let client = workspace.client().clone();
|
||||||
let workspace = workspace.weak_handle();
|
let workspace = workspace.weak_handle();
|
||||||
|
|
||||||
|
let inline_assist_context_store = cx.new(|_cx| ContextStore::new(project.downgrade()));
|
||||||
let context_server_registry =
|
let context_server_registry =
|
||||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||||
|
|
||||||
@@ -678,6 +692,7 @@ impl AgentPanel {
|
|||||||
configuration: None,
|
configuration: None,
|
||||||
configuration_subscription: None,
|
configuration_subscription: None,
|
||||||
context_server_registry,
|
context_server_registry,
|
||||||
|
inline_assist_context_store,
|
||||||
previous_view: None,
|
previous_view: None,
|
||||||
new_thread_menu_handle: PopoverMenuHandle::default(),
|
new_thread_menu_handle: PopoverMenuHandle::default(),
|
||||||
agent_panel_menu_handle: PopoverMenuHandle::default(),
|
agent_panel_menu_handle: PopoverMenuHandle::default(),
|
||||||
@@ -718,6 +733,10 @@ impl AgentPanel {
|
|||||||
&self.prompt_store
|
&self.prompt_store
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn inline_assist_context_store(&self) -> &Entity<ContextStore> {
|
||||||
|
&self.inline_assist_context_store
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn thread_store(&self) -> &Entity<HistoryStore> {
|
pub(crate) fn thread_store(&self) -> &Entity<HistoryStore> {
|
||||||
&self.history_store
|
&self.history_store
|
||||||
}
|
}
|
||||||
@@ -816,7 +835,6 @@ impl AgentPanel {
|
|||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
),
|
),
|
||||||
true,
|
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
@@ -912,12 +930,7 @@ impl AgentPanel {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
this.set_active_view(
|
this.set_active_view(ActiveView::ExternalAgentThread { thread_view }, window, cx);
|
||||||
ActiveView::ExternalAgentThread { thread_view },
|
|
||||||
!loading,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
@@ -959,10 +972,10 @@ impl AgentPanel {
|
|||||||
fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
if matches!(self.active_view, ActiveView::History) {
|
if matches!(self.active_view, ActiveView::History) {
|
||||||
if let Some(previous_view) = self.previous_view.take() {
|
if let Some(previous_view) = self.previous_view.take() {
|
||||||
self.set_active_view(previous_view, true, window, cx);
|
self.set_active_view(previous_view, window, cx);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.set_active_view(ActiveView::History, true, window, cx);
|
self.set_active_view(ActiveView::History, window, cx);
|
||||||
}
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
@@ -1018,7 +1031,6 @@ impl AgentPanel {
|
|||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
),
|
),
|
||||||
true,
|
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
@@ -1164,7 +1176,7 @@ impl AgentPanel {
|
|||||||
let context_server_store = self.project.read(cx).context_server_store();
|
let context_server_store = self.project.read(cx).context_server_store();
|
||||||
let fs = self.fs.clone();
|
let fs = self.fs.clone();
|
||||||
|
|
||||||
self.set_active_view(ActiveView::Configuration, true, window, cx);
|
self.set_active_view(ActiveView::Configuration, window, cx);
|
||||||
self.configuration = Some(cx.new(|cx| {
|
self.configuration = Some(cx.new(|cx| {
|
||||||
AgentConfiguration::new(
|
AgentConfiguration::new(
|
||||||
fs,
|
fs,
|
||||||
@@ -1281,7 +1293,6 @@ impl AgentPanel {
|
|||||||
fn set_active_view(
|
fn set_active_view(
|
||||||
&mut self,
|
&mut self,
|
||||||
new_view: ActiveView,
|
new_view: ActiveView,
|
||||||
focus: bool,
|
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
@@ -1320,9 +1331,7 @@ impl AgentPanel {
|
|||||||
self.active_view = new_view;
|
self.active_view = new_view;
|
||||||
}
|
}
|
||||||
|
|
||||||
if focus {
|
self.focus_handle(cx).focus(window);
|
||||||
self.focus_handle(cx).focus(window);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn populate_recently_opened_menu_section(
|
fn populate_recently_opened_menu_section(
|
||||||
@@ -1457,8 +1466,8 @@ impl AgentPanel {
|
|||||||
self.serialize(cx);
|
self.serialize(cx);
|
||||||
self.external_thread(Some(crate::ExternalAgent::Codex), None, None, window, cx)
|
self.external_thread(Some(crate::ExternalAgent::Codex), None, None, window, cx)
|
||||||
}
|
}
|
||||||
AgentType::Custom { name } => self.external_thread(
|
AgentType::Custom { name, command } => self.external_thread(
|
||||||
Some(crate::ExternalAgent::Custom { name }),
|
Some(crate::ExternalAgent::Custom { name, command }),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
window,
|
window,
|
||||||
@@ -2083,11 +2092,22 @@ impl AgentPanel {
|
|||||||
.cloned()
|
.cloned()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let custom_settings = cx
|
||||||
|
.global::<SettingsStore>()
|
||||||
|
.get::<AllAgentServersSettings>(None)
|
||||||
|
.custom
|
||||||
|
.clone();
|
||||||
|
|
||||||
for agent_name in agent_names {
|
for agent_name in agent_names {
|
||||||
let icon_path = agent_server_store.agent_icon(&agent_name);
|
let icon_path = agent_server_store.agent_icon(&agent_name);
|
||||||
|
|
||||||
let mut entry = ContextMenuEntry::new(agent_name.clone());
|
let mut entry = ContextMenuEntry::new(agent_name.clone());
|
||||||
|
|
||||||
|
let command = custom_settings
|
||||||
|
.get(&agent_name.0)
|
||||||
|
.map(|settings| settings.command.clone())
|
||||||
|
.unwrap_or(placeholder_command());
|
||||||
|
|
||||||
if let Some(icon_path) = icon_path {
|
if let Some(icon_path) = icon_path {
|
||||||
entry = entry.custom_icon_svg(icon_path);
|
entry = entry.custom_icon_svg(icon_path);
|
||||||
} else {
|
} else {
|
||||||
@@ -2097,6 +2117,7 @@ impl AgentPanel {
|
|||||||
.when(
|
.when(
|
||||||
is_agent_selected(AgentType::Custom {
|
is_agent_selected(AgentType::Custom {
|
||||||
name: agent_name.0.clone(),
|
name: agent_name.0.clone(),
|
||||||
|
command: command.clone(),
|
||||||
}),
|
}),
|
||||||
|this| {
|
|this| {
|
||||||
this.action(Box::new(NewExternalAgentThread { agent: None }))
|
this.action(Box::new(NewExternalAgentThread { agent: None }))
|
||||||
@@ -2107,6 +2128,7 @@ impl AgentPanel {
|
|||||||
.handler({
|
.handler({
|
||||||
let workspace = workspace.clone();
|
let workspace = workspace.clone();
|
||||||
let agent_name = agent_name.clone();
|
let agent_name = agent_name.clone();
|
||||||
|
let custom_settings = custom_settings.clone();
|
||||||
move |window, cx| {
|
move |window, cx| {
|
||||||
if let Some(workspace) = workspace.upgrade() {
|
if let Some(workspace) = workspace.upgrade() {
|
||||||
workspace.update(cx, |workspace, cx| {
|
workspace.update(cx, |workspace, cx| {
|
||||||
@@ -2119,6 +2141,17 @@ impl AgentPanel {
|
|||||||
name: agent_name
|
name: agent_name
|
||||||
.clone()
|
.clone()
|
||||||
.into(),
|
.into(),
|
||||||
|
command: custom_settings
|
||||||
|
.get(&agent_name.0)
|
||||||
|
.map(|settings| {
|
||||||
|
settings
|
||||||
|
.command
|
||||||
|
.clone()
|
||||||
|
})
|
||||||
|
.unwrap_or(
|
||||||
|
placeholder_command(
|
||||||
|
),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
@@ -2667,20 +2700,19 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
|
|||||||
cx: &mut Context<RulesLibrary>,
|
cx: &mut Context<RulesLibrary>,
|
||||||
) {
|
) {
|
||||||
InlineAssistant::update_global(cx, |assistant, cx| {
|
InlineAssistant::update_global(cx, |assistant, cx| {
|
||||||
let Some(workspace) = self.workspace.upgrade() else {
|
let Some(project) = self
|
||||||
|
.workspace
|
||||||
|
.upgrade()
|
||||||
|
.map(|workspace| workspace.read(cx).project().downgrade())
|
||||||
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) else {
|
let context_providers = ContextProviders::empty(project, cx);
|
||||||
return;
|
|
||||||
};
|
|
||||||
let project = workspace.read(cx).project().downgrade();
|
|
||||||
assistant.assist(
|
assistant.assist(
|
||||||
prompt_editor,
|
prompt_editor,
|
||||||
self.workspace.clone(),
|
|
||||||
project,
|
|
||||||
panel.read(cx).thread_store().clone(),
|
|
||||||
None,
|
|
||||||
initial_prompt,
|
initial_prompt,
|
||||||
|
context_providers,
|
||||||
|
self.workspace.clone(),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,13 +4,16 @@ mod agent_diff;
|
|||||||
mod agent_model_selector;
|
mod agent_model_selector;
|
||||||
mod agent_panel;
|
mod agent_panel;
|
||||||
mod buffer_codegen;
|
mod buffer_codegen;
|
||||||
mod completion_provider;
|
|
||||||
mod context;
|
mod context;
|
||||||
|
mod context_picker;
|
||||||
mod context_server_configuration;
|
mod context_server_configuration;
|
||||||
|
mod context_store;
|
||||||
|
mod context_strip;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod evals;
|
||||||
mod inline_assistant;
|
mod inline_assistant;
|
||||||
mod inline_prompt_editor;
|
mod inline_prompt_editor;
|
||||||
mod language_model_selector;
|
mod language_model_selector;
|
||||||
mod mention_set;
|
|
||||||
mod profile_selector;
|
mod profile_selector;
|
||||||
mod slash_command;
|
mod slash_command;
|
||||||
mod slash_command_picker;
|
mod slash_command_picker;
|
||||||
@@ -34,9 +37,10 @@ use language::{
|
|||||||
language_settings::{AllLanguageSettings, EditPredictionProvider},
|
language_settings::{AllLanguageSettings, EditPredictionProvider},
|
||||||
};
|
};
|
||||||
use language_model::{
|
use language_model::{
|
||||||
ConfiguredModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
|
ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
|
||||||
};
|
};
|
||||||
use project::DisableAiSettings;
|
use project::DisableAiSettings;
|
||||||
|
use project::agent_server_store::AgentServerCommand;
|
||||||
use prompt_store::PromptBuilder;
|
use prompt_store::PromptBuilder;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -55,6 +59,8 @@ actions!(
|
|||||||
[
|
[
|
||||||
/// Creates a new text-based conversation thread.
|
/// Creates a new text-based conversation thread.
|
||||||
NewTextThread,
|
NewTextThread,
|
||||||
|
/// Toggles the context picker interface for adding files, symbols, or other context.
|
||||||
|
ToggleContextPicker,
|
||||||
/// Toggles the menu to create new agent threads.
|
/// Toggles the menu to create new agent threads.
|
||||||
ToggleNewThreadMenu,
|
ToggleNewThreadMenu,
|
||||||
/// Toggles the navigation menu for switching between threads and views.
|
/// Toggles the navigation menu for switching between threads and views.
|
||||||
@@ -67,6 +73,8 @@ actions!(
|
|||||||
ToggleProfileSelector,
|
ToggleProfileSelector,
|
||||||
/// Cycles through available session modes.
|
/// Cycles through available session modes.
|
||||||
CycleModeSelector,
|
CycleModeSelector,
|
||||||
|
/// Removes all added context from the current conversation.
|
||||||
|
RemoveAllContext,
|
||||||
/// Expands the message editor to full size.
|
/// Expands the message editor to full size.
|
||||||
ExpandMessageEditor,
|
ExpandMessageEditor,
|
||||||
/// Opens the conversation history view.
|
/// Opens the conversation history view.
|
||||||
@@ -89,6 +97,10 @@ actions!(
|
|||||||
FocusLeft,
|
FocusLeft,
|
||||||
/// Moves focus right in the interface.
|
/// Moves focus right in the interface.
|
||||||
FocusRight,
|
FocusRight,
|
||||||
|
/// Removes the currently focused context item.
|
||||||
|
RemoveFocusedContext,
|
||||||
|
/// Accepts the suggested context item.
|
||||||
|
AcceptSuggestedContext,
|
||||||
/// Opens the active thread as a markdown file.
|
/// Opens the active thread as a markdown file.
|
||||||
OpenActiveThreadAsMarkdown,
|
OpenActiveThreadAsMarkdown,
|
||||||
/// Opens the agent diff view to review changes.
|
/// Opens the agent diff view to review changes.
|
||||||
@@ -152,7 +164,18 @@ pub enum ExternalAgent {
|
|||||||
ClaudeCode,
|
ClaudeCode,
|
||||||
Codex,
|
Codex,
|
||||||
NativeAgent,
|
NativeAgent,
|
||||||
Custom { name: SharedString },
|
Custom {
|
||||||
|
name: SharedString,
|
||||||
|
command: AgentServerCommand,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn placeholder_command() -> AgentServerCommand {
|
||||||
|
AgentServerCommand {
|
||||||
|
path: "/placeholder".into(),
|
||||||
|
args: vec![],
|
||||||
|
env: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExternalAgent {
|
impl ExternalAgent {
|
||||||
@@ -176,7 +199,9 @@ impl ExternalAgent {
|
|||||||
Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
|
Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
|
||||||
Self::Codex => Rc::new(agent_servers::Codex),
|
Self::Codex => Rc::new(agent_servers::Codex),
|
||||||
Self::NativeAgent => Rc::new(agent::NativeAgentServer::new(fs, history)),
|
Self::NativeAgent => Rc::new(agent::NativeAgentServer::new(fs, history)),
|
||||||
Self::Custom { name } => Rc::new(agent_servers::CustomAgentServer::new(name.clone())),
|
Self::Custom { name, command: _ } => {
|
||||||
|
Rc::new(agent_servers::CustomAgentServer::new(name.clone()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,6 +236,11 @@ impl ModelUsageContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn language_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||||
|
self.configured_model(cx)
|
||||||
|
.map(|configured_model| configured_model.model)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initializes the `agent` crate.
|
/// Initializes the `agent` crate.
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use crate::{context::LoadedContext, inline_prompt_editor::CodegenStatus};
|
use crate::{
|
||||||
|
context::load_context, context_store::ContextStore, inline_prompt_editor::CodegenStatus,
|
||||||
|
};
|
||||||
use agent_settings::AgentSettings;
|
use agent_settings::AgentSettings;
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use client::telemetry::Telemetry;
|
use client::telemetry::Telemetry;
|
||||||
@@ -6,12 +8,9 @@ use cloud_llm_client::CompletionIntent;
|
|||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
|
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
|
||||||
use futures::{
|
use futures::{
|
||||||
SinkExt, Stream, StreamExt, TryStreamExt as _,
|
SinkExt, Stream, StreamExt, TryStreamExt as _, channel::mpsc, future::LocalBoxFuture, join,
|
||||||
channel::mpsc,
|
|
||||||
future::{LocalBoxFuture, Shared},
|
|
||||||
join,
|
|
||||||
};
|
};
|
||||||
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription, Task};
|
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription, Task, WeakEntity};
|
||||||
use language::{Buffer, IndentKind, Point, TransactionId, line_diff};
|
use language::{Buffer, IndentKind, Point, TransactionId, line_diff};
|
||||||
use language_model::{
|
use language_model::{
|
||||||
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||||
@@ -19,7 +18,8 @@ use language_model::{
|
|||||||
};
|
};
|
||||||
use multi_buffer::MultiBufferRow;
|
use multi_buffer::MultiBufferRow;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use prompt_store::PromptBuilder;
|
use project::Project;
|
||||||
|
use prompt_store::{PromptBuilder, PromptStore};
|
||||||
use rope::Rope;
|
use rope::Rope;
|
||||||
use smol::future::FutureExt;
|
use smol::future::FutureExt;
|
||||||
use std::{
|
use std::{
|
||||||
@@ -43,6 +43,9 @@ pub struct BufferCodegen {
|
|||||||
buffer: Entity<MultiBuffer>,
|
buffer: Entity<MultiBuffer>,
|
||||||
range: Range<Anchor>,
|
range: Range<Anchor>,
|
||||||
initial_transaction_id: Option<TransactionId>,
|
initial_transaction_id: Option<TransactionId>,
|
||||||
|
context_store: Entity<ContextStore>,
|
||||||
|
project: WeakEntity<Project>,
|
||||||
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
telemetry: Arc<Telemetry>,
|
telemetry: Arc<Telemetry>,
|
||||||
builder: Arc<PromptBuilder>,
|
builder: Arc<PromptBuilder>,
|
||||||
pub is_insertion: bool,
|
pub is_insertion: bool,
|
||||||
@@ -53,6 +56,9 @@ impl BufferCodegen {
|
|||||||
buffer: Entity<MultiBuffer>,
|
buffer: Entity<MultiBuffer>,
|
||||||
range: Range<Anchor>,
|
range: Range<Anchor>,
|
||||||
initial_transaction_id: Option<TransactionId>,
|
initial_transaction_id: Option<TransactionId>,
|
||||||
|
context_store: Entity<ContextStore>,
|
||||||
|
project: WeakEntity<Project>,
|
||||||
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
telemetry: Arc<Telemetry>,
|
telemetry: Arc<Telemetry>,
|
||||||
builder: Arc<PromptBuilder>,
|
builder: Arc<PromptBuilder>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
@@ -62,6 +68,9 @@ impl BufferCodegen {
|
|||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
range.clone(),
|
range.clone(),
|
||||||
false,
|
false,
|
||||||
|
Some(context_store.clone()),
|
||||||
|
project.clone(),
|
||||||
|
prompt_store.clone(),
|
||||||
Some(telemetry.clone()),
|
Some(telemetry.clone()),
|
||||||
builder.clone(),
|
builder.clone(),
|
||||||
cx,
|
cx,
|
||||||
@@ -76,6 +85,9 @@ impl BufferCodegen {
|
|||||||
buffer,
|
buffer,
|
||||||
range,
|
range,
|
||||||
initial_transaction_id,
|
initial_transaction_id,
|
||||||
|
context_store,
|
||||||
|
project,
|
||||||
|
prompt_store,
|
||||||
telemetry,
|
telemetry,
|
||||||
builder,
|
builder,
|
||||||
};
|
};
|
||||||
@@ -136,7 +148,6 @@ impl BufferCodegen {
|
|||||||
&mut self,
|
&mut self,
|
||||||
primary_model: Arc<dyn LanguageModel>,
|
primary_model: Arc<dyn LanguageModel>,
|
||||||
user_prompt: String,
|
user_prompt: String,
|
||||||
context_task: Shared<Task<Option<LoadedContext>>>,
|
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let alternative_models = LanguageModelRegistry::read_global(cx)
|
let alternative_models = LanguageModelRegistry::read_global(cx)
|
||||||
@@ -154,6 +165,9 @@ impl BufferCodegen {
|
|||||||
self.buffer.clone(),
|
self.buffer.clone(),
|
||||||
self.range.clone(),
|
self.range.clone(),
|
||||||
false,
|
false,
|
||||||
|
Some(self.context_store.clone()),
|
||||||
|
self.project.clone(),
|
||||||
|
self.prompt_store.clone(),
|
||||||
Some(self.telemetry.clone()),
|
Some(self.telemetry.clone()),
|
||||||
self.builder.clone(),
|
self.builder.clone(),
|
||||||
cx,
|
cx,
|
||||||
@@ -166,7 +180,7 @@ impl BufferCodegen {
|
|||||||
.zip(&self.alternatives)
|
.zip(&self.alternatives)
|
||||||
{
|
{
|
||||||
alternative.update(cx, |alternative, cx| {
|
alternative.update(cx, |alternative, cx| {
|
||||||
alternative.start(user_prompt.clone(), context_task.clone(), model.clone(), cx)
|
alternative.start(user_prompt.clone(), model.clone(), cx)
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,6 +243,9 @@ pub struct CodegenAlternative {
|
|||||||
status: CodegenStatus,
|
status: CodegenStatus,
|
||||||
generation: Task<()>,
|
generation: Task<()>,
|
||||||
diff: Diff,
|
diff: Diff,
|
||||||
|
context_store: Option<Entity<ContextStore>>,
|
||||||
|
project: WeakEntity<Project>,
|
||||||
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
telemetry: Option<Arc<Telemetry>>,
|
telemetry: Option<Arc<Telemetry>>,
|
||||||
_subscription: gpui::Subscription,
|
_subscription: gpui::Subscription,
|
||||||
builder: Arc<PromptBuilder>,
|
builder: Arc<PromptBuilder>,
|
||||||
@@ -247,6 +264,9 @@ impl CodegenAlternative {
|
|||||||
buffer: Entity<MultiBuffer>,
|
buffer: Entity<MultiBuffer>,
|
||||||
range: Range<Anchor>,
|
range: Range<Anchor>,
|
||||||
active: bool,
|
active: bool,
|
||||||
|
context_store: Option<Entity<ContextStore>>,
|
||||||
|
project: WeakEntity<Project>,
|
||||||
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
telemetry: Option<Arc<Telemetry>>,
|
telemetry: Option<Arc<Telemetry>>,
|
||||||
builder: Arc<PromptBuilder>,
|
builder: Arc<PromptBuilder>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
@@ -287,6 +307,9 @@ impl CodegenAlternative {
|
|||||||
status: CodegenStatus::Idle,
|
status: CodegenStatus::Idle,
|
||||||
generation: Task::ready(()),
|
generation: Task::ready(()),
|
||||||
diff: Diff::default(),
|
diff: Diff::default(),
|
||||||
|
context_store,
|
||||||
|
project,
|
||||||
|
prompt_store,
|
||||||
telemetry,
|
telemetry,
|
||||||
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
|
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
|
||||||
builder,
|
builder,
|
||||||
@@ -343,7 +366,6 @@ impl CodegenAlternative {
|
|||||||
pub fn start(
|
pub fn start(
|
||||||
&mut self,
|
&mut self,
|
||||||
user_prompt: String,
|
user_prompt: String,
|
||||||
context_task: Shared<Task<Option<LoadedContext>>>,
|
|
||||||
model: Arc<dyn LanguageModel>,
|
model: Arc<dyn LanguageModel>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
@@ -362,7 +384,7 @@ impl CodegenAlternative {
|
|||||||
if user_prompt.trim().to_lowercase() == "delete" {
|
if user_prompt.trim().to_lowercase() == "delete" {
|
||||||
async { Ok(LanguageModelTextStream::default()) }.boxed_local()
|
async { Ok(LanguageModelTextStream::default()) }.boxed_local()
|
||||||
} else {
|
} else {
|
||||||
let request = self.build_request(&model, user_prompt, context_task, cx)?;
|
let request = self.build_request(&model, user_prompt, cx)?;
|
||||||
cx.spawn(async move |_, cx| {
|
cx.spawn(async move |_, cx| {
|
||||||
Ok(model.stream_completion_text(request.await, cx).await?)
|
Ok(model.stream_completion_text(request.await, cx).await?)
|
||||||
})
|
})
|
||||||
@@ -376,7 +398,6 @@ impl CodegenAlternative {
|
|||||||
&self,
|
&self,
|
||||||
model: &Arc<dyn LanguageModel>,
|
model: &Arc<dyn LanguageModel>,
|
||||||
user_prompt: String,
|
user_prompt: String,
|
||||||
context_task: Shared<Task<Option<LoadedContext>>>,
|
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Result<Task<LanguageModelRequest>> {
|
) -> Result<Task<LanguageModelRequest>> {
|
||||||
let buffer = self.buffer.read(cx).snapshot(cx);
|
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||||
@@ -408,14 +429,22 @@ impl CodegenAlternative {
|
|||||||
|
|
||||||
let prompt = self
|
let prompt = self
|
||||||
.builder
|
.builder
|
||||||
.generate_inline_transformation_prompt(
|
.generate_inline_transformation_prompt(user_prompt, language_name, buffer, range)
|
||||||
user_prompt,
|
|
||||||
language_name,
|
|
||||||
buffer,
|
|
||||||
range.start.0..range.end.0,
|
|
||||||
)
|
|
||||||
.context("generating content prompt")?;
|
.context("generating content prompt")?;
|
||||||
|
|
||||||
|
let context_task = self.context_store.as_ref().and_then(|context_store| {
|
||||||
|
if let Some(project) = self.project.upgrade() {
|
||||||
|
let context = context_store
|
||||||
|
.read(cx)
|
||||||
|
.context()
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
Some(load_context(context, &project, &self.prompt_store, cx))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let temperature = AgentSettings::temperature_for_model(model, cx);
|
let temperature = AgentSettings::temperature_for_model(model, cx);
|
||||||
|
|
||||||
Ok(cx.spawn(async move |_cx| {
|
Ok(cx.spawn(async move |_cx| {
|
||||||
@@ -423,11 +452,12 @@ impl CodegenAlternative {
|
|||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: Vec::new(),
|
content: Vec::new(),
|
||||||
cache: false,
|
cache: false,
|
||||||
reasoning_details: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(context) = context_task.await {
|
if let Some(context_task) = context_task {
|
||||||
context.add_to_request_message(&mut request_message);
|
context_task
|
||||||
|
.await
|
||||||
|
.add_to_request_message(&mut request_message);
|
||||||
}
|
}
|
||||||
|
|
||||||
request_message.content.push(prompt.into());
|
request_message.content.push(prompt.into());
|
||||||
@@ -456,14 +486,6 @@ impl CodegenAlternative {
|
|||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
|
|
||||||
// Make a new snapshot and re-resolve anchor in case the document was modified.
|
|
||||||
// This can happen often if the editor loses focus and is saved + reformatted,
|
|
||||||
// as in https://github.com/zed-industries/zed/issues/39088
|
|
||||||
self.snapshot = self.buffer.read(cx).snapshot(cx);
|
|
||||||
self.range = self.snapshot.anchor_after(self.range.start)
|
|
||||||
..self.snapshot.anchor_after(self.range.end);
|
|
||||||
|
|
||||||
let snapshot = self.snapshot.clone();
|
let snapshot = self.snapshot.clone();
|
||||||
let selected_text = snapshot
|
let selected_text = snapshot
|
||||||
.text_for_range(self.range.start..self.range.end)
|
.text_for_range(self.range.start..self.range.end)
|
||||||
@@ -1053,6 +1075,7 @@ impl Diff {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use fs::FakeFs;
|
||||||
use futures::{
|
use futures::{
|
||||||
Stream,
|
Stream,
|
||||||
stream::{self},
|
stream::{self},
|
||||||
@@ -1084,12 +1107,17 @@ mod tests {
|
|||||||
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
|
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
|
||||||
});
|
});
|
||||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
let project = Project::test(fs, vec![], cx).await;
|
||||||
let codegen = cx.new(|cx| {
|
let codegen = cx.new(|cx| {
|
||||||
CodegenAlternative::new(
|
CodegenAlternative::new(
|
||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
range.clone(),
|
range.clone(),
|
||||||
true,
|
true,
|
||||||
None,
|
None,
|
||||||
|
project.downgrade(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
prompt_builder,
|
prompt_builder,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@@ -1146,12 +1174,17 @@ mod tests {
|
|||||||
snapshot.anchor_before(Point::new(1, 6))..snapshot.anchor_after(Point::new(1, 6))
|
snapshot.anchor_before(Point::new(1, 6))..snapshot.anchor_after(Point::new(1, 6))
|
||||||
});
|
});
|
||||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
let project = Project::test(fs, vec![], cx).await;
|
||||||
let codegen = cx.new(|cx| {
|
let codegen = cx.new(|cx| {
|
||||||
CodegenAlternative::new(
|
CodegenAlternative::new(
|
||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
range.clone(),
|
range.clone(),
|
||||||
true,
|
true,
|
||||||
None,
|
None,
|
||||||
|
project.downgrade(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
prompt_builder,
|
prompt_builder,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@@ -1210,12 +1243,17 @@ mod tests {
|
|||||||
snapshot.anchor_before(Point::new(1, 2))..snapshot.anchor_after(Point::new(1, 2))
|
snapshot.anchor_before(Point::new(1, 2))..snapshot.anchor_after(Point::new(1, 2))
|
||||||
});
|
});
|
||||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
let project = Project::test(fs, vec![], cx).await;
|
||||||
let codegen = cx.new(|cx| {
|
let codegen = cx.new(|cx| {
|
||||||
CodegenAlternative::new(
|
CodegenAlternative::new(
|
||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
range.clone(),
|
range.clone(),
|
||||||
true,
|
true,
|
||||||
None,
|
None,
|
||||||
|
project.downgrade(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
prompt_builder,
|
prompt_builder,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@@ -1274,12 +1312,17 @@ mod tests {
|
|||||||
snapshot.anchor_before(Point::new(0, 0))..snapshot.anchor_after(Point::new(4, 2))
|
snapshot.anchor_before(Point::new(0, 0))..snapshot.anchor_after(Point::new(4, 2))
|
||||||
});
|
});
|
||||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
let project = Project::test(fs, vec![], cx).await;
|
||||||
let codegen = cx.new(|cx| {
|
let codegen = cx.new(|cx| {
|
||||||
CodegenAlternative::new(
|
CodegenAlternative::new(
|
||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
range.clone(),
|
range.clone(),
|
||||||
true,
|
true,
|
||||||
None,
|
None,
|
||||||
|
project.downgrade(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
prompt_builder,
|
prompt_builder,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@@ -1326,12 +1369,17 @@ mod tests {
|
|||||||
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(1, 14))
|
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(1, 14))
|
||||||
});
|
});
|
||||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
let project = Project::test(fs, vec![], cx).await;
|
||||||
let codegen = cx.new(|cx| {
|
let codegen = cx.new(|cx| {
|
||||||
CodegenAlternative::new(
|
CodegenAlternative::new(
|
||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
range.clone(),
|
range.clone(),
|
||||||
false,
|
false,
|
||||||
None,
|
None,
|
||||||
|
project.downgrade(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
prompt_builder,
|
prompt_builder,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
938
crates/agent_ui/src/context_picker.rs
Normal file
938
crates/agent_ui/src/context_picker.rs
Normal file
@@ -0,0 +1,938 @@
|
|||||||
|
mod completion_provider;
|
||||||
|
pub(crate) mod fetch_context_picker;
|
||||||
|
pub(crate) mod file_context_picker;
|
||||||
|
pub(crate) mod rules_context_picker;
|
||||||
|
pub(crate) mod symbol_context_picker;
|
||||||
|
pub(crate) mod thread_context_picker;
|
||||||
|
|
||||||
|
use std::ops::Range;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use agent::{HistoryEntry, HistoryEntryId, HistoryStore};
|
||||||
|
use agent_client_protocol as acp;
|
||||||
|
use anyhow::{Result, anyhow};
|
||||||
|
use collections::HashSet;
|
||||||
|
pub use completion_provider::ContextPickerCompletionProvider;
|
||||||
|
use editor::display_map::{Crease, CreaseId, CreaseMetadata, FoldId};
|
||||||
|
use editor::{Anchor, Editor, ExcerptId, FoldPlaceholder, ToOffset};
|
||||||
|
use fetch_context_picker::FetchContextPicker;
|
||||||
|
use file_context_picker::FileContextPicker;
|
||||||
|
use file_context_picker::render_file_context_entry;
|
||||||
|
use gpui::{
|
||||||
|
App, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task,
|
||||||
|
WeakEntity,
|
||||||
|
};
|
||||||
|
use language::Buffer;
|
||||||
|
use multi_buffer::MultiBufferRow;
|
||||||
|
use project::ProjectPath;
|
||||||
|
use prompt_store::PromptStore;
|
||||||
|
use rules_context_picker::{RulesContextEntry, RulesContextPicker};
|
||||||
|
use symbol_context_picker::SymbolContextPicker;
|
||||||
|
use thread_context_picker::render_thread_context_entry;
|
||||||
|
use ui::{
|
||||||
|
ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*,
|
||||||
|
};
|
||||||
|
use util::paths::PathStyle;
|
||||||
|
use util::rel_path::RelPath;
|
||||||
|
use workspace::{Workspace, notifications::NotifyResultExt};
|
||||||
|
|
||||||
|
use crate::context_picker::thread_context_picker::ThreadContextPicker;
|
||||||
|
use crate::inline_assistant::ContextProviders;
|
||||||
|
use crate::{context::RULES_ICON, context_store::ContextStore};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(crate) enum ContextPickerEntry {
|
||||||
|
Mode(ContextPickerMode),
|
||||||
|
Action(ContextPickerAction),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContextPickerEntry {
|
||||||
|
pub fn keyword(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Mode(mode) => mode.keyword(),
|
||||||
|
Self::Action(action) => action.keyword(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn label(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Mode(mode) => mode.label(),
|
||||||
|
Self::Action(action) => action.label(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn icon(&self) -> IconName {
|
||||||
|
match self {
|
||||||
|
Self::Mode(mode) => mode.icon(),
|
||||||
|
Self::Action(action) => action.icon(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(crate) enum ContextPickerMode {
|
||||||
|
File,
|
||||||
|
Symbol,
|
||||||
|
Fetch,
|
||||||
|
Thread,
|
||||||
|
Rules,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(crate) enum ContextPickerAction {
|
||||||
|
AddSelections,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContextPickerAction {
|
||||||
|
pub fn keyword(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::AddSelections => "selection",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn label(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::AddSelections => "Selection",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn icon(&self) -> IconName {
|
||||||
|
match self {
|
||||||
|
Self::AddSelections => IconName::Reader,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for ContextPickerMode {
|
||||||
|
type Error = String;
|
||||||
|
|
||||||
|
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
"file" => Ok(Self::File),
|
||||||
|
"symbol" => Ok(Self::Symbol),
|
||||||
|
"fetch" => Ok(Self::Fetch),
|
||||||
|
"thread" => Ok(Self::Thread),
|
||||||
|
"rule" => Ok(Self::Rules),
|
||||||
|
_ => Err(format!("Invalid context picker mode: {}", value)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContextPickerMode {
|
||||||
|
pub fn keyword(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::File => "file",
|
||||||
|
Self::Symbol => "symbol",
|
||||||
|
Self::Fetch => "fetch",
|
||||||
|
Self::Thread => "thread",
|
||||||
|
Self::Rules => "rule",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn label(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::File => "Files & Directories",
|
||||||
|
Self::Symbol => "Symbols",
|
||||||
|
Self::Fetch => "Fetch",
|
||||||
|
Self::Thread => "Threads",
|
||||||
|
Self::Rules => "Rules",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn icon(&self) -> IconName {
|
||||||
|
match self {
|
||||||
|
Self::File => IconName::File,
|
||||||
|
Self::Symbol => IconName::Code,
|
||||||
|
Self::Fetch => IconName::ToolWeb,
|
||||||
|
Self::Thread => IconName::Thread,
|
||||||
|
Self::Rules => RULES_ICON,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum ContextPickerState {
|
||||||
|
Default(Entity<ContextMenu>),
|
||||||
|
File(Entity<FileContextPicker>),
|
||||||
|
Symbol(Entity<SymbolContextPicker>),
|
||||||
|
Fetch(Entity<FetchContextPicker>),
|
||||||
|
Thread(Entity<ThreadContextPicker>),
|
||||||
|
Rules(Entity<RulesContextPicker>),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct ContextPicker {
|
||||||
|
mode: ContextPickerState,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
context_store: WeakEntity<ContextStore>,
|
||||||
|
thread_store: Option<WeakEntity<HistoryStore>>,
|
||||||
|
prompt_store: Option<WeakEntity<PromptStore>>,
|
||||||
|
_subscriptions: Vec<Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContextPicker {
|
||||||
|
pub fn new(
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
context_stores: ContextProviders,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let context_store = context_stores.context_store.downgrade();
|
||||||
|
let thread_store = context_stores.thread_store.clone();
|
||||||
|
let prompt_store = context_stores
|
||||||
|
.prompt_store
|
||||||
|
.as_ref()
|
||||||
|
.map(Entity::downgrade)
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let subscriptions = context_store
|
||||||
|
.upgrade()
|
||||||
|
.map(|context_store| {
|
||||||
|
cx.observe(&context_store, |this, _, cx| this.notify_current_picker(cx))
|
||||||
|
})
|
||||||
|
.into_iter()
|
||||||
|
.chain(
|
||||||
|
thread_store
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|thread_store| thread_store.upgrade())
|
||||||
|
.map(|thread_store| {
|
||||||
|
cx.observe(&thread_store, |this, _, cx| this.notify_current_picker(cx))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.collect::<Vec<Subscription>>();
|
||||||
|
|
||||||
|
ContextPicker {
|
||||||
|
mode: ContextPickerState::Default(ContextMenu::build(
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
|menu, _window, _cx| menu,
|
||||||
|
)),
|
||||||
|
workspace,
|
||||||
|
context_store,
|
||||||
|
thread_store,
|
||||||
|
prompt_store,
|
||||||
|
_subscriptions: subscriptions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.mode = ContextPickerState::Default(self.build_menu(window, cx));
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<ContextMenu> {
|
||||||
|
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<_>>();
|
||||||
|
|
||||||
|
let entries = self
|
||||||
|
.workspace
|
||||||
|
.upgrade()
|
||||||
|
.map(|workspace| {
|
||||||
|
available_context_picker_entries(
|
||||||
|
&self.prompt_store,
|
||||||
|
&self.thread_store,
|
||||||
|
&workspace,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
menu.when(has_recent, |menu| {
|
||||||
|
menu.custom_row(|_, _| {
|
||||||
|
div()
|
||||||
|
.mb_1()
|
||||||
|
.child(
|
||||||
|
Label::new("Recent")
|
||||||
|
.color(Color::Muted)
|
||||||
|
.size(LabelSize::Small),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.extend(recent_entries)
|
||||||
|
.when(has_recent, |menu| menu.separator())
|
||||||
|
.extend(entries.into_iter().map(|entry| {
|
||||||
|
let context_picker = context_picker.clone();
|
||||||
|
|
||||||
|
ContextMenuEntry::new(entry.label())
|
||||||
|
.icon(entry.icon())
|
||||||
|
.icon_size(IconSize::XSmall)
|
||||||
|
.icon_color(Color::Muted)
|
||||||
|
.handler(move |window, cx| {
|
||||||
|
context_picker.update(cx, |this, cx| this.select_entry(entry, window, cx))
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
.keep_open_on_confirm(true)
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.subscribe(&menu, move |_, _, _: &DismissEvent, cx| {
|
||||||
|
cx.emit(DismissEvent);
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
menu
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether threads are allowed as context.
|
||||||
|
pub fn allow_threads(&self) -> bool {
|
||||||
|
self.thread_store.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_entry(
|
||||||
|
&mut self,
|
||||||
|
entry: ContextPickerEntry,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let context_picker = cx.entity().downgrade();
|
||||||
|
|
||||||
|
match entry {
|
||||||
|
ContextPickerEntry::Mode(mode) => match mode {
|
||||||
|
ContextPickerMode::File => {
|
||||||
|
self.mode = ContextPickerState::File(cx.new(|cx| {
|
||||||
|
FileContextPicker::new(
|
||||||
|
context_picker.clone(),
|
||||||
|
self.workspace.clone(),
|
||||||
|
self.context_store.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
ContextPickerMode::Symbol => {
|
||||||
|
self.mode = ContextPickerState::Symbol(cx.new(|cx| {
|
||||||
|
SymbolContextPicker::new(
|
||||||
|
context_picker.clone(),
|
||||||
|
self.workspace.clone(),
|
||||||
|
self.context_store.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
ContextPickerMode::Rules => {
|
||||||
|
if let Some(prompt_store) = self.prompt_store.as_ref() {
|
||||||
|
self.mode = ContextPickerState::Rules(cx.new(|cx| {
|
||||||
|
RulesContextPicker::new(
|
||||||
|
prompt_store.clone(),
|
||||||
|
context_picker.clone(),
|
||||||
|
self.context_store.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ContextPickerMode::Fetch => {
|
||||||
|
self.mode = ContextPickerState::Fetch(cx.new(|cx| {
|
||||||
|
FetchContextPicker::new(
|
||||||
|
context_picker.clone(),
|
||||||
|
self.workspace.clone(),
|
||||||
|
self.context_store.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
ContextPickerMode::Thread => {
|
||||||
|
if let Some(thread_store) = self.thread_store.clone() {
|
||||||
|
self.mode = ContextPickerState::Thread(cx.new(|cx| {
|
||||||
|
ThreadContextPicker::new(
|
||||||
|
thread_store,
|
||||||
|
context_picker.clone(),
|
||||||
|
self.context_store.clone(),
|
||||||
|
self.workspace.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ContextPickerEntry::Action(action) => match action {
|
||||||
|
ContextPickerAction::AddSelections => {
|
||||||
|
if let Some((context_store, workspace)) =
|
||||||
|
self.context_store.upgrade().zip(self.workspace.upgrade())
|
||||||
|
{
|
||||||
|
add_selections_as_context(&context_store, &workspace, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.emit(DismissEvent);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
cx.focus_self(window);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_first(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
// Other variants already select their first entry on open automatically
|
||||||
|
if let ContextPickerState::Default(entity) = &self.mode {
|
||||||
|
entity.update(cx, |entity, cx| {
|
||||||
|
entity.select_first(&Default::default(), window, cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recent_menu_item(
|
||||||
|
&self,
|
||||||
|
context_picker: Entity<ContextPicker>,
|
||||||
|
ix: usize,
|
||||||
|
entry: RecentEntry,
|
||||||
|
path_style: PathStyle,
|
||||||
|
) -> ContextMenuItem {
|
||||||
|
match entry {
|
||||||
|
RecentEntry::File {
|
||||||
|
project_path,
|
||||||
|
path_prefix,
|
||||||
|
} => {
|
||||||
|
let context_store = self.context_store.clone();
|
||||||
|
let worktree_id = project_path.worktree_id;
|
||||||
|
let path = project_path.path.clone();
|
||||||
|
|
||||||
|
ContextMenuItem::custom_entry(
|
||||||
|
move |_window, cx| {
|
||||||
|
render_file_context_entry(
|
||||||
|
ElementId::named_usize("ctx-recent", ix),
|
||||||
|
worktree_id,
|
||||||
|
&path,
|
||||||
|
&path_prefix,
|
||||||
|
false,
|
||||||
|
path_style,
|
||||||
|
context_store.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.into_any()
|
||||||
|
},
|
||||||
|
move |window, cx| {
|
||||||
|
context_picker.update(cx, |this, cx| {
|
||||||
|
this.add_recent_file(project_path.clone(), window, cx);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
RecentEntry::Thread(thread) => {
|
||||||
|
let context_store = self.context_store.clone();
|
||||||
|
let view_thread = thread.clone();
|
||||||
|
|
||||||
|
ContextMenuItem::custom_entry(
|
||||||
|
move |_window, cx| {
|
||||||
|
render_thread_context_entry(&view_thread, context_store.clone(), cx)
|
||||||
|
.into_any()
|
||||||
|
},
|
||||||
|
move |window, cx| {
|
||||||
|
context_picker.update(cx, |this, cx| {
|
||||||
|
this.add_recent_thread(thread.clone(), window, cx)
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_recent_file(
|
||||||
|
&self,
|
||||||
|
project_path: ProjectPath,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let Some(context_store) = self.context_store.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let task = context_store.update(cx, |context_store, cx| {
|
||||||
|
context_store.add_file_from_path(project_path.clone(), true, cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.spawn_in(window, async move |_, cx| task.await.notify_async_err(cx))
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_recent_thread(
|
||||||
|
&self,
|
||||||
|
entry: HistoryEntry,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
let Some(context_store) = self.context_store.upgrade() else {
|
||||||
|
return Task::ready(Err(anyhow!("context store not available")));
|
||||||
|
};
|
||||||
|
let Some(project) = self
|
||||||
|
.workspace
|
||||||
|
.upgrade()
|
||||||
|
.map(|workspace| workspace.read(cx).project().clone())
|
||||||
|
else {
|
||||||
|
return Task::ready(Err(anyhow!("project not available")));
|
||||||
|
};
|
||||||
|
|
||||||
|
match entry {
|
||||||
|
HistoryEntry::AcpThread(thread) => {
|
||||||
|
let Some(thread_store) = self
|
||||||
|
.thread_store
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|thread_store| thread_store.upgrade())
|
||||||
|
else {
|
||||||
|
return Task::ready(Err(anyhow!("thread store not available")));
|
||||||
|
};
|
||||||
|
let load_thread_task =
|
||||||
|
agent::load_agent_thread(thread.id, thread_store, project, cx);
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
let thread = load_thread_task.await?;
|
||||||
|
context_store.update(cx, |context_store, cx| {
|
||||||
|
context_store.add_thread(thread, true, cx);
|
||||||
|
})?;
|
||||||
|
this.update(cx, |_this, cx| cx.notify())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
HistoryEntry::TextThread(thread) => {
|
||||||
|
let Some(thread_store) = self
|
||||||
|
.thread_store
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|thread_store| thread_store.upgrade())
|
||||||
|
else {
|
||||||
|
return Task::ready(Err(anyhow!("text thread store not available")));
|
||||||
|
};
|
||||||
|
|
||||||
|
let task = thread_store.update(cx, |this, cx| {
|
||||||
|
this.load_text_thread(thread.path.clone(), cx)
|
||||||
|
});
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
let thread = task.await?;
|
||||||
|
context_store.update(cx, |context_store, cx| {
|
||||||
|
context_store.add_text_thread(thread, true, cx);
|
||||||
|
})?;
|
||||||
|
this.update(cx, |_this, cx| cx.notify())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recent_entries(&self, cx: &mut App) -> Vec<RecentEntry> {
|
||||||
|
let Some(workspace) = self.workspace.upgrade() else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(context_store) = self.context_store.upgrade() else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
|
||||||
|
recent_context_picker_entries_with_store(
|
||||||
|
context_store,
|
||||||
|
self.thread_store.clone(),
|
||||||
|
workspace,
|
||||||
|
None,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify_current_picker(&mut self, cx: &mut Context<Self>) {
|
||||||
|
match &self.mode {
|
||||||
|
ContextPickerState::Default(entity) => entity.update(cx, |_, cx| cx.notify()),
|
||||||
|
ContextPickerState::File(entity) => entity.update(cx, |_, cx| cx.notify()),
|
||||||
|
ContextPickerState::Symbol(entity) => entity.update(cx, |_, cx| cx.notify()),
|
||||||
|
ContextPickerState::Fetch(entity) => entity.update(cx, |_, cx| cx.notify()),
|
||||||
|
ContextPickerState::Thread(entity) => entity.update(cx, |_, cx| cx.notify()),
|
||||||
|
ContextPickerState::Rules(entity) => entity.update(cx, |_, cx| cx.notify()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<DismissEvent> for ContextPicker {}
|
||||||
|
|
||||||
|
impl Focusable for ContextPicker {
|
||||||
|
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||||
|
match &self.mode {
|
||||||
|
ContextPickerState::Default(menu) => menu.focus_handle(cx),
|
||||||
|
ContextPickerState::File(file_picker) => file_picker.focus_handle(cx),
|
||||||
|
ContextPickerState::Symbol(symbol_picker) => symbol_picker.focus_handle(cx),
|
||||||
|
ContextPickerState::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
|
||||||
|
ContextPickerState::Thread(thread_picker) => thread_picker.focus_handle(cx),
|
||||||
|
ContextPickerState::Rules(user_rules_picker) => user_rules_picker.focus_handle(cx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ContextPicker {
|
||||||
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
v_flex()
|
||||||
|
.w(px(400.))
|
||||||
|
.min_w(px(400.))
|
||||||
|
.map(|parent| match &self.mode {
|
||||||
|
ContextPickerState::Default(menu) => parent.child(menu.clone()),
|
||||||
|
ContextPickerState::File(file_picker) => parent.child(file_picker.clone()),
|
||||||
|
ContextPickerState::Symbol(symbol_picker) => parent.child(symbol_picker.clone()),
|
||||||
|
ContextPickerState::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
|
||||||
|
ContextPickerState::Thread(thread_picker) => parent.child(thread_picker.clone()),
|
||||||
|
ContextPickerState::Rules(user_rules_picker) => {
|
||||||
|
parent.child(user_rules_picker.clone())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) enum RecentEntry {
|
||||||
|
File {
|
||||||
|
project_path: ProjectPath,
|
||||||
|
path_prefix: Arc<RelPath>,
|
||||||
|
},
|
||||||
|
Thread(HistoryEntry),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn available_context_picker_entries(
|
||||||
|
prompt_store: &Option<WeakEntity<PromptStore>>,
|
||||||
|
thread_store: &Option<WeakEntity<HistoryStore>>,
|
||||||
|
workspace: &Entity<Workspace>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Vec<ContextPickerEntry> {
|
||||||
|
let mut entries = vec![
|
||||||
|
ContextPickerEntry::Mode(ContextPickerMode::File),
|
||||||
|
ContextPickerEntry::Mode(ContextPickerMode::Symbol),
|
||||||
|
];
|
||||||
|
|
||||||
|
let has_selection = workspace
|
||||||
|
.read(cx)
|
||||||
|
.active_item(cx)
|
||||||
|
.and_then(|item| item.downcast::<Editor>())
|
||||||
|
.is_some_and(|editor| {
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
editor.has_non_empty_selection(&editor.display_snapshot(cx))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if has_selection {
|
||||||
|
entries.push(ContextPickerEntry::Action(
|
||||||
|
ContextPickerAction::AddSelections,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if thread_store.is_some() {
|
||||||
|
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Thread));
|
||||||
|
}
|
||||||
|
|
||||||
|
if prompt_store.is_some() {
|
||||||
|
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules));
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch));
|
||||||
|
|
||||||
|
entries
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recent_context_picker_entries_with_store(
|
||||||
|
context_store: Entity<ContextStore>,
|
||||||
|
thread_store: Option<WeakEntity<HistoryStore>>,
|
||||||
|
workspace: Entity<Workspace>,
|
||||||
|
exclude_path: Option<ProjectPath>,
|
||||||
|
cx: &App,
|
||||||
|
) -> Vec<RecentEntry> {
|
||||||
|
let project = workspace.read(cx).project();
|
||||||
|
|
||||||
|
let mut exclude_paths = context_store.read(cx).file_paths(cx);
|
||||||
|
exclude_paths.extend(exclude_path);
|
||||||
|
|
||||||
|
let exclude_paths = exclude_paths
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|project_path| project.read(cx).absolute_path(&project_path, cx))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let exclude_threads = context_store.read(cx).thread_ids();
|
||||||
|
|
||||||
|
recent_context_picker_entries(thread_store, workspace, &exclude_paths, exclude_threads, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn recent_context_picker_entries(
|
||||||
|
thread_store: Option<WeakEntity<HistoryStore>>,
|
||||||
|
workspace: Entity<Workspace>,
|
||||||
|
exclude_paths: &HashSet<PathBuf>,
|
||||||
|
exclude_threads: &HashSet<acp::SessionId>,
|
||||||
|
cx: &App,
|
||||||
|
) -> Vec<RecentEntry> {
|
||||||
|
let mut recent = Vec::with_capacity(6);
|
||||||
|
let workspace = workspace.read(cx);
|
||||||
|
let project = workspace.project().read(cx);
|
||||||
|
let include_root_name = workspace.visible_worktrees(cx).count() > 1;
|
||||||
|
|
||||||
|
recent.extend(
|
||||||
|
workspace
|
||||||
|
.recent_navigation_history_iter(cx)
|
||||||
|
.filter(|(_, abs_path)| {
|
||||||
|
abs_path
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|path| !exclude_paths.contains(path.as_path()))
|
||||||
|
})
|
||||||
|
.take(4)
|
||||||
|
.filter_map(|(project_path, _)| {
|
||||||
|
project
|
||||||
|
.worktree_for_id(project_path.worktree_id, cx)
|
||||||
|
.map(|worktree| {
|
||||||
|
let path_prefix = if include_root_name {
|
||||||
|
worktree.read(cx).root_name().into()
|
||||||
|
} else {
|
||||||
|
RelPath::empty().into()
|
||||||
|
};
|
||||||
|
RecentEntry::File {
|
||||||
|
project_path,
|
||||||
|
path_prefix,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(thread_store) = thread_store.and_then(|store| store.upgrade()) {
|
||||||
|
const RECENT_THREADS_COUNT: usize = 2;
|
||||||
|
recent.extend(
|
||||||
|
thread_store
|
||||||
|
.read(cx)
|
||||||
|
.recently_opened_entries(cx)
|
||||||
|
.iter()
|
||||||
|
.filter(|e| match e.id() {
|
||||||
|
HistoryEntryId::AcpThread(session_id) => !exclude_threads.contains(&session_id),
|
||||||
|
HistoryEntryId::TextThread(path) => {
|
||||||
|
!exclude_paths.contains(&path.to_path_buf())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.take(RECENT_THREADS_COUNT)
|
||||||
|
.map(|thread| RecentEntry::Thread(thread.clone())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
recent
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_selections_as_context(
|
||||||
|
context_store: &Entity<ContextStore>,
|
||||||
|
workspace: &Entity<Workspace>,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
let selection_ranges = selection_ranges(workspace, cx);
|
||||||
|
context_store.update(cx, |context_store, cx| {
|
||||||
|
for (buffer, range) in selection_ranges {
|
||||||
|
context_store.add_selection(buffer, range, cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn selection_ranges(
|
||||||
|
workspace: &Entity<Workspace>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Vec<(Entity<Buffer>, Range<text::Anchor>)> {
|
||||||
|
let Some(editor) = workspace
|
||||||
|
.read(cx)
|
||||||
|
.active_item(cx)
|
||||||
|
.and_then(|item| item.act_as::<Editor>(cx))
|
||||||
|
else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
let selections = editor.selections.all_adjusted(&editor.display_snapshot(cx));
|
||||||
|
|
||||||
|
let buffer = editor.buffer().clone().read(cx);
|
||||||
|
let snapshot = buffer.snapshot(cx);
|
||||||
|
|
||||||
|
selections
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
|
||||||
|
.flat_map(|range| {
|
||||||
|
let (start_buffer, start) = buffer.text_anchor_for_position(range.start, cx)?;
|
||||||
|
let (end_buffer, end) = buffer.text_anchor_for_position(range.end, cx)?;
|
||||||
|
if start_buffer != end_buffer {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some((start_buffer, start..end))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn insert_crease_for_mention(
|
||||||
|
excerpt_id: ExcerptId,
|
||||||
|
crease_start: text::Anchor,
|
||||||
|
content_len: usize,
|
||||||
|
crease_label: SharedString,
|
||||||
|
crease_icon_path: SharedString,
|
||||||
|
editor_entity: Entity<Editor>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Option<CreaseId> {
|
||||||
|
editor_entity.update(cx, |editor, cx| {
|
||||||
|
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||||
|
|
||||||
|
let start = snapshot.anchor_in_excerpt(excerpt_id, crease_start)?;
|
||||||
|
|
||||||
|
let start = start.bias_right(&snapshot);
|
||||||
|
let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
|
||||||
|
|
||||||
|
let crease = crease_for_mention(
|
||||||
|
crease_label,
|
||||||
|
crease_icon_path,
|
||||||
|
start..end,
|
||||||
|
editor_entity.downgrade(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let ids = editor.insert_creases(vec![crease.clone()], cx);
|
||||||
|
editor.fold_creases(vec![crease], false, window, cx);
|
||||||
|
|
||||||
|
Some(ids[0])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn crease_for_mention(
|
||||||
|
label: SharedString,
|
||||||
|
icon_path: SharedString,
|
||||||
|
range: Range<Anchor>,
|
||||||
|
editor_entity: WeakEntity<Editor>,
|
||||||
|
) -> Crease<Anchor> {
|
||||||
|
let placeholder = FoldPlaceholder {
|
||||||
|
render: render_fold_icon_button(icon_path.clone(), label.clone(), editor_entity),
|
||||||
|
merge_adjacent: false,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
|
||||||
|
|
||||||
|
Crease::inline(range, placeholder, fold_toggle("mention"), render_trailer)
|
||||||
|
.with_metadata(CreaseMetadata { icon_path, label })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_fold_icon_button(
|
||||||
|
icon_path: SharedString,
|
||||||
|
label: SharedString,
|
||||||
|
editor: WeakEntity<Editor>,
|
||||||
|
) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
|
||||||
|
Arc::new({
|
||||||
|
move |fold_id, fold_range, cx| {
|
||||||
|
let is_in_text_selection = editor
|
||||||
|
.update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
ButtonLike::new(fold_id)
|
||||||
|
.style(ButtonStyle::Filled)
|
||||||
|
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||||
|
.toggle_state(is_in_text_selection)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
Icon::from_path(icon_path.clone())
|
||||||
|
.size(IconSize::XSmall)
|
||||||
|
.color(Color::Muted),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Label::new(label.clone())
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.buffer_font(cx)
|
||||||
|
.single_line(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fold_toggle(
|
||||||
|
name: &'static str,
|
||||||
|
) -> impl Fn(
|
||||||
|
MultiBufferRow,
|
||||||
|
bool,
|
||||||
|
Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
|
||||||
|
&mut Window,
|
||||||
|
&mut App,
|
||||||
|
) -> AnyElement {
|
||||||
|
move |row, is_folded, fold, _window, _cx| {
|
||||||
|
Disclosure::new((name, row.0 as u64), !is_folded)
|
||||||
|
.toggle_state(is_folded)
|
||||||
|
.on_click(move |_e, window, cx| fold(!is_folded, window, cx))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MentionLink;
|
||||||
|
|
||||||
|
impl MentionLink {
|
||||||
|
const FILE: &str = "@file";
|
||||||
|
const SYMBOL: &str = "@symbol";
|
||||||
|
const SELECTION: &str = "@selection";
|
||||||
|
const THREAD: &str = "@thread";
|
||||||
|
const FETCH: &str = "@fetch";
|
||||||
|
const RULE: &str = "@rule";
|
||||||
|
|
||||||
|
const TEXT_THREAD_URL_PREFIX: &str = "text-thread://";
|
||||||
|
|
||||||
|
pub fn for_file(file_name: &str, full_path: &str) -> String {
|
||||||
|
format!("[@{}]({}:{})", file_name, Self::FILE, full_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn for_symbol(symbol_name: &str, full_path: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"[@{}]({}:{}:{})",
|
||||||
|
symbol_name,
|
||||||
|
Self::SYMBOL,
|
||||||
|
full_path,
|
||||||
|
symbol_name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn for_selection(file_name: &str, full_path: &str, line_range: Range<usize>) -> String {
|
||||||
|
format!(
|
||||||
|
"[@{} ({}-{})]({}:{}:{}-{})",
|
||||||
|
file_name,
|
||||||
|
line_range.start + 1,
|
||||||
|
line_range.end + 1,
|
||||||
|
Self::SELECTION,
|
||||||
|
full_path,
|
||||||
|
line_range.start,
|
||||||
|
line_range.end
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn for_thread(thread: &HistoryEntry) -> String {
|
||||||
|
match thread {
|
||||||
|
HistoryEntry::AcpThread(thread) => {
|
||||||
|
format!("[@{}]({}:{})", thread.title, Self::THREAD, thread.id)
|
||||||
|
}
|
||||||
|
HistoryEntry::TextThread(thread) => {
|
||||||
|
let filename = thread
|
||||||
|
.path
|
||||||
|
.file_name()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string_lossy();
|
||||||
|
let escaped_filename = urlencoding::encode(&filename);
|
||||||
|
format!(
|
||||||
|
"[@{}]({}:{}{})",
|
||||||
|
thread.title,
|
||||||
|
Self::THREAD,
|
||||||
|
Self::TEXT_THREAD_URL_PREFIX,
|
||||||
|
escaped_filename
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn for_fetch(url: &str) -> String {
|
||||||
|
format!("[@{}]({}:{})", url, Self::FETCH, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn for_rule(rule: &RulesContextEntry) -> String {
|
||||||
|
format!("[@{}]({}:{})", rule.title, Self::RULE, rule.prompt_id.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
1699
crates/agent_ui/src/context_picker/completion_provider.rs
Normal file
1699
crates/agent_ui/src/context_picker/completion_provider.rs
Normal file
File diff suppressed because it is too large
Load Diff
252
crates/agent_ui/src/context_picker/fetch_context_picker.rs
Normal file
252
crates/agent_ui/src/context_picker/fetch_context_picker.rs
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::{Context as _, Result, bail};
|
||||||
|
use futures::AsyncReadExt as _;
|
||||||
|
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
|
||||||
|
use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
|
||||||
|
use http_client::{AsyncBody, HttpClientWithUrl};
|
||||||
|
use picker::{Picker, PickerDelegate};
|
||||||
|
use ui::{Context, ListItem, Window, prelude::*};
|
||||||
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
use crate::{context_picker::ContextPicker, context_store::ContextStore};
|
||||||
|
|
||||||
|
pub struct FetchContextPicker {
|
||||||
|
picker: Entity<Picker<FetchContextPickerDelegate>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FetchContextPicker {
|
||||||
|
pub fn new(
|
||||||
|
context_picker: WeakEntity<ContextPicker>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
context_store: WeakEntity<ContextStore>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let delegate = FetchContextPickerDelegate::new(context_picker, workspace, context_store);
|
||||||
|
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
||||||
|
|
||||||
|
Self { picker }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Focusable for FetchContextPicker {
|
||||||
|
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||||
|
self.picker.focus_handle(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for FetchContextPicker {
|
||||||
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
self.picker.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||||
|
enum ContentType {
|
||||||
|
Html,
|
||||||
|
Plaintext,
|
||||||
|
Json,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FetchContextPickerDelegate {
|
||||||
|
context_picker: WeakEntity<ContextPicker>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
context_store: WeakEntity<ContextStore>,
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FetchContextPickerDelegate {
|
||||||
|
pub fn new(
|
||||||
|
context_picker: WeakEntity<ContextPicker>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
context_store: WeakEntity<ContextStore>,
|
||||||
|
) -> Self {
|
||||||
|
FetchContextPickerDelegate {
|
||||||
|
context_picker,
|
||||||
|
workspace,
|
||||||
|
context_store,
|
||||||
|
url: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn fetch_url_content(
|
||||||
|
http_client: Arc<HttpClientWithUrl>,
|
||||||
|
url: String,
|
||||||
|
) -> Result<String> {
|
||||||
|
let url = if !url.starts_with("https://") && !url.starts_with("http://") {
|
||||||
|
format!("https://{url}")
|
||||||
|
} else {
|
||||||
|
url
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
|
||||||
|
|
||||||
|
let mut body = Vec::new();
|
||||||
|
response
|
||||||
|
.body_mut()
|
||||||
|
.read_to_end(&mut body)
|
||||||
|
.await
|
||||||
|
.context("error reading response body")?;
|
||||||
|
|
||||||
|
if response.status().is_client_error() {
|
||||||
|
let text = String::from_utf8_lossy(body.as_slice());
|
||||||
|
bail!(
|
||||||
|
"status error {}, response: {text:?}",
|
||||||
|
response.status().as_u16()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(content_type) = response.headers().get("content-type") else {
|
||||||
|
bail!("missing Content-Type header");
|
||||||
|
};
|
||||||
|
let content_type = content_type
|
||||||
|
.to_str()
|
||||||
|
.context("invalid Content-Type header")?;
|
||||||
|
let content_type = match content_type {
|
||||||
|
"text/html" => ContentType::Html,
|
||||||
|
"text/plain" => ContentType::Plaintext,
|
||||||
|
"application/json" => ContentType::Json,
|
||||||
|
_ => ContentType::Html,
|
||||||
|
};
|
||||||
|
|
||||||
|
match content_type {
|
||||||
|
ContentType::Html => {
|
||||||
|
let mut handlers: Vec<TagHandler> = vec![
|
||||||
|
Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
|
||||||
|
Rc::new(RefCell::new(markdown::ParagraphHandler)),
|
||||||
|
Rc::new(RefCell::new(markdown::HeadingHandler)),
|
||||||
|
Rc::new(RefCell::new(markdown::ListHandler)),
|
||||||
|
Rc::new(RefCell::new(markdown::TableHandler::new())),
|
||||||
|
Rc::new(RefCell::new(markdown::StyledTextHandler)),
|
||||||
|
];
|
||||||
|
if url.contains("wikipedia.org") {
|
||||||
|
use html_to_markdown::structure::wikipedia;
|
||||||
|
|
||||||
|
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
|
||||||
|
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
|
||||||
|
handlers.push(Rc::new(
|
||||||
|
RefCell::new(wikipedia::WikipediaCodeHandler::new()),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
|
||||||
|
}
|
||||||
|
|
||||||
|
convert_html_to_markdown(&body[..], &mut handlers)
|
||||||
|
}
|
||||||
|
ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
|
||||||
|
ContentType::Json => {
|
||||||
|
let json: serde_json::Value = serde_json::from_slice(&body)?;
|
||||||
|
|
||||||
|
Ok(format!(
|
||||||
|
"```json\n{}\n```",
|
||||||
|
serde_json::to_string_pretty(&json)?
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PickerDelegate for FetchContextPickerDelegate {
|
||||||
|
type ListItem = ListItem;
|
||||||
|
|
||||||
|
fn match_count(&self) -> usize {
|
||||||
|
if self.url.is_empty() { 0 } else { 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
|
||||||
|
Some("Enter the URL that you would like to fetch".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_index(&self) -> usize {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_selected_index(
|
||||||
|
&mut self,
|
||||||
|
_ix: usize,
|
||||||
|
_window: &mut Window,
|
||||||
|
_cx: &mut Context<Picker<Self>>,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||||
|
"Enter a URL…".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_matches(
|
||||||
|
&mut self,
|
||||||
|
query: String,
|
||||||
|
_window: &mut Window,
|
||||||
|
_cx: &mut Context<Picker<Self>>,
|
||||||
|
) -> Task<()> {
|
||||||
|
self.url = query;
|
||||||
|
|
||||||
|
Task::ready(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||||
|
let Some(workspace) = self.workspace.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let http_client = workspace.read(cx).client().http_client();
|
||||||
|
let url = self.url.clone();
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let text = cx
|
||||||
|
.background_spawn(fetch_url_content(http_client, url.clone()))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.delegate.context_store.update(cx, |context_store, cx| {
|
||||||
|
context_store.add_fetched_url(url, text, cx)
|
||||||
|
})
|
||||||
|
})??;
|
||||||
|
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||||
|
self.context_picker
|
||||||
|
.update(cx, |_, cx| {
|
||||||
|
cx.emit(DismissEvent);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_match(
|
||||||
|
&self,
|
||||||
|
ix: usize,
|
||||||
|
selected: bool,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Picker<Self>>,
|
||||||
|
) -> Option<Self::ListItem> {
|
||||||
|
let added = self
|
||||||
|
.context_store
|
||||||
|
.upgrade()
|
||||||
|
.is_some_and(|context_store| context_store.read(cx).includes_url(&self.url));
|
||||||
|
|
||||||
|
Some(
|
||||||
|
ListItem::new(ix)
|
||||||
|
.inset(true)
|
||||||
|
.toggle_state(selected)
|
||||||
|
.child(Label::new(self.url.clone()))
|
||||||
|
.when(added, |child| {
|
||||||
|
child.disabled(true).end_slot(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
Icon::new(IconName::Check)
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Success),
|
||||||
|
)
|
||||||
|
.child(Label::new("Added").size(LabelSize::Small)),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
392
crates/agent_ui/src/context_picker/file_context_picker.rs
Normal file
392
crates/agent_ui/src/context_picker/file_context_picker.rs
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
|
|
||||||
|
use file_icons::FileIcons;
|
||||||
|
use fuzzy::PathMatch;
|
||||||
|
use gpui::{
|
||||||
|
App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
|
||||||
|
};
|
||||||
|
use picker::{Picker, PickerDelegate};
|
||||||
|
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
|
||||||
|
use ui::{ListItem, Tooltip, prelude::*};
|
||||||
|
use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath};
|
||||||
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
context_picker::ContextPicker,
|
||||||
|
context_store::{ContextStore, FileInclusion},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct FileContextPicker {
|
||||||
|
picker: Entity<Picker<FileContextPickerDelegate>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileContextPicker {
|
||||||
|
pub fn new(
|
||||||
|
context_picker: WeakEntity<ContextPicker>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
context_store: WeakEntity<ContextStore>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let delegate = FileContextPickerDelegate::new(context_picker, workspace, context_store);
|
||||||
|
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
||||||
|
|
||||||
|
Self { picker }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Focusable for FileContextPicker {
|
||||||
|
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||||
|
self.picker.focus_handle(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for FileContextPicker {
|
||||||
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
self.picker.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FileContextPickerDelegate {
|
||||||
|
context_picker: WeakEntity<ContextPicker>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
context_store: WeakEntity<ContextStore>,
|
||||||
|
matches: Vec<FileMatch>,
|
||||||
|
selected_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileContextPickerDelegate {
|
||||||
|
pub fn new(
|
||||||
|
context_picker: WeakEntity<ContextPicker>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
context_store: WeakEntity<ContextStore>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
context_picker,
|
||||||
|
workspace,
|
||||||
|
context_store,
|
||||||
|
matches: Vec::new(),
|
||||||
|
selected_index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PickerDelegate for FileContextPickerDelegate {
|
||||||
|
type ListItem = ListItem;
|
||||||
|
|
||||||
|
fn match_count(&self) -> usize {
|
||||||
|
self.matches.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_index(&self) -> usize {
|
||||||
|
self.selected_index
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_selected_index(
|
||||||
|
&mut self,
|
||||||
|
ix: usize,
|
||||||
|
_window: &mut Window,
|
||||||
|
_cx: &mut Context<Picker<Self>>,
|
||||||
|
) {
|
||||||
|
self.selected_index = ix;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||||
|
"Search files & directories…".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_matches(
|
||||||
|
&mut self,
|
||||||
|
query: String,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Picker<Self>>,
|
||||||
|
) -> Task<()> {
|
||||||
|
let Some(workspace) = self.workspace.upgrade() else {
|
||||||
|
return Task::ready(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let search_task = search_files(query, Arc::<AtomicBool>::default(), &workspace, cx);
|
||||||
|
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
// TODO: This should be probably be run in the background.
|
||||||
|
let paths = search_task.await;
|
||||||
|
|
||||||
|
this.update(cx, |this, _cx| {
|
||||||
|
this.delegate.matches = paths;
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||||
|
let Some(FileMatch { mat, .. }) = self.matches.get(self.selected_index) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let project_path = ProjectPath {
|
||||||
|
worktree_id: WorktreeId::from_usize(mat.worktree_id),
|
||||||
|
path: mat.path.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let is_directory = mat.is_dir;
|
||||||
|
|
||||||
|
self.context_store
|
||||||
|
.update(cx, |context_store, cx| {
|
||||||
|
if is_directory {
|
||||||
|
context_store
|
||||||
|
.add_directory(&project_path, true, cx)
|
||||||
|
.log_err();
|
||||||
|
} else {
|
||||||
|
context_store
|
||||||
|
.add_file_from_path(project_path.clone(), true, cx)
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||||
|
self.context_picker
|
||||||
|
.update(cx, |_, cx| {
|
||||||
|
cx.emit(DismissEvent);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_match(
|
||||||
|
&self,
|
||||||
|
ix: usize,
|
||||||
|
selected: bool,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Picker<Self>>,
|
||||||
|
) -> Option<Self::ListItem> {
|
||||||
|
let FileMatch { mat, .. } = &self.matches.get(ix)?;
|
||||||
|
let workspace = self.workspace.upgrade()?;
|
||||||
|
let path_style = workspace.read(cx).path_style(cx);
|
||||||
|
|
||||||
|
Some(
|
||||||
|
ListItem::new(ix)
|
||||||
|
.inset(true)
|
||||||
|
.toggle_state(selected)
|
||||||
|
.child(render_file_context_entry(
|
||||||
|
ElementId::named_usize("file-ctx-picker", ix),
|
||||||
|
WorktreeId::from_usize(mat.worktree_id),
|
||||||
|
&mat.path,
|
||||||
|
&mat.path_prefix,
|
||||||
|
mat.is_dir,
|
||||||
|
path_style,
|
||||||
|
self.context_store.clone(),
|
||||||
|
cx,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FileMatch {
|
||||||
|
pub mat: PathMatch,
|
||||||
|
pub is_recent: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn search_files(
|
||||||
|
query: String,
|
||||||
|
cancellation_flag: Arc<AtomicBool>,
|
||||||
|
workspace: &Entity<Workspace>,
|
||||||
|
cx: &App,
|
||||||
|
) -> Task<Vec<FileMatch>> {
|
||||||
|
if query.is_empty() {
|
||||||
|
let workspace = workspace.read(cx);
|
||||||
|
let project = workspace.project().read(cx);
|
||||||
|
let visible_worktrees = workspace.visible_worktrees(cx).collect::<Vec<_>>();
|
||||||
|
let include_root_name = visible_worktrees.len() > 1;
|
||||||
|
|
||||||
|
let recent_matches = workspace
|
||||||
|
.recent_navigation_history(Some(10), cx)
|
||||||
|
.into_iter()
|
||||||
|
.map(|(project_path, _)| {
|
||||||
|
let path_prefix = if include_root_name {
|
||||||
|
project
|
||||||
|
.worktree_for_id(project_path.worktree_id, cx)
|
||||||
|
.map(|wt| wt.read(cx).root_name().into())
|
||||||
|
.unwrap_or_else(|| RelPath::empty().into())
|
||||||
|
} else {
|
||||||
|
RelPath::empty().into()
|
||||||
|
};
|
||||||
|
|
||||||
|
FileMatch {
|
||||||
|
mat: PathMatch {
|
||||||
|
score: 0.,
|
||||||
|
positions: Vec::new(),
|
||||||
|
worktree_id: project_path.worktree_id.to_usize(),
|
||||||
|
path: project_path.path,
|
||||||
|
path_prefix,
|
||||||
|
distance_to_relative_ancestor: 0,
|
||||||
|
is_dir: false,
|
||||||
|
},
|
||||||
|
is_recent: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let file_matches = visible_worktrees.into_iter().flat_map(|worktree| {
|
||||||
|
let worktree = worktree.read(cx);
|
||||||
|
let path_prefix: Arc<RelPath> = if include_root_name {
|
||||||
|
worktree.root_name().into()
|
||||||
|
} else {
|
||||||
|
RelPath::empty().into()
|
||||||
|
};
|
||||||
|
worktree.entries(false, 0).map(move |entry| FileMatch {
|
||||||
|
mat: PathMatch {
|
||||||
|
score: 0.,
|
||||||
|
positions: Vec::new(),
|
||||||
|
worktree_id: worktree.id().to_usize(),
|
||||||
|
path: entry.path.clone(),
|
||||||
|
path_prefix: path_prefix.clone(),
|
||||||
|
distance_to_relative_ancestor: 0,
|
||||||
|
is_dir: entry.is_dir(),
|
||||||
|
},
|
||||||
|
is_recent: false,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
Task::ready(recent_matches.chain(file_matches).collect())
|
||||||
|
} else {
|
||||||
|
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
|
||||||
|
let include_root_name = worktrees.len() > 1;
|
||||||
|
let candidate_sets = worktrees
|
||||||
|
.into_iter()
|
||||||
|
.map(|worktree| {
|
||||||
|
let worktree = worktree.read(cx);
|
||||||
|
|
||||||
|
PathMatchCandidateSet {
|
||||||
|
snapshot: worktree.snapshot(),
|
||||||
|
include_ignored: worktree.root_entry().is_some_and(|entry| entry.is_ignored),
|
||||||
|
include_root_name,
|
||||||
|
candidates: project::Candidates::Entries,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let executor = cx.background_executor().clone();
|
||||||
|
cx.foreground_executor().spawn(async move {
|
||||||
|
fuzzy::match_path_sets(
|
||||||
|
candidate_sets.as_slice(),
|
||||||
|
query.as_str(),
|
||||||
|
&None,
|
||||||
|
false,
|
||||||
|
100,
|
||||||
|
&cancellation_flag,
|
||||||
|
executor,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.map(|mat| FileMatch {
|
||||||
|
mat,
|
||||||
|
is_recent: false,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extract_file_name_and_directory(
|
||||||
|
path: &RelPath,
|
||||||
|
path_prefix: &RelPath,
|
||||||
|
path_style: PathStyle,
|
||||||
|
) -> (SharedString, Option<SharedString>) {
|
||||||
|
// If path is empty, this means we're matching with the root directory itself
|
||||||
|
// so we use the path_prefix as the name
|
||||||
|
if path.is_empty() && !path_prefix.is_empty() {
|
||||||
|
return (path_prefix.display(path_style).to_string().into(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
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()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_file_context_entry(
|
||||||
|
id: ElementId,
|
||||||
|
worktree_id: WorktreeId,
|
||||||
|
path: &Arc<RelPath>,
|
||||||
|
path_prefix: &Arc<RelPath>,
|
||||||
|
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 added = context_store.upgrade().and_then(|context_store| {
|
||||||
|
let project_path = ProjectPath {
|
||||||
|
worktree_id,
|
||||||
|
path: path.clone(),
|
||||||
|
};
|
||||||
|
if is_directory {
|
||||||
|
context_store
|
||||||
|
.read(cx)
|
||||||
|
.path_included_in_directory(&project_path, cx)
|
||||||
|
} else {
|
||||||
|
context_store.read(cx).file_path_included(&project_path, cx)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let file_icon = if is_directory {
|
||||||
|
FileIcons::get_folder_icon(false, path.as_std_path(), cx)
|
||||||
|
} else {
|
||||||
|
FileIcons::get_icon(path.as_std_path(), cx)
|
||||||
|
}
|
||||||
|
.map(Icon::from_path)
|
||||||
|
.unwrap_or_else(|| Icon::new(IconName::File));
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.id(id)
|
||||||
|
.gap_1p5()
|
||||||
|
.w_full()
|
||||||
|
.child(file_icon.size(IconSize::Small).color(Color::Muted))
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.child(Label::new(file_name))
|
||||||
|
.children(directory.map(|directory| {
|
||||||
|
Label::new(directory)
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.color(Color::Muted)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.when_some(added, |el, added| match added {
|
||||||
|
FileInclusion::Direct => el.child(
|
||||||
|
h_flex()
|
||||||
|
.w_full()
|
||||||
|
.justify_end()
|
||||||
|
.gap_0p5()
|
||||||
|
.child(
|
||||||
|
Icon::new(IconName::Check)
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Success),
|
||||||
|
)
|
||||||
|
.child(Label::new("Added").size(LabelSize::Small)),
|
||||||
|
),
|
||||||
|
FileInclusion::InDirectory { full_path } => {
|
||||||
|
let directory_full_path = full_path.to_string_lossy().into_owned();
|
||||||
|
|
||||||
|
el.child(
|
||||||
|
h_flex()
|
||||||
|
.w_full()
|
||||||
|
.justify_end()
|
||||||
|
.gap_0p5()
|
||||||
|
.child(
|
||||||
|
Icon::new(IconName::Check)
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Success),
|
||||||
|
)
|
||||||
|
.child(Label::new("Included").size(LabelSize::Small)),
|
||||||
|
)
|
||||||
|
.tooltip(Tooltip::text(format!("in {directory_full_path}")))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
224
crates/agent_ui/src/context_picker/rules_context_picker.rs
Normal file
224
crates/agent_ui/src/context_picker/rules_context_picker.rs
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
|
|
||||||
|
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
|
||||||
|
use picker::{Picker, PickerDelegate};
|
||||||
|
use prompt_store::{PromptId, PromptStore, UserPromptId};
|
||||||
|
use ui::{ListItem, prelude::*};
|
||||||
|
use util::ResultExt as _;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
context::RULES_ICON,
|
||||||
|
context_picker::ContextPicker,
|
||||||
|
context_store::{self, ContextStore},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct RulesContextPicker {
|
||||||
|
picker: Entity<Picker<RulesContextPickerDelegate>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RulesContextPicker {
|
||||||
|
pub fn new(
|
||||||
|
prompt_store: WeakEntity<PromptStore>,
|
||||||
|
context_picker: WeakEntity<ContextPicker>,
|
||||||
|
context_store: WeakEntity<context_store::ContextStore>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let delegate = RulesContextPickerDelegate::new(prompt_store, context_picker, context_store);
|
||||||
|
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
||||||
|
|
||||||
|
RulesContextPicker { picker }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Focusable for RulesContextPicker {
|
||||||
|
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||||
|
self.picker.focus_handle(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for RulesContextPicker {
|
||||||
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
self.picker.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RulesContextEntry {
|
||||||
|
pub prompt_id: UserPromptId,
|
||||||
|
pub title: SharedString,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RulesContextPickerDelegate {
|
||||||
|
prompt_store: WeakEntity<PromptStore>,
|
||||||
|
context_picker: WeakEntity<ContextPicker>,
|
||||||
|
context_store: WeakEntity<context_store::ContextStore>,
|
||||||
|
matches: Vec<RulesContextEntry>,
|
||||||
|
selected_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RulesContextPickerDelegate {
|
||||||
|
pub fn new(
|
||||||
|
prompt_store: WeakEntity<PromptStore>,
|
||||||
|
context_picker: WeakEntity<ContextPicker>,
|
||||||
|
context_store: WeakEntity<context_store::ContextStore>,
|
||||||
|
) -> Self {
|
||||||
|
RulesContextPickerDelegate {
|
||||||
|
prompt_store,
|
||||||
|
context_picker,
|
||||||
|
context_store,
|
||||||
|
matches: Vec::new(),
|
||||||
|
selected_index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PickerDelegate for RulesContextPickerDelegate {
|
||||||
|
type ListItem = ListItem;
|
||||||
|
|
||||||
|
fn match_count(&self) -> usize {
|
||||||
|
self.matches.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_index(&self) -> usize {
|
||||||
|
self.selected_index
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_selected_index(
|
||||||
|
&mut self,
|
||||||
|
ix: usize,
|
||||||
|
_window: &mut Window,
|
||||||
|
_cx: &mut Context<Picker<Self>>,
|
||||||
|
) {
|
||||||
|
self.selected_index = ix;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||||
|
"Search available rules…".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_matches(
|
||||||
|
&mut self,
|
||||||
|
query: String,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Picker<Self>>,
|
||||||
|
) -> Task<()> {
|
||||||
|
let Some(prompt_store) = self.prompt_store.upgrade() else {
|
||||||
|
return Task::ready(());
|
||||||
|
};
|
||||||
|
let search_task = search_rules(query, Arc::new(AtomicBool::default()), &prompt_store, cx);
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let matches = search_task.await;
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.delegate.matches = matches;
|
||||||
|
this.delegate.selected_index = 0;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||||
|
let Some(entry) = self.matches.get(self.selected_index) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.context_store
|
||||||
|
.update(cx, |context_store, cx| {
|
||||||
|
context_store.add_rules(entry.prompt_id, true, cx)
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||||
|
self.context_picker
|
||||||
|
.update(cx, |_, cx| {
|
||||||
|
cx.emit(DismissEvent);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_match(
|
||||||
|
&self,
|
||||||
|
ix: usize,
|
||||||
|
selected: bool,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Picker<Self>>,
|
||||||
|
) -> Option<Self::ListItem> {
|
||||||
|
let thread = &self.matches.get(ix)?;
|
||||||
|
|
||||||
|
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
|
||||||
|
render_thread_context_entry(thread, self.context_store.clone(), cx),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_thread_context_entry(
|
||||||
|
user_rules: &RulesContextEntry,
|
||||||
|
context_store: WeakEntity<ContextStore>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Div {
|
||||||
|
let added = context_store.upgrade().is_some_and(|context_store| {
|
||||||
|
context_store
|
||||||
|
.read(cx)
|
||||||
|
.includes_user_rules(user_rules.prompt_id)
|
||||||
|
});
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.gap_1p5()
|
||||||
|
.w_full()
|
||||||
|
.justify_between()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1p5()
|
||||||
|
.max_w_72()
|
||||||
|
.child(
|
||||||
|
Icon::new(RULES_ICON)
|
||||||
|
.size(IconSize::XSmall)
|
||||||
|
.color(Color::Muted),
|
||||||
|
)
|
||||||
|
.child(Label::new(user_rules.title.clone()).truncate()),
|
||||||
|
)
|
||||||
|
.when(added, |el| {
|
||||||
|
el.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
Icon::new(IconName::Check)
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Success),
|
||||||
|
)
|
||||||
|
.child(Label::new("Added").size(LabelSize::Small)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn search_rules(
|
||||||
|
query: String,
|
||||||
|
cancellation_flag: Arc<AtomicBool>,
|
||||||
|
prompt_store: &Entity<PromptStore>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Task<Vec<RulesContextEntry>> {
|
||||||
|
let search_task = prompt_store.read(cx).search(query, cancellation_flag, cx);
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
search_task
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|metadata| {
|
||||||
|
// Default prompts are filtered out as they are automatically included.
|
||||||
|
if metadata.default {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
match metadata.id {
|
||||||
|
PromptId::EditWorkflow => None,
|
||||||
|
PromptId::User { uuid } => Some(RulesContextEntry {
|
||||||
|
prompt_id: uuid,
|
||||||
|
title: metadata.title?,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
}
|
||||||
415
crates/agent_ui/src/context_picker/symbol_context_picker.rs
Normal file
415
crates/agent_ui/src/context_picker/symbol_context_picker.rs
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
use std::cmp::Reverse;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
|
|
||||||
|
use anyhow::{Result, anyhow};
|
||||||
|
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 _;
|
||||||
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
context::AgentContextHandle, context_picker::ContextPicker, context_store::ContextStore,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct SymbolContextPicker {
|
||||||
|
picker: Entity<Picker<SymbolContextPickerDelegate>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SymbolContextPicker {
|
||||||
|
pub fn new(
|
||||||
|
context_picker: WeakEntity<ContextPicker>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
context_store: WeakEntity<ContextStore>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let delegate = SymbolContextPickerDelegate::new(context_picker, workspace, context_store);
|
||||||
|
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
||||||
|
|
||||||
|
Self { picker }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Focusable for SymbolContextPicker {
|
||||||
|
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||||
|
self.picker.focus_handle(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for SymbolContextPicker {
|
||||||
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
self.picker.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SymbolContextPickerDelegate {
|
||||||
|
context_picker: WeakEntity<ContextPicker>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
context_store: WeakEntity<ContextStore>,
|
||||||
|
matches: Vec<SymbolEntry>,
|
||||||
|
selected_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SymbolContextPickerDelegate {
|
||||||
|
pub fn new(
|
||||||
|
context_picker: WeakEntity<ContextPicker>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
context_store: WeakEntity<ContextStore>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
context_picker,
|
||||||
|
workspace,
|
||||||
|
context_store,
|
||||||
|
matches: Vec::new(),
|
||||||
|
selected_index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PickerDelegate for SymbolContextPickerDelegate {
|
||||||
|
type ListItem = ListItem;
|
||||||
|
|
||||||
|
fn match_count(&self) -> usize {
|
||||||
|
self.matches.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_index(&self) -> usize {
|
||||||
|
self.selected_index
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_selected_index(
|
||||||
|
&mut self,
|
||||||
|
ix: usize,
|
||||||
|
_window: &mut Window,
|
||||||
|
_cx: &mut Context<Picker<Self>>,
|
||||||
|
) {
|
||||||
|
self.selected_index = ix;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||||
|
"Search symbols…".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_matches(
|
||||||
|
&mut self,
|
||||||
|
query: String,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Picker<Self>>,
|
||||||
|
) -> Task<()> {
|
||||||
|
let Some(workspace) = self.workspace.upgrade() else {
|
||||||
|
return Task::ready(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let search_task = search_symbols(query, Arc::<AtomicBool>::default(), &workspace, cx);
|
||||||
|
let context_store = self.context_store.clone();
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let symbols = search_task.await;
|
||||||
|
|
||||||
|
let symbol_entries = context_store
|
||||||
|
.read_with(cx, |context_store, cx| {
|
||||||
|
compute_symbol_entries(symbols, context_store, cx)
|
||||||
|
})
|
||||||
|
.log_err()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
this.update(cx, |this, _cx| {
|
||||||
|
this.delegate.matches = symbol_entries;
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||||
|
let Some(mat) = self.matches.get(self.selected_index) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(workspace) = self.workspace.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let add_symbol_task = add_symbol(
|
||||||
|
mat.symbol.clone(),
|
||||||
|
true,
|
||||||
|
workspace,
|
||||||
|
self.context_store.clone(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
let selected_index = self.selected_index;
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
let (_, included) = add_symbol_task.await?;
|
||||||
|
this.update(cx, |this, _| {
|
||||||
|
if let Some(mat) = this.delegate.matches.get_mut(selected_index) {
|
||||||
|
mat.is_included = included;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||||
|
self.context_picker
|
||||||
|
.update(cx, |_, cx| {
|
||||||
|
cx.emit(DismissEvent);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_match(
|
||||||
|
&self,
|
||||||
|
ix: usize,
|
||||||
|
selected: bool,
|
||||||
|
_window: &mut Window,
|
||||||
|
_: &mut Context<Picker<Self>>,
|
||||||
|
) -> Option<Self::ListItem> {
|
||||||
|
let mat = &self.matches.get(ix)?;
|
||||||
|
|
||||||
|
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
|
||||||
|
render_symbol_context_entry(ElementId::named_usize("symbol-ctx-picker", ix), mat),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct SymbolEntry {
|
||||||
|
pub symbol: Symbol,
|
||||||
|
pub is_included: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn add_symbol(
|
||||||
|
symbol: Symbol,
|
||||||
|
remove_if_exists: bool,
|
||||||
|
workspace: Entity<Workspace>,
|
||||||
|
context_store: WeakEntity<ContextStore>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> 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)
|
||||||
|
});
|
||||||
|
cx.spawn(async move |cx| {
|
||||||
|
let buffer = open_buffer_task.await?;
|
||||||
|
let document_symbols = project
|
||||||
|
.update(cx, |project, cx| project.document_symbols(&buffer, cx))?
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Try to find a matching document symbol. Document symbols include
|
||||||
|
// not only the symbol itself (e.g. function name), but they also
|
||||||
|
// include the context that they contain (e.g. function body).
|
||||||
|
let (name, range, enclosing_range) = if let Some(DocumentSymbol {
|
||||||
|
name,
|
||||||
|
range,
|
||||||
|
selection_range,
|
||||||
|
..
|
||||||
|
}) =
|
||||||
|
find_matching_symbol(&symbol, document_symbols.as_slice())
|
||||||
|
{
|
||||||
|
(name, selection_range, range)
|
||||||
|
} else {
|
||||||
|
// If we do not find a matching document symbol, fall back to
|
||||||
|
// just the symbol itself
|
||||||
|
(symbol.name, symbol.range.clone(), symbol.range)
|
||||||
|
};
|
||||||
|
|
||||||
|
let (range, enclosing_range) = buffer.read_with(cx, |buffer, _| {
|
||||||
|
(
|
||||||
|
buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
|
||||||
|
buffer.anchor_after(enclosing_range.start)
|
||||||
|
..buffer.anchor_before(enclosing_range.end),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
context_store.update(cx, move |context_store, cx| {
|
||||||
|
context_store.add_symbol(
|
||||||
|
buffer,
|
||||||
|
name.into(),
|
||||||
|
range,
|
||||||
|
enclosing_range,
|
||||||
|
remove_if_exists,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_matching_symbol(symbol: &Symbol, candidates: &[DocumentSymbol]) -> Option<DocumentSymbol> {
|
||||||
|
let mut candidates = candidates.iter();
|
||||||
|
let mut candidate = candidates.next()?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if candidate.range.start > symbol.range.end {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if candidate.range.end < symbol.range.start {
|
||||||
|
candidate = candidates.next()?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if candidate.selection_range == symbol.range {
|
||||||
|
return Some(candidate.clone());
|
||||||
|
}
|
||||||
|
if candidate.range.start <= symbol.range.start && symbol.range.end <= candidate.range.end {
|
||||||
|
candidates = candidate.children.iter();
|
||||||
|
candidate = candidates.next()?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SymbolMatch {
|
||||||
|
pub symbol: Symbol,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn search_symbols(
|
||||||
|
query: String,
|
||||||
|
cancellation_flag: Arc<AtomicBool>,
|
||||||
|
workspace: &Entity<Workspace>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Task<Vec<SymbolMatch>> {
|
||||||
|
let symbols_task = workspace.update(cx, |workspace, cx| {
|
||||||
|
workspace
|
||||||
|
.project()
|
||||||
|
.update(cx, |project, cx| project.symbols(&query, cx))
|
||||||
|
});
|
||||||
|
let project = workspace.read(cx).project().clone();
|
||||||
|
cx.spawn(async move |cx| {
|
||||||
|
let Some(symbols) = symbols_task.await.log_err() else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let Some((visible_match_candidates, external_match_candidates)): Option<(Vec<_>, Vec<_>)> =
|
||||||
|
project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
symbols
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(id, symbol)| {
|
||||||
|
StringMatchCandidate::new(id, symbol.label.filter_text())
|
||||||
|
})
|
||||||
|
.partition(|candidate| 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,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.log_err()
|
||||||
|
else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAX_MATCHES: usize = 100;
|
||||||
|
let mut visible_matches = cx.background_executor().block(fuzzy::match_strings(
|
||||||
|
&visible_match_candidates,
|
||||||
|
&query,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
MAX_MATCHES,
|
||||||
|
&cancellation_flag,
|
||||||
|
cx.background_executor().clone(),
|
||||||
|
));
|
||||||
|
let mut external_matches = cx.background_executor().block(fuzzy::match_strings(
|
||||||
|
&external_match_candidates,
|
||||||
|
&query,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
MAX_MATCHES - visible_matches.len().min(MAX_MATCHES),
|
||||||
|
&cancellation_flag,
|
||||||
|
cx.background_executor().clone(),
|
||||||
|
));
|
||||||
|
let sort_key_for_match = |mat: &StringMatch| {
|
||||||
|
let symbol = &symbols[mat.candidate_id];
|
||||||
|
(Reverse(OrderedFloat(mat.score)), symbol.label.filter_text())
|
||||||
|
};
|
||||||
|
|
||||||
|
visible_matches.sort_unstable_by_key(sort_key_for_match);
|
||||||
|
external_matches.sort_unstable_by_key(sort_key_for_match);
|
||||||
|
let mut matches = visible_matches;
|
||||||
|
matches.append(&mut external_matches);
|
||||||
|
|
||||||
|
matches
|
||||||
|
.into_iter()
|
||||||
|
.map(|mut mat| {
|
||||||
|
let symbol = symbols[mat.candidate_id].clone();
|
||||||
|
let filter_start = symbol.label.filter_range.start;
|
||||||
|
for position in &mut mat.positions {
|
||||||
|
*position += filter_start;
|
||||||
|
}
|
||||||
|
SymbolMatch { symbol }
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_symbol_entries(
|
||||||
|
symbols: Vec<SymbolMatch>,
|
||||||
|
context_store: &ContextStore,
|
||||||
|
cx: &App,
|
||||||
|
) -> Vec<SymbolEntry> {
|
||||||
|
symbols
|
||||||
|
.into_iter()
|
||||||
|
.map(|SymbolMatch { symbol, .. }| SymbolEntry {
|
||||||
|
is_included: context_store.includes_symbol(&symbol, cx),
|
||||||
|
symbol,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 symbol_location = format!("{} L{}", path, entry.symbol.range.start.0.row + 1);
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.id(id)
|
||||||
|
.gap_1p5()
|
||||||
|
.w_full()
|
||||||
|
.child(
|
||||||
|
Icon::new(IconName::Code)
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.child(Label::new(&entry.symbol.name))
|
||||||
|
.child(
|
||||||
|
Label::new(symbol_location)
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.when(entry.is_included, |el| {
|
||||||
|
el.child(
|
||||||
|
h_flex()
|
||||||
|
.w_full()
|
||||||
|
.justify_end()
|
||||||
|
.gap_0p5()
|
||||||
|
.child(
|
||||||
|
Icon::new(IconName::Check)
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Success),
|
||||||
|
)
|
||||||
|
.child(Label::new("Added").size(LabelSize::Small)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
280
crates/agent_ui/src/context_picker/thread_context_picker.rs
Normal file
280
crates/agent_ui/src/context_picker/thread_context_picker.rs
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
context_picker::ContextPicker,
|
||||||
|
context_store::{self, ContextStore},
|
||||||
|
};
|
||||||
|
use agent::{HistoryEntry, HistoryStore};
|
||||||
|
use fuzzy::StringMatchCandidate;
|
||||||
|
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
|
||||||
|
use picker::{Picker, PickerDelegate};
|
||||||
|
use ui::{ListItem, prelude::*};
|
||||||
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
pub struct ThreadContextPicker {
|
||||||
|
picker: Entity<Picker<ThreadContextPickerDelegate>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ThreadContextPicker {
|
||||||
|
pub fn new(
|
||||||
|
thread_store: WeakEntity<HistoryStore>,
|
||||||
|
context_picker: WeakEntity<ContextPicker>,
|
||||||
|
context_store: WeakEntity<context_store::ContextStore>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let delegate = ThreadContextPickerDelegate::new(
|
||||||
|
thread_store,
|
||||||
|
context_picker,
|
||||||
|
context_store,
|
||||||
|
workspace,
|
||||||
|
);
|
||||||
|
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
||||||
|
|
||||||
|
ThreadContextPicker { picker }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Focusable for ThreadContextPicker {
|
||||||
|
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||||
|
self.picker.focus_handle(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ThreadContextPicker {
|
||||||
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
self.picker.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ThreadContextPickerDelegate {
|
||||||
|
thread_store: WeakEntity<HistoryStore>,
|
||||||
|
context_picker: WeakEntity<ContextPicker>,
|
||||||
|
context_store: WeakEntity<context_store::ContextStore>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
matches: Vec<HistoryEntry>,
|
||||||
|
selected_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ThreadContextPickerDelegate {
|
||||||
|
pub fn new(
|
||||||
|
thread_store: WeakEntity<HistoryStore>,
|
||||||
|
context_picker: WeakEntity<ContextPicker>,
|
||||||
|
context_store: WeakEntity<context_store::ContextStore>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
) -> Self {
|
||||||
|
ThreadContextPickerDelegate {
|
||||||
|
thread_store,
|
||||||
|
context_picker,
|
||||||
|
context_store,
|
||||||
|
workspace,
|
||||||
|
matches: Vec::new(),
|
||||||
|
selected_index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PickerDelegate for ThreadContextPickerDelegate {
|
||||||
|
type ListItem = ListItem;
|
||||||
|
|
||||||
|
fn match_count(&self) -> usize {
|
||||||
|
self.matches.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_index(&self) -> usize {
|
||||||
|
self.selected_index
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_selected_index(
|
||||||
|
&mut self,
|
||||||
|
ix: usize,
|
||||||
|
_window: &mut Window,
|
||||||
|
_cx: &mut Context<Picker<Self>>,
|
||||||
|
) {
|
||||||
|
self.selected_index = ix;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||||
|
"Search threads…".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_matches(
|
||||||
|
&mut self,
|
||||||
|
query: String,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Picker<Self>>,
|
||||||
|
) -> Task<()> {
|
||||||
|
let Some(thread_store) = self.thread_store.upgrade() else {
|
||||||
|
return Task::ready(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let search_task = search_threads(query, Arc::new(AtomicBool::default()), &thread_store, cx);
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let matches = search_task.await;
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.delegate.matches = matches;
|
||||||
|
this.delegate.selected_index = 0;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||||
|
let Some(project) = self
|
||||||
|
.workspace
|
||||||
|
.upgrade()
|
||||||
|
.map(|w| w.read(cx).project().clone())
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some((entry, thread_store)) = self
|
||||||
|
.matches
|
||||||
|
.get(self.selected_index)
|
||||||
|
.zip(self.thread_store.upgrade())
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match entry {
|
||||||
|
HistoryEntry::AcpThread(thread) => {
|
||||||
|
let load_thread_task =
|
||||||
|
agent::load_agent_thread(thread.id.clone(), thread_store, project, cx);
|
||||||
|
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
let thread = load_thread_task.await?;
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.delegate
|
||||||
|
.context_store
|
||||||
|
.update(cx, |context_store, cx| {
|
||||||
|
context_store.add_thread(thread, true, cx)
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
HistoryEntry::TextThread(thread) => {
|
||||||
|
let task = thread_store.update(cx, |this, cx| {
|
||||||
|
this.load_text_thread(thread.path.clone(), cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
let thread = task.await?;
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.delegate
|
||||||
|
.context_store
|
||||||
|
.update(cx, |context_store, cx| {
|
||||||
|
context_store.add_text_thread(thread, true, cx)
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||||
|
self.context_picker
|
||||||
|
.update(cx, |_, cx| {
|
||||||
|
cx.emit(DismissEvent);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_match(
|
||||||
|
&self,
|
||||||
|
ix: usize,
|
||||||
|
selected: bool,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Picker<Self>>,
|
||||||
|
) -> Option<Self::ListItem> {
|
||||||
|
let thread = &self.matches.get(ix)?;
|
||||||
|
|
||||||
|
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
|
||||||
|
render_thread_context_entry(thread, self.context_store.clone(), cx),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_thread_context_entry(
|
||||||
|
entry: &HistoryEntry,
|
||||||
|
context_store: WeakEntity<ContextStore>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Div {
|
||||||
|
let is_added = match entry {
|
||||||
|
HistoryEntry::AcpThread(thread) => context_store
|
||||||
|
.upgrade()
|
||||||
|
.is_some_and(|ctx_store| ctx_store.read(cx).includes_thread(&thread.id)),
|
||||||
|
HistoryEntry::TextThread(thread) => context_store
|
||||||
|
.upgrade()
|
||||||
|
.is_some_and(|ctx_store| ctx_store.read(cx).includes_text_thread(&thread.path)),
|
||||||
|
};
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.gap_1p5()
|
||||||
|
.w_full()
|
||||||
|
.justify_between()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1p5()
|
||||||
|
.max_w_72()
|
||||||
|
.child(
|
||||||
|
Icon::new(IconName::Thread)
|
||||||
|
.size(IconSize::XSmall)
|
||||||
|
.color(Color::Muted),
|
||||||
|
)
|
||||||
|
.child(Label::new(entry.title().clone()).truncate()),
|
||||||
|
)
|
||||||
|
.when(is_added, |el| {
|
||||||
|
el.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
Icon::new(IconName::Check)
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Success),
|
||||||
|
)
|
||||||
|
.child(Label::new("Added").size(LabelSize::Small)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn search_threads(
|
||||||
|
query: String,
|
||||||
|
cancellation_flag: Arc<AtomicBool>,
|
||||||
|
thread_store: &Entity<HistoryStore>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Task<Vec<HistoryEntry>> {
|
||||||
|
let threads = thread_store.read(cx).entries().collect();
|
||||||
|
if query.is_empty() {
|
||||||
|
return Task::ready(threads);
|
||||||
|
}
|
||||||
|
|
||||||
|
let executor = cx.background_executor().clone();
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let candidates = threads
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(id, thread)| StringMatchCandidate::new(id, thread.title()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let matches = fuzzy::match_strings(
|
||||||
|
&candidates,
|
||||||
|
&query,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
100,
|
||||||
|
&cancellation_flag,
|
||||||
|
executor,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
matches
|
||||||
|
.into_iter()
|
||||||
|
.map(|mat| threads[mat.candidate_id].clone())
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
618
crates/agent_ui/src/context_store.rs
Normal file
618
crates/agent_ui/src/context_store.rs
Normal file
@@ -0,0 +1,618 @@
|
|||||||
|
use crate::context::{
|
||||||
|
AgentContextHandle, AgentContextKey, ContextId, ContextKind, DirectoryContextHandle,
|
||||||
|
FetchedUrlContext, FileContextHandle, ImageContext, RulesContextHandle, SelectionContextHandle,
|
||||||
|
SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle,
|
||||||
|
};
|
||||||
|
use agent_client_protocol as acp;
|
||||||
|
use anyhow::{Context as _, Result, anyhow};
|
||||||
|
use assistant_text_thread::TextThread;
|
||||||
|
use collections::{HashSet, IndexSet};
|
||||||
|
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 prompt_store::UserPromptId;
|
||||||
|
use ref_cast::RefCast as _;
|
||||||
|
use std::{
|
||||||
|
ops::Range,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
use text::{Anchor, OffsetRangeExt};
|
||||||
|
|
||||||
|
pub struct ContextStore {
|
||||||
|
project: WeakEntity<Project>,
|
||||||
|
next_context_id: ContextId,
|
||||||
|
context_set: IndexSet<AgentContextKey>,
|
||||||
|
context_thread_ids: HashSet<acp::SessionId>,
|
||||||
|
context_text_thread_paths: HashSet<Arc<Path>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ContextStoreEvent {
|
||||||
|
ContextRemoved(AgentContextKey),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<ContextStoreEvent> for ContextStore {}
|
||||||
|
|
||||||
|
impl ContextStore {
|
||||||
|
pub fn project(&self) -> WeakEntity<Project> {
|
||||||
|
self.project.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(project: WeakEntity<Project>) -> Self {
|
||||||
|
Self {
|
||||||
|
project,
|
||||||
|
next_context_id: ContextId::zero(),
|
||||||
|
context_set: IndexSet::default(),
|
||||||
|
context_thread_ids: HashSet::default(),
|
||||||
|
context_text_thread_paths: HashSet::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn context(&self) -> impl Iterator<Item = &AgentContextHandle> {
|
||||||
|
self.context_set.iter().map(|entry| entry.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.context_set.clear();
|
||||||
|
self.context_thread_ids.clear();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_file_from_path(
|
||||||
|
&mut self,
|
||||||
|
project_path: ProjectPath,
|
||||||
|
remove_if_exists: bool,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Task<Result<Option<AgentContextHandle>>> {
|
||||||
|
let Some(project) = self.project.upgrade() else {
|
||||||
|
return Task::ready(Err(anyhow!("failed to read project")));
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_image_file(&project, &project_path, cx) {
|
||||||
|
self.add_image_from_path(project_path, remove_if_exists, cx)
|
||||||
|
} else {
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
let open_buffer_task = project.update(cx, |project, cx| {
|
||||||
|
project.open_buffer(project_path.clone(), cx)
|
||||||
|
})?;
|
||||||
|
let buffer = open_buffer_task.await?;
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.add_file_from_buffer(&project_path, buffer, remove_if_exists, cx)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_file_from_buffer(
|
||||||
|
&mut self,
|
||||||
|
project_path: &ProjectPath,
|
||||||
|
buffer: Entity<Buffer>,
|
||||||
|
remove_if_exists: bool,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Option<AgentContextHandle> {
|
||||||
|
let context_id = self.next_context_id.post_inc();
|
||||||
|
let context = AgentContextHandle::File(FileContextHandle { buffer, context_id });
|
||||||
|
|
||||||
|
if let Some(key) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
|
||||||
|
if remove_if_exists {
|
||||||
|
self.remove_context(&context, cx);
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(key.as_ref().clone())
|
||||||
|
}
|
||||||
|
} else if self.path_included_in_directory(project_path, cx).is_some() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
self.insert_context(context.clone(), cx);
|
||||||
|
Some(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_directory(
|
||||||
|
&mut self,
|
||||||
|
project_path: &ProjectPath,
|
||||||
|
remove_if_exists: bool,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Result<Option<AgentContextHandle>> {
|
||||||
|
let project = self.project.upgrade().context("failed to read project")?;
|
||||||
|
let entry_id = project
|
||||||
|
.read(cx)
|
||||||
|
.entry_for_path(project_path, cx)
|
||||||
|
.map(|entry| entry.id)
|
||||||
|
.context("no entry found for directory context")?;
|
||||||
|
|
||||||
|
let context_id = self.next_context_id.post_inc();
|
||||||
|
let context = AgentContextHandle::Directory(DirectoryContextHandle {
|
||||||
|
entry_id,
|
||||||
|
context_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
let context =
|
||||||
|
if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
|
||||||
|
if remove_if_exists {
|
||||||
|
self.remove_context(&context, cx);
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(existing.as_ref().clone())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.insert_context(context.clone(), cx);
|
||||||
|
Some(context)
|
||||||
|
};
|
||||||
|
|
||||||
|
anyhow::Ok(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_symbol(
|
||||||
|
&mut self,
|
||||||
|
buffer: Entity<Buffer>,
|
||||||
|
symbol: SharedString,
|
||||||
|
range: Range<Anchor>,
|
||||||
|
enclosing_range: Range<Anchor>,
|
||||||
|
remove_if_exists: bool,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> (Option<AgentContextHandle>, bool) {
|
||||||
|
let context_id = self.next_context_id.post_inc();
|
||||||
|
let context = AgentContextHandle::Symbol(SymbolContextHandle {
|
||||||
|
buffer,
|
||||||
|
symbol,
|
||||||
|
range,
|
||||||
|
enclosing_range,
|
||||||
|
context_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(key) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
|
||||||
|
let handle = if remove_if_exists {
|
||||||
|
self.remove_context(&context, cx);
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(key.as_ref().clone())
|
||||||
|
};
|
||||||
|
return (handle, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let included = self.insert_context(context.clone(), cx);
|
||||||
|
(Some(context), included)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_thread(
|
||||||
|
&mut self,
|
||||||
|
thread: Entity<agent::Thread>,
|
||||||
|
remove_if_exists: bool,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Option<AgentContextHandle> {
|
||||||
|
let context_id = self.next_context_id.post_inc();
|
||||||
|
let context = AgentContextHandle::Thread(ThreadContextHandle { thread, context_id });
|
||||||
|
|
||||||
|
if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
|
||||||
|
if remove_if_exists {
|
||||||
|
self.remove_context(&context, cx);
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(existing.as_ref().clone())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.insert_context(context.clone(), cx);
|
||||||
|
Some(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_text_thread(
|
||||||
|
&mut self,
|
||||||
|
text_thread: Entity<TextThread>,
|
||||||
|
remove_if_exists: bool,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Option<AgentContextHandle> {
|
||||||
|
let context_id = self.next_context_id.post_inc();
|
||||||
|
let context = AgentContextHandle::TextThread(TextThreadContextHandle {
|
||||||
|
text_thread,
|
||||||
|
context_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
|
||||||
|
if remove_if_exists {
|
||||||
|
self.remove_context(&context, cx);
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(existing.as_ref().clone())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.insert_context(context.clone(), cx);
|
||||||
|
Some(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_rules(
|
||||||
|
&mut self,
|
||||||
|
prompt_id: UserPromptId,
|
||||||
|
remove_if_exists: bool,
|
||||||
|
cx: &mut Context<ContextStore>,
|
||||||
|
) -> Option<AgentContextHandle> {
|
||||||
|
let context_id = self.next_context_id.post_inc();
|
||||||
|
let context = AgentContextHandle::Rules(RulesContextHandle {
|
||||||
|
prompt_id,
|
||||||
|
context_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
|
||||||
|
if remove_if_exists {
|
||||||
|
self.remove_context(&context, cx);
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(existing.as_ref().clone())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.insert_context(context.clone(), cx);
|
||||||
|
Some(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_fetched_url(
|
||||||
|
&mut self,
|
||||||
|
url: String,
|
||||||
|
text: impl Into<SharedString>,
|
||||||
|
cx: &mut Context<ContextStore>,
|
||||||
|
) -> AgentContextHandle {
|
||||||
|
let context = AgentContextHandle::FetchedUrl(FetchedUrlContext {
|
||||||
|
url: url.into(),
|
||||||
|
text: text.into(),
|
||||||
|
context_id: self.next_context_id.post_inc(),
|
||||||
|
});
|
||||||
|
|
||||||
|
self.insert_context(context.clone(), cx);
|
||||||
|
context
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_image_from_path(
|
||||||
|
&mut self,
|
||||||
|
project_path: ProjectPath,
|
||||||
|
remove_if_exists: bool,
|
||||||
|
cx: &mut Context<ContextStore>,
|
||||||
|
) -> Task<Result<Option<AgentContextHandle>>> {
|
||||||
|
let project = self.project.clone();
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
let open_image_task = project.update(cx, |project, cx| {
|
||||||
|
project.open_image(project_path.clone(), cx)
|
||||||
|
})?;
|
||||||
|
let image_item = open_image_task.await?;
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
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()),
|
||||||
|
item.image.clone(),
|
||||||
|
remove_if_exists,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_image_instance(&mut self, image: Arc<Image>, cx: &mut Context<ContextStore>) {
|
||||||
|
self.insert_image(None, None, image, false, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_image(
|
||||||
|
&mut self,
|
||||||
|
project_path: Option<ProjectPath>,
|
||||||
|
full_path: Option<String>,
|
||||||
|
image: Arc<Image>,
|
||||||
|
remove_if_exists: bool,
|
||||||
|
cx: &mut Context<ContextStore>,
|
||||||
|
) -> Option<AgentContextHandle> {
|
||||||
|
let image_task = LanguageModelImage::from_image(image.clone(), cx).shared();
|
||||||
|
let context = AgentContextHandle::Image(ImageContext {
|
||||||
|
project_path,
|
||||||
|
full_path,
|
||||||
|
original_image: image,
|
||||||
|
image_task,
|
||||||
|
context_id: self.next_context_id.post_inc(),
|
||||||
|
});
|
||||||
|
if self.has_context(&context) && remove_if_exists {
|
||||||
|
self.remove_context(&context, cx);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.insert_context(context.clone(), cx);
|
||||||
|
Some(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_selection(
|
||||||
|
&mut self,
|
||||||
|
buffer: Entity<Buffer>,
|
||||||
|
range: Range<Anchor>,
|
||||||
|
cx: &mut Context<ContextStore>,
|
||||||
|
) {
|
||||||
|
let context_id = self.next_context_id.post_inc();
|
||||||
|
let context = AgentContextHandle::Selection(SelectionContextHandle {
|
||||||
|
buffer,
|
||||||
|
range,
|
||||||
|
context_id,
|
||||||
|
});
|
||||||
|
self.insert_context(context, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_suggested_context(
|
||||||
|
&mut self,
|
||||||
|
suggested: &SuggestedContext,
|
||||||
|
cx: &mut Context<ContextStore>,
|
||||||
|
) {
|
||||||
|
match suggested {
|
||||||
|
SuggestedContext::File {
|
||||||
|
buffer,
|
||||||
|
icon_path: _,
|
||||||
|
name: _,
|
||||||
|
} => {
|
||||||
|
if let Some(buffer) = buffer.upgrade() {
|
||||||
|
let context_id = self.next_context_id.post_inc();
|
||||||
|
self.insert_context(
|
||||||
|
AgentContextHandle::File(FileContextHandle { buffer, context_id }),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
SuggestedContext::TextThread {
|
||||||
|
text_thread,
|
||||||
|
name: _,
|
||||||
|
} => {
|
||||||
|
if let Some(text_thread) = text_thread.upgrade() {
|
||||||
|
let context_id = self.next_context_id.post_inc();
|
||||||
|
self.insert_context(
|
||||||
|
AgentContextHandle::TextThread(TextThreadContextHandle {
|
||||||
|
text_thread,
|
||||||
|
context_id,
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_context(&mut self, context: AgentContextHandle, cx: &mut Context<Self>) -> bool {
|
||||||
|
match &context {
|
||||||
|
// AgentContextHandle::Thread(thread_context) => {
|
||||||
|
// if let Some(thread_store) = self.thread_store.clone() {
|
||||||
|
// thread_context.thread.update(cx, |thread, cx| {
|
||||||
|
// thread.start_generating_detailed_summary_if_needed(thread_store, cx);
|
||||||
|
// });
|
||||||
|
// self.context_thread_ids
|
||||||
|
// .insert(thread_context.thread.read(cx).id().clone());
|
||||||
|
// } else {
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
AgentContextHandle::TextThread(text_thread_context) => {
|
||||||
|
self.context_text_thread_paths
|
||||||
|
.extend(text_thread_context.text_thread.read(cx).path().cloned());
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
let inserted = self.context_set.insert(AgentContextKey(context));
|
||||||
|
if inserted {
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
inserted
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_context(&mut self, context: &AgentContextHandle, cx: &mut Context<Self>) {
|
||||||
|
if let Some((_, key)) = self
|
||||||
|
.context_set
|
||||||
|
.shift_remove_full(AgentContextKey::ref_cast(context))
|
||||||
|
{
|
||||||
|
match context {
|
||||||
|
AgentContextHandle::Thread(thread_context) => {
|
||||||
|
self.context_thread_ids
|
||||||
|
.remove(thread_context.thread.read(cx).id());
|
||||||
|
}
|
||||||
|
AgentContextHandle::TextThread(text_thread_context) => {
|
||||||
|
if let Some(path) = text_thread_context.text_thread.read(cx).path() {
|
||||||
|
self.context_text_thread_paths.remove(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
cx.emit(ContextStoreEvent::ContextRemoved(key));
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_context(&mut self, context: &AgentContextHandle) -> bool {
|
||||||
|
self.context_set
|
||||||
|
.contains(AgentContextKey::ref_cast(context))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether this file path is already included directly in the context, or if it will be
|
||||||
|
/// included in the context via a directory.
|
||||||
|
pub fn file_path_included(&self, path: &ProjectPath, cx: &App) -> Option<FileInclusion> {
|
||||||
|
let project = self.project.upgrade()?.read(cx);
|
||||||
|
self.context().find_map(|context| match context {
|
||||||
|
AgentContextHandle::File(file_context) => {
|
||||||
|
FileInclusion::check_file(file_context, path, cx)
|
||||||
|
}
|
||||||
|
AgentContextHandle::Image(image_context) => {
|
||||||
|
FileInclusion::check_image(image_context, path)
|
||||||
|
}
|
||||||
|
AgentContextHandle::Directory(directory_context) => {
|
||||||
|
FileInclusion::check_directory(directory_context, path, project, cx)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path_included_in_directory(
|
||||||
|
&self,
|
||||||
|
path: &ProjectPath,
|
||||||
|
cx: &App,
|
||||||
|
) -> Option<FileInclusion> {
|
||||||
|
let project = self.project.upgrade()?.read(cx);
|
||||||
|
self.context().find_map(|context| match context {
|
||||||
|
AgentContextHandle::Directory(directory_context) => {
|
||||||
|
FileInclusion::check_directory(directory_context, path, project, cx)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn includes_symbol(&self, symbol: &Symbol, cx: &App) -> bool {
|
||||||
|
self.context().any(|context| match context {
|
||||||
|
AgentContextHandle::Symbol(context) => {
|
||||||
|
if context.symbol != symbol.name {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let buffer = context.buffer.read(cx);
|
||||||
|
let Some(context_path) = buffer.project_path(cx) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if symbol.path != SymbolLocation::InProject(context_path) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let context_range = context.range.to_point_utf16(&buffer.snapshot());
|
||||||
|
context_range.start == symbol.range.start.0
|
||||||
|
&& context_range.end == symbol.range.end.0
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn includes_thread(&self, thread_id: &acp::SessionId) -> bool {
|
||||||
|
self.context_thread_ids.contains(thread_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn includes_text_thread(&self, path: &Arc<Path>) -> bool {
|
||||||
|
self.context_text_thread_paths.contains(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn includes_user_rules(&self, prompt_id: UserPromptId) -> bool {
|
||||||
|
self.context_set
|
||||||
|
.contains(&RulesContextHandle::lookup_key(prompt_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn includes_url(&self, url: impl Into<SharedString>) -> bool {
|
||||||
|
self.context_set
|
||||||
|
.contains(&FetchedUrlContext::lookup_key(url.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_url_context(&self, url: SharedString) -> Option<AgentContextHandle> {
|
||||||
|
self.context_set
|
||||||
|
.get(&FetchedUrlContext::lookup_key(url))
|
||||||
|
.map(|key| key.as_ref().clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn file_paths(&self, cx: &App) -> HashSet<ProjectPath> {
|
||||||
|
self.context()
|
||||||
|
.filter_map(|context| match context {
|
||||||
|
AgentContextHandle::File(file) => {
|
||||||
|
let buffer = file.buffer.read(cx);
|
||||||
|
buffer.project_path(cx)
|
||||||
|
}
|
||||||
|
AgentContextHandle::Directory(_)
|
||||||
|
| AgentContextHandle::Symbol(_)
|
||||||
|
| AgentContextHandle::Thread(_)
|
||||||
|
| AgentContextHandle::Selection(_)
|
||||||
|
| AgentContextHandle::FetchedUrl(_)
|
||||||
|
| AgentContextHandle::TextThread(_)
|
||||||
|
| AgentContextHandle::Rules(_)
|
||||||
|
| AgentContextHandle::Image(_) => None,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn thread_ids(&self) -> &HashSet<acp::SessionId> {
|
||||||
|
&self.context_thread_ids
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum SuggestedContext {
|
||||||
|
File {
|
||||||
|
name: SharedString,
|
||||||
|
icon_path: Option<SharedString>,
|
||||||
|
buffer: WeakEntity<Buffer>,
|
||||||
|
},
|
||||||
|
TextThread {
|
||||||
|
name: SharedString,
|
||||||
|
text_thread: WeakEntity<TextThread>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SuggestedContext {
|
||||||
|
pub fn name(&self) -> &SharedString {
|
||||||
|
match self {
|
||||||
|
Self::File { name, .. } => name,
|
||||||
|
Self::TextThread { name, .. } => name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn icon_path(&self) -> Option<SharedString> {
|
||||||
|
match self {
|
||||||
|
Self::File { icon_path, .. } => icon_path.clone(),
|
||||||
|
Self::TextThread { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn kind(&self) -> ContextKind {
|
||||||
|
match self {
|
||||||
|
Self::File { .. } => ContextKind::File,
|
||||||
|
Self::TextThread { .. } => ContextKind::TextThread,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum FileInclusion {
|
||||||
|
Direct,
|
||||||
|
InDirectory { full_path: PathBuf },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileInclusion {
|
||||||
|
fn check_file(file_context: &FileContextHandle, path: &ProjectPath, cx: &App) -> Option<Self> {
|
||||||
|
let file_path = file_context.buffer.read(cx).project_path(cx)?;
|
||||||
|
if path == &file_path {
|
||||||
|
Some(FileInclusion::Direct)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_image(image_context: &ImageContext, path: &ProjectPath) -> Option<Self> {
|
||||||
|
let image_path = image_context.project_path.as_ref()?;
|
||||||
|
if path == image_path {
|
||||||
|
Some(FileInclusion::Direct)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_directory(
|
||||||
|
directory_context: &DirectoryContextHandle,
|
||||||
|
path: &ProjectPath,
|
||||||
|
project: &Project,
|
||||||
|
cx: &App,
|
||||||
|
) -> Option<Self> {
|
||||||
|
let worktree = project
|
||||||
|
.worktree_for_entry(directory_context.entry_id, cx)?
|
||||||
|
.read(cx);
|
||||||
|
let entry = worktree.entry_for_id(directory_context.entry_id)?;
|
||||||
|
let directory_path = ProjectPath {
|
||||||
|
worktree_id: worktree.id(),
|
||||||
|
path: entry.path.clone(),
|
||||||
|
};
|
||||||
|
if path.starts_with(&directory_path) {
|
||||||
|
if path == &directory_path {
|
||||||
|
Some(FileInclusion::Direct)
|
||||||
|
} else {
|
||||||
|
Some(FileInclusion::InDirectory {
|
||||||
|
full_path: worktree.full_path(&entry.path),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
617
crates/agent_ui/src/context_strip.rs
Normal file
617
crates/agent_ui/src/context_strip.rs
Normal file
@@ -0,0 +1,617 @@
|
|||||||
|
use crate::{
|
||||||
|
AcceptSuggestedContext, AgentPanel, FocusDown, FocusLeft, FocusRight, FocusUp,
|
||||||
|
ModelUsageContext, RemoveAllContext, RemoveFocusedContext, ToggleContextPicker,
|
||||||
|
context_picker::ContextPicker,
|
||||||
|
inline_assistant::ContextProviders,
|
||||||
|
ui::{AddedContext, ContextPill},
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
|
context::AgentContextHandle,
|
||||||
|
context_store::{ContextStore, SuggestedContext},
|
||||||
|
};
|
||||||
|
use collections::HashSet;
|
||||||
|
use editor::Editor;
|
||||||
|
use gpui::{
|
||||||
|
App, Bounds, ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
|
Subscription, Task, WeakEntity,
|
||||||
|
};
|
||||||
|
use itertools::Itertools;
|
||||||
|
use project::ProjectItem;
|
||||||
|
use prompt_store::PromptStore;
|
||||||
|
use rope::Point;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use text::ToPoint as _;
|
||||||
|
use ui::{PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
|
||||||
|
use util::ResultExt as _;
|
||||||
|
use workspace::Workspace;
|
||||||
|
use zed_actions::assistant::OpenRulesLibrary;
|
||||||
|
|
||||||
|
pub struct ContextStrip {
|
||||||
|
context_store: Entity<ContextStore>,
|
||||||
|
context_picker: Entity<ContextPicker>,
|
||||||
|
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
suggest_context_kind: SuggestContextKind,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
prompt_store: Option<WeakEntity<PromptStore>>,
|
||||||
|
_subscriptions: Vec<Subscription>,
|
||||||
|
focused_index: Option<usize>,
|
||||||
|
children_bounds: Option<Vec<Bounds<Pixels>>>,
|
||||||
|
model_usage_context: ModelUsageContext,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContextStrip {
|
||||||
|
pub fn new(
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
inline_services: ContextProviders,
|
||||||
|
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||||
|
suggest_context_kind: SuggestContextKind,
|
||||||
|
model_usage_context: ModelUsageContext,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let context_picker = cx.new(|cx| {
|
||||||
|
ContextPicker::new(
|
||||||
|
workspace.clone(),
|
||||||
|
thread_store.clone(),
|
||||||
|
prompt_store.clone(),
|
||||||
|
context_store.downgrade(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let focus_handle = cx.focus_handle();
|
||||||
|
|
||||||
|
let subscriptions = vec![
|
||||||
|
cx.observe(&context_store, |_, _, cx| cx.notify()),
|
||||||
|
cx.subscribe_in(&context_picker, window, Self::handle_context_picker_event),
|
||||||
|
cx.on_focus(&focus_handle, window, Self::handle_focus),
|
||||||
|
cx.on_blur(&focus_handle, window, Self::handle_blur),
|
||||||
|
];
|
||||||
|
|
||||||
|
Self {
|
||||||
|
context_store: context_store.clone(),
|
||||||
|
context_picker,
|
||||||
|
context_picker_menu_handle,
|
||||||
|
focus_handle,
|
||||||
|
suggest_context_kind,
|
||||||
|
workspace,
|
||||||
|
prompt_store,
|
||||||
|
_subscriptions: subscriptions,
|
||||||
|
focused_index: None,
|
||||||
|
children_bounds: None,
|
||||||
|
model_usage_context,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether or not the context strip has items to display
|
||||||
|
pub fn has_context_items(&self, cx: &App) -> bool {
|
||||||
|
self.context_store.read(cx).context().next().is_some()
|
||||||
|
|| self.suggested_context(cx).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn added_contexts(&self, cx: &App) -> Vec<AddedContext> {
|
||||||
|
if let Some(workspace) = self.workspace.upgrade() {
|
||||||
|
let project = workspace.read(cx).project().read(cx);
|
||||||
|
let prompt_store = self.prompt_store.as_ref().and_then(|p| p.upgrade());
|
||||||
|
|
||||||
|
let current_model = self.model_usage_context.language_model(cx);
|
||||||
|
|
||||||
|
self.context_store
|
||||||
|
.read(cx)
|
||||||
|
.context()
|
||||||
|
.flat_map(|context| {
|
||||||
|
AddedContext::new_pending(
|
||||||
|
context.clone(),
|
||||||
|
prompt_store.as_ref(),
|
||||||
|
project,
|
||||||
|
current_model.as_ref(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn suggested_context(&self, cx: &App) -> Option<SuggestedContext> {
|
||||||
|
match self.suggest_context_kind {
|
||||||
|
SuggestContextKind::Thread => self.suggested_thread(cx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn suggested_thread(&self, cx: &App) -> Option<SuggestedContext> {
|
||||||
|
if !self.context_picker.read(cx).allow_threads() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let workspace = self.workspace.upgrade()?;
|
||||||
|
let panel = workspace.read(cx).panel::<AgentPanel>(cx)?.read(cx);
|
||||||
|
|
||||||
|
if let Some(active_text_thread_editor) = panel.active_text_thread_editor() {
|
||||||
|
let text_thread = active_text_thread_editor.read(cx).text_thread();
|
||||||
|
let weak_text_thread = text_thread.downgrade();
|
||||||
|
let text_thread = text_thread.read(cx);
|
||||||
|
let path = text_thread.path()?;
|
||||||
|
|
||||||
|
if self.context_store.read(cx).includes_text_thread(path) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(SuggestedContext::TextThread {
|
||||||
|
name: text_thread.summary().or_default(),
|
||||||
|
text_thread: weak_text_thread,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_context_picker_event(
|
||||||
|
&mut self,
|
||||||
|
_picker: &Entity<ContextPicker>,
|
||||||
|
_event: &DismissEvent,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
cx.emit(ContextStripEvent::PickerDismissed);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_focus(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.focused_index = self.last_pill_index();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_blur(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.focused_index = None;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_left(&mut self, _: &FocusLeft, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.focused_index = match self.focused_index {
|
||||||
|
Some(index) if index > 0 => Some(index - 1),
|
||||||
|
_ => self.last_pill_index(),
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_right(&mut self, _: &FocusRight, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let Some(last_index) = self.last_pill_index() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.focused_index = match self.focused_index {
|
||||||
|
Some(index) if index < last_index => Some(index + 1),
|
||||||
|
_ => Some(0),
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_up(&mut self, _: &FocusUp, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let Some(focused_index) = self.focused_index else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if focused_index == 0 {
|
||||||
|
return cx.emit(ContextStripEvent::BlurredUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some((focused, pills)) = self.focused_bounds(focused_index) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let iter = pills[..focused_index].iter().enumerate().rev();
|
||||||
|
self.focused_index = Self::find_best_horizontal_match(focused, iter).or(Some(0));
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_down(&mut self, _: &FocusDown, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let Some(focused_index) = self.focused_index else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let last_index = self.last_pill_index();
|
||||||
|
|
||||||
|
if self.focused_index == last_index {
|
||||||
|
return cx.emit(ContextStripEvent::BlurredDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some((focused, pills)) = self.focused_bounds(focused_index) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let iter = pills.iter().enumerate().skip(focused_index + 1);
|
||||||
|
self.focused_index = Self::find_best_horizontal_match(focused, iter).or(last_index);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focused_bounds(&self, focused: usize) -> Option<(&Bounds<Pixels>, &[Bounds<Pixels>])> {
|
||||||
|
let pill_bounds = self.pill_bounds()?;
|
||||||
|
let focused = pill_bounds.get(focused)?;
|
||||||
|
|
||||||
|
Some((focused, pill_bounds))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pill_bounds(&self) -> Option<&[Bounds<Pixels>]> {
|
||||||
|
let bounds = self.children_bounds.as_ref()?;
|
||||||
|
let eraser = if bounds.len() < 3 { 0 } else { 1 };
|
||||||
|
let pills = &bounds[1..bounds.len() - eraser];
|
||||||
|
|
||||||
|
if pills.is_empty() { None } else { Some(pills) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn last_pill_index(&self) -> Option<usize> {
|
||||||
|
Some(self.pill_bounds()?.len() - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_best_horizontal_match<'a>(
|
||||||
|
focused: &'a Bounds<Pixels>,
|
||||||
|
iter: impl Iterator<Item = (usize, &'a Bounds<Pixels>)>,
|
||||||
|
) -> Option<usize> {
|
||||||
|
let mut best = None;
|
||||||
|
|
||||||
|
let focused_left = focused.left();
|
||||||
|
let focused_right = focused.right();
|
||||||
|
|
||||||
|
for (index, probe) in iter {
|
||||||
|
if probe.origin.y == focused.origin.y {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let overlap = probe.right().min(focused_right) - probe.left().max(focused_left);
|
||||||
|
|
||||||
|
best = match best {
|
||||||
|
Some((_, prev_overlap, y)) if probe.origin.y != y || prev_overlap > overlap => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Some(_) | None => Some((index, overlap, probe.origin.y)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
best.map(|(index, _, _)| index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_context(&mut self, context: &AgentContextHandle, window: &mut Window, cx: &mut App) {
|
||||||
|
let Some(workspace) = self.workspace.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match context {
|
||||||
|
AgentContextHandle::File(file_context) => {
|
||||||
|
if let Some(project_path) = file_context.project_path(cx) {
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
workspace
|
||||||
|
.open_path(project_path, None, true, window, cx)
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentContextHandle::Directory(directory_context) => {
|
||||||
|
let entry_id = directory_context.entry_id;
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
workspace.project().update(cx, |_project, cx| {
|
||||||
|
cx.emit(project::Event::RevealInProjectPanel(entry_id));
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentContextHandle::Symbol(symbol_context) => {
|
||||||
|
let buffer = symbol_context.buffer.read(cx);
|
||||||
|
if let Some(project_path) = buffer.project_path(cx) {
|
||||||
|
let snapshot = buffer.snapshot();
|
||||||
|
let target_position = symbol_context.range.start.to_point(&snapshot);
|
||||||
|
open_editor_at_position(project_path, target_position, &workspace, window, cx)
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentContextHandle::Selection(selection_context) => {
|
||||||
|
let buffer = selection_context.buffer.read(cx);
|
||||||
|
if let Some(project_path) = buffer.project_path(cx) {
|
||||||
|
let snapshot = buffer.snapshot();
|
||||||
|
let target_position = selection_context.range.start.to_point(&snapshot);
|
||||||
|
|
||||||
|
open_editor_at_position(project_path, target_position, &workspace, window, cx)
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentContextHandle::FetchedUrl(fetched_url_context) => {
|
||||||
|
cx.open_url(&fetched_url_context.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentContextHandle::Thread(_thread_context) => {}
|
||||||
|
|
||||||
|
AgentContextHandle::TextThread(text_thread_context) => {
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||||
|
let context = text_thread_context.text_thread.clone();
|
||||||
|
window.defer(cx, move |window, cx| {
|
||||||
|
panel.update(cx, |panel, cx| {
|
||||||
|
panel.open_text_thread(context, window, cx)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentContextHandle::Rules(rules_context) => window.dispatch_action(
|
||||||
|
Box::new(OpenRulesLibrary {
|
||||||
|
prompt_to_select: Some(rules_context.prompt_id.0),
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
|
||||||
|
AgentContextHandle::Image(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_focused_context(
|
||||||
|
&mut self,
|
||||||
|
_: &RemoveFocusedContext,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
if let Some(index) = self.focused_index {
|
||||||
|
let added_contexts = self.added_contexts(cx);
|
||||||
|
let Some(context) = added_contexts.get(index) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.context_store.update(cx, |this, cx| {
|
||||||
|
this.remove_context(&context.handle, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
let is_now_empty = added_contexts.len() == 1;
|
||||||
|
if is_now_empty {
|
||||||
|
cx.emit(ContextStripEvent::BlurredEmpty);
|
||||||
|
} else {
|
||||||
|
self.focused_index = Some(index.saturating_sub(1));
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_suggested_focused(&self, added_contexts: &Vec<AddedContext>) -> bool {
|
||||||
|
// We only suggest one item after the actual context
|
||||||
|
self.focused_index == Some(added_contexts.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accept_suggested_context(
|
||||||
|
&mut self,
|
||||||
|
_: &AcceptSuggestedContext,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
if let Some(suggested) = self.suggested_context(cx)
|
||||||
|
&& self.is_suggested_focused(&self.added_contexts(cx))
|
||||||
|
{
|
||||||
|
self.add_suggested_context(&suggested, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_suggested_context(&mut self, suggested: &SuggestedContext, cx: &mut Context<Self>) {
|
||||||
|
self.context_store.update(cx, |context_store, cx| {
|
||||||
|
context_store.add_suggested_context(suggested, cx)
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Focusable for ContextStrip {
|
||||||
|
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ContextStrip {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let context_picker = self.context_picker.clone();
|
||||||
|
let focus_handle = self.focus_handle.clone();
|
||||||
|
|
||||||
|
let added_contexts = self.added_contexts(cx);
|
||||||
|
let dupe_names = added_contexts
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.name.clone())
|
||||||
|
.sorted()
|
||||||
|
.tuple_windows()
|
||||||
|
.filter(|(a, b)| a == b)
|
||||||
|
.map(|(a, _)| a)
|
||||||
|
.collect::<HashSet<SharedString>>();
|
||||||
|
let no_added_context = added_contexts.is_empty();
|
||||||
|
|
||||||
|
let suggested_context = self.suggested_context(cx).map(|suggested_context| {
|
||||||
|
(
|
||||||
|
suggested_context,
|
||||||
|
self.is_suggested_focused(&added_contexts),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.flex_wrap()
|
||||||
|
.gap_1()
|
||||||
|
.track_focus(&focus_handle)
|
||||||
|
.key_context("ContextStrip")
|
||||||
|
.on_action(cx.listener(Self::focus_up))
|
||||||
|
.on_action(cx.listener(Self::focus_right))
|
||||||
|
.on_action(cx.listener(Self::focus_down))
|
||||||
|
.on_action(cx.listener(Self::focus_left))
|
||||||
|
.on_action(cx.listener(Self::remove_focused_context))
|
||||||
|
.on_action(cx.listener(Self::accept_suggested_context))
|
||||||
|
.on_children_prepainted({
|
||||||
|
let entity = cx.entity().downgrade();
|
||||||
|
move |children_bounds, _window, cx| {
|
||||||
|
entity
|
||||||
|
.update(cx, |this, _| {
|
||||||
|
this.children_bounds = Some(children_bounds);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
PopoverMenu::new("context-picker")
|
||||||
|
.menu({
|
||||||
|
let context_picker = context_picker.clone();
|
||||||
|
move |window, cx| {
|
||||||
|
context_picker.update(cx, |this, cx| {
|
||||||
|
this.init(window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(context_picker.clone())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_open({
|
||||||
|
let context_picker = context_picker.downgrade();
|
||||||
|
Rc::new(move |window, cx| {
|
||||||
|
context_picker
|
||||||
|
.update(cx, |context_picker, cx| {
|
||||||
|
context_picker.select_first(window, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.trigger_with_tooltip(
|
||||||
|
IconButton::new("add-context", IconName::Plus)
|
||||||
|
.icon_size(IconSize::Small)
|
||||||
|
.style(ui::ButtonStyle::Filled),
|
||||||
|
{
|
||||||
|
let focus_handle = focus_handle.clone();
|
||||||
|
move |_window, cx| {
|
||||||
|
Tooltip::for_action_in(
|
||||||
|
"Add Context",
|
||||||
|
&ToggleContextPicker,
|
||||||
|
&focus_handle,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.attach(gpui::Corner::TopLeft)
|
||||||
|
.anchor(gpui::Corner::BottomLeft)
|
||||||
|
.offset(gpui::Point {
|
||||||
|
x: px(0.0),
|
||||||
|
y: px(-2.0),
|
||||||
|
})
|
||||||
|
.with_handle(self.context_picker_menu_handle.clone()),
|
||||||
|
)
|
||||||
|
.children(
|
||||||
|
added_contexts
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, added_context)| {
|
||||||
|
let name = added_context.name.clone();
|
||||||
|
let context = added_context.handle.clone();
|
||||||
|
ContextPill::added(
|
||||||
|
added_context,
|
||||||
|
dupe_names.contains(&name),
|
||||||
|
self.focused_index == Some(i),
|
||||||
|
Some({
|
||||||
|
let context = context.clone();
|
||||||
|
let context_store = self.context_store.clone();
|
||||||
|
Rc::new(cx.listener(move |_this, _event, _window, cx| {
|
||||||
|
context_store.update(cx, |this, cx| {
|
||||||
|
this.remove_context(&context, cx);
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
}))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.on_click({
|
||||||
|
Rc::new(cx.listener(move |this, event: &ClickEvent, window, cx| {
|
||||||
|
if event.click_count() > 1 {
|
||||||
|
this.open_context(&context, window, cx);
|
||||||
|
} else {
|
||||||
|
this.focused_index = Some(i);
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.when_some(suggested_context, |el, (suggested, focused)| {
|
||||||
|
el.child(
|
||||||
|
ContextPill::suggested(
|
||||||
|
suggested.name().clone(),
|
||||||
|
suggested.icon_path(),
|
||||||
|
suggested.kind(),
|
||||||
|
focused,
|
||||||
|
)
|
||||||
|
.on_click(Rc::new(cx.listener(
|
||||||
|
move |this, _event, _window, cx| {
|
||||||
|
this.add_suggested_context(&suggested, cx);
|
||||||
|
},
|
||||||
|
))),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when(!no_added_context, {
|
||||||
|
move |parent| {
|
||||||
|
parent.child(
|
||||||
|
IconButton::new("remove-all-context", IconName::Eraser)
|
||||||
|
.icon_size(IconSize::Small)
|
||||||
|
.tooltip({
|
||||||
|
let focus_handle = focus_handle.clone();
|
||||||
|
move |_window, cx| {
|
||||||
|
Tooltip::for_action_in(
|
||||||
|
"Remove All Context",
|
||||||
|
&RemoveAllContext,
|
||||||
|
&focus_handle,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_click(cx.listener({
|
||||||
|
let focus_handle = focus_handle.clone();
|
||||||
|
move |_this, _event, window, cx| {
|
||||||
|
focus_handle.dispatch_action(&RemoveAllContext, window, cx);
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ContextStripEvent {
|
||||||
|
PickerDismissed,
|
||||||
|
BlurredEmpty,
|
||||||
|
BlurredDown,
|
||||||
|
BlurredUp,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<ContextStripEvent> for ContextStrip {}
|
||||||
|
|
||||||
|
pub enum SuggestContextKind {
|
||||||
|
Thread,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_editor_at_position(
|
||||||
|
project_path: project::ProjectPath,
|
||||||
|
target_position: Point,
|
||||||
|
workspace: &Entity<Workspace>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Task<()> {
|
||||||
|
let open_task = workspace.update(cx, |workspace, cx| {
|
||||||
|
workspace.open_path(project_path, None, true, window, cx)
|
||||||
|
});
|
||||||
|
window.spawn(cx, async move |cx| {
|
||||||
|
if let Some(active_editor) = open_task
|
||||||
|
.await
|
||||||
|
.log_err()
|
||||||
|
.and_then(|item| item.downcast::<Editor>())
|
||||||
|
{
|
||||||
|
active_editor
|
||||||
|
.downgrade()
|
||||||
|
.update_in(cx, |editor, window, cx| {
|
||||||
|
editor.go_to_singleton_buffer_point(target_position, window, cx);
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
1869
crates/agent_ui/src/evals.rs
Normal file
1869
crates/agent_ui/src/evals.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,11 +4,10 @@ use std::ops::Range;
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::context::load_context;
|
|
||||||
use crate::mention_set::MentionSet;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AgentPanel,
|
AgentPanel,
|
||||||
buffer_codegen::{BufferCodegen, CodegenAlternative, CodegenEvent},
|
buffer_codegen::{BufferCodegen, CodegenAlternative, CodegenEvent},
|
||||||
|
context_store::ContextStore,
|
||||||
inline_prompt_editor::{CodegenStatus, InlineAssistId, PromptEditor, PromptEditorEvent},
|
inline_prompt_editor::{CodegenStatus, InlineAssistId, PromptEditor, PromptEditorEvent},
|
||||||
terminal_inline_assistant::TerminalInlineAssistant,
|
terminal_inline_assistant::TerminalInlineAssistant,
|
||||||
};
|
};
|
||||||
@@ -18,7 +17,6 @@ use anyhow::{Context as _, Result};
|
|||||||
use client::telemetry::Telemetry;
|
use client::telemetry::Telemetry;
|
||||||
use collections::{HashMap, HashSet, VecDeque, hash_map};
|
use collections::{HashMap, HashSet, VecDeque, hash_map};
|
||||||
use editor::EditorSnapshot;
|
use editor::EditorSnapshot;
|
||||||
use editor::MultiBufferOffset;
|
|
||||||
use editor::RowExt;
|
use editor::RowExt;
|
||||||
use editor::SelectionEffects;
|
use editor::SelectionEffects;
|
||||||
use editor::scroll::ScrollOffset;
|
use editor::scroll::ScrollOffset;
|
||||||
@@ -32,7 +30,6 @@ use editor::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use futures::FutureExt;
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task, UpdateGlobal,
|
App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task, UpdateGlobal,
|
||||||
WeakEntity, Window, point,
|
WeakEntity, Window, point,
|
||||||
@@ -91,6 +88,38 @@ enum InlineAssistTarget {
|
|||||||
Terminal(Entity<TerminalView>),
|
Terminal(Entity<TerminalView>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct ContextProviders {
|
||||||
|
pub(crate) workspace: Option<WeakEntity<Workspace>>,
|
||||||
|
pub(crate) prompt_store: Option<Entity<PromptStore>>,
|
||||||
|
pub(crate) thread_store: Option<WeakEntity<HistoryStore>>,
|
||||||
|
pub(crate) context_store: Entity<ContextStore>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContextProviders {
|
||||||
|
pub(crate) fn project(&self, cx: &mut App) -> WeakEntity<Project> {
|
||||||
|
self.context_store.read(cx).project()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn from_panel(agent_panel: &AgentPanel) -> Self {
|
||||||
|
Self {
|
||||||
|
workspace: Some(agent_panel.workspace()),
|
||||||
|
prompt_store: agent_panel.prompt_store().as_ref().cloned(),
|
||||||
|
thread_store: Some(agent_panel.thread_store().downgrade()),
|
||||||
|
context_store: agent_panel.inline_assist_context_store().clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn empty(project: WeakEntity<Project>, cx: &mut App) -> Self {
|
||||||
|
Self {
|
||||||
|
workspace: None,
|
||||||
|
prompt_store: None,
|
||||||
|
thread_store: None,
|
||||||
|
context_store: cx.new(|_| ContextStore::new(project.clone())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct InlineAssistant {
|
pub struct InlineAssistant {
|
||||||
next_assist_id: InlineAssistId,
|
next_assist_id: InlineAssistId,
|
||||||
next_assist_group_id: InlineAssistGroupId,
|
next_assist_group_id: InlineAssistGroupId,
|
||||||
@@ -216,10 +245,16 @@ impl InlineAssistant {
|
|||||||
if let Some(editor) = item.act_as::<Editor>(cx) {
|
if let Some(editor) = item.act_as::<Editor>(cx) {
|
||||||
editor.update(cx, |editor, cx| {
|
editor.update(cx, |editor, cx| {
|
||||||
if is_ai_enabled {
|
if is_ai_enabled {
|
||||||
|
let panel = workspace.read(cx).panel::<AgentPanel>(cx);
|
||||||
|
let thread_store = panel
|
||||||
|
.as_ref()
|
||||||
|
.map(|agent_panel| agent_panel.read(cx).thread_store().downgrade());
|
||||||
|
|
||||||
editor.add_code_action_provider(
|
editor.add_code_action_provider(
|
||||||
Rc::new(AssistantCodeActionProvider {
|
Rc::new(AssistantCodeActionProvider {
|
||||||
editor: cx.entity().downgrade(),
|
editor: cx.entity().downgrade(),
|
||||||
workspace: workspace.downgrade(),
|
workspace: workspace.downgrade(),
|
||||||
|
thread_store,
|
||||||
}),
|
}),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
@@ -231,6 +266,9 @@ impl InlineAssistant {
|
|||||||
editor.cancel(&Default::default(), window, cx);
|
editor.cancel(&Default::default(), window, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove the Assistant1 code action provider, as it still might be registered.
|
||||||
|
editor.remove_code_action_provider("assistant".into(), window, cx);
|
||||||
} else {
|
} else {
|
||||||
editor.remove_code_action_provider(
|
editor.remove_code_action_provider(
|
||||||
ASSISTANT_CODE_ACTION_PROVIDER_ID.into(),
|
ASSISTANT_CODE_ACTION_PROVIDER_ID.into(),
|
||||||
@@ -269,10 +307,8 @@ impl InlineAssistant {
|
|||||||
let Some(agent_panel) = workspace.panel::<AgentPanel>(cx) else {
|
let Some(agent_panel) = workspace.panel::<AgentPanel>(cx) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let agent_panel = agent_panel.read(cx);
|
|
||||||
|
|
||||||
let prompt_store = agent_panel.prompt_store().as_ref().cloned();
|
let services = ContextProviders::from_panel(agent_panel.read(cx));
|
||||||
let thread_store = agent_panel.thread_store().clone();
|
|
||||||
|
|
||||||
let handle_assist =
|
let handle_assist =
|
||||||
|window: &mut Window, cx: &mut Context<Workspace>| match inline_assist_target {
|
|window: &mut Window, cx: &mut Context<Workspace>| match inline_assist_target {
|
||||||
@@ -281,9 +317,6 @@ impl InlineAssistant {
|
|||||||
assistant.assist(
|
assistant.assist(
|
||||||
&active_editor,
|
&active_editor,
|
||||||
cx.entity().downgrade(),
|
cx.entity().downgrade(),
|
||||||
workspace.project().downgrade(),
|
|
||||||
thread_store,
|
|
||||||
prompt_store,
|
|
||||||
action.prompt.clone(),
|
action.prompt.clone(),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
@@ -295,10 +328,8 @@ impl InlineAssistant {
|
|||||||
assistant.assist(
|
assistant.assist(
|
||||||
&active_terminal,
|
&active_terminal,
|
||||||
cx.entity().downgrade(),
|
cx.entity().downgrade(),
|
||||||
workspace.project().downgrade(),
|
|
||||||
thread_store,
|
|
||||||
prompt_store,
|
|
||||||
action.prompt.clone(),
|
action.prompt.clone(),
|
||||||
|
services,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@@ -464,22 +495,22 @@ impl InlineAssistant {
|
|||||||
Some((codegen_ranges, newest_selection))
|
Some((codegen_ranges, newest_selection))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn batch_assist(
|
pub fn assist(
|
||||||
&mut self,
|
&mut self,
|
||||||
editor: &Entity<Editor>,
|
editor: &Entity<Editor>,
|
||||||
workspace: WeakEntity<Workspace>,
|
|
||||||
project: WeakEntity<Project>,
|
|
||||||
thread_store: Entity<HistoryStore>,
|
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
initial_prompt: Option<String>,
|
initial_prompt: Option<String>,
|
||||||
|
services: ContextProviders,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
codegen_ranges: &[Range<Anchor>],
|
|
||||||
newest_selection: Option<Selection<Point>>,
|
|
||||||
initial_transaction_id: Option<TransactionId>,
|
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Option<InlineAssistId> {
|
) {
|
||||||
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
|
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
|
||||||
|
|
||||||
|
let Some((codegen_ranges, newest_selection)) =
|
||||||
|
self.codegen_ranges(editor, &snapshot, window, cx)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let assist_group_id = self.next_assist_group_id.post_inc();
|
let assist_group_id = self.next_assist_group_id.post_inc();
|
||||||
let prompt_buffer = cx.new(|cx| {
|
let prompt_buffer = cx.new(|cx| {
|
||||||
MultiBuffer::singleton(
|
MultiBuffer::singleton(
|
||||||
@@ -490,41 +521,17 @@ impl InlineAssistant {
|
|||||||
|
|
||||||
let mut assists = Vec::new();
|
let mut assists = Vec::new();
|
||||||
let mut assist_to_focus = None;
|
let mut assist_to_focus = None;
|
||||||
|
|
||||||
for range in codegen_ranges {
|
for range in codegen_ranges {
|
||||||
let assist_id = self.next_assist_id.post_inc();
|
let (assist_id, prompt_block_id, end_block_id, prompt_editor) = self.single_assist(
|
||||||
let codegen = cx.new(|cx| {
|
range.clone(),
|
||||||
BufferCodegen::new(
|
editor,
|
||||||
editor.read(cx).buffer().clone(),
|
prompt_buffer.clone(),
|
||||||
range.clone(),
|
services.clone(),
|
||||||
initial_transaction_id,
|
window,
|
||||||
self.telemetry.clone(),
|
cx,
|
||||||
self.prompt_builder.clone(),
|
);
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
let editor_margins = Arc::new(Mutex::new(EditorMargins::default()));
|
if assist_to_focus.is_none() {
|
||||||
let prompt_editor = cx.new(|cx| {
|
|
||||||
PromptEditor::new_buffer(
|
|
||||||
assist_id,
|
|
||||||
editor_margins,
|
|
||||||
self.prompt_history.clone(),
|
|
||||||
prompt_buffer.clone(),
|
|
||||||
codegen.clone(),
|
|
||||||
self.fs.clone(),
|
|
||||||
thread_store.clone(),
|
|
||||||
prompt_store.clone(),
|
|
||||||
project.clone(),
|
|
||||||
workspace.clone(),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(newest_selection) = newest_selection.as_ref()
|
|
||||||
&& assist_to_focus.is_none()
|
|
||||||
{
|
|
||||||
let focus_assist = if newest_selection.reversed {
|
let focus_assist = if newest_selection.reversed {
|
||||||
range.start.to_point(&snapshot) == newest_selection.start
|
range.start.to_point(&snapshot) == newest_selection.start
|
||||||
} else {
|
} else {
|
||||||
@@ -535,12 +542,9 @@ impl InlineAssistant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let [prompt_block_id, end_block_id] =
|
|
||||||
self.insert_assist_blocks(editor, &range, &prompt_editor, cx);
|
|
||||||
|
|
||||||
assists.push((
|
assists.push((
|
||||||
assist_id,
|
assist_id,
|
||||||
range.clone(),
|
range,
|
||||||
prompt_editor,
|
prompt_editor,
|
||||||
prompt_block_id,
|
prompt_block_id,
|
||||||
end_block_id,
|
end_block_id,
|
||||||
@@ -551,15 +555,6 @@ impl InlineAssistant {
|
|||||||
.assists_by_editor
|
.assists_by_editor
|
||||||
.entry(editor.downgrade())
|
.entry(editor.downgrade())
|
||||||
.or_insert_with(|| EditorInlineAssists::new(editor, window, cx));
|
.or_insert_with(|| EditorInlineAssists::new(editor, window, cx));
|
||||||
|
|
||||||
let assist_to_focus = if let Some(focus_id) = assist_to_focus {
|
|
||||||
Some(focus_id)
|
|
||||||
} else if assists.len() >= 1 {
|
|
||||||
Some(assists[0].0)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut assist_group = InlineAssistGroup::new();
|
let mut assist_group = InlineAssistGroup::new();
|
||||||
for (assist_id, range, prompt_editor, prompt_block_id, end_block_id) in assists {
|
for (assist_id, range, prompt_editor, prompt_block_id, end_block_id) in assists {
|
||||||
let codegen = prompt_editor.read(cx).codegen().clone();
|
let codegen = prompt_editor.read(cx).codegen().clone();
|
||||||
@@ -583,63 +578,86 @@ impl InlineAssistant {
|
|||||||
assist_group.assist_ids.push(assist_id);
|
assist_group.assist_ids.push(assist_id);
|
||||||
editor_assists.assist_ids.push(assist_id);
|
editor_assists.assist_ids.push(assist_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.assist_groups.insert(assist_group_id, assist_group);
|
self.assist_groups.insert(assist_group_id, assist_group);
|
||||||
|
|
||||||
assist_to_focus
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn assist(
|
|
||||||
&mut self,
|
|
||||||
editor: &Entity<Editor>,
|
|
||||||
workspace: WeakEntity<Workspace>,
|
|
||||||
project: WeakEntity<Project>,
|
|
||||||
thread_store: Entity<HistoryStore>,
|
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
initial_prompt: Option<String>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) {
|
|
||||||
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
|
|
||||||
|
|
||||||
let Some((codegen_ranges, newest_selection)) =
|
|
||||||
self.codegen_ranges(editor, &snapshot, window, cx)
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let assist_to_focus = self.batch_assist(
|
|
||||||
editor,
|
|
||||||
workspace,
|
|
||||||
project,
|
|
||||||
thread_store,
|
|
||||||
prompt_store,
|
|
||||||
initial_prompt,
|
|
||||||
window,
|
|
||||||
&codegen_ranges,
|
|
||||||
Some(newest_selection),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(assist_id) = assist_to_focus {
|
if let Some(assist_id) = assist_to_focus {
|
||||||
self.focus_assist(assist_id, window, cx);
|
self.focus_assist(assist_id, window, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn single_assist(
|
||||||
|
&mut self,
|
||||||
|
range: Range<Anchor>,
|
||||||
|
editor: &Entity<Editor>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
prompt_buffer: Entity<MultiBuffer>,
|
||||||
|
services: ContextProviders,
|
||||||
|
project: &WeakEntity<Project>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> (
|
||||||
|
InlineAssistId,
|
||||||
|
CustomBlockId,
|
||||||
|
CustomBlockId,
|
||||||
|
Entity<PromptEditor<BufferCodegen>>,
|
||||||
|
) {
|
||||||
|
let assist_id = self.next_assist_id.post_inc();
|
||||||
|
let codegen = cx.new(|cx| {
|
||||||
|
BufferCodegen::new(
|
||||||
|
editor.read(cx).buffer().clone(),
|
||||||
|
range.clone(),
|
||||||
|
None,
|
||||||
|
context_store.clone(),
|
||||||
|
project.clone(),
|
||||||
|
prompt_store.clone(),
|
||||||
|
self.telemetry.clone(),
|
||||||
|
self.prompt_builder.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let editor_margins = Arc::new(Mutex::new(EditorMargins::default()));
|
||||||
|
let prompt_editor = cx.new(|cx| {
|
||||||
|
PromptEditor::new_buffer(
|
||||||
|
assist_id,
|
||||||
|
editor_margins,
|
||||||
|
self.prompt_history.clone(),
|
||||||
|
prompt_buffer.clone(),
|
||||||
|
codegen.clone(),
|
||||||
|
self.fs.clone(),
|
||||||
|
context_store.clone(),
|
||||||
|
workspace.clone(),
|
||||||
|
thread_store.clone(),
|
||||||
|
prompt_store.as_ref().map(|s| s.downgrade()),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let [prompt_block_id, end_block_id] =
|
||||||
|
self.insert_assist_blocks(editor, &range, &prompt_editor, cx);
|
||||||
|
|
||||||
|
(assist_id, prompt_block_id, end_block_id, prompt_editor)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn suggest_assist(
|
pub fn suggest_assist(
|
||||||
&mut self,
|
&mut self,
|
||||||
editor: &Entity<Editor>,
|
editor: &Entity<Editor>,
|
||||||
mut range: Range<Anchor>,
|
mut range: Range<Anchor>,
|
||||||
initial_prompt: String,
|
prompt_buffer: Entity<MultiBuffer>,
|
||||||
initial_transaction_id: Option<TransactionId>,
|
initial_transaction_id: Option<TransactionId>,
|
||||||
focus: bool,
|
focus: bool,
|
||||||
workspace: Entity<Workspace>,
|
workspace: Entity<Workspace>,
|
||||||
thread_store: Entity<HistoryStore>,
|
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
|
thread_store: Option<WeakEntity<HistoryStore>>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> InlineAssistId {
|
) -> InlineAssistId {
|
||||||
|
/// !!!!!!!!
|
||||||
|
let assist_group_id = self.next_assist_group_id.post_inc();
|
||||||
|
|
||||||
|
let assist_id = self.next_assist_id.post_inc();
|
||||||
|
|
||||||
let buffer = editor.read(cx).buffer().clone();
|
let buffer = editor.read(cx).buffer().clone();
|
||||||
{
|
{
|
||||||
let snapshot = buffer.read(cx).read(cx);
|
let snapshot = buffer.read(cx).read(cx);
|
||||||
@@ -648,22 +666,68 @@ impl InlineAssistant {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let project = workspace.read(cx).project().downgrade();
|
let project = workspace.read(cx).project().downgrade();
|
||||||
|
let context_store = cx.new(|_cx| ContextStore::new(project.clone()));
|
||||||
|
|
||||||
let assist_id = self
|
let codegen = cx.new(|cx| {
|
||||||
.batch_assist(
|
BufferCodegen::new(
|
||||||
editor,
|
editor.read(cx).buffer().clone(),
|
||||||
workspace.downgrade(),
|
range.clone(),
|
||||||
project,
|
|
||||||
thread_store,
|
|
||||||
prompt_store,
|
|
||||||
Some(initial_prompt),
|
|
||||||
window,
|
|
||||||
&[range],
|
|
||||||
None,
|
|
||||||
initial_transaction_id,
|
initial_transaction_id,
|
||||||
|
context_store.clone(),
|
||||||
|
project,
|
||||||
|
prompt_store.clone(),
|
||||||
|
self.telemetry.clone(),
|
||||||
|
self.prompt_builder.clone(),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.expect("batch_assist returns an id if there's only one range");
|
});
|
||||||
|
|
||||||
|
let editor_margins = Arc::new(Mutex::new(EditorMargins::default()));
|
||||||
|
let prompt_editor = cx.new(|cx| {
|
||||||
|
PromptEditor::new_buffer(
|
||||||
|
assist_id,
|
||||||
|
editor_margins,
|
||||||
|
self.prompt_history.clone(),
|
||||||
|
prompt_buffer.clone(),
|
||||||
|
codegen.clone(),
|
||||||
|
self.fs.clone(),
|
||||||
|
context_store,
|
||||||
|
workspace.downgrade(),
|
||||||
|
thread_store,
|
||||||
|
prompt_store.map(|s| s.downgrade()),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let [prompt_block_id, end_block_id] =
|
||||||
|
self.insert_assist_blocks(editor, &range, &prompt_editor, cx);
|
||||||
|
|
||||||
|
let editor_assists = self
|
||||||
|
.assists_by_editor
|
||||||
|
.entry(editor.downgrade())
|
||||||
|
.or_insert_with(|| EditorInlineAssists::new(editor, window, cx));
|
||||||
|
|
||||||
|
let mut assist_group = InlineAssistGroup::new();
|
||||||
|
self.assists.insert(
|
||||||
|
assist_id,
|
||||||
|
InlineAssist::new(
|
||||||
|
assist_id,
|
||||||
|
assist_group_id,
|
||||||
|
editor,
|
||||||
|
&prompt_editor,
|
||||||
|
prompt_block_id,
|
||||||
|
end_block_id,
|
||||||
|
range,
|
||||||
|
codegen.clone(),
|
||||||
|
workspace.downgrade(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assist_group.assist_ids.push(assist_id);
|
||||||
|
editor_assists.assist_ids.push(assist_id);
|
||||||
|
self.assist_groups.insert(assist_group_id, assist_group);
|
||||||
|
|
||||||
if focus {
|
if focus {
|
||||||
self.focus_assist(assist_id, window, cx);
|
self.focus_assist(assist_id, window, cx);
|
||||||
@@ -804,7 +868,7 @@ impl InlineAssistant {
|
|||||||
(
|
(
|
||||||
editor
|
editor
|
||||||
.selections
|
.selections
|
||||||
.newest::<MultiBufferOffset>(&editor.display_snapshot(cx)),
|
.newest::<usize>(&editor.display_snapshot(cx)),
|
||||||
editor.buffer().read(cx).snapshot(cx),
|
editor.buffer().read(cx).snapshot(cx),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
@@ -837,7 +901,7 @@ impl InlineAssistant {
|
|||||||
(
|
(
|
||||||
editor
|
editor
|
||||||
.selections
|
.selections
|
||||||
.newest::<MultiBufferOffset>(&editor.display_snapshot(cx)),
|
.newest::<usize>(&editor.display_snapshot(cx)),
|
||||||
editor.buffer().read(cx).snapshot(cx),
|
editor.buffer().read(cx).snapshot(cx),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
@@ -854,14 +918,12 @@ impl InlineAssistant {
|
|||||||
} else {
|
} else {
|
||||||
let distance_from_selection = assist_range
|
let distance_from_selection = assist_range
|
||||||
.start
|
.start
|
||||||
.0
|
.abs_diff(selection.start)
|
||||||
.abs_diff(selection.start.0)
|
.min(assist_range.start.abs_diff(selection.end))
|
||||||
.min(assist_range.start.0.abs_diff(selection.end.0))
|
|
||||||
+ assist_range
|
+ assist_range
|
||||||
.end
|
.end
|
||||||
.0
|
.abs_diff(selection.start)
|
||||||
.abs_diff(selection.start.0)
|
.min(assist_range.end.abs_diff(selection.end));
|
||||||
.min(assist_range.end.0.abs_diff(selection.end.0));
|
|
||||||
match closest_assist_fallback {
|
match closest_assist_fallback {
|
||||||
Some((_, old_distance)) => {
|
Some((_, old_distance)) => {
|
||||||
if distance_from_selection < old_distance {
|
if distance_from_selection < old_distance {
|
||||||
@@ -938,7 +1000,7 @@ impl InlineAssistant {
|
|||||||
EditorEvent::Edited { transaction_id } => {
|
EditorEvent::Edited { transaction_id } => {
|
||||||
let buffer = editor.read(cx).buffer().read(cx);
|
let buffer = editor.read(cx).buffer().read(cx);
|
||||||
let edited_ranges =
|
let edited_ranges =
|
||||||
buffer.edited_ranges_for_transaction::<MultiBufferOffset>(*transaction_id, cx);
|
buffer.edited_ranges_for_transaction::<usize>(*transaction_id, cx);
|
||||||
let snapshot = buffer.snapshot(cx);
|
let snapshot = buffer.snapshot(cx);
|
||||||
|
|
||||||
for assist_id in editor_assists.assist_ids.clone() {
|
for assist_id in editor_assists.assist_ids.clone() {
|
||||||
@@ -1277,8 +1339,7 @@ impl InlineAssistant {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some((user_prompt, mention_set)) = assist.user_prompt(cx).zip(assist.mention_set(cx))
|
let Some(user_prompt) = assist.user_prompt(cx) else {
|
||||||
else {
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1294,12 +1355,9 @@ impl InlineAssistant {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let context_task = load_context(&mention_set, cx).shared();
|
|
||||||
assist
|
assist
|
||||||
.codegen
|
.codegen
|
||||||
.update(cx, |codegen, cx| {
|
.update(cx, |codegen, cx| codegen.start(model, user_prompt, cx))
|
||||||
codegen.start(model, user_prompt, context_task, cx)
|
|
||||||
})
|
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1445,7 +1503,6 @@ impl InlineAssistant {
|
|||||||
multi_buffer.update(cx, |multi_buffer, cx| {
|
multi_buffer.update(cx, |multi_buffer, cx| {
|
||||||
multi_buffer.push_excerpts(
|
multi_buffer.push_excerpts(
|
||||||
old_buffer.clone(),
|
old_buffer.clone(),
|
||||||
// todo(lw): buffer_start and buffer_end might come from different snapshots!
|
|
||||||
Some(ExcerptRange::new(buffer_start..buffer_end)),
|
Some(ExcerptRange::new(buffer_start..buffer_end)),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
@@ -1766,11 +1823,6 @@ impl InlineAssist {
|
|||||||
let decorations = self.decorations.as_ref()?;
|
let decorations = self.decorations.as_ref()?;
|
||||||
Some(decorations.prompt_editor.read(cx).prompt(cx))
|
Some(decorations.prompt_editor.read(cx).prompt(cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mention_set(&self, cx: &App) -> Option<Entity<MentionSet>> {
|
|
||||||
let decorations = self.decorations.as_ref()?;
|
|
||||||
Some(decorations.prompt_editor.read(cx).mention_set().clone())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct InlineAssistDecorations {
|
struct InlineAssistDecorations {
|
||||||
@@ -1783,9 +1835,10 @@ struct InlineAssistDecorations {
|
|||||||
struct AssistantCodeActionProvider {
|
struct AssistantCodeActionProvider {
|
||||||
editor: WeakEntity<Editor>,
|
editor: WeakEntity<Editor>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
|
thread_store: Option<WeakEntity<HistoryStore>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const ASSISTANT_CODE_ACTION_PROVIDER_ID: &str = "assistant";
|
const ASSISTANT_CODE_ACTION_PROVIDER_ID: &str = "assistant2";
|
||||||
|
|
||||||
impl CodeActionProvider for AssistantCodeActionProvider {
|
impl CodeActionProvider for AssistantCodeActionProvider {
|
||||||
fn id(&self) -> Arc<str> {
|
fn id(&self) -> Arc<str> {
|
||||||
@@ -1853,20 +1906,10 @@ impl CodeActionProvider for AssistantCodeActionProvider {
|
|||||||
) -> Task<Result<ProjectTransaction>> {
|
) -> Task<Result<ProjectTransaction>> {
|
||||||
let editor = self.editor.clone();
|
let editor = self.editor.clone();
|
||||||
let workspace = self.workspace.clone();
|
let workspace = self.workspace.clone();
|
||||||
|
let thread_store = self.thread_store.clone();
|
||||||
let prompt_store = PromptStore::global(cx);
|
let prompt_store = PromptStore::global(cx);
|
||||||
window.spawn(cx, async move |cx| {
|
window.spawn(cx, async move |cx| {
|
||||||
let workspace = workspace.upgrade().context("workspace was released")?;
|
let workspace = workspace.upgrade().context("workspace was released")?;
|
||||||
let thread_store = cx.update(|_window, cx| {
|
|
||||||
anyhow::Ok(
|
|
||||||
workspace
|
|
||||||
.read(cx)
|
|
||||||
.panel::<AgentPanel>(cx)
|
|
||||||
.context("missing agent panel")?
|
|
||||||
.read(cx)
|
|
||||||
.thread_store()
|
|
||||||
.clone(),
|
|
||||||
)
|
|
||||||
})??;
|
|
||||||
let editor = editor.upgrade().context("editor was released")?;
|
let editor = editor.upgrade().context("editor was released")?;
|
||||||
let range = editor
|
let range = editor
|
||||||
.update(cx, |editor, cx| {
|
.update(cx, |editor, cx| {
|
||||||
@@ -1901,16 +1944,20 @@ impl CodeActionProvider for AssistantCodeActionProvider {
|
|||||||
.context("invalid range")?;
|
.context("invalid range")?;
|
||||||
|
|
||||||
let prompt_store = prompt_store.await.ok();
|
let prompt_store = prompt_store.await.ok();
|
||||||
|
|
||||||
|
const PROMPT: &'static str = "Fix Diagnostics";
|
||||||
cx.update_global(|assistant: &mut InlineAssistant, window, cx| {
|
cx.update_global(|assistant: &mut InlineAssistant, window, cx| {
|
||||||
|
let prompt_buffer = cx.new(|cx| Buffer::local(PROMPT, cx));
|
||||||
|
let prompt_buffer = cx.new(|cx| MultiBuffer::singleton(prompt_buffer, cx));
|
||||||
let assist_id = assistant.suggest_assist(
|
let assist_id = assistant.suggest_assist(
|
||||||
&editor,
|
&editor,
|
||||||
range,
|
range,
|
||||||
"Fix Diagnostics".into(),
|
prompt_buffer,
|
||||||
None,
|
None,
|
||||||
true,
|
true,
|
||||||
workspace,
|
workspace,
|
||||||
thread_store,
|
|
||||||
prompt_store,
|
prompt_store,
|
||||||
|
thread_store,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
use agent::HistoryStore;
|
use agent::HistoryStore;
|
||||||
use collections::{HashMap, VecDeque};
|
use collections::{HashMap, VecDeque};
|
||||||
use editor::actions::Paste;
|
use editor::actions::Paste;
|
||||||
use editor::code_context_menus::CodeContextMenu;
|
|
||||||
use editor::display_map::{CreaseId, EditorMargins};
|
use editor::display_map::{CreaseId, EditorMargins};
|
||||||
use editor::{AnchorRangeExt as _, MultiBufferOffset, ToOffset as _};
|
use editor::{Addon, AnchorRangeExt as _};
|
||||||
use editor::{
|
use editor::{
|
||||||
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
|
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
|
||||||
actions::{MoveDown, MoveUp},
|
actions::{MoveDown, MoveUp},
|
||||||
};
|
};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, Context, CursorStyle, Entity, EventEmitter, FocusHandle, Focusable,
|
AnyElement, App, ClipboardEntry, Context, CursorStyle, Entity, EventEmitter, FocusHandle,
|
||||||
Subscription, TextStyle, WeakEntity, Window,
|
Focusable, Subscription, TextStyle, WeakEntity, Window,
|
||||||
};
|
};
|
||||||
use language_model::{LanguageModel, LanguageModelRegistry};
|
use language_model::{LanguageModel, LanguageModelRegistry};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::Project;
|
|
||||||
use prompt_store::PromptStore;
|
use prompt_store::PromptStore;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::cmp;
|
use std::cmp;
|
||||||
@@ -30,21 +28,23 @@ use zed_actions::agent::ToggleModelSelector;
|
|||||||
|
|
||||||
use crate::agent_model_selector::AgentModelSelector;
|
use crate::agent_model_selector::AgentModelSelector;
|
||||||
use crate::buffer_codegen::BufferCodegen;
|
use crate::buffer_codegen::BufferCodegen;
|
||||||
use crate::completion_provider::{
|
use crate::context::{AgentContextHandle, AgentContextKey};
|
||||||
PromptCompletionProvider, PromptCompletionProviderDelegate, PromptContextType,
|
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
|
||||||
};
|
use crate::context_store::{ContextStore, ContextStoreEvent};
|
||||||
use crate::mention_set::paste_images_as_context;
|
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
||||||
use crate::mention_set::{MentionSet, crease_for_mention};
|
use crate::inline_assistant::ContextProviders;
|
||||||
use crate::terminal_codegen::TerminalCodegen;
|
use crate::terminal_codegen::TerminalCodegen;
|
||||||
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext};
|
use crate::{
|
||||||
|
CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext, RemoveAllContext,
|
||||||
|
ToggleContextPicker,
|
||||||
|
};
|
||||||
|
|
||||||
pub struct PromptEditor<T> {
|
pub struct PromptEditor<T> {
|
||||||
pub editor: Entity<Editor>,
|
pub editor: Entity<Editor>,
|
||||||
mode: PromptEditorMode,
|
mode: PromptEditorMode,
|
||||||
mention_set: Entity<MentionSet>,
|
context_store: Entity<ContextStore>,
|
||||||
history_store: Entity<HistoryStore>,
|
context_strip: Entity<ContextStrip>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||||
workspace: WeakEntity<Workspace>,
|
|
||||||
model_selector: Entity<AgentModelSelector>,
|
model_selector: Entity<AgentModelSelector>,
|
||||||
edited_since_done: bool,
|
edited_since_done: bool,
|
||||||
prompt_history: VecDeque<String>,
|
prompt_history: VecDeque<String>,
|
||||||
@@ -52,6 +52,7 @@ pub struct PromptEditor<T> {
|
|||||||
pending_prompt: String,
|
pending_prompt: String,
|
||||||
_codegen_subscription: Subscription,
|
_codegen_subscription: Subscription,
|
||||||
editor_subscriptions: Vec<Subscription>,
|
editor_subscriptions: Vec<Subscription>,
|
||||||
|
_context_strip_subscription: Subscription,
|
||||||
show_rate_limit_notice: bool,
|
show_rate_limit_notice: bool,
|
||||||
_phantom: std::marker::PhantomData<T>,
|
_phantom: std::marker::PhantomData<T>,
|
||||||
}
|
}
|
||||||
@@ -98,19 +99,6 @@ impl<T: 'static> Render for PromptEditor<T> {
|
|||||||
|
|
||||||
buttons.extend(self.render_buttons(window, cx));
|
buttons.extend(self.render_buttons(window, cx));
|
||||||
|
|
||||||
let menu_visible = self.is_completions_menu_visible(cx);
|
|
||||||
let add_context_button = IconButton::new("add-context", IconName::AtSign)
|
|
||||||
.icon_size(IconSize::Small)
|
|
||||||
.icon_color(Color::Muted)
|
|
||||||
.when(!menu_visible, |this| {
|
|
||||||
this.tooltip(move |_window, cx| {
|
|
||||||
Tooltip::with_meta("Add Context", None, "Or type @ to include context", cx)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
|
||||||
this.trigger_completion_menu(window, cx);
|
|
||||||
}));
|
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.key_context("PromptEditor")
|
.key_context("PromptEditor")
|
||||||
.capture_action(cx.listener(Self::paste))
|
.capture_action(cx.listener(Self::paste))
|
||||||
@@ -127,6 +115,7 @@ impl<T: 'static> Render for PromptEditor<T> {
|
|||||||
h_flex()
|
h_flex()
|
||||||
.items_start()
|
.items_start()
|
||||||
.cursor(CursorStyle::Arrow)
|
.cursor(CursorStyle::Arrow)
|
||||||
|
.on_action(cx.listener(Self::toggle_context_picker))
|
||||||
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
|
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
|
||||||
this.model_selector
|
this.model_selector
|
||||||
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
|
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
|
||||||
@@ -135,6 +124,7 @@ impl<T: 'static> Render for PromptEditor<T> {
|
|||||||
.on_action(cx.listener(Self::cancel))
|
.on_action(cx.listener(Self::cancel))
|
||||||
.on_action(cx.listener(Self::move_up))
|
.on_action(cx.listener(Self::move_up))
|
||||||
.on_action(cx.listener(Self::move_down))
|
.on_action(cx.listener(Self::move_down))
|
||||||
|
.on_action(cx.listener(Self::remove_all_context))
|
||||||
.capture_action(cx.listener(Self::cycle_prev))
|
.capture_action(cx.listener(Self::cycle_prev))
|
||||||
.capture_action(cx.listener(Self::cycle_next))
|
.capture_action(cx.listener(Self::cycle_next))
|
||||||
.child(
|
.child(
|
||||||
@@ -193,7 +183,7 @@ impl<T: 'static> Render for PromptEditor<T> {
|
|||||||
.pl_1()
|
.pl_1()
|
||||||
.items_start()
|
.items_start()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.child(add_context_button)
|
.child(self.context_strip.clone())
|
||||||
.child(self.model_selector.clone()),
|
.child(self.model_selector.clone()),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -225,19 +215,6 @@ impl<T: 'static> PromptEditor<T> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assign_completion_provider(&mut self, cx: &mut Context<Self>) {
|
|
||||||
self.editor.update(cx, |editor, cx| {
|
|
||||||
editor.set_completion_provider(Some(Rc::new(PromptCompletionProvider::new(
|
|
||||||
PromptEditorCompletionProviderDelegate,
|
|
||||||
cx.weak_entity(),
|
|
||||||
self.mention_set.clone(),
|
|
||||||
self.history_store.clone(),
|
|
||||||
self.prompt_store.clone(),
|
|
||||||
self.workspace.clone(),
|
|
||||||
))));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_show_cursor_when_unfocused(
|
pub fn set_show_cursor_when_unfocused(
|
||||||
&mut self,
|
&mut self,
|
||||||
show_cursor_when_unfocused: bool,
|
show_cursor_when_unfocused: bool,
|
||||||
@@ -250,40 +227,27 @@ impl<T: 'static> PromptEditor<T> {
|
|||||||
|
|
||||||
pub fn unlink(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn unlink(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let prompt = self.prompt(cx);
|
let prompt = self.prompt(cx);
|
||||||
let existing_creases = self.editor.update(cx, |editor, cx| {
|
let existing_creases = self.editor.update(cx, extract_message_creases);
|
||||||
extract_message_creases(editor, &self.mention_set, window, cx)
|
|
||||||
});
|
|
||||||
let focus = self.editor.focus_handle(cx).contains_focused(window, cx);
|
let focus = self.editor.focus_handle(cx).contains_focused(window, cx);
|
||||||
let mut creases = vec![];
|
|
||||||
self.editor = cx.new(|cx| {
|
self.editor = cx.new(|cx| {
|
||||||
let mut editor = Editor::auto_height(1, Self::MAX_LINES as usize, window, cx);
|
let mut editor = Editor::auto_height(1, Self::MAX_LINES as usize, window, cx);
|
||||||
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
|
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
|
||||||
editor.set_placeholder_text("Add a prompt…", window, cx);
|
editor.set_placeholder_text("Add a prompt…", window, cx);
|
||||||
editor.set_text(prompt, window, cx);
|
editor.set_text(prompt, window, cx);
|
||||||
creases = insert_message_creases(&mut editor, &existing_creases, window, cx);
|
insert_message_creases(
|
||||||
|
&mut editor,
|
||||||
|
&existing_creases,
|
||||||
|
&self.context_store,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
if focus {
|
if focus {
|
||||||
window.focus(&editor.focus_handle(cx));
|
window.focus(&editor.focus_handle(cx));
|
||||||
}
|
}
|
||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
|
|
||||||
self.mention_set.update(cx, |mention_set, _cx| {
|
|
||||||
debug_assert_eq!(
|
|
||||||
creases.len(),
|
|
||||||
mention_set.creases().len(),
|
|
||||||
"Missing creases"
|
|
||||||
);
|
|
||||||
|
|
||||||
let mentions = mention_set
|
|
||||||
.clear()
|
|
||||||
.zip(creases)
|
|
||||||
.map(|((_, value), id)| (id, value))
|
|
||||||
.collect::<HashMap<_, _>>();
|
|
||||||
mention_set.set_mentions(mentions);
|
|
||||||
});
|
|
||||||
|
|
||||||
self.assign_completion_provider(cx);
|
|
||||||
self.subscribe_to_editor(window, cx);
|
self.subscribe_to_editor(window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,29 +275,43 @@ impl<T: 'static> PromptEditor<T> {
|
|||||||
self.editor.read(cx).text(cx)
|
self.editor.read(cx).text(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
|
fn paste(&mut self, _: &Paste, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
if inline_assistant_model_supports_images(cx)
|
let images = cx
|
||||||
&& let Some(task) =
|
.read_from_clipboard()
|
||||||
paste_images_as_context(self.editor.clone(), self.mention_set.clone(), window, cx)
|
.map(|item| {
|
||||||
{
|
item.into_entries()
|
||||||
task.detach();
|
.filter_map(|entry| {
|
||||||
|
if let ClipboardEntry::Image(image) = entry {
|
||||||
|
Some(image)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if images.is_empty() {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
cx.stop_propagation();
|
||||||
|
|
||||||
|
self.context_store.update(cx, |store, cx| {
|
||||||
|
for image in images {
|
||||||
|
store.add_image_instance(Arc::new(image), cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_prompt_editor_events(
|
fn handle_prompt_editor_events(
|
||||||
&mut self,
|
&mut self,
|
||||||
editor: &Entity<Editor>,
|
_: &Entity<Editor>,
|
||||||
event: &EditorEvent,
|
event: &EditorEvent,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
EditorEvent::Edited { .. } => {
|
EditorEvent::Edited { .. } => {
|
||||||
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
|
|
||||||
|
|
||||||
self.mention_set
|
|
||||||
.update(cx, |mention_set, _cx| mention_set.remove_invalid(&snapshot));
|
|
||||||
|
|
||||||
if let Some(workspace) = window.root::<Workspace>().flatten() {
|
if let Some(workspace) = window.root::<Workspace>().flatten() {
|
||||||
workspace.update(cx, |workspace, cx| {
|
workspace.update(cx, |workspace, cx| {
|
||||||
let is_via_ssh = workspace.project().read(cx).is_via_remote_server();
|
let is_via_ssh = workspace.project().read(cx).is_via_remote_server();
|
||||||
@@ -344,7 +322,7 @@ impl<T: 'static> PromptEditor<T> {
|
|||||||
.log_edit_event("inline assist", is_via_ssh);
|
.log_edit_event("inline assist", is_via_ssh);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let prompt = snapshot.text();
|
let prompt = self.editor.read(cx).text(cx);
|
||||||
if self
|
if self
|
||||||
.prompt_history_ix
|
.prompt_history_ix
|
||||||
.is_none_or(|ix| self.prompt_history[ix] != prompt)
|
.is_none_or(|ix| self.prompt_history[ix] != prompt)
|
||||||
@@ -366,44 +344,23 @@ impl<T: 'static> PromptEditor<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_completions_menu_visible(&self, cx: &App) -> bool {
|
fn toggle_context_picker(
|
||||||
self.editor
|
&mut self,
|
||||||
.read(cx)
|
_: &ToggleContextPicker,
|
||||||
.context_menu()
|
window: &mut Window,
|
||||||
.borrow()
|
cx: &mut Context<Self>,
|
||||||
.as_ref()
|
) {
|
||||||
.is_some_and(|menu| matches!(menu, CodeContextMenu::Completions(_)) && menu.visible())
|
self.context_picker_menu_handle.toggle(window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn trigger_completion_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn remove_all_context(
|
||||||
self.editor.update(cx, |editor, cx| {
|
&mut self,
|
||||||
let menu_is_open = editor.context_menu().borrow().as_ref().is_some_and(|menu| {
|
_: &RemoveAllContext,
|
||||||
matches!(menu, CodeContextMenu::Completions(_)) && menu.visible()
|
_window: &mut Window,
|
||||||
});
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
let has_at_sign = {
|
self.context_store.update(cx, |store, cx| store.clear(cx));
|
||||||
let snapshot = editor.display_snapshot(cx);
|
cx.notify();
|
||||||
let cursor = editor.selections.newest::<text::Point>(&snapshot).head();
|
|
||||||
let offset = cursor.to_offset(&snapshot);
|
|
||||||
if offset.0 > 0 {
|
|
||||||
snapshot
|
|
||||||
.buffer_snapshot()
|
|
||||||
.reversed_chars_at(offset)
|
|
||||||
.next()
|
|
||||||
.map(|sign| sign == '@')
|
|
||||||
.unwrap_or(false)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if menu_is_open && has_at_sign {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
editor.insert("@", window, cx);
|
|
||||||
editor.show_completions(&editor::actions::ShowCompletions, window, cx);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cancel(
|
fn cancel(
|
||||||
@@ -478,6 +435,8 @@ impl<T: 'static> PromptEditor<T> {
|
|||||||
editor.move_to_end(&Default::default(), window, cx)
|
editor.move_to_end(&Default::default(), window, cx)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else if self.context_strip.read(cx).has_context_items(cx) {
|
||||||
|
self.context_strip.focus_handle(cx).focus(window);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -751,7 +710,6 @@ impl<T: 'static> PromptEditor<T> {
|
|||||||
EditorStyle {
|
EditorStyle {
|
||||||
background: colors.editor_background,
|
background: colors.editor_background,
|
||||||
local_player: cx.theme().players().local(),
|
local_player: cx.theme().players().local(),
|
||||||
syntax: cx.theme().syntax().clone(),
|
|
||||||
text: text_style,
|
text: text_style,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
@@ -759,6 +717,21 @@ impl<T: 'static> PromptEditor<T> {
|
|||||||
})
|
})
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_context_strip_event(
|
||||||
|
&mut self,
|
||||||
|
_context_strip: &Entity<ContextStrip>,
|
||||||
|
event: &ContextStripEvent,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
match event {
|
||||||
|
ContextStripEvent::PickerDismissed
|
||||||
|
| ContextStripEvent::BlurredEmpty
|
||||||
|
| ContextStripEvent::BlurredUp => self.editor.focus_handle(cx).focus(window),
|
||||||
|
ContextStripEvent::BlurredDown => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum PromptEditorMode {
|
pub enum PromptEditorMode {
|
||||||
@@ -793,36 +766,6 @@ impl InlineAssistId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PromptEditorCompletionProviderDelegate;
|
|
||||||
|
|
||||||
fn inline_assistant_model_supports_images(cx: &App) -> bool {
|
|
||||||
LanguageModelRegistry::read_global(cx)
|
|
||||||
.inline_assistant_model()
|
|
||||||
.map_or(false, |m| m.model.supports_images())
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PromptCompletionProviderDelegate for PromptEditorCompletionProviderDelegate {
|
|
||||||
fn supported_modes(&self, _cx: &App) -> Vec<PromptContextType> {
|
|
||||||
vec![
|
|
||||||
PromptContextType::File,
|
|
||||||
PromptContextType::Symbol,
|
|
||||||
PromptContextType::Thread,
|
|
||||||
PromptContextType::Fetch,
|
|
||||||
PromptContextType::Rules,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn supports_images(&self, cx: &App) -> bool {
|
|
||||||
inline_assistant_model_supports_images(cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn available_commands(&self, _cx: &App) -> Vec<crate::completion_provider::AvailableCommand> {
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn confirm_command(&self, _cx: &mut App) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PromptEditor<BufferCodegen> {
|
impl PromptEditor<BufferCodegen> {
|
||||||
pub fn new_buffer(
|
pub fn new_buffer(
|
||||||
id: InlineAssistId,
|
id: InlineAssistId,
|
||||||
@@ -831,14 +774,13 @@ impl PromptEditor<BufferCodegen> {
|
|||||||
prompt_buffer: Entity<MultiBuffer>,
|
prompt_buffer: Entity<MultiBuffer>,
|
||||||
codegen: Entity<BufferCodegen>,
|
codegen: Entity<BufferCodegen>,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
history_store: Entity<HistoryStore>,
|
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
project: WeakEntity<Project>,
|
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
|
services: ContextProviders,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<PromptEditor<BufferCodegen>>,
|
cx: &mut Context<PromptEditor<BufferCodegen>>,
|
||||||
) -> PromptEditor<BufferCodegen> {
|
) -> PromptEditor<BufferCodegen> {
|
||||||
let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
|
let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
|
||||||
|
let codegen_buffer = codegen.read(cx).buffer(cx).read(cx).as_singleton();
|
||||||
let mode = PromptEditorMode::Buffer {
|
let mode = PromptEditorMode::Buffer {
|
||||||
id,
|
id,
|
||||||
codegen,
|
codegen,
|
||||||
@@ -862,6 +804,7 @@ impl PromptEditor<BufferCodegen> {
|
|||||||
// typing in one will make what you typed appear in all of them.
|
// typing in one will make what you typed appear in all of them.
|
||||||
editor.set_show_cursor_when_unfocused(true, cx);
|
editor.set_show_cursor_when_unfocused(true, cx);
|
||||||
editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
|
editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
|
||||||
|
editor.register_addon(ContextCreasesAddon::new());
|
||||||
editor.set_context_menu_options(ContextMenuOptions {
|
editor.set_context_menu_options(ContextMenuOptions {
|
||||||
min_entries_visible: 12,
|
min_entries_visible: 12,
|
||||||
max_entries_visible: 12,
|
max_entries_visible: 12,
|
||||||
@@ -871,17 +814,43 @@ impl PromptEditor<BufferCodegen> {
|
|||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
|
|
||||||
let mention_set =
|
let prompt_editor_entity = prompt_editor.downgrade();
|
||||||
cx.new(|_cx| MentionSet::new(project, history_store.clone(), prompt_store.clone()));
|
prompt_editor.update(cx, |editor, _| {
|
||||||
|
editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
|
||||||
|
workspace.clone(),
|
||||||
|
context_store.downgrade(),
|
||||||
|
thread_store.clone(),
|
||||||
|
prompt_store.clone(),
|
||||||
|
prompt_editor_entity,
|
||||||
|
codegen_buffer.as_ref().map(Entity::downgrade),
|
||||||
|
))));
|
||||||
|
});
|
||||||
|
|
||||||
|
let context_picker_menu_handle = PopoverMenuHandle::default();
|
||||||
let model_selector_menu_handle = PopoverMenuHandle::default();
|
let model_selector_menu_handle = PopoverMenuHandle::default();
|
||||||
|
|
||||||
|
let context_strip = cx.new(|cx| {
|
||||||
|
ContextStrip::new(
|
||||||
|
context_store.clone(),
|
||||||
|
workspace.clone(),
|
||||||
|
thread_store.clone(),
|
||||||
|
prompt_store,
|
||||||
|
context_picker_menu_handle.clone(),
|
||||||
|
SuggestContextKind::Thread,
|
||||||
|
ModelUsageContext::InlineAssistant,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let context_strip_subscription =
|
||||||
|
cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event);
|
||||||
|
|
||||||
let mut this: PromptEditor<BufferCodegen> = PromptEditor {
|
let mut this: PromptEditor<BufferCodegen> = PromptEditor {
|
||||||
editor: prompt_editor.clone(),
|
editor: prompt_editor.clone(),
|
||||||
mention_set,
|
context_store,
|
||||||
history_store,
|
context_strip,
|
||||||
prompt_store,
|
context_picker_menu_handle,
|
||||||
workspace,
|
|
||||||
model_selector: cx.new(|cx| {
|
model_selector: cx.new(|cx| {
|
||||||
AgentModelSelector::new(
|
AgentModelSelector::new(
|
||||||
fs,
|
fs,
|
||||||
@@ -898,12 +867,12 @@ impl PromptEditor<BufferCodegen> {
|
|||||||
pending_prompt: String::new(),
|
pending_prompt: String::new(),
|
||||||
_codegen_subscription: codegen_subscription,
|
_codegen_subscription: codegen_subscription,
|
||||||
editor_subscriptions: Vec::new(),
|
editor_subscriptions: Vec::new(),
|
||||||
|
_context_strip_subscription: context_strip_subscription,
|
||||||
show_rate_limit_notice: false,
|
show_rate_limit_notice: false,
|
||||||
mode,
|
mode,
|
||||||
_phantom: Default::default(),
|
_phantom: Default::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.assign_completion_provider(cx);
|
|
||||||
this.subscribe_to_editor(window, cx);
|
this.subscribe_to_editor(window, cx);
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
@@ -949,10 +918,6 @@ impl PromptEditor<BufferCodegen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mention_set(&self) -> &Entity<MentionSet> {
|
|
||||||
&self.mention_set
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn editor_margins(&self) -> &Arc<Mutex<EditorMargins>> {
|
pub fn editor_margins(&self) -> &Arc<Mutex<EditorMargins>> {
|
||||||
match &self.mode {
|
match &self.mode {
|
||||||
PromptEditorMode::Buffer { editor_margins, .. } => editor_margins,
|
PromptEditorMode::Buffer { editor_margins, .. } => editor_margins,
|
||||||
@@ -979,9 +944,7 @@ impl PromptEditor<TerminalCodegen> {
|
|||||||
prompt_buffer: Entity<MultiBuffer>,
|
prompt_buffer: Entity<MultiBuffer>,
|
||||||
codegen: Entity<TerminalCodegen>,
|
codegen: Entity<TerminalCodegen>,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
history_store: Entity<HistoryStore>,
|
context_providers: ContextProviders,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
project: WeakEntity<Project>,
|
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
@@ -1014,17 +977,43 @@ impl PromptEditor<TerminalCodegen> {
|
|||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
|
|
||||||
let mention_set =
|
let prompt_editor_entity = prompt_editor.downgrade();
|
||||||
cx.new(|_cx| MentionSet::new(project, history_store.clone(), prompt_store.clone()));
|
prompt_editor.update(cx, |editor, _| {
|
||||||
|
editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
|
||||||
|
workspace.clone(),
|
||||||
|
context_store.downgrade(),
|
||||||
|
thread_store.clone(),
|
||||||
|
prompt_store.clone(),
|
||||||
|
prompt_editor_entity,
|
||||||
|
None,
|
||||||
|
))));
|
||||||
|
});
|
||||||
|
|
||||||
|
let context_picker_menu_handle = PopoverMenuHandle::default();
|
||||||
let model_selector_menu_handle = PopoverMenuHandle::default();
|
let model_selector_menu_handle = PopoverMenuHandle::default();
|
||||||
|
|
||||||
|
let context_strip = cx.new(|cx| {
|
||||||
|
ContextStrip::new(
|
||||||
|
context_store.clone(),
|
||||||
|
workspace.clone(),
|
||||||
|
thread_store.clone(),
|
||||||
|
prompt_store.clone(),
|
||||||
|
context_picker_menu_handle.clone(),
|
||||||
|
SuggestContextKind::Thread,
|
||||||
|
ModelUsageContext::InlineAssistant,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let context_strip_subscription =
|
||||||
|
cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event);
|
||||||
|
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
editor: prompt_editor.clone(),
|
editor: prompt_editor.clone(),
|
||||||
mention_set,
|
context_store,
|
||||||
history_store,
|
context_strip,
|
||||||
prompt_store,
|
context_picker_menu_handle,
|
||||||
workspace,
|
|
||||||
model_selector: cx.new(|cx| {
|
model_selector: cx.new(|cx| {
|
||||||
AgentModelSelector::new(
|
AgentModelSelector::new(
|
||||||
fs,
|
fs,
|
||||||
@@ -1041,12 +1030,12 @@ impl PromptEditor<TerminalCodegen> {
|
|||||||
pending_prompt: String::new(),
|
pending_prompt: String::new(),
|
||||||
_codegen_subscription: codegen_subscription,
|
_codegen_subscription: codegen_subscription,
|
||||||
editor_subscriptions: Vec::new(),
|
editor_subscriptions: Vec::new(),
|
||||||
|
_context_strip_subscription: context_strip_subscription,
|
||||||
mode,
|
mode,
|
||||||
show_rate_limit_notice: false,
|
show_rate_limit_notice: false,
|
||||||
_phantom: Default::default(),
|
_phantom: Default::default(),
|
||||||
};
|
};
|
||||||
this.count_lines(cx);
|
this.count_lines(cx);
|
||||||
this.assign_completion_provider(cx);
|
|
||||||
this.subscribe_to_editor(window, cx);
|
this.subscribe_to_editor(window, cx);
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
@@ -1093,10 +1082,6 @@ impl PromptEditor<TerminalCodegen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mention_set(&self) -> &Entity<MentionSet> {
|
|
||||||
&self.mention_set
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn codegen(&self) -> &Entity<TerminalCodegen> {
|
pub fn codegen(&self) -> &Entity<TerminalCodegen> {
|
||||||
match &self.mode {
|
match &self.mode {
|
||||||
PromptEditorMode::Buffer { .. } => unreachable!(),
|
PromptEditorMode::Buffer { .. } => unreachable!(),
|
||||||
@@ -1176,41 +1161,131 @@ impl GenerationMode {
|
|||||||
|
|
||||||
/// Stored information that can be used to resurrect a context crease when creating an editor for a past message.
|
/// Stored information that can be used to resurrect a context crease when creating an editor for a past message.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct MessageCrease {
|
pub struct MessageCrease {
|
||||||
range: Range<MultiBufferOffset>,
|
pub range: Range<usize>,
|
||||||
icon_path: SharedString,
|
pub icon_path: SharedString,
|
||||||
label: SharedString,
|
pub label: SharedString,
|
||||||
|
/// None for a deserialized message, Some otherwise.
|
||||||
|
pub context: Option<AgentContextHandle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_message_creases(
|
#[derive(Default)]
|
||||||
|
pub struct ContextCreasesAddon {
|
||||||
|
creases: HashMap<AgentContextKey, Vec<(CreaseId, SharedString)>>,
|
||||||
|
_subscription: Option<Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Addon for ContextCreasesAddon {
|
||||||
|
fn to_any(&self) -> &dyn std::any::Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
|
||||||
|
Some(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContextCreasesAddon {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
creases: HashMap::default(),
|
||||||
|
_subscription: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_creases(
|
||||||
|
&mut self,
|
||||||
|
context_store: &Entity<ContextStore>,
|
||||||
|
key: AgentContextKey,
|
||||||
|
creases: impl IntoIterator<Item = (CreaseId, SharedString)>,
|
||||||
|
cx: &mut Context<Editor>,
|
||||||
|
) {
|
||||||
|
self.creases.entry(key).or_default().extend(creases);
|
||||||
|
self._subscription = Some(
|
||||||
|
cx.subscribe(context_store, |editor, _, event, cx| match event {
|
||||||
|
ContextStoreEvent::ContextRemoved(key) => {
|
||||||
|
let Some(this) = editor.addon_mut::<Self>() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let (crease_ids, replacement_texts): (Vec<_>, Vec<_>) = this
|
||||||
|
.creases
|
||||||
|
.remove(key)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.unzip();
|
||||||
|
let ranges = editor
|
||||||
|
.remove_creases(crease_ids, cx)
|
||||||
|
.into_iter()
|
||||||
|
.map(|(_, range)| range)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
editor.unfold_ranges(&ranges, false, false, cx);
|
||||||
|
editor.edit(ranges.into_iter().zip(replacement_texts), cx);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_inner(self) -> HashMap<AgentContextKey, Vec<(CreaseId, SharedString)>> {
|
||||||
|
self.creases
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extract_message_creases(
|
||||||
editor: &mut Editor,
|
editor: &mut Editor,
|
||||||
mention_set: &Entity<MentionSet>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<'_, Editor>,
|
cx: &mut Context<'_, Editor>,
|
||||||
) -> Vec<MessageCrease> {
|
) -> Vec<MessageCrease> {
|
||||||
let creases = mention_set.read(cx).creases();
|
let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||||
let snapshot = editor.snapshot(window, cx);
|
let mut contexts_by_crease_id = editor
|
||||||
snapshot
|
.addon_mut::<ContextCreasesAddon>()
|
||||||
.crease_snapshot
|
.map(std::mem::take)
|
||||||
.creases()
|
.unwrap_or_default()
|
||||||
.filter(|(id, _)| creases.contains(id))
|
.into_inner()
|
||||||
.filter_map(|(_, crease)| {
|
.into_iter()
|
||||||
let metadata = crease.metadata()?.clone();
|
.flat_map(|(key, creases)| {
|
||||||
Some(MessageCrease {
|
let context = key.0;
|
||||||
range: crease.range().to_offset(snapshot.buffer()),
|
creases
|
||||||
label: metadata.label,
|
.into_iter()
|
||||||
icon_path: metadata.icon_path,
|
.map(move |(id, _)| (id, context.clone()))
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.collect()
|
.collect::<HashMap<_, _>>();
|
||||||
|
// Filter the addon's list of creases based on what the editor reports,
|
||||||
|
// since the addon might have removed creases in it.
|
||||||
|
|
||||||
|
editor.display_map.update(cx, |display_map, cx| {
|
||||||
|
display_map
|
||||||
|
.snapshot(cx)
|
||||||
|
.crease_snapshot
|
||||||
|
.creases()
|
||||||
|
.filter_map(|(id, crease)| {
|
||||||
|
Some((
|
||||||
|
id,
|
||||||
|
(
|
||||||
|
crease.range().to_offset(&buffer_snapshot),
|
||||||
|
crease.metadata()?.clone(),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.map(|(id, (range, metadata))| {
|
||||||
|
let context = contexts_by_crease_id.remove(&id);
|
||||||
|
MessageCrease {
|
||||||
|
range,
|
||||||
|
context,
|
||||||
|
label: metadata.label,
|
||||||
|
icon_path: metadata.icon_path,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_message_creases(
|
pub fn insert_message_creases(
|
||||||
editor: &mut Editor,
|
editor: &mut Editor,
|
||||||
message_creases: &[MessageCrease],
|
message_creases: &[MessageCrease],
|
||||||
|
context_store: &Entity<ContextStore>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<'_, Editor>,
|
cx: &mut Context<'_, Editor>,
|
||||||
) -> Vec<CreaseId> {
|
) {
|
||||||
let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
|
let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||||
let creases = message_creases
|
let creases = message_creases
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1227,5 +1302,12 @@ fn insert_message_creases(
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let ids = editor.insert_creases(creases.clone(), cx);
|
let ids = editor.insert_creases(creases.clone(), cx);
|
||||||
editor.fold_creases(creases, false, window, cx);
|
editor.fold_creases(creases, false, window, cx);
|
||||||
ids
|
if let Some(addon) = editor.addon_mut::<ContextCreasesAddon>() {
|
||||||
|
for (crease, id) in message_creases.iter().zip(ids) {
|
||||||
|
if let Some(context) = crease.context.as_ref() {
|
||||||
|
let key = AgentContextKey(context.clone());
|
||||||
|
addon.add_creases(context_store, key, vec![(id, crease.label.clone())], cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,14 @@ use std::{cmp::Reverse, sync::Arc};
|
|||||||
|
|
||||||
use collections::IndexMap;
|
use collections::IndexMap;
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
||||||
use gpui::{
|
use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task};
|
||||||
Action, AnyElement, App, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, Task,
|
|
||||||
};
|
|
||||||
use language_model::{
|
use language_model::{
|
||||||
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId,
|
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId,
|
||||||
LanguageModelRegistry,
|
LanguageModelRegistry,
|
||||||
};
|
};
|
||||||
use ordered_float::OrderedFloat;
|
use ordered_float::OrderedFloat;
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use ui::{KeyBinding, ListItem, ListItemSpacing, prelude::*};
|
use ui::{ListItem, ListItemSpacing, prelude::*};
|
||||||
use zed_actions::agent::OpenSettings;
|
|
||||||
|
|
||||||
type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &mut App) + 'static>;
|
type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &mut App) + 'static>;
|
||||||
type GetActiveModel = Arc<dyn Fn(&App) -> Option<ConfiguredModel> + 'static>;
|
type GetActiveModel = Arc<dyn Fn(&App) -> Option<ConfiguredModel> + 'static>;
|
||||||
@@ -23,7 +20,6 @@ pub fn language_model_selector(
|
|||||||
get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
|
get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
|
||||||
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
|
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
|
||||||
popover_styles: bool,
|
popover_styles: bool,
|
||||||
focus_handle: FocusHandle,
|
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<LanguageModelSelector>,
|
cx: &mut Context<LanguageModelSelector>,
|
||||||
) -> LanguageModelSelector {
|
) -> LanguageModelSelector {
|
||||||
@@ -31,7 +27,6 @@ pub fn language_model_selector(
|
|||||||
get_active_model,
|
get_active_model,
|
||||||
on_model_changed,
|
on_model_changed,
|
||||||
popover_styles,
|
popover_styles,
|
||||||
focus_handle,
|
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
@@ -93,7 +88,6 @@ pub struct LanguageModelPickerDelegate {
|
|||||||
_authenticate_all_providers_task: Task<()>,
|
_authenticate_all_providers_task: Task<()>,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
popover_styles: bool,
|
popover_styles: bool,
|
||||||
focus_handle: FocusHandle,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LanguageModelPickerDelegate {
|
impl LanguageModelPickerDelegate {
|
||||||
@@ -101,7 +95,6 @@ impl LanguageModelPickerDelegate {
|
|||||||
get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
|
get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
|
||||||
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
|
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
|
||||||
popover_styles: bool,
|
popover_styles: bool,
|
||||||
focus_handle: FocusHandle,
|
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Picker<Self>>,
|
cx: &mut Context<Picker<Self>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@@ -135,7 +128,6 @@ impl LanguageModelPickerDelegate {
|
|||||||
},
|
},
|
||||||
)],
|
)],
|
||||||
popover_styles,
|
popover_styles,
|
||||||
focus_handle,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -529,8 +521,6 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
|||||||
_window: &mut Window,
|
_window: &mut Window,
|
||||||
cx: &mut Context<Picker<Self>>,
|
cx: &mut Context<Picker<Self>>,
|
||||||
) -> Option<gpui::AnyElement> {
|
) -> Option<gpui::AnyElement> {
|
||||||
let focus_handle = self.focus_handle.clone();
|
|
||||||
|
|
||||||
if !self.popover_styles {
|
if !self.popover_styles {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -538,19 +528,22 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
|||||||
Some(
|
Some(
|
||||||
h_flex()
|
h_flex()
|
||||||
.w_full()
|
.w_full()
|
||||||
.p_1p5()
|
|
||||||
.border_t_1()
|
.border_t_1()
|
||||||
.border_color(cx.theme().colors().border_variant)
|
.border_color(cx.theme().colors().border_variant)
|
||||||
|
.p_1()
|
||||||
|
.gap_4()
|
||||||
|
.justify_between()
|
||||||
.child(
|
.child(
|
||||||
Button::new("configure", "Configure")
|
Button::new("configure", "Configure")
|
||||||
.full_width()
|
.icon(IconName::Settings)
|
||||||
.style(ButtonStyle::Outlined)
|
.icon_size(IconSize::Small)
|
||||||
.key_binding(
|
.icon_color(Color::Muted)
|
||||||
KeyBinding::for_action_in(&OpenSettings, &focus_handle, cx)
|
.icon_position(IconPosition::Start)
|
||||||
.map(|kb| kb.size(rems_from_px(12.))),
|
|
||||||
)
|
|
||||||
.on_click(|_, window, cx| {
|
.on_click(|_, window, cx| {
|
||||||
window.dispatch_action(OpenSettings.boxed_clone(), cx);
|
window.dispatch_action(
|
||||||
|
zed_actions::agent::OpenSettings.boxed_clone(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.into_any(),
|
.into_any(),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
context::load_context,
|
context::load_context,
|
||||||
|
context_store::ContextStore,
|
||||||
|
inline_assistant::ContextProviders,
|
||||||
inline_prompt_editor::{
|
inline_prompt_editor::{
|
||||||
CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId,
|
CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId,
|
||||||
},
|
},
|
||||||
@@ -71,10 +73,8 @@ impl TerminalInlineAssistant {
|
|||||||
&mut self,
|
&mut self,
|
||||||
terminal_view: &Entity<TerminalView>,
|
terminal_view: &Entity<TerminalView>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
project: WeakEntity<Project>,
|
|
||||||
thread_store: Entity<HistoryStore>,
|
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
initial_prompt: Option<String>,
|
initial_prompt: Option<String>,
|
||||||
|
context_providers: ContextProviders,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) {
|
) {
|
||||||
@@ -86,6 +86,7 @@ impl TerminalInlineAssistant {
|
|||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
let codegen = cx.new(|_| TerminalCodegen::new(terminal, self.telemetry.clone()));
|
let codegen = cx.new(|_| TerminalCodegen::new(terminal, self.telemetry.clone()));
|
||||||
|
|
||||||
let prompt_editor = cx.new(|cx| {
|
let prompt_editor = cx.new(|cx| {
|
||||||
@@ -95,9 +96,7 @@ impl TerminalInlineAssistant {
|
|||||||
prompt_buffer.clone(),
|
prompt_buffer.clone(),
|
||||||
codegen,
|
codegen,
|
||||||
self.fs.clone(),
|
self.fs.clone(),
|
||||||
thread_store.clone(),
|
context_providers,
|
||||||
prompt_store.clone(),
|
|
||||||
project.clone(),
|
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
@@ -117,6 +116,8 @@ impl TerminalInlineAssistant {
|
|||||||
terminal_view,
|
terminal_view,
|
||||||
prompt_editor,
|
prompt_editor,
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
|
context_store,
|
||||||
|
prompt_store,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
@@ -223,10 +224,6 @@ impl TerminalInlineAssistant {
|
|||||||
assist_id: TerminalInlineAssistId,
|
assist_id: TerminalInlineAssistId,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Result<Task<LanguageModelRequest>> {
|
) -> Result<Task<LanguageModelRequest>> {
|
||||||
let ConfiguredModel { model, .. } = LanguageModelRegistry::read_global(cx)
|
|
||||||
.inline_assistant_model()
|
|
||||||
.context("No inline assistant model")?;
|
|
||||||
|
|
||||||
let assist = self.assists.get(&assist_id).context("invalid assist")?;
|
let assist = self.assists.get(&assist_id).context("invalid assist")?;
|
||||||
|
|
||||||
let shell = std::env::var("SHELL").ok();
|
let shell = std::env::var("SHELL").ok();
|
||||||
@@ -243,31 +240,45 @@ impl TerminalInlineAssistant {
|
|||||||
.ok()
|
.ok()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let prompt_editor = assist.prompt_editor.clone().context("invalid assist")?;
|
|
||||||
|
|
||||||
let prompt = self.prompt_builder.generate_terminal_assistant_prompt(
|
let prompt = self.prompt_builder.generate_terminal_assistant_prompt(
|
||||||
&prompt_editor.read(cx).prompt(cx),
|
&assist
|
||||||
|
.prompt_editor
|
||||||
|
.clone()
|
||||||
|
.context("invalid assist")?
|
||||||
|
.read(cx)
|
||||||
|
.prompt(cx),
|
||||||
shell.as_deref(),
|
shell.as_deref(),
|
||||||
working_directory.as_deref(),
|
working_directory.as_deref(),
|
||||||
&latest_output,
|
&latest_output,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let temperature = AgentSettings::temperature_for_model(&model, cx);
|
let contexts = assist
|
||||||
|
.context_store
|
||||||
|
.read(cx)
|
||||||
|
.context()
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let context_load_task = assist.workspace.update(cx, |workspace, cx| {
|
||||||
|
let project = workspace.project();
|
||||||
|
load_context(contexts, project, &assist.prompt_store, cx)
|
||||||
|
})?;
|
||||||
|
|
||||||
let mention_set = prompt_editor.read(cx).mention_set().clone();
|
let ConfiguredModel { model, .. } = LanguageModelRegistry::read_global(cx)
|
||||||
let load_context_task = load_context(&mention_set, cx);
|
.inline_assistant_model()
|
||||||
|
.context("No inline assistant model")?;
|
||||||
|
|
||||||
|
let temperature = AgentSettings::temperature_for_model(&model, cx);
|
||||||
|
|
||||||
Ok(cx.background_spawn(async move {
|
Ok(cx.background_spawn(async move {
|
||||||
let mut request_message = LanguageModelRequestMessage {
|
let mut request_message = LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec![],
|
content: vec![],
|
||||||
cache: false,
|
cache: false,
|
||||||
reasoning_details: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(context) = load_context_task.await {
|
context_load_task
|
||||||
context.add_to_request_message(&mut request_message);
|
.await
|
||||||
}
|
.add_to_request_message(&mut request_message);
|
||||||
|
|
||||||
request_message.content.push(prompt.into());
|
request_message.content.push(prompt.into());
|
||||||
|
|
||||||
@@ -395,6 +406,8 @@ struct TerminalInlineAssist {
|
|||||||
prompt_editor: Option<Entity<PromptEditor<TerminalCodegen>>>,
|
prompt_editor: Option<Entity<PromptEditor<TerminalCodegen>>>,
|
||||||
codegen: Entity<TerminalCodegen>,
|
codegen: Entity<TerminalCodegen>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
|
context_store: Entity<ContextStore>,
|
||||||
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,6 +417,7 @@ impl TerminalInlineAssist {
|
|||||||
terminal: &Entity<TerminalView>,
|
terminal: &Entity<TerminalView>,
|
||||||
prompt_editor: Entity<PromptEditor<TerminalCodegen>>,
|
prompt_editor: Entity<PromptEditor<TerminalCodegen>>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
|
context_providers: ContextProviders,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@@ -413,6 +427,8 @@ impl TerminalInlineAssist {
|
|||||||
prompt_editor: Some(prompt_editor.clone()),
|
prompt_editor: Some(prompt_editor.clone()),
|
||||||
codegen: codegen.clone(),
|
codegen: codegen.clone(),
|
||||||
workspace,
|
workspace,
|
||||||
|
context_store,
|
||||||
|
prompt_store,
|
||||||
_subscriptions: vec![
|
_subscriptions: vec![
|
||||||
window.subscribe(&prompt_editor, cx, |prompt_editor, event, window, cx| {
|
window.subscribe(&prompt_editor, cx, |prompt_editor, event, window, cx| {
|
||||||
TerminalInlineAssistant::update_global(cx, |this, cx| {
|
TerminalInlineAssistant::update_global(cx, |this, cx| {
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ use assistant_slash_commands::{DefaultSlashCommand, FileSlashCommand, selections
|
|||||||
use client::{proto, zed_urls};
|
use client::{proto, zed_urls};
|
||||||
use collections::{BTreeSet, HashMap, HashSet, hash_map};
|
use collections::{BTreeSet, HashMap, HashSet, hash_map};
|
||||||
use editor::{
|
use editor::{
|
||||||
Anchor, Editor, EditorEvent, MenuEditPredictionsPolicy, MultiBuffer, MultiBufferOffset,
|
Anchor, Editor, EditorEvent, MenuEditPredictionsPolicy, MultiBuffer, MultiBufferSnapshot,
|
||||||
MultiBufferSnapshot, RowExt, ToOffset as _, ToPoint,
|
RowExt, ToOffset as _, ToPoint,
|
||||||
actions::{MoveToEndOfLine, Newline, ShowCompletions},
|
actions::{MoveToEndOfLine, Newline, ShowCompletions},
|
||||||
display_map::{
|
display_map::{
|
||||||
BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata, CustomBlockId, FoldId,
|
BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata, CustomBlockId, FoldId,
|
||||||
@@ -22,11 +22,11 @@ use editor::{FoldPlaceholder, display_map::CreaseId};
|
|||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, Animation, AnimationExt, AnyElement, App, ClipboardEntry, ClipboardItem, Empty, Entity,
|
Action, Animation, AnimationExt, AnyElement, AnyView, App, ClipboardEntry, ClipboardItem,
|
||||||
EventEmitter, FocusHandle, Focusable, FontWeight, Global, InteractiveElement, IntoElement,
|
Empty, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, InteractiveElement,
|
||||||
ParentElement, Pixels, Render, RenderImage, SharedString, Size, StatefulInteractiveElement,
|
IntoElement, ParentElement, Pixels, Render, RenderImage, SharedString, Size,
|
||||||
Styled, Subscription, Task, WeakEntity, actions, div, img, point, prelude::*,
|
StatefulInteractiveElement, Styled, Subscription, Task, WeakEntity, actions, div, img, point,
|
||||||
pulsating_between, size,
|
prelude::*, pulsating_between, size,
|
||||||
};
|
};
|
||||||
use language::{
|
use language::{
|
||||||
BufferSnapshot, LspAdapterDelegate, ToOffset,
|
BufferSnapshot, LspAdapterDelegate, ToOffset,
|
||||||
@@ -66,7 +66,7 @@ use workspace::{
|
|||||||
};
|
};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
Save, Toast, Workspace,
|
Save, Toast, Workspace,
|
||||||
item::{self, FollowableItem, Item},
|
item::{self, FollowableItem, Item, ItemHandle},
|
||||||
notifications::NotificationId,
|
notifications::NotificationId,
|
||||||
pane,
|
pane,
|
||||||
searchable::{SearchEvent, SearchableItem},
|
searchable::{SearchEvent, SearchableItem},
|
||||||
@@ -280,8 +280,6 @@ impl TextThreadEditor {
|
|||||||
.thought_process_output_sections()
|
.thought_process_output_sections()
|
||||||
.to_vec();
|
.to_vec();
|
||||||
let slash_commands = text_thread.read(cx).slash_commands().clone();
|
let slash_commands = text_thread.read(cx).slash_commands().clone();
|
||||||
let focus_handle = editor.read(cx).focus_handle(cx);
|
|
||||||
|
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
text_thread,
|
text_thread,
|
||||||
slash_commands,
|
slash_commands,
|
||||||
@@ -317,7 +315,6 @@ impl TextThreadEditor {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
true, // Use popover styles for picker
|
true, // Use popover styles for picker
|
||||||
focus_handle,
|
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@@ -393,7 +390,7 @@ impl TextThreadEditor {
|
|||||||
let cursor = user_message
|
let cursor = user_message
|
||||||
.start
|
.start
|
||||||
.to_offset(self.text_thread.read(cx).buffer().read(cx));
|
.to_offset(self.text_thread.read(cx).buffer().read(cx));
|
||||||
MultiBufferOffset(cursor)..MultiBufferOffset(cursor)
|
cursor..cursor
|
||||||
};
|
};
|
||||||
self.editor.update(cx, |editor, cx| {
|
self.editor.update(cx, |editor, cx| {
|
||||||
editor.change_selections(Default::default(), window, cx, |selections| {
|
editor.change_selections(Default::default(), window, cx, |selections| {
|
||||||
@@ -434,7 +431,7 @@ impl TextThreadEditor {
|
|||||||
let cursors = self.cursors(cx);
|
let cursors = self.cursors(cx);
|
||||||
self.text_thread.update(cx, |text_thread, cx| {
|
self.text_thread.update(cx, |text_thread, cx| {
|
||||||
let messages = text_thread
|
let messages = text_thread
|
||||||
.messages_for_offsets(cursors.into_iter().map(|cursor| cursor.0), cx)
|
.messages_for_offsets(cursors, cx)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|message| message.id)
|
.map(|message| message.id)
|
||||||
.collect();
|
.collect();
|
||||||
@@ -442,11 +439,9 @@ impl TextThreadEditor {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cursors(&self, cx: &mut App) -> Vec<MultiBufferOffset> {
|
fn cursors(&self, cx: &mut App) -> Vec<usize> {
|
||||||
let selections = self.editor.update(cx, |editor, cx| {
|
let selections = self.editor.update(cx, |editor, cx| {
|
||||||
editor
|
editor.selections.all::<usize>(&editor.display_snapshot(cx))
|
||||||
.selections
|
|
||||||
.all::<MultiBufferOffset>(&editor.display_snapshot(cx))
|
|
||||||
});
|
});
|
||||||
selections
|
selections
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -1585,11 +1580,7 @@ impl TextThreadEditor {
|
|||||||
fn get_clipboard_contents(
|
fn get_clipboard_contents(
|
||||||
&mut self,
|
&mut self,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> (
|
) -> (String, CopyMetadata, Vec<text::Selection<usize>>) {
|
||||||
String,
|
|
||||||
CopyMetadata,
|
|
||||||
Vec<text::Selection<MultiBufferOffset>>,
|
|
||||||
) {
|
|
||||||
let (mut selection, creases) = self.editor.update(cx, |editor, cx| {
|
let (mut selection, creases) = self.editor.update(cx, |editor, cx| {
|
||||||
let mut selection = editor
|
let mut selection = editor
|
||||||
.selections
|
.selections
|
||||||
@@ -1647,26 +1638,30 @@ impl TextThreadEditor {
|
|||||||
|
|
||||||
// If selection is empty, we want to copy the entire line
|
// If selection is empty, we want to copy the entire line
|
||||||
if selection.range().is_empty() {
|
if selection.range().is_empty() {
|
||||||
let snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx);
|
let snapshot = text_thread.buffer().read(cx).snapshot();
|
||||||
let point = snapshot.offset_to_point(selection.range().start);
|
let point = snapshot.offset_to_point(selection.range().start);
|
||||||
selection.start = snapshot.point_to_offset(Point::new(point.row, 0));
|
selection.start = snapshot.point_to_offset(Point::new(point.row, 0));
|
||||||
selection.end = snapshot
|
selection.end = snapshot
|
||||||
.point_to_offset(cmp::min(Point::new(point.row + 1, 0), snapshot.max_point()));
|
.point_to_offset(cmp::min(Point::new(point.row + 1, 0), snapshot.max_point()));
|
||||||
for chunk in snapshot.text_for_range(selection.range()) {
|
for chunk in text_thread
|
||||||
|
.buffer()
|
||||||
|
.read(cx)
|
||||||
|
.text_for_range(selection.range())
|
||||||
|
{
|
||||||
text.push_str(chunk);
|
text.push_str(chunk);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for message in text_thread.messages(cx) {
|
for message in text_thread.messages(cx) {
|
||||||
if message.offset_range.start >= selection.range().end.0 {
|
if message.offset_range.start >= selection.range().end {
|
||||||
break;
|
break;
|
||||||
} else if message.offset_range.end >= selection.range().start.0 {
|
} else if message.offset_range.end >= selection.range().start {
|
||||||
let range = cmp::max(message.offset_range.start, selection.range().start.0)
|
let range = cmp::max(message.offset_range.start, selection.range().start)
|
||||||
..cmp::min(message.offset_range.end, selection.range().end.0);
|
..cmp::min(message.offset_range.end, selection.range().end);
|
||||||
if !range.is_empty() {
|
if !range.is_empty() {
|
||||||
for chunk in text_thread.buffer().read(cx).text_for_range(range) {
|
for chunk in text_thread.buffer().read(cx).text_for_range(range) {
|
||||||
text.push_str(chunk);
|
text.push_str(chunk);
|
||||||
}
|
}
|
||||||
if message.offset_range.end < selection.range().end.0 {
|
if message.offset_range.end < selection.range().end {
|
||||||
text.push('\n');
|
text.push('\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1748,7 +1743,7 @@ impl TextThreadEditor {
|
|||||||
self.editor.update(cx, |editor, cx| {
|
self.editor.update(cx, |editor, cx| {
|
||||||
let paste_position = editor
|
let paste_position = editor
|
||||||
.selections
|
.selections
|
||||||
.newest::<MultiBufferOffset>(&editor.display_snapshot(cx))
|
.newest::<usize>(&editor.display_snapshot(cx))
|
||||||
.head();
|
.head();
|
||||||
editor.paste(action, window, cx);
|
editor.paste(action, window, cx);
|
||||||
|
|
||||||
@@ -1796,16 +1791,13 @@ impl TextThreadEditor {
|
|||||||
editor.transact(window, cx, |editor, _window, cx| {
|
editor.transact(window, cx, |editor, _window, cx| {
|
||||||
let edits = editor
|
let edits = editor
|
||||||
.selections
|
.selections
|
||||||
.all::<MultiBufferOffset>(&editor.display_snapshot(cx))
|
.all::<usize>(&editor.display_snapshot(cx))
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|selection| (selection.start..selection.end, "\n"));
|
.map(|selection| (selection.start..selection.end, "\n"));
|
||||||
editor.edit(edits, cx);
|
editor.edit(edits, cx);
|
||||||
|
|
||||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||||
for selection in editor
|
for selection in editor.selections.all::<usize>(&editor.display_snapshot(cx)) {
|
||||||
.selections
|
|
||||||
.all::<MultiBufferOffset>(&editor.display_snapshot(cx))
|
|
||||||
{
|
|
||||||
image_positions.push(snapshot.anchor_before(selection.end));
|
image_positions.push(snapshot.anchor_before(selection.end));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1897,7 +1889,7 @@ impl TextThreadEditor {
|
|||||||
let range = selection
|
let range = selection
|
||||||
.map(|endpoint| endpoint.to_offset(&buffer))
|
.map(|endpoint| endpoint.to_offset(&buffer))
|
||||||
.range();
|
.range();
|
||||||
text_thread.split_message(range.start.0..range.end.0, cx);
|
text_thread.split_message(range, cx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2591,11 +2583,11 @@ impl Item for TextThreadEditor {
|
|||||||
type_id: TypeId,
|
type_id: TypeId,
|
||||||
self_handle: &'a Entity<Self>,
|
self_handle: &'a Entity<Self>,
|
||||||
_: &'a App,
|
_: &'a App,
|
||||||
) -> Option<gpui::AnyEntity> {
|
) -> Option<AnyView> {
|
||||||
if type_id == TypeId::of::<Self>() {
|
if type_id == TypeId::of::<Self>() {
|
||||||
Some(self_handle.clone().into())
|
Some(self_handle.to_any())
|
||||||
} else if type_id == TypeId::of::<Editor>() {
|
} else if type_id == TypeId::of::<Editor>() {
|
||||||
Some(self.editor.clone().into())
|
Some(self.editor.to_any())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -2971,7 +2963,7 @@ pub fn make_lsp_adapter_delegate(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use editor::{MultiBufferOffset, SelectionEffects};
|
use editor::SelectionEffects;
|
||||||
use fs::FakeFs;
|
use fs::FakeFs;
|
||||||
use gpui::{App, TestAppContext, VisualTestContext};
|
use gpui::{App, TestAppContext, VisualTestContext};
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
@@ -3177,16 +3169,15 @@ mod tests {
|
|||||||
text_thread: &Entity<TextThread>,
|
text_thread: &Entity<TextThread>,
|
||||||
message_ix: usize,
|
message_ix: usize,
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
) -> Range<MultiBufferOffset> {
|
) -> Range<usize> {
|
||||||
let range = text_thread.update(cx, |text_thread, cx| {
|
text_thread.update(cx, |text_thread, cx| {
|
||||||
text_thread
|
text_thread
|
||||||
.messages(cx)
|
.messages(cx)
|
||||||
.nth(message_ix)
|
.nth(message_ix)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.anchor_range
|
.anchor_range
|
||||||
.to_offset(&text_thread.buffer().read(cx).snapshot())
|
.to_offset(&text_thread.buffer().read(cx).snapshot())
|
||||||
});
|
})
|
||||||
MultiBufferOffset(range.start)..MultiBufferOffset(range.end)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assert_copy_paste_text_thread_editor<T: editor::ToOffset>(
|
fn assert_copy_paste_text_thread_editor<T: editor::ToOffset>(
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ mod acp_onboarding_modal;
|
|||||||
mod agent_notification;
|
mod agent_notification;
|
||||||
mod burn_mode_tooltip;
|
mod burn_mode_tooltip;
|
||||||
mod claude_code_onboarding_modal;
|
mod claude_code_onboarding_modal;
|
||||||
|
mod context_pill;
|
||||||
mod end_trial_upsell;
|
mod end_trial_upsell;
|
||||||
mod hold_for_default;
|
|
||||||
mod onboarding_modal;
|
mod onboarding_modal;
|
||||||
mod unavailable_editing_tooltip;
|
mod unavailable_editing_tooltip;
|
||||||
mod usage_callout;
|
mod usage_callout;
|
||||||
@@ -12,8 +12,8 @@ pub use acp_onboarding_modal::*;
|
|||||||
pub use agent_notification::*;
|
pub use agent_notification::*;
|
||||||
pub use burn_mode_tooltip::*;
|
pub use burn_mode_tooltip::*;
|
||||||
pub use claude_code_onboarding_modal::*;
|
pub use claude_code_onboarding_modal::*;
|
||||||
|
pub use context_pill::*;
|
||||||
pub use end_trial_upsell::*;
|
pub use end_trial_upsell::*;
|
||||||
pub use hold_for_default::*;
|
|
||||||
pub use onboarding_modal::*;
|
pub use onboarding_modal::*;
|
||||||
pub use unavailable_editing_tooltip::*;
|
pub use unavailable_editing_tooltip::*;
|
||||||
pub use usage_callout::*;
|
pub use usage_callout::*;
|
||||||
|
|||||||
858
crates/agent_ui/src/ui/context_pill.rs
Normal file
858
crates/agent_ui/src/ui/context_pill.rs
Normal file
@@ -0,0 +1,858 @@
|
|||||||
|
use std::{ops::Range, path::Path, rc::Rc, sync::Arc, time::Duration};
|
||||||
|
|
||||||
|
use file_icons::FileIcons;
|
||||||
|
use futures::FutureExt as _;
|
||||||
|
use gpui::{
|
||||||
|
Animation, AnimationExt as _, AnyView, ClickEvent, Entity, Image, MouseButton, Task,
|
||||||
|
pulsating_between,
|
||||||
|
};
|
||||||
|
use language_model::LanguageModelImage;
|
||||||
|
use project::Project;
|
||||||
|
use prompt_store::PromptStore;
|
||||||
|
use rope::Point;
|
||||||
|
use ui::{IconButtonShape, Tooltip, prelude::*, tooltip_container};
|
||||||
|
use util::paths::PathStyle;
|
||||||
|
|
||||||
|
use crate::context::{
|
||||||
|
AgentContextHandle, ContextId, ContextKind, DirectoryContextHandle, FetchedUrlContext,
|
||||||
|
FileContextHandle, ImageContext, ImageStatus, RulesContextHandle, SelectionContextHandle,
|
||||||
|
SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub enum ContextPill {
|
||||||
|
Added {
|
||||||
|
context: AddedContext,
|
||||||
|
dupe_name: bool,
|
||||||
|
focused: bool,
|
||||||
|
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
||||||
|
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
||||||
|
},
|
||||||
|
Suggested {
|
||||||
|
name: SharedString,
|
||||||
|
icon_path: Option<SharedString>,
|
||||||
|
kind: ContextKind,
|
||||||
|
focused: bool,
|
||||||
|
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContextPill {
|
||||||
|
pub fn added(
|
||||||
|
context: AddedContext,
|
||||||
|
dupe_name: bool,
|
||||||
|
focused: bool,
|
||||||
|
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
||||||
|
) -> Self {
|
||||||
|
Self::Added {
|
||||||
|
context,
|
||||||
|
dupe_name,
|
||||||
|
on_remove,
|
||||||
|
focused,
|
||||||
|
on_click: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn suggested(
|
||||||
|
name: SharedString,
|
||||||
|
icon_path: Option<SharedString>,
|
||||||
|
kind: ContextKind,
|
||||||
|
focused: bool,
|
||||||
|
) -> Self {
|
||||||
|
Self::Suggested {
|
||||||
|
name,
|
||||||
|
icon_path,
|
||||||
|
kind,
|
||||||
|
focused,
|
||||||
|
on_click: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_click(mut self, listener: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>) -> Self {
|
||||||
|
match &mut self {
|
||||||
|
ContextPill::Added { on_click, .. } => {
|
||||||
|
*on_click = Some(listener);
|
||||||
|
}
|
||||||
|
ContextPill::Suggested { on_click, .. } => {
|
||||||
|
*on_click = Some(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> ElementId {
|
||||||
|
match self {
|
||||||
|
Self::Added { context, .. } => context.handle.element_id("context-pill".into()),
|
||||||
|
Self::Suggested { .. } => "suggested-context-pill".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn icon(&self) -> Icon {
|
||||||
|
match self {
|
||||||
|
Self::Suggested {
|
||||||
|
icon_path: Some(icon_path),
|
||||||
|
..
|
||||||
|
} => Icon::from_path(icon_path),
|
||||||
|
Self::Suggested { kind, .. } => Icon::new(kind.icon()),
|
||||||
|
Self::Added { context, .. } => context.icon(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for ContextPill {
|
||||||
|
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
let color = cx.theme().colors();
|
||||||
|
|
||||||
|
let base_pill = h_flex()
|
||||||
|
.id(self.id())
|
||||||
|
.pl_1()
|
||||||
|
.pb(px(1.))
|
||||||
|
.border_1()
|
||||||
|
.rounded_sm()
|
||||||
|
.gap_1()
|
||||||
|
.child(self.icon().size(IconSize::XSmall).color(Color::Muted));
|
||||||
|
|
||||||
|
match &self {
|
||||||
|
ContextPill::Added {
|
||||||
|
context,
|
||||||
|
dupe_name,
|
||||||
|
on_remove,
|
||||||
|
focused,
|
||||||
|
on_click,
|
||||||
|
} => {
|
||||||
|
let status_is_error = matches!(context.status, ContextStatus::Error { .. });
|
||||||
|
let status_is_warning = matches!(context.status, ContextStatus::Warning { .. });
|
||||||
|
|
||||||
|
base_pill
|
||||||
|
.pr(if on_remove.is_some() { px(2.) } else { px(4.) })
|
||||||
|
.map(|pill| {
|
||||||
|
if status_is_error {
|
||||||
|
pill.bg(cx.theme().status().error_background)
|
||||||
|
.border_color(cx.theme().status().error_border)
|
||||||
|
} else if status_is_warning {
|
||||||
|
pill.bg(cx.theme().status().warning_background)
|
||||||
|
.border_color(cx.theme().status().warning_border)
|
||||||
|
} else if *focused {
|
||||||
|
pill.bg(color.element_background)
|
||||||
|
.border_color(color.border_focused)
|
||||||
|
} else {
|
||||||
|
pill.bg(color.element_background)
|
||||||
|
.border_color(color.border.opacity(0.5))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.id("context-data")
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
div().max_w_64().child(
|
||||||
|
Label::new(context.name.clone())
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.truncate(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.when_some(context.parent.as_ref(), |element, parent_name| {
|
||||||
|
if *dupe_name {
|
||||||
|
element.child(
|
||||||
|
Label::new(parent_name.clone())
|
||||||
|
.size(LabelSize::XSmall)
|
||||||
|
.color(Color::Muted),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
element
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.when_some(context.tooltip.as_ref(), |element, tooltip| {
|
||||||
|
element.tooltip(Tooltip::text(tooltip.clone()))
|
||||||
|
})
|
||||||
|
.map(|element| match &context.status {
|
||||||
|
ContextStatus::Ready => element
|
||||||
|
.when_some(
|
||||||
|
context.render_hover.as_ref(),
|
||||||
|
|element, render_hover| {
|
||||||
|
let render_hover = render_hover.clone();
|
||||||
|
element.hoverable_tooltip(move |window, cx| {
|
||||||
|
render_hover(window, cx)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.into_any(),
|
||||||
|
ContextStatus::Loading { message } => element
|
||||||
|
.tooltip(ui::Tooltip::text(message.clone()))
|
||||||
|
.with_animation(
|
||||||
|
"pulsating-ctx-pill",
|
||||||
|
Animation::new(Duration::from_secs(2))
|
||||||
|
.repeat()
|
||||||
|
.with_easing(pulsating_between(0.4, 0.8)),
|
||||||
|
|label, delta| label.opacity(delta),
|
||||||
|
)
|
||||||
|
.into_any_element(),
|
||||||
|
ContextStatus::Warning { message }
|
||||||
|
| ContextStatus::Error { message } => element
|
||||||
|
.tooltip(ui::Tooltip::text(message.clone()))
|
||||||
|
.into_any_element(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.when_some(on_remove.as_ref(), |element, on_remove| {
|
||||||
|
element.child(
|
||||||
|
IconButton::new(
|
||||||
|
context.handle.element_id("remove".into()),
|
||||||
|
IconName::Close,
|
||||||
|
)
|
||||||
|
.shape(IconButtonShape::Square)
|
||||||
|
.icon_size(IconSize::XSmall)
|
||||||
|
.tooltip(Tooltip::text("Remove Context"))
|
||||||
|
.on_click({
|
||||||
|
let on_remove = on_remove.clone();
|
||||||
|
move |event, window, cx| on_remove(event, window, cx)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when_some(on_click.as_ref(), |element, on_click| {
|
||||||
|
let on_click = on_click.clone();
|
||||||
|
element.cursor_pointer().on_click(move |event, window, cx| {
|
||||||
|
on_click(event, window, cx);
|
||||||
|
cx.stop_propagation();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
ContextPill::Suggested {
|
||||||
|
name,
|
||||||
|
icon_path: _,
|
||||||
|
kind: _,
|
||||||
|
focused,
|
||||||
|
on_click,
|
||||||
|
} => base_pill
|
||||||
|
.cursor_pointer()
|
||||||
|
.pr_1()
|
||||||
|
.border_dashed()
|
||||||
|
.map(|pill| {
|
||||||
|
if *focused {
|
||||||
|
pill.border_color(color.border_focused)
|
||||||
|
.bg(color.element_background.opacity(0.5))
|
||||||
|
} else {
|
||||||
|
pill.border_color(color.border)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.hover(|style| style.bg(color.element_hover.opacity(0.5)))
|
||||||
|
.child(
|
||||||
|
div().max_w_64().child(
|
||||||
|
Label::new(name.clone())
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.color(Color::Muted)
|
||||||
|
.truncate(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.tooltip(|_window, cx| {
|
||||||
|
Tooltip::with_meta("Suggested Context", None, "Click to add it", cx)
|
||||||
|
})
|
||||||
|
.when_some(on_click.as_ref(), |element, on_click| {
|
||||||
|
let on_click = on_click.clone();
|
||||||
|
element.on_click(move |event, window, cx| {
|
||||||
|
on_click(event, window, cx);
|
||||||
|
cx.stop_propagation();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.into_any(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ContextStatus {
|
||||||
|
Ready,
|
||||||
|
Loading { message: SharedString },
|
||||||
|
Error { message: SharedString },
|
||||||
|
Warning { message: SharedString },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(RegisterComponent)]
|
||||||
|
pub struct AddedContext {
|
||||||
|
pub handle: AgentContextHandle,
|
||||||
|
pub kind: ContextKind,
|
||||||
|
pub name: SharedString,
|
||||||
|
pub parent: Option<SharedString>,
|
||||||
|
pub tooltip: Option<SharedString>,
|
||||||
|
pub icon_path: Option<SharedString>,
|
||||||
|
pub status: ContextStatus,
|
||||||
|
pub render_hover: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AddedContext {
|
||||||
|
pub fn icon(&self) -> Icon {
|
||||||
|
match &self.status {
|
||||||
|
ContextStatus::Warning { .. } => Icon::new(IconName::Warning).color(Color::Warning),
|
||||||
|
ContextStatus::Error { .. } => Icon::new(IconName::XCircle).color(Color::Error),
|
||||||
|
_ => {
|
||||||
|
if let Some(icon_path) = &self.icon_path {
|
||||||
|
Icon::from_path(icon_path)
|
||||||
|
} else {
|
||||||
|
Icon::new(self.kind.icon())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Creates an `AddedContext` by retrieving relevant details of `AgentContext`. This returns a
|
||||||
|
/// `None` if `DirectoryContext` or `RulesContext` no longer exist.
|
||||||
|
///
|
||||||
|
/// TODO: `None` cases are unremovable from `ContextStore` and so are a very minor memory leak.
|
||||||
|
pub fn new_pending(
|
||||||
|
handle: AgentContextHandle,
|
||||||
|
prompt_store: Option<&Entity<PromptStore>>,
|
||||||
|
project: &Project,
|
||||||
|
model: Option<&Arc<dyn language_model::LanguageModel>>,
|
||||||
|
cx: &App,
|
||||||
|
) -> Option<AddedContext> {
|
||||||
|
match handle {
|
||||||
|
AgentContextHandle::File(handle) => {
|
||||||
|
Self::pending_file(handle, project.path_style(cx), 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::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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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);
|
||||||
|
AddedContext {
|
||||||
|
kind: ContextKind::File,
|
||||||
|
name,
|
||||||
|
parent,
|
||||||
|
tooltip: Some(SharedString::new(full_path)),
|
||||||
|
icon_path: FileIcons::get_icon(Path::new(full_path), cx),
|
||||||
|
status: ContextStatus::Ready,
|
||||||
|
render_hover: None,
|
||||||
|
handle: AgentContextHandle::File(handle),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pending_directory(
|
||||||
|
handle: DirectoryContextHandle,
|
||||||
|
project: &Project,
|
||||||
|
cx: &App,
|
||||||
|
) -> Option<AddedContext> {
|
||||||
|
let worktree = project.worktree_for_entry(handle.entry_id, cx)?.read(cx);
|
||||||
|
let entry = worktree.entry_for_id(handle.entry_id)?;
|
||||||
|
let full_path = worktree
|
||||||
|
.full_path(&entry.path)
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
Some(Self::directory(handle, &full_path, project.path_style(cx)))
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
AddedContext {
|
||||||
|
kind: ContextKind::Directory,
|
||||||
|
name,
|
||||||
|
parent,
|
||||||
|
tooltip: Some(SharedString::new(full_path)),
|
||||||
|
icon_path: None,
|
||||||
|
status: ContextStatus::Ready,
|
||||||
|
render_hover: None,
|
||||||
|
handle: AgentContextHandle::Directory(handle),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
Some(AddedContext {
|
||||||
|
kind: ContextKind::Symbol,
|
||||||
|
name: handle.symbol.clone(),
|
||||||
|
parent: Some(excerpt.file_name_and_range.clone()),
|
||||||
|
tooltip: None,
|
||||||
|
icon_path: None,
|
||||||
|
status: ContextStatus::Ready,
|
||||||
|
render_hover: {
|
||||||
|
let handle = handle.clone();
|
||||||
|
Some(Rc::new(move |_, cx| {
|
||||||
|
excerpt.hover_view(handle.text(cx), cx).into()
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
handle: AgentContextHandle::Symbol(handle),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn 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,
|
||||||
|
);
|
||||||
|
Some(AddedContext {
|
||||||
|
kind: ContextKind::Selection,
|
||||||
|
name: excerpt.file_name_and_range.clone(),
|
||||||
|
parent: excerpt.parent_name.clone(),
|
||||||
|
tooltip: None,
|
||||||
|
icon_path: excerpt.icon_path.clone(),
|
||||||
|
status: ContextStatus::Ready,
|
||||||
|
render_hover: {
|
||||||
|
let handle = handle.clone();
|
||||||
|
Some(Rc::new(move |_, cx| {
|
||||||
|
excerpt.hover_view(handle.text(cx), cx).into()
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
handle: AgentContextHandle::Selection(handle),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetched_url(context: FetchedUrlContext) -> AddedContext {
|
||||||
|
AddedContext {
|
||||||
|
kind: ContextKind::FetchedUrl,
|
||||||
|
name: context.url.clone(),
|
||||||
|
parent: None,
|
||||||
|
tooltip: None,
|
||||||
|
icon_path: None,
|
||||||
|
status: ContextStatus::Ready,
|
||||||
|
render_hover: None,
|
||||||
|
handle: AgentContextHandle::FetchedUrl(context),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pending_thread(handle: ThreadContextHandle, cx: &App) -> AddedContext {
|
||||||
|
AddedContext {
|
||||||
|
kind: ContextKind::Thread,
|
||||||
|
name: handle.title(cx),
|
||||||
|
parent: None,
|
||||||
|
tooltip: None,
|
||||||
|
icon_path: None,
|
||||||
|
status: if handle.thread.read(cx).is_generating_summary() {
|
||||||
|
ContextStatus::Loading {
|
||||||
|
message: "Summarizing…".into(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ContextStatus::Ready
|
||||||
|
},
|
||||||
|
render_hover: {
|
||||||
|
let thread = handle.thread.clone();
|
||||||
|
Some(Rc::new(move |_, cx| {
|
||||||
|
let text = thread
|
||||||
|
.update(cx, |thread, cx| thread.summary(cx))
|
||||||
|
.now_or_never()
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or_else(|| SharedString::from(thread.read(cx).to_markdown()));
|
||||||
|
ContextPillHover::new_text(text, cx).into()
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
handle: AgentContextHandle::Thread(handle),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pending_text_thread(handle: TextThreadContextHandle, cx: &App) -> AddedContext {
|
||||||
|
AddedContext {
|
||||||
|
kind: ContextKind::TextThread,
|
||||||
|
name: handle.title(cx),
|
||||||
|
parent: None,
|
||||||
|
tooltip: None,
|
||||||
|
icon_path: None,
|
||||||
|
status: ContextStatus::Ready,
|
||||||
|
render_hover: {
|
||||||
|
let text_thread = handle.text_thread.clone();
|
||||||
|
Some(Rc::new(move |_, cx| {
|
||||||
|
let text = text_thread.read(cx).to_xml(cx);
|
||||||
|
ContextPillHover::new_text(text.into(), cx).into()
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
handle: AgentContextHandle::TextThread(handle),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pending_rules(
|
||||||
|
handle: RulesContextHandle,
|
||||||
|
prompt_store: Option<&Entity<PromptStore>>,
|
||||||
|
cx: &App,
|
||||||
|
) -> Option<AddedContext> {
|
||||||
|
let title = prompt_store
|
||||||
|
.as_ref()?
|
||||||
|
.read(cx)
|
||||||
|
.metadata(handle.prompt_id.into())?
|
||||||
|
.title
|
||||||
|
.unwrap_or_else(|| "Unnamed Rule".into());
|
||||||
|
Some(AddedContext {
|
||||||
|
kind: ContextKind::Rules,
|
||||||
|
name: title,
|
||||||
|
parent: None,
|
||||||
|
tooltip: None,
|
||||||
|
icon_path: None,
|
||||||
|
status: ContextStatus::Ready,
|
||||||
|
render_hover: None,
|
||||||
|
handle: AgentContextHandle::Rules(handle),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (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);
|
||||||
|
(name, parent, icon_path)
|
||||||
|
} else {
|
||||||
|
("Image".into(), None, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
let status = match context.status(model) {
|
||||||
|
ImageStatus::Loading => ContextStatus::Loading {
|
||||||
|
message: "Loading…".into(),
|
||||||
|
},
|
||||||
|
ImageStatus::Error => ContextStatus::Error {
|
||||||
|
message: "Failed to load Image".into(),
|
||||||
|
},
|
||||||
|
ImageStatus::Warning => ContextStatus::Warning {
|
||||||
|
message: format!(
|
||||||
|
"{} doesn't support attaching Images as Context",
|
||||||
|
model.map(|m| m.name().0).unwrap_or_else(|| "Model".into())
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
},
|
||||||
|
ImageStatus::Ready => ContextStatus::Ready,
|
||||||
|
};
|
||||||
|
|
||||||
|
AddedContext {
|
||||||
|
kind: ContextKind::Image,
|
||||||
|
name,
|
||||||
|
parent,
|
||||||
|
tooltip: None,
|
||||||
|
icon_path,
|
||||||
|
status,
|
||||||
|
render_hover: Some(Rc::new({
|
||||||
|
let image = context.original_image.clone();
|
||||||
|
move |_, cx| {
|
||||||
|
let image = image.clone();
|
||||||
|
ContextPillHover::new(cx, move |_, _| {
|
||||||
|
gpui::img(image.clone())
|
||||||
|
.max_w_96()
|
||||||
|
.max_h_96()
|
||||||
|
.into_any_element()
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
handle: AgentContextHandle::Image(context),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_file_name_and_directory_from_full_path(
|
||||||
|
path: &str,
|
||||||
|
path_style: PathStyle,
|
||||||
|
) -> (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)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct ContextFileExcerpt {
|
||||||
|
pub file_name_and_range: SharedString,
|
||||||
|
pub full_path_and_range: SharedString,
|
||||||
|
pub parent_name: Option<SharedString>,
|
||||||
|
pub icon_path: Option<SharedString>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContextFileExcerpt {
|
||||||
|
pub fn new(full_path: &str, line_range: Range<Point>, path_style: PathStyle, cx: &App) -> Self {
|
||||||
|
let (parent, file_name) = path_style.split(full_path);
|
||||||
|
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();
|
||||||
|
full_path_and_range.push_str(&line_range_text);
|
||||||
|
let mut file_name_and_range = file_name.to_owned();
|
||||||
|
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 icon_path = FileIcons::get_icon(Path::new(full_path), cx);
|
||||||
|
|
||||||
|
ContextFileExcerpt {
|
||||||
|
file_name_and_range: file_name_and_range.into(),
|
||||||
|
full_path_and_range: full_path_and_range.into(),
|
||||||
|
parent_name,
|
||||||
|
icon_path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hover_view(&self, text: SharedString, cx: &mut App) -> Entity<ContextPillHover> {
|
||||||
|
let icon_path = self.icon_path.clone();
|
||||||
|
let full_path_and_range = self.full_path_and_range.clone();
|
||||||
|
ContextPillHover::new(cx, move |_, cx| {
|
||||||
|
v_flex()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_0p5()
|
||||||
|
.w_full()
|
||||||
|
.max_w_full()
|
||||||
|
.border_b_1()
|
||||||
|
.border_color(cx.theme().colors().border.opacity(0.6))
|
||||||
|
.children(
|
||||||
|
icon_path
|
||||||
|
.clone()
|
||||||
|
.map(Icon::from_path)
|
||||||
|
.map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
// TODO: make this truncate on the left.
|
||||||
|
Label::new(full_path_and_range.clone())
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.ml_1(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.id("context-pill-hover-contents")
|
||||||
|
.overflow_scroll()
|
||||||
|
.max_w_128()
|
||||||
|
.max_h_96()
|
||||||
|
.child(Label::new(text.clone()).buffer_font(cx)),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ContextPillHover {
|
||||||
|
render_hover: Box<dyn Fn(&mut Window, &mut App) -> AnyElement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContextPillHover {
|
||||||
|
fn new(
|
||||||
|
cx: &mut App,
|
||||||
|
render_hover: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
|
||||||
|
) -> Entity<Self> {
|
||||||
|
cx.new(|_| Self {
|
||||||
|
render_hover: Box::new(render_hover),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_text(content: SharedString, cx: &mut App) -> Entity<Self> {
|
||||||
|
Self::new(cx, move |_, _| {
|
||||||
|
div()
|
||||||
|
.id("context-pill-hover-contents")
|
||||||
|
.overflow_scroll()
|
||||||
|
.max_w_128()
|
||||||
|
.max_h_96()
|
||||||
|
.child(content.clone())
|
||||||
|
.into_any_element()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ContextPillHover {
|
||||||
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
tooltip_container(cx, move |this, cx| {
|
||||||
|
this.occlude()
|
||||||
|
.on_mouse_move(|_, _, cx| cx.stop_propagation())
|
||||||
|
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||||
|
.child((self.render_hover)(window, cx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for AddedContext {
|
||||||
|
fn scope() -> ComponentScope {
|
||||||
|
ComponentScope::Agent
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sort_name() -> &'static str {
|
||||||
|
"AddedContext"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
|
||||||
|
let mut next_context_id = ContextId::zero();
|
||||||
|
let image_ready = (
|
||||||
|
"Ready",
|
||||||
|
AddedContext::image(
|
||||||
|
ImageContext {
|
||||||
|
context_id: next_context_id.post_inc(),
|
||||||
|
project_path: None,
|
||||||
|
full_path: None,
|
||||||
|
original_image: Arc::new(Image::empty()),
|
||||||
|
image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
PathStyle::local(),
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let image_loading = (
|
||||||
|
"Loading",
|
||||||
|
AddedContext::image(
|
||||||
|
ImageContext {
|
||||||
|
context_id: next_context_id.post_inc(),
|
||||||
|
project_path: None,
|
||||||
|
full_path: None,
|
||||||
|
original_image: Arc::new(Image::empty()),
|
||||||
|
image_task: cx
|
||||||
|
.background_spawn(async move {
|
||||||
|
smol::Timer::after(Duration::from_secs(60 * 5)).await;
|
||||||
|
Some(LanguageModelImage::empty())
|
||||||
|
})
|
||||||
|
.shared(),
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
PathStyle::local(),
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let image_error = (
|
||||||
|
"Error",
|
||||||
|
AddedContext::image(
|
||||||
|
ImageContext {
|
||||||
|
context_id: next_context_id.post_inc(),
|
||||||
|
project_path: None,
|
||||||
|
full_path: None,
|
||||||
|
original_image: Arc::new(Image::empty()),
|
||||||
|
image_task: Task::ready(None).shared(),
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
PathStyle::local(),
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Some(
|
||||||
|
v_flex()
|
||||||
|
.gap_6()
|
||||||
|
.children(
|
||||||
|
vec![image_ready, image_loading, image_error]
|
||||||
|
.into_iter()
|
||||||
|
.map(|(text, context)| {
|
||||||
|
single_example(
|
||||||
|
text,
|
||||||
|
ContextPill::added(context, false, false, None).into_any_element(),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_any(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use gpui::App;
|
||||||
|
use language_model::{LanguageModel, fake_provider::FakeLanguageModel};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn test_image_context_warning_for_unsupported_model(cx: &mut App) {
|
||||||
|
let model: Arc<dyn LanguageModel> = Arc::new(FakeLanguageModel::default());
|
||||||
|
assert!(!model.supports_images());
|
||||||
|
|
||||||
|
let image_context = ImageContext {
|
||||||
|
context_id: ContextId::zero(),
|
||||||
|
project_path: None,
|
||||||
|
original_image: Arc::new(Image::empty()),
|
||||||
|
image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
|
||||||
|
full_path: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let added_context =
|
||||||
|
AddedContext::image(image_context, Some(&model), PathStyle::local(), cx);
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
added_context.status,
|
||||||
|
ContextStatus::Warning { .. }
|
||||||
|
));
|
||||||
|
|
||||||
|
assert!(matches!(added_context.kind, ContextKind::Image));
|
||||||
|
assert_eq!(added_context.name.as_ref(), "Image");
|
||||||
|
assert!(added_context.parent.is_none());
|
||||||
|
assert!(added_context.icon_path.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn test_image_context_ready_for_no_model(cx: &mut App) {
|
||||||
|
let image_context = ImageContext {
|
||||||
|
context_id: ContextId::zero(),
|
||||||
|
project_path: None,
|
||||||
|
original_image: Arc::new(Image::empty()),
|
||||||
|
image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
|
||||||
|
full_path: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let added_context = AddedContext::image(image_context, None, PathStyle::local(), cx);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(added_context.status, ContextStatus::Ready),
|
||||||
|
"Expected ready status when no model provided"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(matches!(added_context.kind, ContextKind::Image));
|
||||||
|
assert_eq!(added_context.name.as_ref(), "Image");
|
||||||
|
assert!(added_context.parent.is_none());
|
||||||
|
assert!(added_context.icon_path.is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
use gpui::{App, IntoElement, Modifiers, RenderOnce, Window};
|
|
||||||
use ui::{prelude::*, render_modifiers};
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct HoldForDefault {
|
|
||||||
is_default: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HoldForDefault {
|
|
||||||
pub fn new(is_default: bool) -> Self {
|
|
||||||
Self { is_default }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for HoldForDefault {
|
|
||||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
||||||
h_flex()
|
|
||||||
.pt_1()
|
|
||||||
.border_t_1()
|
|
||||||
.border_color(cx.theme().colors().border_variant)
|
|
||||||
.gap_0p5()
|
|
||||||
.text_sm()
|
|
||||||
.text_color(Color::Muted.color(cx))
|
|
||||||
.child("Hold")
|
|
||||||
.child(h_flex().flex_shrink_0().children(render_modifiers(
|
|
||||||
&Modifiers::secondary_key(),
|
|
||||||
PlatformStyle::platform(),
|
|
||||||
None,
|
|
||||||
Some(TextSize::Default.rems(cx).into()),
|
|
||||||
true,
|
|
||||||
)))
|
|
||||||
.child(div().map(|this| {
|
|
||||||
if self.is_default {
|
|
||||||
this.child("to unset as default")
|
|
||||||
} else {
|
|
||||||
this.child("to set as default")
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -67,13 +67,6 @@ pub enum Model {
|
|||||||
alias = "claude-opus-4-1-thinking-latest"
|
alias = "claude-opus-4-1-thinking-latest"
|
||||||
)]
|
)]
|
||||||
ClaudeOpus4_1Thinking,
|
ClaudeOpus4_1Thinking,
|
||||||
#[serde(rename = "claude-opus-4-5", alias = "claude-opus-4-5-latest")]
|
|
||||||
ClaudeOpus4_5,
|
|
||||||
#[serde(
|
|
||||||
rename = "claude-opus-4-5-thinking",
|
|
||||||
alias = "claude-opus-4-5-thinking-latest"
|
|
||||||
)]
|
|
||||||
ClaudeOpus4_5Thinking,
|
|
||||||
#[serde(rename = "claude-sonnet-4", alias = "claude-sonnet-4-latest")]
|
#[serde(rename = "claude-sonnet-4", alias = "claude-sonnet-4-latest")]
|
||||||
ClaudeSonnet4,
|
ClaudeSonnet4,
|
||||||
#[serde(
|
#[serde(
|
||||||
@@ -138,14 +131,6 @@ impl Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_id(id: &str) -> Result<Self> {
|
pub fn from_id(id: &str) -> Result<Self> {
|
||||||
if id.starts_with("claude-opus-4-5-thinking") {
|
|
||||||
return Ok(Self::ClaudeOpus4_5Thinking);
|
|
||||||
}
|
|
||||||
|
|
||||||
if id.starts_with("claude-opus-4-5") {
|
|
||||||
return Ok(Self::ClaudeOpus4_5);
|
|
||||||
}
|
|
||||||
|
|
||||||
if id.starts_with("claude-opus-4-1-thinking") {
|
if id.starts_with("claude-opus-4-1-thinking") {
|
||||||
return Ok(Self::ClaudeOpus4_1Thinking);
|
return Ok(Self::ClaudeOpus4_1Thinking);
|
||||||
}
|
}
|
||||||
@@ -223,8 +208,6 @@ impl Model {
|
|||||||
Self::ClaudeOpus4_1 => "claude-opus-4-1-latest",
|
Self::ClaudeOpus4_1 => "claude-opus-4-1-latest",
|
||||||
Self::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest",
|
Self::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest",
|
||||||
Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-thinking-latest",
|
Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-thinking-latest",
|
||||||
Self::ClaudeOpus4_5 => "claude-opus-4-5-latest",
|
|
||||||
Self::ClaudeOpus4_5Thinking => "claude-opus-4-5-thinking-latest",
|
|
||||||
Self::ClaudeSonnet4 => "claude-sonnet-4-latest",
|
Self::ClaudeSonnet4 => "claude-sonnet-4-latest",
|
||||||
Self::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest",
|
Self::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest",
|
||||||
Self::ClaudeSonnet4_5 => "claude-sonnet-4-5-latest",
|
Self::ClaudeSonnet4_5 => "claude-sonnet-4-5-latest",
|
||||||
@@ -247,7 +230,6 @@ impl Model {
|
|||||||
match self {
|
match self {
|
||||||
Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking => "claude-opus-4-20250514",
|
Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking => "claude-opus-4-20250514",
|
||||||
Self::ClaudeOpus4_1 | Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-20250805",
|
Self::ClaudeOpus4_1 | Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-20250805",
|
||||||
Self::ClaudeOpus4_5 | Self::ClaudeOpus4_5Thinking => "claude-opus-4-5-20251101",
|
|
||||||
Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514",
|
Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514",
|
||||||
Self::ClaudeSonnet4_5 | Self::ClaudeSonnet4_5Thinking => "claude-sonnet-4-5-20250929",
|
Self::ClaudeSonnet4_5 | Self::ClaudeSonnet4_5Thinking => "claude-sonnet-4-5-20250929",
|
||||||
Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
|
Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
|
||||||
@@ -267,8 +249,6 @@ impl Model {
|
|||||||
Self::ClaudeOpus4_1 => "Claude Opus 4.1",
|
Self::ClaudeOpus4_1 => "Claude Opus 4.1",
|
||||||
Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
|
Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
|
||||||
Self::ClaudeOpus4_1Thinking => "Claude Opus 4.1 Thinking",
|
Self::ClaudeOpus4_1Thinking => "Claude Opus 4.1 Thinking",
|
||||||
Self::ClaudeOpus4_5 => "Claude Opus 4.5",
|
|
||||||
Self::ClaudeOpus4_5Thinking => "Claude Opus 4.5 Thinking",
|
|
||||||
Self::ClaudeSonnet4 => "Claude Sonnet 4",
|
Self::ClaudeSonnet4 => "Claude Sonnet 4",
|
||||||
Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
|
Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
|
||||||
Self::ClaudeSonnet4_5 => "Claude Sonnet 4.5",
|
Self::ClaudeSonnet4_5 => "Claude Sonnet 4.5",
|
||||||
@@ -294,8 +274,6 @@ impl Model {
|
|||||||
| Self::ClaudeOpus4_1
|
| Self::ClaudeOpus4_1
|
||||||
| Self::ClaudeOpus4Thinking
|
| Self::ClaudeOpus4Thinking
|
||||||
| Self::ClaudeOpus4_1Thinking
|
| Self::ClaudeOpus4_1Thinking
|
||||||
| Self::ClaudeOpus4_5
|
|
||||||
| Self::ClaudeOpus4_5Thinking
|
|
||||||
| Self::ClaudeSonnet4
|
| Self::ClaudeSonnet4
|
||||||
| Self::ClaudeSonnet4Thinking
|
| Self::ClaudeSonnet4Thinking
|
||||||
| Self::ClaudeSonnet4_5
|
| Self::ClaudeSonnet4_5
|
||||||
@@ -325,8 +303,6 @@ impl Model {
|
|||||||
| Self::ClaudeOpus4_1
|
| Self::ClaudeOpus4_1
|
||||||
| Self::ClaudeOpus4Thinking
|
| Self::ClaudeOpus4Thinking
|
||||||
| Self::ClaudeOpus4_1Thinking
|
| Self::ClaudeOpus4_1Thinking
|
||||||
| Self::ClaudeOpus4_5
|
|
||||||
| Self::ClaudeOpus4_5Thinking
|
|
||||||
| Self::ClaudeSonnet4
|
| Self::ClaudeSonnet4
|
||||||
| Self::ClaudeSonnet4Thinking
|
| Self::ClaudeSonnet4Thinking
|
||||||
| Self::ClaudeSonnet4_5
|
| Self::ClaudeSonnet4_5
|
||||||
@@ -350,8 +326,6 @@ impl Model {
|
|||||||
| Self::ClaudeOpus4_1
|
| Self::ClaudeOpus4_1
|
||||||
| Self::ClaudeOpus4Thinking
|
| Self::ClaudeOpus4Thinking
|
||||||
| Self::ClaudeOpus4_1Thinking
|
| Self::ClaudeOpus4_1Thinking
|
||||||
| Self::ClaudeOpus4_5
|
|
||||||
| Self::ClaudeOpus4_5Thinking
|
|
||||||
| Self::ClaudeSonnet4
|
| Self::ClaudeSonnet4
|
||||||
| Self::ClaudeSonnet4Thinking
|
| Self::ClaudeSonnet4Thinking
|
||||||
| Self::ClaudeSonnet4_5
|
| Self::ClaudeSonnet4_5
|
||||||
@@ -374,8 +348,6 @@ impl Model {
|
|||||||
| Self::ClaudeOpus4_1
|
| Self::ClaudeOpus4_1
|
||||||
| Self::ClaudeOpus4Thinking
|
| Self::ClaudeOpus4Thinking
|
||||||
| Self::ClaudeOpus4_1Thinking
|
| Self::ClaudeOpus4_1Thinking
|
||||||
| Self::ClaudeOpus4_5
|
|
||||||
| Self::ClaudeOpus4_5Thinking
|
|
||||||
| Self::ClaudeSonnet4
|
| Self::ClaudeSonnet4
|
||||||
| Self::ClaudeSonnet4Thinking
|
| Self::ClaudeSonnet4Thinking
|
||||||
| Self::ClaudeSonnet4_5
|
| Self::ClaudeSonnet4_5
|
||||||
@@ -400,7 +372,6 @@ impl Model {
|
|||||||
match self {
|
match self {
|
||||||
Self::ClaudeOpus4
|
Self::ClaudeOpus4
|
||||||
| Self::ClaudeOpus4_1
|
| Self::ClaudeOpus4_1
|
||||||
| Self::ClaudeOpus4_5
|
|
||||||
| Self::ClaudeSonnet4
|
| Self::ClaudeSonnet4
|
||||||
| Self::ClaudeSonnet4_5
|
| Self::ClaudeSonnet4_5
|
||||||
| Self::Claude3_5Sonnet
|
| Self::Claude3_5Sonnet
|
||||||
@@ -412,7 +383,6 @@ impl Model {
|
|||||||
| Self::Claude3Haiku => AnthropicModelMode::Default,
|
| Self::Claude3Haiku => AnthropicModelMode::Default,
|
||||||
Self::ClaudeOpus4Thinking
|
Self::ClaudeOpus4Thinking
|
||||||
| Self::ClaudeOpus4_1Thinking
|
| Self::ClaudeOpus4_1Thinking
|
||||||
| Self::ClaudeOpus4_5Thinking
|
|
||||||
| Self::ClaudeSonnet4Thinking
|
| Self::ClaudeSonnet4Thinking
|
||||||
| Self::ClaudeSonnet4_5Thinking
|
| Self::ClaudeSonnet4_5Thinking
|
||||||
| Self::ClaudeHaiku4_5Thinking
|
| Self::ClaudeHaiku4_5Thinking
|
||||||
@@ -423,8 +393,13 @@ impl Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn beta_headers(&self) -> Option<String> {
|
pub const DEFAULT_BETA_HEADERS: &[&str] = &["prompt-caching-2024-07-31"];
|
||||||
let mut headers = vec![];
|
|
||||||
|
pub fn beta_headers(&self) -> String {
|
||||||
|
let mut headers = Self::DEFAULT_BETA_HEADERS
|
||||||
|
.iter()
|
||||||
|
.map(|header| header.to_string())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => {
|
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => {
|
||||||
@@ -445,11 +420,7 @@ impl Model {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if headers.is_empty() {
|
headers.join(",")
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(headers.join(","))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tool_model_id(&self) -> &str {
|
pub fn tool_model_id(&self) -> &str {
|
||||||
@@ -465,12 +436,56 @@ impl Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn complete(
|
||||||
|
client: &dyn HttpClient,
|
||||||
|
api_url: &str,
|
||||||
|
api_key: &str,
|
||||||
|
request: Request,
|
||||||
|
beta_headers: String,
|
||||||
|
) -> Result<Response, AnthropicError> {
|
||||||
|
let uri = format!("{api_url}/v1/messages");
|
||||||
|
let request_builder = HttpRequest::builder()
|
||||||
|
.method(Method::POST)
|
||||||
|
.uri(uri)
|
||||||
|
.header("Anthropic-Version", "2023-06-01")
|
||||||
|
.header("Anthropic-Beta", beta_headers)
|
||||||
|
.header("X-Api-Key", api_key.trim())
|
||||||
|
.header("Content-Type", "application/json");
|
||||||
|
|
||||||
|
let serialized_request =
|
||||||
|
serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?;
|
||||||
|
let request = request_builder
|
||||||
|
.body(AsyncBody::from(serialized_request))
|
||||||
|
.map_err(AnthropicError::BuildRequestBody)?;
|
||||||
|
|
||||||
|
let mut response = client
|
||||||
|
.send(request)
|
||||||
|
.await
|
||||||
|
.map_err(AnthropicError::HttpSend)?;
|
||||||
|
let status_code = response.status();
|
||||||
|
let mut body = String::new();
|
||||||
|
response
|
||||||
|
.body_mut()
|
||||||
|
.read_to_string(&mut body)
|
||||||
|
.await
|
||||||
|
.map_err(AnthropicError::ReadResponse)?;
|
||||||
|
|
||||||
|
if status_code.is_success() {
|
||||||
|
Ok(serde_json::from_str(&body).map_err(AnthropicError::DeserializeResponse)?)
|
||||||
|
} else {
|
||||||
|
Err(AnthropicError::HttpResponseError {
|
||||||
|
status_code,
|
||||||
|
message: body,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn stream_completion(
|
pub async fn stream_completion(
|
||||||
client: &dyn HttpClient,
|
client: &dyn HttpClient,
|
||||||
api_url: &str,
|
api_url: &str,
|
||||||
api_key: &str,
|
api_key: &str,
|
||||||
request: Request,
|
request: Request,
|
||||||
beta_headers: Option<String>,
|
beta_headers: String,
|
||||||
) -> Result<BoxStream<'static, Result<Event, AnthropicError>>, AnthropicError> {
|
) -> Result<BoxStream<'static, Result<Event, AnthropicError>>, AnthropicError> {
|
||||||
stream_completion_with_rate_limit_info(client, api_url, api_key, request, beta_headers)
|
stream_completion_with_rate_limit_info(client, api_url, api_key, request, beta_headers)
|
||||||
.await
|
.await
|
||||||
@@ -568,7 +583,7 @@ pub async fn stream_completion_with_rate_limit_info(
|
|||||||
api_url: &str,
|
api_url: &str,
|
||||||
api_key: &str,
|
api_key: &str,
|
||||||
request: Request,
|
request: Request,
|
||||||
beta_headers: Option<String>,
|
beta_headers: String,
|
||||||
) -> Result<
|
) -> Result<
|
||||||
(
|
(
|
||||||
BoxStream<'static, Result<Event, AnthropicError>>,
|
BoxStream<'static, Result<Event, AnthropicError>>,
|
||||||
@@ -582,17 +597,13 @@ pub async fn stream_completion_with_rate_limit_info(
|
|||||||
};
|
};
|
||||||
let uri = format!("{api_url}/v1/messages");
|
let uri = format!("{api_url}/v1/messages");
|
||||||
|
|
||||||
let mut request_builder = HttpRequest::builder()
|
let request_builder = HttpRequest::builder()
|
||||||
.method(Method::POST)
|
.method(Method::POST)
|
||||||
.uri(uri)
|
.uri(uri)
|
||||||
.header("Anthropic-Version", "2023-06-01")
|
.header("Anthropic-Version", "2023-06-01")
|
||||||
|
.header("Anthropic-Beta", beta_headers)
|
||||||
.header("X-Api-Key", api_key.trim())
|
.header("X-Api-Key", api_key.trim())
|
||||||
.header("Content-Type", "application/json");
|
.header("Content-Type", "application/json");
|
||||||
|
|
||||||
if let Some(beta_headers) = beta_headers {
|
|
||||||
request_builder = request_builder.header("Anthropic-Beta", beta_headers);
|
|
||||||
}
|
|
||||||
|
|
||||||
let serialized_request =
|
let serialized_request =
|
||||||
serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?;
|
serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?;
|
||||||
let request = request_builder
|
let request = request_builder
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ impl PasswordProxy {
|
|||||||
.await
|
.await
|
||||||
.with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?;
|
.with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?;
|
||||||
make_file_executable(&askpass_script_path).await?;
|
make_file_executable(&askpass_script_path).await?;
|
||||||
|
// todo(shell): There might be no powershell on the system
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let askpass_helper = format!(
|
let askpass_helper = format!(
|
||||||
"powershell.exe -ExecutionPolicy Bypass -File {}",
|
"powershell.exe -ExecutionPolicy Bypass -File {}",
|
||||||
@@ -374,7 +375,7 @@ fn generate_askpass_script(
|
|||||||
Ok(format!(
|
Ok(format!(
|
||||||
r#"
|
r#"
|
||||||
$ErrorActionPreference = 'Stop';
|
$ErrorActionPreference = 'Stop';
|
||||||
($args -join [char]0) | {askpass_program} --askpass={askpass_socket} 2> $null
|
($args -join [char]0) | & {askpass_program} --askpass={askpass_socket} 2> $null
|
||||||
"#,
|
"#,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ fs.workspace = true
|
|||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
fuzzy.workspace = true
|
fuzzy.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
itertools.workspace = true
|
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
language_model.workspace = true
|
language_model.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
|||||||
@@ -880,9 +880,10 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
|||||||
let num_sections = rng.random_range(0..=3);
|
let num_sections = rng.random_range(0..=3);
|
||||||
let mut section_start = 0;
|
let mut section_start = 0;
|
||||||
for _ in 0..num_sections {
|
for _ in 0..num_sections {
|
||||||
let section_end = output_text.floor_char_boundary(
|
let mut section_end = rng.random_range(section_start..=output_text.len());
|
||||||
rng.random_range(section_start..=output_text.len()),
|
while !output_text.is_char_boundary(section_end) {
|
||||||
);
|
section_end += 1;
|
||||||
|
}
|
||||||
events.push(Ok(SlashCommandEvent::StartSection {
|
events.push(Ok(SlashCommandEvent::StartSection {
|
||||||
icon: IconName::Ai,
|
icon: IconName::Ai,
|
||||||
label: "section".into(),
|
label: "section".into(),
|
||||||
|
|||||||
@@ -7,16 +7,14 @@ use assistant_slash_command::{
|
|||||||
use assistant_slash_commands::FileCommandMetadata;
|
use assistant_slash_commands::FileCommandMetadata;
|
||||||
use client::{self, ModelRequestUsage, RequestUsage, proto, telemetry::Telemetry};
|
use client::{self, ModelRequestUsage, RequestUsage, proto, telemetry::Telemetry};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use cloud_llm_client::{CompletionIntent, UsageLimit};
|
use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use fs::{Fs, RenameOptions};
|
use fs::{Fs, RenameOptions};
|
||||||
|
|
||||||
use futures::{FutureExt, StreamExt, future::Shared};
|
use futures::{FutureExt, StreamExt, future::Shared};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext as _, Context, Entity, EventEmitter, RenderImage, SharedString, Subscription,
|
App, AppContext as _, Context, Entity, EventEmitter, RenderImage, SharedString, Subscription,
|
||||||
Task,
|
Task,
|
||||||
};
|
};
|
||||||
use itertools::Itertools as _;
|
|
||||||
use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
|
use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
|
||||||
use language_model::{
|
use language_model::{
|
||||||
LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent,
|
LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent,
|
||||||
@@ -669,7 +667,7 @@ pub struct TextThread {
|
|||||||
buffer: Entity<Buffer>,
|
buffer: Entity<Buffer>,
|
||||||
pub(crate) parsed_slash_commands: Vec<ParsedSlashCommand>,
|
pub(crate) parsed_slash_commands: Vec<ParsedSlashCommand>,
|
||||||
invoked_slash_commands: HashMap<InvokedSlashCommandId, InvokedSlashCommand>,
|
invoked_slash_commands: HashMap<InvokedSlashCommandId, InvokedSlashCommand>,
|
||||||
edits_since_last_parse: language::Subscription<usize>,
|
edits_since_last_parse: language::Subscription,
|
||||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||||
pub(crate) slash_command_output_sections: Vec<SlashCommandOutputSection<language::Anchor>>,
|
pub(crate) slash_command_output_sections: Vec<SlashCommandOutputSection<language::Anchor>>,
|
||||||
thought_process_output_sections: Vec<ThoughtProcessOutputSection<language::Anchor>>,
|
thought_process_output_sections: Vec<ThoughtProcessOutputSection<language::Anchor>>,
|
||||||
@@ -1418,7 +1416,6 @@ impl TextThread {
|
|||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec!["Respond only with OK, nothing else.".into()],
|
content: vec!["Respond only with OK, nothing else.".into()],
|
||||||
cache: false,
|
cache: false,
|
||||||
reasoning_details: None,
|
|
||||||
});
|
});
|
||||||
req
|
req
|
||||||
};
|
};
|
||||||
@@ -1854,17 +1851,14 @@ impl TextThread {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ensure_trailing_newline
|
if ensure_trailing_newline
|
||||||
&& buffer
|
&& buffer.contains_str_at(command_range_end, "\n")
|
||||||
.chars_at(command_range_end)
|
|
||||||
.next()
|
|
||||||
.is_some_and(|c| c == '\n')
|
|
||||||
{
|
{
|
||||||
if let Some((prev_char, '\n')) =
|
let newline_offset = insert_position.saturating_sub(1);
|
||||||
buffer.reversed_chars_at(insert_position).next_tuple()
|
if buffer.contains_str_at(newline_offset, "\n")
|
||||||
&& last_section_range.is_none_or(|last_section_range| {
|
&& last_section_range.is_none_or(|last_section_range| {
|
||||||
!last_section_range
|
!last_section_range
|
||||||
.to_offset(buffer)
|
.to_offset(buffer)
|
||||||
.contains(&(insert_position - prev_char.len_utf8()))
|
.contains(&newline_offset)
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
deletions.push((command_range_end..command_range_end + 1, ""));
|
deletions.push((command_range_end..command_range_end + 1, ""));
|
||||||
@@ -2079,22 +2073,16 @@ impl TextThread {
|
|||||||
});
|
});
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
LanguageModelCompletionEvent::Started |
|
LanguageModelCompletionEvent::StatusUpdate(status_update) => {
|
||||||
LanguageModelCompletionEvent::Queued {..} |
|
if let CompletionRequestStatus::UsageUpdated { amount, limit } = status_update {
|
||||||
LanguageModelCompletionEvent::ToolUseLimitReached { .. } => {}
|
this.update_model_request_usage(
|
||||||
LanguageModelCompletionEvent::UsageUpdated { amount, limit } => {
|
amount as u32,
|
||||||
this.update_model_request_usage(
|
limit,
|
||||||
amount as u32,
|
cx,
|
||||||
limit,
|
);
|
||||||
cx,
|
}
|
||||||
);
|
|
||||||
}
|
}
|
||||||
LanguageModelCompletionEvent::StartMessage { .. } => {}
|
LanguageModelCompletionEvent::StartMessage { .. } => {}
|
||||||
LanguageModelCompletionEvent::ReasoningDetails(_) => {
|
|
||||||
// ReasoningDetails are metadata (signatures, encrypted data, format info)
|
|
||||||
// used for request/response validation, not UI content.
|
|
||||||
// The displayable thinking text is already handled by the Thinking event.
|
|
||||||
}
|
|
||||||
LanguageModelCompletionEvent::Stop(reason) => {
|
LanguageModelCompletionEvent::Stop(reason) => {
|
||||||
stop_reason = reason;
|
stop_reason = reason;
|
||||||
}
|
}
|
||||||
@@ -2318,7 +2306,6 @@ impl TextThread {
|
|||||||
role: message.role,
|
role: message.role,
|
||||||
content: Vec::new(),
|
content: Vec::new(),
|
||||||
cache: message.cache.as_ref().is_some_and(|cache| cache.is_anchor),
|
cache: message.cache.as_ref().is_some_and(|cache| cache.is_anchor),
|
||||||
reasoning_details: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
while let Some(content) = contents.peek() {
|
while let Some(content) = contents.peek() {
|
||||||
@@ -2690,7 +2677,6 @@ impl TextThread {
|
|||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec![SUMMARIZE_THREAD_PROMPT.into()],
|
content: vec![SUMMARIZE_THREAD_PROMPT.into()],
|
||||||
cache: false,
|
cache: false,
|
||||||
reasoning_details: None,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// If there is no summary, it is set with `done: false` so that "Loading Summary…" can
|
// If there is no summary, it is set with `done: false` so that "Loading Summary…" can
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ http_client.workspace = true
|
|||||||
log.workspace = true
|
log.workspace = true
|
||||||
paths.workspace = true
|
paths.workspace = true
|
||||||
release_channel.workspace = true
|
release_channel.workspace = true
|
||||||
semver.workspace = true
|
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
settings.workspace = true
|
settings.workspace = true
|
||||||
|
|||||||
@@ -2,13 +2,12 @@ use anyhow::{Context as _, Result};
|
|||||||
use client::Client;
|
use client::Client;
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, Global, Task, Window,
|
App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, Global, SemanticVersion,
|
||||||
actions,
|
Task, Window, actions,
|
||||||
};
|
};
|
||||||
use http_client::{HttpClient, HttpClientWithUrl};
|
use http_client::{HttpClient, HttpClientWithUrl};
|
||||||
use paths::remote_servers_dir;
|
use paths::remote_servers_dir;
|
||||||
use release_channel::{AppCommitSha, ReleaseChannel};
|
use release_channel::{AppCommitSha, ReleaseChannel};
|
||||||
use semver::Version;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{RegisterSetting, Settings, SettingsStore};
|
use settings::{RegisterSetting, Settings, SettingsStore};
|
||||||
use smol::fs::File;
|
use smol::fs::File;
|
||||||
@@ -45,7 +44,7 @@ actions!(
|
|||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum VersionCheckType {
|
pub enum VersionCheckType {
|
||||||
Sha(AppCommitSha),
|
Sha(AppCommitSha),
|
||||||
Semantic(Version),
|
Semantic(SemanticVersion),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
#[derive(Serialize, Debug)]
|
||||||
@@ -101,7 +100,7 @@ impl AutoUpdateStatus {
|
|||||||
|
|
||||||
pub struct AutoUpdater {
|
pub struct AutoUpdater {
|
||||||
status: AutoUpdateStatus,
|
status: AutoUpdateStatus,
|
||||||
current_version: Version,
|
current_version: SemanticVersion,
|
||||||
client: Arc<Client>,
|
client: Arc<Client>,
|
||||||
pending_poll: Option<Task<Option<()>>>,
|
pending_poll: Option<Task<Option<()>>>,
|
||||||
quit_subscription: Option<gpui::Subscription>,
|
quit_subscription: Option<gpui::Subscription>,
|
||||||
@@ -257,7 +256,7 @@ pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut App) -> Option<()> {
|
|||||||
match release_channel {
|
match release_channel {
|
||||||
ReleaseChannel::Stable | ReleaseChannel::Preview => {
|
ReleaseChannel::Stable | ReleaseChannel::Preview => {
|
||||||
let auto_updater = auto_updater.read(cx);
|
let auto_updater = auto_updater.read(cx);
|
||||||
let current_version = auto_updater.current_version.clone();
|
let current_version = auto_updater.current_version;
|
||||||
let release_channel = release_channel.dev_name();
|
let release_channel = release_channel.dev_name();
|
||||||
let path = format!("/releases/{release_channel}/{current_version}");
|
let path = format!("/releases/{release_channel}/{current_version}");
|
||||||
let url = &auto_updater.client.http_client().build_url(&path);
|
let url = &auto_updater.client.http_client().build_url(&path);
|
||||||
@@ -323,7 +322,7 @@ impl AutoUpdater {
|
|||||||
cx.default_global::<GlobalAutoUpdate>().0.clone()
|
cx.default_global::<GlobalAutoUpdate>().0.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new(current_version: Version, client: Arc<Client>, cx: &mut Context<Self>) -> Self {
|
fn new(current_version: SemanticVersion, client: Arc<Client>, cx: &mut Context<Self>) -> Self {
|
||||||
// On windows, executable files cannot be overwritten while they are
|
// On windows, executable files cannot be overwritten while they are
|
||||||
// running, so we must wait to overwrite the application until quitting
|
// running, so we must wait to overwrite the application until quitting
|
||||||
// or restarting. When quitting the app, we spawn the auto update helper
|
// or restarting. When quitting the app, we spawn the auto update helper
|
||||||
@@ -401,8 +400,8 @@ impl AutoUpdater {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn current_version(&self) -> Version {
|
pub fn current_version(&self) -> SemanticVersion {
|
||||||
self.current_version.clone()
|
self.current_version
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn status(&self) -> AutoUpdateStatus {
|
pub fn status(&self) -> AutoUpdateStatus {
|
||||||
@@ -423,7 +422,7 @@ impl AutoUpdater {
|
|||||||
// Ok(None).
|
// Ok(None).
|
||||||
pub async fn download_remote_server_release(
|
pub async fn download_remote_server_release(
|
||||||
release_channel: ReleaseChannel,
|
release_channel: ReleaseChannel,
|
||||||
version: Option<Version>,
|
version: Option<SemanticVersion>,
|
||||||
os: &str,
|
os: &str,
|
||||||
arch: &str,
|
arch: &str,
|
||||||
set_status: impl Fn(&str, &mut AsyncApp) + Send + 'static,
|
set_status: impl Fn(&str, &mut AsyncApp) + Send + 'static,
|
||||||
@@ -470,7 +469,7 @@ impl AutoUpdater {
|
|||||||
|
|
||||||
pub async fn get_remote_server_release_url(
|
pub async fn get_remote_server_release_url(
|
||||||
channel: ReleaseChannel,
|
channel: ReleaseChannel,
|
||||||
version: Option<Version>,
|
version: Option<SemanticVersion>,
|
||||||
os: &str,
|
os: &str,
|
||||||
arch: &str,
|
arch: &str,
|
||||||
cx: &mut AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
@@ -492,7 +491,7 @@ impl AutoUpdater {
|
|||||||
async fn get_release_asset(
|
async fn get_release_asset(
|
||||||
this: &Entity<Self>,
|
this: &Entity<Self>,
|
||||||
release_channel: ReleaseChannel,
|
release_channel: ReleaseChannel,
|
||||||
version: Option<Version>,
|
version: Option<SemanticVersion>,
|
||||||
asset: &str,
|
asset: &str,
|
||||||
os: &str,
|
os: &str,
|
||||||
arch: &str,
|
arch: &str,
|
||||||
@@ -555,7 +554,7 @@ impl AutoUpdater {
|
|||||||
this.read_with(cx, |this, cx| {
|
this.read_with(cx, |this, cx| {
|
||||||
(
|
(
|
||||||
this.client.http_client(),
|
this.client.http_client(),
|
||||||
this.current_version.clone(),
|
this.current_version,
|
||||||
this.status.clone(),
|
this.status.clone(),
|
||||||
ReleaseChannel::try_global(cx).unwrap_or(ReleaseChannel::Stable),
|
ReleaseChannel::try_global(cx).unwrap_or(ReleaseChannel::Stable),
|
||||||
)
|
)
|
||||||
@@ -628,19 +627,16 @@ impl AutoUpdater {
|
|||||||
fn check_if_fetched_version_is_newer(
|
fn check_if_fetched_version_is_newer(
|
||||||
release_channel: ReleaseChannel,
|
release_channel: ReleaseChannel,
|
||||||
app_commit_sha: Result<Option<String>>,
|
app_commit_sha: Result<Option<String>>,
|
||||||
installed_version: Version,
|
installed_version: SemanticVersion,
|
||||||
fetched_version: String,
|
fetched_version: String,
|
||||||
status: AutoUpdateStatus,
|
status: AutoUpdateStatus,
|
||||||
) -> Result<Option<VersionCheckType>> {
|
) -> Result<Option<VersionCheckType>> {
|
||||||
let parsed_fetched_version = fetched_version.parse::<Version>();
|
let parsed_fetched_version = fetched_version.parse::<SemanticVersion>();
|
||||||
|
|
||||||
if let AutoUpdateStatus::Updated { version, .. } = status {
|
if let AutoUpdateStatus::Updated { version, .. } = status {
|
||||||
match version {
|
match version {
|
||||||
VersionCheckType::Sha(cached_version) => {
|
VersionCheckType::Sha(cached_version) => {
|
||||||
let should_download = parsed_fetched_version
|
let should_download = fetched_version != cached_version.full();
|
||||||
.as_ref()
|
|
||||||
.ok()
|
|
||||||
.is_none_or(|version| version.build.as_str() != cached_version.full());
|
|
||||||
let newer_version = should_download
|
let newer_version = should_download
|
||||||
.then(|| VersionCheckType::Sha(AppCommitSha::new(fetched_version)));
|
.then(|| VersionCheckType::Sha(AppCommitSha::new(fetched_version)));
|
||||||
return Ok(newer_version);
|
return Ok(newer_version);
|
||||||
@@ -659,12 +655,7 @@ impl AutoUpdater {
|
|||||||
let should_download = app_commit_sha
|
let should_download = app_commit_sha
|
||||||
.ok()
|
.ok()
|
||||||
.flatten()
|
.flatten()
|
||||||
.map(|sha| {
|
.map(|sha| fetched_version != sha)
|
||||||
parsed_fetched_version
|
|
||||||
.as_ref()
|
|
||||||
.ok()
|
|
||||||
.is_none_or(|version| version.build.as_str() != sha)
|
|
||||||
})
|
|
||||||
.unwrap_or(true);
|
.unwrap_or(true);
|
||||||
let newer_version = should_download
|
let newer_version = should_download
|
||||||
.then(|| VersionCheckType::Sha(AppCommitSha::new(fetched_version)));
|
.then(|| VersionCheckType::Sha(AppCommitSha::new(fetched_version)));
|
||||||
@@ -717,8 +708,8 @@ impl AutoUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn check_if_fetched_version_is_newer_non_nightly(
|
fn check_if_fetched_version_is_newer_non_nightly(
|
||||||
installed_version: Version,
|
installed_version: SemanticVersion,
|
||||||
fetched_version: Version,
|
fetched_version: SemanticVersion,
|
||||||
) -> Result<Option<VersionCheckType>> {
|
) -> Result<Option<VersionCheckType>> {
|
||||||
let should_download = fetched_version > installed_version;
|
let should_download = fetched_version > installed_version;
|
||||||
let newer_version = should_download.then(|| VersionCheckType::Semantic(fetched_version));
|
let newer_version = should_download.then(|| VersionCheckType::Semantic(fetched_version));
|
||||||
@@ -1029,7 +1020,7 @@ mod tests {
|
|||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
settings::init(cx);
|
settings::init(cx);
|
||||||
|
|
||||||
let current_version = semver::Version::new(0, 100, 0);
|
let current_version = SemanticVersion::new(0, 100, 0);
|
||||||
release_channel::init_test(current_version, ReleaseChannel::Stable, cx);
|
release_channel::init_test(current_version, ReleaseChannel::Stable, cx);
|
||||||
|
|
||||||
let clock = Arc::new(FakeSystemClock::new());
|
let clock = Arc::new(FakeSystemClock::new());
|
||||||
@@ -1068,7 +1059,7 @@ mod tests {
|
|||||||
|
|
||||||
auto_updater.read_with(cx, |updater, _| {
|
auto_updater.read_with(cx, |updater, _| {
|
||||||
assert_eq!(updater.status(), AutoUpdateStatus::Idle);
|
assert_eq!(updater.status(), AutoUpdateStatus::Idle);
|
||||||
assert_eq!(updater.current_version(), semver::Version::new(0, 100, 0));
|
assert_eq!(updater.current_version(), SemanticVersion::new(0, 100, 0));
|
||||||
});
|
});
|
||||||
|
|
||||||
release_available.store(true, atomic::Ordering::SeqCst);
|
release_available.store(true, atomic::Ordering::SeqCst);
|
||||||
@@ -1087,7 +1078,7 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
status,
|
status,
|
||||||
AutoUpdateStatus::Downloading {
|
AutoUpdateStatus::Downloading {
|
||||||
version: VersionCheckType::Semantic(semver::Version::new(0, 100, 1))
|
version: VersionCheckType::Semantic(SemanticVersion::new(0, 100, 1))
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1117,7 +1108,7 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
status,
|
status,
|
||||||
AutoUpdateStatus::Updated {
|
AutoUpdateStatus::Updated {
|
||||||
version: VersionCheckType::Semantic(semver::Version::new(0, 100, 1))
|
version: VersionCheckType::Semantic(SemanticVersion::new(0, 100, 1))
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
let will_restart = cx.expect_restart();
|
let will_restart = cx.expect_restart();
|
||||||
@@ -1131,9 +1122,9 @@ mod tests {
|
|||||||
fn test_stable_does_not_update_when_fetched_version_is_not_higher() {
|
fn test_stable_does_not_update_when_fetched_version_is_not_higher() {
|
||||||
let release_channel = ReleaseChannel::Stable;
|
let release_channel = ReleaseChannel::Stable;
|
||||||
let app_commit_sha = Ok(Some("a".to_string()));
|
let app_commit_sha = Ok(Some("a".to_string()));
|
||||||
let installed_version = semver::Version::new(1, 0, 0);
|
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||||
let status = AutoUpdateStatus::Idle;
|
let status = AutoUpdateStatus::Idle;
|
||||||
let fetched_version = semver::Version::new(1, 0, 0);
|
let fetched_version = SemanticVersion::new(1, 0, 0);
|
||||||
|
|
||||||
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
||||||
release_channel,
|
release_channel,
|
||||||
@@ -1150,9 +1141,9 @@ mod tests {
|
|||||||
fn test_stable_does_update_when_fetched_version_is_higher() {
|
fn test_stable_does_update_when_fetched_version_is_higher() {
|
||||||
let release_channel = ReleaseChannel::Stable;
|
let release_channel = ReleaseChannel::Stable;
|
||||||
let app_commit_sha = Ok(Some("a".to_string()));
|
let app_commit_sha = Ok(Some("a".to_string()));
|
||||||
let installed_version = semver::Version::new(1, 0, 0);
|
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||||
let status = AutoUpdateStatus::Idle;
|
let status = AutoUpdateStatus::Idle;
|
||||||
let fetched_version = semver::Version::new(1, 0, 1);
|
let fetched_version = SemanticVersion::new(1, 0, 1);
|
||||||
|
|
||||||
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
||||||
release_channel,
|
release_channel,
|
||||||
@@ -1172,11 +1163,11 @@ mod tests {
|
|||||||
fn test_stable_does_not_update_when_fetched_version_is_not_higher_than_cached() {
|
fn test_stable_does_not_update_when_fetched_version_is_not_higher_than_cached() {
|
||||||
let release_channel = ReleaseChannel::Stable;
|
let release_channel = ReleaseChannel::Stable;
|
||||||
let app_commit_sha = Ok(Some("a".to_string()));
|
let app_commit_sha = Ok(Some("a".to_string()));
|
||||||
let installed_version = semver::Version::new(1, 0, 0);
|
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||||
let status = AutoUpdateStatus::Updated {
|
let status = AutoUpdateStatus::Updated {
|
||||||
version: VersionCheckType::Semantic(semver::Version::new(1, 0, 1)),
|
version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
|
||||||
};
|
};
|
||||||
let fetched_version = semver::Version::new(1, 0, 1);
|
let fetched_version = SemanticVersion::new(1, 0, 1);
|
||||||
|
|
||||||
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
||||||
release_channel,
|
release_channel,
|
||||||
@@ -1193,11 +1184,11 @@ mod tests {
|
|||||||
fn test_stable_does_update_when_fetched_version_is_higher_than_cached() {
|
fn test_stable_does_update_when_fetched_version_is_higher_than_cached() {
|
||||||
let release_channel = ReleaseChannel::Stable;
|
let release_channel = ReleaseChannel::Stable;
|
||||||
let app_commit_sha = Ok(Some("a".to_string()));
|
let app_commit_sha = Ok(Some("a".to_string()));
|
||||||
let installed_version = semver::Version::new(1, 0, 0);
|
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||||
let status = AutoUpdateStatus::Updated {
|
let status = AutoUpdateStatus::Updated {
|
||||||
version: VersionCheckType::Semantic(semver::Version::new(1, 0, 1)),
|
version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
|
||||||
};
|
};
|
||||||
let fetched_version = semver::Version::new(1, 0, 2);
|
let fetched_version = SemanticVersion::new(1, 0, 2);
|
||||||
|
|
||||||
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
||||||
release_channel,
|
release_channel,
|
||||||
@@ -1217,10 +1208,9 @@ mod tests {
|
|||||||
fn test_nightly_does_not_update_when_fetched_sha_is_same() {
|
fn test_nightly_does_not_update_when_fetched_sha_is_same() {
|
||||||
let release_channel = ReleaseChannel::Nightly;
|
let release_channel = ReleaseChannel::Nightly;
|
||||||
let app_commit_sha = Ok(Some("a".to_string()));
|
let app_commit_sha = Ok(Some("a".to_string()));
|
||||||
let mut installed_version = semver::Version::new(1, 0, 0);
|
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||||
installed_version.build = semver::BuildMetadata::new("a").unwrap();
|
|
||||||
let status = AutoUpdateStatus::Idle;
|
let status = AutoUpdateStatus::Idle;
|
||||||
let fetched_sha = "1.0.0+a".to_string();
|
let fetched_sha = "a".to_string();
|
||||||
|
|
||||||
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
||||||
release_channel,
|
release_channel,
|
||||||
@@ -1237,7 +1227,7 @@ mod tests {
|
|||||||
fn test_nightly_does_update_when_fetched_sha_is_not_same() {
|
fn test_nightly_does_update_when_fetched_sha_is_not_same() {
|
||||||
let release_channel = ReleaseChannel::Nightly;
|
let release_channel = ReleaseChannel::Nightly;
|
||||||
let app_commit_sha = Ok(Some("a".to_string()));
|
let app_commit_sha = Ok(Some("a".to_string()));
|
||||||
let installed_version = semver::Version::new(1, 0, 0);
|
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||||
let status = AutoUpdateStatus::Idle;
|
let status = AutoUpdateStatus::Idle;
|
||||||
let fetched_sha = "b".to_string();
|
let fetched_sha = "b".to_string();
|
||||||
|
|
||||||
@@ -1256,15 +1246,14 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_nightly_does_not_update_when_fetched_version_is_same_as_cached() {
|
fn test_nightly_does_not_update_when_fetched_sha_is_same_as_cached() {
|
||||||
let release_channel = ReleaseChannel::Nightly;
|
let release_channel = ReleaseChannel::Nightly;
|
||||||
let app_commit_sha = Ok(Some("a".to_string()));
|
let app_commit_sha = Ok(Some("a".to_string()));
|
||||||
let mut installed_version = semver::Version::new(1, 0, 0);
|
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||||
installed_version.build = semver::BuildMetadata::new("a").unwrap();
|
|
||||||
let status = AutoUpdateStatus::Updated {
|
let status = AutoUpdateStatus::Updated {
|
||||||
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
|
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
|
||||||
};
|
};
|
||||||
let fetched_sha = "1.0.0+b".to_string();
|
let fetched_sha = "b".to_string();
|
||||||
|
|
||||||
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
||||||
release_channel,
|
release_channel,
|
||||||
@@ -1281,12 +1270,11 @@ mod tests {
|
|||||||
fn test_nightly_does_update_when_fetched_sha_is_not_same_as_cached() {
|
fn test_nightly_does_update_when_fetched_sha_is_not_same_as_cached() {
|
||||||
let release_channel = ReleaseChannel::Nightly;
|
let release_channel = ReleaseChannel::Nightly;
|
||||||
let app_commit_sha = Ok(Some("a".to_string()));
|
let app_commit_sha = Ok(Some("a".to_string()));
|
||||||
let mut installed_version = semver::Version::new(1, 0, 0);
|
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||||
installed_version.build = semver::BuildMetadata::new("a").unwrap();
|
|
||||||
let status = AutoUpdateStatus::Updated {
|
let status = AutoUpdateStatus::Updated {
|
||||||
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
|
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
|
||||||
};
|
};
|
||||||
let fetched_sha = "1.0.0+c".to_string();
|
let fetched_sha = "c".to_string();
|
||||||
|
|
||||||
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
||||||
release_channel,
|
release_channel,
|
||||||
@@ -1306,7 +1294,7 @@ mod tests {
|
|||||||
fn test_nightly_does_update_when_installed_versions_sha_cannot_be_retrieved() {
|
fn test_nightly_does_update_when_installed_versions_sha_cannot_be_retrieved() {
|
||||||
let release_channel = ReleaseChannel::Nightly;
|
let release_channel = ReleaseChannel::Nightly;
|
||||||
let app_commit_sha = Ok(None);
|
let app_commit_sha = Ok(None);
|
||||||
let installed_version = semver::Version::new(1, 0, 0);
|
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||||
let status = AutoUpdateStatus::Idle;
|
let status = AutoUpdateStatus::Idle;
|
||||||
let fetched_sha = "a".to_string();
|
let fetched_sha = "a".to_string();
|
||||||
|
|
||||||
@@ -1329,11 +1317,11 @@ mod tests {
|
|||||||
{
|
{
|
||||||
let release_channel = ReleaseChannel::Nightly;
|
let release_channel = ReleaseChannel::Nightly;
|
||||||
let app_commit_sha = Ok(None);
|
let app_commit_sha = Ok(None);
|
||||||
let installed_version = semver::Version::new(1, 0, 0);
|
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||||
let status = AutoUpdateStatus::Updated {
|
let status = AutoUpdateStatus::Updated {
|
||||||
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
|
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
|
||||||
};
|
};
|
||||||
let fetched_sha = "1.0.0+b".to_string();
|
let fetched_sha = "b".to_string();
|
||||||
|
|
||||||
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
|
||||||
release_channel,
|
release_channel,
|
||||||
@@ -1351,7 +1339,7 @@ mod tests {
|
|||||||
{
|
{
|
||||||
let release_channel = ReleaseChannel::Nightly;
|
let release_channel = ReleaseChannel::Nightly;
|
||||||
let app_commit_sha = Ok(None);
|
let app_commit_sha = Ok(None);
|
||||||
let installed_version = semver::Version::new(1, 0, 0);
|
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||||
let status = AutoUpdateStatus::Updated {
|
let status = AutoUpdateStatus::Updated {
|
||||||
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
|
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -51,13 +51,6 @@ pub enum Model {
|
|||||||
alias = "claude-opus-4-1-thinking-latest"
|
alias = "claude-opus-4-1-thinking-latest"
|
||||||
)]
|
)]
|
||||||
ClaudeOpus4_1Thinking,
|
ClaudeOpus4_1Thinking,
|
||||||
#[serde(rename = "claude-opus-4-5", alias = "claude-opus-4-5-latest")]
|
|
||||||
ClaudeOpus4_5,
|
|
||||||
#[serde(
|
|
||||||
rename = "claude-opus-4-5-thinking",
|
|
||||||
alias = "claude-opus-4-5-thinking-latest"
|
|
||||||
)]
|
|
||||||
ClaudeOpus4_5Thinking,
|
|
||||||
#[serde(rename = "claude-3-5-sonnet-v2", alias = "claude-3-5-sonnet-latest")]
|
#[serde(rename = "claude-3-5-sonnet-v2", alias = "claude-3-5-sonnet-latest")]
|
||||||
Claude3_5SonnetV2,
|
Claude3_5SonnetV2,
|
||||||
#[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
|
#[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
|
||||||
@@ -148,19 +141,7 @@ impl Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_id(id: &str) -> anyhow::Result<Self> {
|
pub fn from_id(id: &str) -> anyhow::Result<Self> {
|
||||||
if id.starts_with("claude-opus-4-5-thinking") {
|
if id.starts_with("claude-3-5-sonnet-v2") {
|
||||||
Ok(Self::ClaudeOpus4_5Thinking)
|
|
||||||
} else if id.starts_with("claude-opus-4-5") {
|
|
||||||
Ok(Self::ClaudeOpus4_5)
|
|
||||||
} else if id.starts_with("claude-opus-4-1-thinking") {
|
|
||||||
Ok(Self::ClaudeOpus4_1Thinking)
|
|
||||||
} else if id.starts_with("claude-opus-4-1") {
|
|
||||||
Ok(Self::ClaudeOpus4_1)
|
|
||||||
} else if id.starts_with("claude-opus-4-thinking") {
|
|
||||||
Ok(Self::ClaudeOpus4Thinking)
|
|
||||||
} else if id.starts_with("claude-opus-4") {
|
|
||||||
Ok(Self::ClaudeOpus4)
|
|
||||||
} else if id.starts_with("claude-3-5-sonnet-v2") {
|
|
||||||
Ok(Self::Claude3_5SonnetV2)
|
Ok(Self::Claude3_5SonnetV2)
|
||||||
} else if id.starts_with("claude-3-opus") {
|
} else if id.starts_with("claude-3-opus") {
|
||||||
Ok(Self::Claude3Opus)
|
Ok(Self::Claude3Opus)
|
||||||
@@ -197,8 +178,6 @@ impl Model {
|
|||||||
Model::ClaudeOpus4_1 => "claude-opus-4-1",
|
Model::ClaudeOpus4_1 => "claude-opus-4-1",
|
||||||
Model::ClaudeOpus4Thinking => "claude-opus-4-thinking",
|
Model::ClaudeOpus4Thinking => "claude-opus-4-thinking",
|
||||||
Model::ClaudeOpus4_1Thinking => "claude-opus-4-1-thinking",
|
Model::ClaudeOpus4_1Thinking => "claude-opus-4-1-thinking",
|
||||||
Model::ClaudeOpus4_5 => "claude-opus-4-5",
|
|
||||||
Model::ClaudeOpus4_5Thinking => "claude-opus-4-5-thinking",
|
|
||||||
Model::Claude3_5SonnetV2 => "claude-3-5-sonnet-v2",
|
Model::Claude3_5SonnetV2 => "claude-3-5-sonnet-v2",
|
||||||
Model::Claude3_5Sonnet => "claude-3-5-sonnet",
|
Model::Claude3_5Sonnet => "claude-3-5-sonnet",
|
||||||
Model::Claude3Opus => "claude-3-opus",
|
Model::Claude3Opus => "claude-3-opus",
|
||||||
@@ -266,9 +245,6 @@ impl Model {
|
|||||||
Model::ClaudeOpus4_1 | Model::ClaudeOpus4_1Thinking => {
|
Model::ClaudeOpus4_1 | Model::ClaudeOpus4_1Thinking => {
|
||||||
"anthropic.claude-opus-4-1-20250805-v1:0"
|
"anthropic.claude-opus-4-1-20250805-v1:0"
|
||||||
}
|
}
|
||||||
Model::ClaudeOpus4_5 | Model::ClaudeOpus4_5Thinking => {
|
|
||||||
"anthropic.claude-opus-4-5-20251101-v1:0"
|
|
||||||
}
|
|
||||||
Model::Claude3_5SonnetV2 => "anthropic.claude-3-5-sonnet-20241022-v2:0",
|
Model::Claude3_5SonnetV2 => "anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||||
Model::Claude3_5Sonnet => "anthropic.claude-3-5-sonnet-20240620-v1:0",
|
Model::Claude3_5Sonnet => "anthropic.claude-3-5-sonnet-20240620-v1:0",
|
||||||
Model::Claude3Opus => "anthropic.claude-3-opus-20240229-v1:0",
|
Model::Claude3Opus => "anthropic.claude-3-opus-20240229-v1:0",
|
||||||
@@ -333,8 +309,6 @@ impl Model {
|
|||||||
Self::ClaudeOpus4_1 => "Claude Opus 4.1",
|
Self::ClaudeOpus4_1 => "Claude Opus 4.1",
|
||||||
Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
|
Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
|
||||||
Self::ClaudeOpus4_1Thinking => "Claude Opus 4.1 Thinking",
|
Self::ClaudeOpus4_1Thinking => "Claude Opus 4.1 Thinking",
|
||||||
Self::ClaudeOpus4_5 => "Claude Opus 4.5",
|
|
||||||
Self::ClaudeOpus4_5Thinking => "Claude Opus 4.5 Thinking",
|
|
||||||
Self::Claude3_5SonnetV2 => "Claude 3.5 Sonnet v2",
|
Self::Claude3_5SonnetV2 => "Claude 3.5 Sonnet v2",
|
||||||
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
|
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
|
||||||
Self::Claude3Opus => "Claude 3 Opus",
|
Self::Claude3Opus => "Claude 3 Opus",
|
||||||
@@ -405,9 +379,7 @@ impl Model {
|
|||||||
| Self::ClaudeSonnet4_5
|
| Self::ClaudeSonnet4_5
|
||||||
| Self::ClaudeSonnet4_5Thinking
|
| Self::ClaudeSonnet4_5Thinking
|
||||||
| Self::ClaudeOpus4Thinking
|
| Self::ClaudeOpus4Thinking
|
||||||
| Self::ClaudeOpus4_1Thinking
|
| Self::ClaudeOpus4_1Thinking => 200_000,
|
||||||
| Self::ClaudeOpus4_5
|
|
||||||
| Self::ClaudeOpus4_5Thinking => 200_000,
|
|
||||||
Self::AmazonNovaPremier => 1_000_000,
|
Self::AmazonNovaPremier => 1_000_000,
|
||||||
Self::PalmyraWriterX5 => 1_000_000,
|
Self::PalmyraWriterX5 => 1_000_000,
|
||||||
Self::PalmyraWriterX4 => 128_000,
|
Self::PalmyraWriterX4 => 128_000,
|
||||||
@@ -421,11 +393,7 @@ impl Model {
|
|||||||
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3_5Haiku => 4_096,
|
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3_5Haiku => 4_096,
|
||||||
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => 128_000,
|
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => 128_000,
|
||||||
Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => 64_000,
|
Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => 64_000,
|
||||||
Self::ClaudeSonnet4_5
|
Self::ClaudeSonnet4_5 | Self::ClaudeSonnet4_5Thinking | Self::ClaudeHaiku4_5 => 64_000,
|
||||||
| Self::ClaudeSonnet4_5Thinking
|
|
||||||
| Self::ClaudeHaiku4_5
|
|
||||||
| Self::ClaudeOpus4_5
|
|
||||||
| Self::ClaudeOpus4_5Thinking => 64_000,
|
|
||||||
Self::ClaudeOpus4
|
Self::ClaudeOpus4
|
||||||
| Self::ClaudeOpus4Thinking
|
| Self::ClaudeOpus4Thinking
|
||||||
| Self::ClaudeOpus4_1
|
| Self::ClaudeOpus4_1
|
||||||
@@ -450,8 +418,6 @@ impl Model {
|
|||||||
| Self::ClaudeOpus4Thinking
|
| Self::ClaudeOpus4Thinking
|
||||||
| Self::ClaudeOpus4_1
|
| Self::ClaudeOpus4_1
|
||||||
| Self::ClaudeOpus4_1Thinking
|
| Self::ClaudeOpus4_1Thinking
|
||||||
| Self::ClaudeOpus4_5
|
|
||||||
| Self::ClaudeOpus4_5Thinking
|
|
||||||
| Self::ClaudeSonnet4
|
| Self::ClaudeSonnet4
|
||||||
| Self::ClaudeSonnet4Thinking
|
| Self::ClaudeSonnet4Thinking
|
||||||
| Self::ClaudeSonnet4_5
|
| Self::ClaudeSonnet4_5
|
||||||
@@ -477,8 +443,6 @@ impl Model {
|
|||||||
| Self::ClaudeOpus4Thinking
|
| Self::ClaudeOpus4Thinking
|
||||||
| Self::ClaudeOpus4_1
|
| Self::ClaudeOpus4_1
|
||||||
| Self::ClaudeOpus4_1Thinking
|
| Self::ClaudeOpus4_1Thinking
|
||||||
| Self::ClaudeOpus4_5
|
|
||||||
| Self::ClaudeOpus4_5Thinking
|
|
||||||
| Self::ClaudeSonnet4
|
| Self::ClaudeSonnet4
|
||||||
| Self::ClaudeSonnet4Thinking
|
| Self::ClaudeSonnet4Thinking
|
||||||
| Self::ClaudeSonnet4_5
|
| Self::ClaudeSonnet4_5
|
||||||
@@ -520,9 +484,7 @@ impl Model {
|
|||||||
| Self::ClaudeOpus4
|
| Self::ClaudeOpus4
|
||||||
| Self::ClaudeOpus4Thinking
|
| Self::ClaudeOpus4Thinking
|
||||||
| Self::ClaudeOpus4_1
|
| Self::ClaudeOpus4_1
|
||||||
| Self::ClaudeOpus4_1Thinking
|
| Self::ClaudeOpus4_1Thinking => true,
|
||||||
| Self::ClaudeOpus4_5
|
|
||||||
| Self::ClaudeOpus4_5Thinking => true,
|
|
||||||
|
|
||||||
// Custom models - check if they have cache configuration
|
// Custom models - check if they have cache configuration
|
||||||
Self::Custom {
|
Self::Custom {
|
||||||
@@ -544,9 +506,7 @@ impl Model {
|
|||||||
| Self::ClaudeOpus4
|
| Self::ClaudeOpus4
|
||||||
| Self::ClaudeOpus4Thinking
|
| Self::ClaudeOpus4Thinking
|
||||||
| Self::ClaudeOpus4_1
|
| Self::ClaudeOpus4_1
|
||||||
| Self::ClaudeOpus4_1Thinking
|
| Self::ClaudeOpus4_1Thinking => Some(BedrockModelCacheConfiguration {
|
||||||
| Self::ClaudeOpus4_5
|
|
||||||
| Self::ClaudeOpus4_5Thinking => Some(BedrockModelCacheConfiguration {
|
|
||||||
max_cache_anchors: 4,
|
max_cache_anchors: 4,
|
||||||
min_total_token: 1024,
|
min_total_token: 1024,
|
||||||
}),
|
}),
|
||||||
@@ -575,11 +535,11 @@ impl Model {
|
|||||||
budget_tokens: Some(4096),
|
budget_tokens: Some(4096),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Model::ClaudeOpus4Thinking
|
Model::ClaudeOpus4Thinking | Model::ClaudeOpus4_1Thinking => {
|
||||||
| Model::ClaudeOpus4_1Thinking
|
BedrockModelMode::Thinking {
|
||||||
| Model::ClaudeOpus4_5Thinking => BedrockModelMode::Thinking {
|
budget_tokens: Some(4096),
|
||||||
budget_tokens: Some(4096),
|
}
|
||||||
},
|
}
|
||||||
_ => BedrockModelMode::Default,
|
_ => BedrockModelMode::Default,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -633,8 +593,6 @@ impl Model {
|
|||||||
| Model::ClaudeOpus4Thinking
|
| Model::ClaudeOpus4Thinking
|
||||||
| Model::ClaudeOpus4_1
|
| Model::ClaudeOpus4_1
|
||||||
| Model::ClaudeOpus4_1Thinking
|
| Model::ClaudeOpus4_1Thinking
|
||||||
| Model::ClaudeOpus4_5
|
|
||||||
| Model::ClaudeOpus4_5Thinking
|
|
||||||
| Model::Claude3Haiku
|
| Model::Claude3Haiku
|
||||||
| Model::Claude3Opus
|
| Model::Claude3Opus
|
||||||
| Model::Claude3Sonnet
|
| Model::Claude3Sonnet
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ impl Render for Breadcrumbs {
|
|||||||
.upgrade()
|
.upgrade()
|
||||||
.zip(zed_actions::outline::TOGGLE_OUTLINE.get())
|
.zip(zed_actions::outline::TOGGLE_OUTLINE.get())
|
||||||
{
|
{
|
||||||
callback(editor.to_any_view(), window, cx);
|
callback(editor.to_any(), window, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -147,7 +147,6 @@ impl std::fmt::Debug for BufferDiffInner {
|
|||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.debug_struct("BufferDiffSnapshot")
|
f.debug_struct("BufferDiffSnapshot")
|
||||||
.field("hunks", &self.hunks)
|
.field("hunks", &self.hunks)
|
||||||
.field("remote_id", &self.base_text.remote_id())
|
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ collections = { workspace = true, features = ["test-support"] }
|
|||||||
gpui = { workspace = true, features = ["test-support"] }
|
gpui = { workspace = true, features = ["test-support"] }
|
||||||
rpc = { workspace = true, features = ["test-support"] }
|
rpc = { workspace = true, features = ["test-support"] }
|
||||||
client = { workspace = true, features = ["test-support"] }
|
client = { workspace = true, features = ["test-support"] }
|
||||||
semver.workspace = true
|
|
||||||
settings = { workspace = true, features = ["test-support"] }
|
settings = { workspace = true, features = ["test-support"] }
|
||||||
util = { workspace = true, features = ["test-support"] }
|
util = { workspace = true, features = ["test-support"] }
|
||||||
http_client = { workspace = true, features = ["test-support"] }
|
http_client = { workspace = true, features = ["test-support"] }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use client::{Client, UserStore};
|
use client::{Client, UserStore};
|
||||||
use clock::FakeSystemClock;
|
use clock::FakeSystemClock;
|
||||||
use gpui::{App, AppContext as _, Entity};
|
use gpui::{App, AppContext as _, Entity, SemanticVersion};
|
||||||
use http_client::FakeHttpClient;
|
use http_client::FakeHttpClient;
|
||||||
use rpc::proto::{self};
|
use rpc::proto::{self};
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
@@ -236,7 +236,7 @@ fn test_dangling_channel_paths(cx: &mut App) {
|
|||||||
fn init_test(cx: &mut App) -> Entity<ChannelStore> {
|
fn init_test(cx: &mut App) -> Entity<ChannelStore> {
|
||||||
let settings_store = SettingsStore::test(cx);
|
let settings_store = SettingsStore::test(cx);
|
||||||
cx.set_global(settings_store);
|
cx.set_global(settings_store);
|
||||||
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
release_channel::init(SemanticVersion::default(), cx);
|
||||||
|
|
||||||
let clock = Arc::new(FakeSystemClock::new());
|
let clock = Arc::new(FakeSystemClock::new());
|
||||||
let http = FakeHttpClient::with_404_response();
|
let http = FakeHttpClient::with_404_response();
|
||||||
|
|||||||
@@ -34,10 +34,6 @@ util.workspace = true
|
|||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
rayon.workspace = true
|
rayon.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
serde_json.workspace = true
|
|
||||||
util = { workspace = true, features = ["test-support"] }
|
|
||||||
|
|
||||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
|
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
|
||||||
exec.workspace = true
|
exec.workspace = true
|
||||||
fork.workspace = true
|
fork.workspace = true
|
||||||
|
|||||||
@@ -23,7 +23,4 @@ fn main() {
|
|||||||
|
|
||||||
println!("cargo:rustc-env=ZED_COMMIT_SHA={git_sha}");
|
println!("cargo:rustc-env=ZED_COMMIT_SHA={git_sha}");
|
||||||
}
|
}
|
||||||
if let Some(build_identifier) = option_env!("GITHUB_RUN_NUMBER") {
|
|
||||||
println!("cargo:rustc-env=ZED_BUILD_ID={build_identifier}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ use clap::Parser;
|
|||||||
use cli::{CliRequest, CliResponse, IpcHandshake, ipc::IpcOneShotServer};
|
use cli::{CliRequest, CliResponse, IpcHandshake, ipc::IpcOneShotServer};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::{
|
use std::{
|
||||||
env,
|
env, fs, io,
|
||||||
ffi::OsStr,
|
|
||||||
fs, io,
|
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process::ExitStatus,
|
process::ExitStatus,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
@@ -131,177 +129,37 @@ struct Args {
|
|||||||
askpass: Option<String>,
|
askpass: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses a path containing a position (e.g. `path:line:column`)
|
|
||||||
/// and returns its canonicalized string representation.
|
|
||||||
///
|
|
||||||
/// If a part of path doesn't exist, it will canonicalize the
|
|
||||||
/// existing part and append the non-existing part.
|
|
||||||
///
|
|
||||||
/// This method must return an absolute path, as many zed
|
|
||||||
/// crates assume absolute paths.
|
|
||||||
fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
|
fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
|
||||||
match Path::new(argument_str).canonicalize() {
|
let canonicalized = match Path::new(argument_str).canonicalize() {
|
||||||
Ok(existing_path) => Ok(PathWithPosition::from_path(existing_path)),
|
Ok(existing_path) => PathWithPosition::from_path(existing_path),
|
||||||
Err(_) => PathWithPosition::parse_str(argument_str).map_path(|mut path| {
|
Err(_) => {
|
||||||
|
let path = PathWithPosition::parse_str(argument_str);
|
||||||
let curdir = env::current_dir().context("retrieving current directory")?;
|
let curdir = env::current_dir().context("retrieving current directory")?;
|
||||||
let mut children = Vec::new();
|
path.map_path(|path| match fs::canonicalize(&path) {
|
||||||
let root;
|
Ok(path) => Ok(path),
|
||||||
loop {
|
Err(e) => {
|
||||||
// canonicalize handles './', and '/'.
|
if let Some(mut parent) = path.parent() {
|
||||||
if let Ok(canonicalized) = fs::canonicalize(&path) {
|
if parent == Path::new("") {
|
||||||
root = canonicalized;
|
parent = &curdir
|
||||||
break;
|
}
|
||||||
|
match fs::canonicalize(parent) {
|
||||||
|
Ok(parent) => Ok(parent.join(path.file_name().unwrap())),
|
||||||
|
Err(_) => Err(e),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// The comparison to `curdir` is just a shortcut
|
})
|
||||||
// since we know it is canonical. The other one
|
}
|
||||||
// is if `argument_str` is a string that starts
|
.with_context(|| format!("parsing as path with position {argument_str}"))?,
|
||||||
// with a name (e.g. "foo/bar").
|
};
|
||||||
if path == curdir || path == Path::new("") {
|
Ok(canonicalized.to_string(|path| path.to_string_lossy().into_owned()))
|
||||||
root = curdir;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
children.push(
|
|
||||||
path.file_name()
|
|
||||||
.with_context(|| format!("parsing as path with position {argument_str}"))?
|
|
||||||
.to_owned(),
|
|
||||||
);
|
|
||||||
if !path.pop() {
|
|
||||||
unreachable!("parsing as path with position {argument_str}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(children.iter().rev().fold(root, |mut path, child| {
|
|
||||||
path.push(child);
|
|
||||||
path
|
|
||||||
}))
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
.map(|path_with_pos| path_with_pos.to_string(|path| path.to_string_lossy().into_owned()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use serde_json::json;
|
|
||||||
use util::path;
|
|
||||||
use util::paths::SanitizedPath;
|
|
||||||
use util::test::TempTree;
|
|
||||||
|
|
||||||
macro_rules! assert_path_eq {
|
|
||||||
($left:expr, $right:expr) => {
|
|
||||||
assert_eq!(
|
|
||||||
SanitizedPath::new(Path::new(&$left)),
|
|
||||||
SanitizedPath::new(Path::new(&$right))
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cwd() -> PathBuf {
|
|
||||||
env::current_dir().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
static CWD_LOCK: Mutex<()> = Mutex::new(());
|
|
||||||
|
|
||||||
fn with_cwd<T>(path: &Path, f: impl FnOnce() -> anyhow::Result<T>) -> anyhow::Result<T> {
|
|
||||||
let _lock = CWD_LOCK.lock();
|
|
||||||
let old_cwd = cwd();
|
|
||||||
env::set_current_dir(path)?;
|
|
||||||
let result = f();
|
|
||||||
env::set_current_dir(old_cwd)?;
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_non_existing_path() {
|
|
||||||
// Absolute path
|
|
||||||
let result = parse_path_with_position(path!("/non/existing/path.txt")).unwrap();
|
|
||||||
assert_path_eq!(result, path!("/non/existing/path.txt"));
|
|
||||||
|
|
||||||
// Absolute path in cwd
|
|
||||||
let path = cwd().join(path!("non/existing/path.txt"));
|
|
||||||
let expected = path.to_string_lossy().to_string();
|
|
||||||
let result = parse_path_with_position(&expected).unwrap();
|
|
||||||
assert_path_eq!(result, expected);
|
|
||||||
|
|
||||||
// Relative path
|
|
||||||
let result = parse_path_with_position(path!("non/existing/path.txt")).unwrap();
|
|
||||||
assert_path_eq!(result, expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_existing_path() {
|
|
||||||
let temp_tree = TempTree::new(json!({
|
|
||||||
"file.txt": "",
|
|
||||||
}));
|
|
||||||
let file_path = temp_tree.path().join("file.txt");
|
|
||||||
let expected = file_path.to_string_lossy().to_string();
|
|
||||||
|
|
||||||
// Absolute path
|
|
||||||
let result = parse_path_with_position(file_path.to_str().unwrap()).unwrap();
|
|
||||||
assert_path_eq!(result, expected);
|
|
||||||
|
|
||||||
// Relative path
|
|
||||||
let result = with_cwd(temp_tree.path(), || parse_path_with_position("file.txt")).unwrap();
|
|
||||||
assert_path_eq!(result, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE:
|
|
||||||
// While POSIX symbolic links are somewhat supported on Windows, they are an opt in by the user, and thus
|
|
||||||
// we assume that they are not supported out of the box.
|
|
||||||
#[cfg(not(windows))]
|
|
||||||
#[test]
|
|
||||||
fn test_parse_symlink_file() {
|
|
||||||
let temp_tree = TempTree::new(json!({
|
|
||||||
"target.txt": "",
|
|
||||||
}));
|
|
||||||
let target_path = temp_tree.path().join("target.txt");
|
|
||||||
let symlink_path = temp_tree.path().join("symlink.txt");
|
|
||||||
std::os::unix::fs::symlink(&target_path, &symlink_path).unwrap();
|
|
||||||
|
|
||||||
// Absolute path
|
|
||||||
let result = parse_path_with_position(symlink_path.to_str().unwrap()).unwrap();
|
|
||||||
assert_eq!(result, target_path.to_string_lossy());
|
|
||||||
|
|
||||||
// Relative path
|
|
||||||
let result =
|
|
||||||
with_cwd(temp_tree.path(), || parse_path_with_position("symlink.txt")).unwrap();
|
|
||||||
assert_eq!(result, target_path.to_string_lossy());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
|
||||||
#[test]
|
|
||||||
fn test_parse_symlink_dir() {
|
|
||||||
let temp_tree = TempTree::new(json!({
|
|
||||||
"some": {
|
|
||||||
"dir": { // symlink target
|
|
||||||
"ec": {
|
|
||||||
"tory": {
|
|
||||||
"file.txt": "",
|
|
||||||
}}}}}));
|
|
||||||
|
|
||||||
let target_file_path = temp_tree.path().join("some/dir/ec/tory/file.txt");
|
|
||||||
let expected = target_file_path.to_string_lossy();
|
|
||||||
|
|
||||||
let dir_path = temp_tree.path().join("some/dir");
|
|
||||||
let symlink_path = temp_tree.path().join("symlink");
|
|
||||||
std::os::unix::fs::symlink(&dir_path, &symlink_path).unwrap();
|
|
||||||
|
|
||||||
// Absolute path
|
|
||||||
let result =
|
|
||||||
parse_path_with_position(symlink_path.join("ec/tory/file.txt").to_str().unwrap())
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
|
|
||||||
// Relative path
|
|
||||||
let result = with_cwd(temp_tree.path(), || {
|
|
||||||
parse_path_with_position("symlink/ec/tory/file.txt")
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_path_in_wsl(source: &str, wsl: &str) -> Result<String> {
|
fn parse_path_in_wsl(source: &str, wsl: &str) -> Result<String> {
|
||||||
let mut source = PathWithPosition::parse_str(source);
|
let mut source = PathWithPosition::parse_str(source);
|
||||||
|
let mut command = util::command::new_std_command("wsl.exe");
|
||||||
|
|
||||||
let (user, distro_name) = if let Some((user, distro)) = wsl.split_once('@') {
|
let (user, distro_name) = if let Some((user, distro)) = wsl.split_once('@') {
|
||||||
if user.is_empty() {
|
if user.is_empty() {
|
||||||
@@ -312,35 +170,22 @@ fn parse_path_in_wsl(source: &str, wsl: &str) -> Result<String> {
|
|||||||
(None, wsl)
|
(None, wsl)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut args = vec!["--distribution", distro_name];
|
|
||||||
if let Some(user) = user {
|
if let Some(user) = user {
|
||||||
args.push("--user");
|
command.arg("--user").arg(user);
|
||||||
args.push(user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let command = [
|
let output = command
|
||||||
OsStr::new("realpath"),
|
.arg("--distribution")
|
||||||
OsStr::new("-s"),
|
.arg(distro_name)
|
||||||
source.path.as_ref(),
|
|
||||||
];
|
|
||||||
|
|
||||||
let output = util::command::new_std_command("wsl.exe")
|
|
||||||
.args(&args)
|
|
||||||
.arg("--exec")
|
.arg("--exec")
|
||||||
.args(&command)
|
.arg("wslpath")
|
||||||
|
.arg("-m")
|
||||||
|
.arg(&source.path)
|
||||||
.output()?;
|
.output()?;
|
||||||
let result = if output.status.success() {
|
|
||||||
String::from_utf8_lossy(&output.stdout).to_string()
|
|
||||||
} else {
|
|
||||||
let fallback = util::command::new_std_command("wsl.exe")
|
|
||||||
.args(&args)
|
|
||||||
.arg("--")
|
|
||||||
.args(&command)
|
|
||||||
.output()?;
|
|
||||||
String::from_utf8_lossy(&fallback.stdout).to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
source.path = Path::new(result.trim()).to_owned();
|
let result = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let prefix = format!("//wsl.localhost/{}", distro_name);
|
||||||
|
source.path = Path::new(result.trim().strip_prefix(&prefix).unwrap_or(&result)).to_owned();
|
||||||
|
|
||||||
Ok(source.to_string(|path| path.to_string_lossy().into_owned()))
|
Ok(source.to_string(|path| path.to_string_lossy().into_owned()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ settings = { workspace = true, features = ["test-support"] }
|
|||||||
util = { workspace = true, features = ["test-support"] }
|
util = { workspace = true, features = ["test-support"] }
|
||||||
|
|
||||||
[target.'cfg(target_os = "windows")'.dependencies]
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
semver.workspace = true
|
|
||||||
windows.workspace = true
|
windows.workspace = true
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ pub fn os_version() -> String {
|
|||||||
let mut info = unsafe { std::mem::zeroed() };
|
let mut info = unsafe { std::mem::zeroed() };
|
||||||
let status = unsafe { windows::Wdk::System::SystemServices::RtlGetVersion(&mut info) };
|
let status = unsafe { windows::Wdk::System::SystemServices::RtlGetVersion(&mut info) };
|
||||||
if status.is_ok() {
|
if status.is_ok() {
|
||||||
semver::Version::new(
|
gpui::SemanticVersion::new(
|
||||||
info.dwMajorVersion as _,
|
info.dwMajorVersion as _,
|
||||||
info.dwMinorVersion as _,
|
info.dwMinorVersion as _,
|
||||||
info.dwBuildNumber as _,
|
info.dwBuildNumber as _,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::{
|
use std::{
|
||||||
fmt::{Display, Write as _},
|
fmt::{Display, Write as _},
|
||||||
ops::{Add, Range, Sub},
|
ops::{Add, Range, Sub},
|
||||||
path::Path,
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use strum::EnumIter;
|
use strum::EnumIter;
|
||||||
@@ -17,7 +17,7 @@ pub struct PlanContextRetrievalRequest {
|
|||||||
pub excerpt_path: Arc<Path>,
|
pub excerpt_path: Arc<Path>,
|
||||||
pub excerpt_line_range: Range<Line>,
|
pub excerpt_line_range: Range<Line>,
|
||||||
pub cursor_file_max_row: Line,
|
pub cursor_file_max_row: Line,
|
||||||
pub events: Vec<Arc<Event>>,
|
pub events: Vec<Event>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -36,7 +36,7 @@ pub struct PredictEditsRequest {
|
|||||||
pub signatures: Vec<Signature>,
|
pub signatures: Vec<Signature>,
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
||||||
pub referenced_declarations: Vec<ReferencedDeclaration>,
|
pub referenced_declarations: Vec<ReferencedDeclaration>,
|
||||||
pub events: Vec<Arc<Event>>,
|
pub events: Vec<Event>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub can_collect_data: bool,
|
pub can_collect_data: bool,
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
||||||
@@ -80,8 +80,6 @@ pub enum PromptFormat {
|
|||||||
Minimal,
|
Minimal,
|
||||||
/// One-sentence instructions + FIM-like template
|
/// One-sentence instructions + FIM-like template
|
||||||
MinimalQwen,
|
MinimalQwen,
|
||||||
/// No instructions, Qwen chat + Seed-Coder 1120 FIM-like template
|
|
||||||
SeedCoder1120,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PromptFormat {
|
impl PromptFormat {
|
||||||
@@ -110,7 +108,6 @@ impl std::fmt::Display for PromptFormat {
|
|||||||
PromptFormat::OldTextNewText => write!(f, "Old Text / New Text"),
|
PromptFormat::OldTextNewText => write!(f, "Old Text / New Text"),
|
||||||
PromptFormat::Minimal => write!(f, "Minimal"),
|
PromptFormat::Minimal => write!(f, "Minimal"),
|
||||||
PromptFormat::MinimalQwen => write!(f, "Minimal + Qwen FIM"),
|
PromptFormat::MinimalQwen => write!(f, "Minimal + Qwen FIM"),
|
||||||
PromptFormat::SeedCoder1120 => write!(f, "Seed-Coder 1120"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,11 +117,10 @@ impl std::fmt::Display for PromptFormat {
|
|||||||
#[serde(tag = "event")]
|
#[serde(tag = "event")]
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
BufferChange {
|
BufferChange {
|
||||||
path: Arc<Path>,
|
path: Option<PathBuf>,
|
||||||
old_path: Arc<Path>,
|
old_path: Option<PathBuf>,
|
||||||
diff: String,
|
diff: String,
|
||||||
predicted: bool,
|
predicted: bool,
|
||||||
in_open_source_repo: bool,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,21 +132,23 @@ impl Display for Event {
|
|||||||
old_path,
|
old_path,
|
||||||
diff,
|
diff,
|
||||||
predicted,
|
predicted,
|
||||||
..
|
|
||||||
} => {
|
} => {
|
||||||
|
let new_path = path.as_deref().unwrap_or(Path::new("untitled"));
|
||||||
|
let old_path = old_path.as_deref().unwrap_or(new_path);
|
||||||
|
|
||||||
if *predicted {
|
if *predicted {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"// User accepted prediction:\n--- a/{}\n+++ b/{}\n{diff}",
|
"// User accepted prediction:\n--- a/{}\n+++ b/{}\n{diff}",
|
||||||
DiffPathFmt(old_path),
|
DiffPathFmt(old_path),
|
||||||
DiffPathFmt(path)
|
DiffPathFmt(new_path)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"--- a/{}\n+++ b/{}\n{diff}",
|
"--- a/{}\n+++ b/{}\n{diff}",
|
||||||
DiffPathFmt(old_path),
|
DiffPathFmt(old_path),
|
||||||
DiffPathFmt(path)
|
DiffPathFmt(new_path)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -299,11 +297,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_event_display() {
|
fn test_event_display() {
|
||||||
let ev = Event::BufferChange {
|
let ev = Event::BufferChange {
|
||||||
path: Path::new("untitled").into(),
|
path: None,
|
||||||
old_path: Path::new("untitled").into(),
|
old_path: None,
|
||||||
diff: "@@ -1,2 +1,2 @@\n-a\n-b\n".into(),
|
diff: "@@ -1,2 +1,2 @@\n-a\n-b\n".into(),
|
||||||
predicted: false,
|
predicted: false,
|
||||||
in_open_source_repo: true,
|
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ev.to_string(),
|
ev.to_string(),
|
||||||
@@ -317,11 +314,10 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let ev = Event::BufferChange {
|
let ev = Event::BufferChange {
|
||||||
path: Path::new("foo/bar.txt").into(),
|
path: Some(PathBuf::from("foo/bar.txt")),
|
||||||
old_path: Path::new("foo/bar.txt").into(),
|
old_path: Some(PathBuf::from("foo/bar.txt")),
|
||||||
diff: "@@ -1,2 +1,2 @@\n-a\n-b\n".into(),
|
diff: "@@ -1,2 +1,2 @@\n-a\n-b\n".into(),
|
||||||
predicted: false,
|
predicted: false,
|
||||||
in_open_source_repo: true,
|
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ev.to_string(),
|
ev.to_string(),
|
||||||
@@ -335,11 +331,10 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let ev = Event::BufferChange {
|
let ev = Event::BufferChange {
|
||||||
path: Path::new("abc.txt").into(),
|
path: Some(PathBuf::from("abc.txt")),
|
||||||
old_path: Path::new("123.txt").into(),
|
old_path: Some(PathBuf::from("123.txt")),
|
||||||
diff: "@@ -1,2 +1,2 @@\n-a\n-b\n".into(),
|
diff: "@@ -1,2 +1,2 @@\n-a\n-b\n".into(),
|
||||||
predicted: false,
|
predicted: false,
|
||||||
in_open_source_repo: true,
|
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ev.to_string(),
|
ev.to_string(),
|
||||||
@@ -353,11 +348,10 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let ev = Event::BufferChange {
|
let ev = Event::BufferChange {
|
||||||
path: Path::new("abc.txt").into(),
|
path: Some(PathBuf::from("abc.txt")),
|
||||||
old_path: Path::new("123.txt").into(),
|
old_path: Some(PathBuf::from("123.txt")),
|
||||||
diff: "@@ -1,2 +1,2 @@\n-a\n-b\n".into(),
|
diff: "@@ -1,2 +1,2 @@\n-a\n-b\n".into(),
|
||||||
predicted: true,
|
predicted: true,
|
||||||
in_open_source_repo: true,
|
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ev.to_string(),
|
ev.to_string(),
|
||||||
|
|||||||
@@ -169,18 +169,15 @@ pub fn build_prompt(
|
|||||||
) -> Result<(String, SectionLabels)> {
|
) -> Result<(String, SectionLabels)> {
|
||||||
let mut section_labels = Default::default();
|
let mut section_labels = Default::default();
|
||||||
|
|
||||||
let prompt_data = PromptData {
|
|
||||||
events: request.events.clone(),
|
|
||||||
cursor_point: request.cursor_point,
|
|
||||||
cursor_path: request.excerpt_path.clone(),
|
|
||||||
included_files: request.included_files.clone(),
|
|
||||||
};
|
|
||||||
match request.prompt_format {
|
match request.prompt_format {
|
||||||
PromptFormat::MinimalQwen => {
|
PromptFormat::MinimalQwen => {
|
||||||
return Ok((MinimalQwenPrompt.render(&prompt_data), section_labels));
|
let prompt = MinimalQwenPrompt {
|
||||||
}
|
events: request.events.clone(),
|
||||||
PromptFormat::SeedCoder1120 => {
|
cursor_point: request.cursor_point,
|
||||||
return Ok((SeedCoder1120Prompt.render(&prompt_data), section_labels));
|
cursor_path: request.excerpt_path.clone(),
|
||||||
|
included_files: request.included_files.clone(),
|
||||||
|
};
|
||||||
|
return Ok((prompt.render(), section_labels));
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
};
|
};
|
||||||
@@ -211,7 +208,6 @@ pub fn build_prompt(
|
|||||||
}
|
}
|
||||||
PromptFormat::OnlySnippets => vec![],
|
PromptFormat::OnlySnippets => vec![],
|
||||||
PromptFormat::MinimalQwen => unreachable!(),
|
PromptFormat::MinimalQwen => unreachable!(),
|
||||||
PromptFormat::SeedCoder1120 => unreachable!(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut prompt = match request.prompt_format {
|
let mut prompt = match request.prompt_format {
|
||||||
@@ -222,7 +218,6 @@ pub fn build_prompt(
|
|||||||
PromptFormat::OnlySnippets => String::new(),
|
PromptFormat::OnlySnippets => String::new(),
|
||||||
PromptFormat::Minimal => STUDENT_MODEL_INSTRUCTIONS.to_string(),
|
PromptFormat::Minimal => STUDENT_MODEL_INSTRUCTIONS.to_string(),
|
||||||
PromptFormat::MinimalQwen => unreachable!(),
|
PromptFormat::MinimalQwen => unreachable!(),
|
||||||
PromptFormat::SeedCoder1120 => unreachable!(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if request.events.is_empty() {
|
if request.events.is_empty() {
|
||||||
@@ -333,13 +328,6 @@ pub fn build_prompt(
|
|||||||
Ok((prompt, section_labels))
|
Ok((prompt, section_labels))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generation_params(prompt_format: PromptFormat) -> GenerationParams {
|
|
||||||
match prompt_format {
|
|
||||||
PromptFormat::SeedCoder1120 => SeedCoder1120Prompt::generation_params(),
|
|
||||||
_ => GenerationParams::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write_codeblock<'a>(
|
pub fn write_codeblock<'a>(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
excerpts: impl IntoIterator<Item = &'a Excerpt>,
|
excerpts: impl IntoIterator<Item = &'a Excerpt>,
|
||||||
@@ -432,7 +420,7 @@ pub fn write_excerpts<'a>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push_events(output: &mut String, events: &[Arc<predict_edits_v3::Event>]) {
|
pub fn push_events(output: &mut String, events: &[predict_edits_v3::Event]) {
|
||||||
if events.is_empty() {
|
if events.is_empty() {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -798,7 +786,6 @@ impl<'a> SyntaxBasedPrompt<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
PromptFormat::MinimalQwen => unreachable!(),
|
PromptFormat::MinimalQwen => unreachable!(),
|
||||||
PromptFormat::SeedCoder1120 => unreachable!(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let push_full_snippet = |output: &mut String| {
|
let push_full_snippet = |output: &mut String| {
|
||||||
@@ -909,34 +896,19 @@ fn declaration_size(declaration: &ReferencedDeclaration, style: DeclarationStyle
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PromptData {
|
struct MinimalQwenPrompt {
|
||||||
events: Vec<Arc<Event>>,
|
events: Vec<Event>,
|
||||||
cursor_point: Point,
|
cursor_point: Point,
|
||||||
cursor_path: Arc<Path>, // TODO: make a common struct with cursor_point
|
cursor_path: Arc<Path>, // TODO: make a common struct with cursor_point
|
||||||
included_files: Vec<IncludedFile>,
|
included_files: Vec<IncludedFile>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
impl MinimalQwenPrompt {
|
||||||
pub struct GenerationParams {
|
const INSTRUCTIONS: &str = "You are a code completion assistant that analyzes edit history to identify and systematically complete incomplete refactorings or patterns across the entire codebase.\n";
|
||||||
pub temperature: Option<f32>,
|
|
||||||
pub top_p: Option<f32>,
|
|
||||||
pub stop: Option<Vec<String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
trait PromptFormatter {
|
fn render(&self) -> String {
|
||||||
fn render(&self, data: &PromptData) -> String;
|
let edit_history = self.fmt_edit_history();
|
||||||
|
let context = self.fmt_context();
|
||||||
fn generation_params() -> GenerationParams {
|
|
||||||
return GenerationParams::default();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MinimalQwenPrompt;
|
|
||||||
|
|
||||||
impl PromptFormatter for MinimalQwenPrompt {
|
|
||||||
fn render(&self, data: &PromptData) -> String {
|
|
||||||
let edit_history = self.fmt_edit_history(data);
|
|
||||||
let context = self.fmt_context(data);
|
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
"{instructions}\n\n{edit_history}\n\n{context}",
|
"{instructions}\n\n{edit_history}\n\n{context}",
|
||||||
@@ -945,17 +917,13 @@ impl PromptFormatter for MinimalQwenPrompt {
|
|||||||
context = context
|
context = context
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl MinimalQwenPrompt {
|
fn fmt_edit_history(&self) -> String {
|
||||||
const INSTRUCTIONS: &str = "You are a code completion assistant that analyzes edit history to identify and systematically complete incomplete refactorings or patterns across the entire codebase.\n";
|
if self.events.is_empty() {
|
||||||
|
|
||||||
fn fmt_edit_history(&self, data: &PromptData) -> String {
|
|
||||||
if data.events.is_empty() {
|
|
||||||
"(No edit history)\n\n".to_string()
|
"(No edit history)\n\n".to_string()
|
||||||
} else {
|
} else {
|
||||||
let mut events_str = String::new();
|
let mut events_str = String::new();
|
||||||
push_events(&mut events_str, &data.events);
|
push_events(&mut events_str, &self.events);
|
||||||
format!(
|
format!(
|
||||||
"The following are the latest edits made by the user, from earlier to later.\n\n{}",
|
"The following are the latest edits made by the user, from earlier to later.\n\n{}",
|
||||||
events_str
|
events_str
|
||||||
@@ -963,18 +931,18 @@ impl MinimalQwenPrompt {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fmt_context(&self, data: &PromptData) -> String {
|
fn fmt_context(&self) -> String {
|
||||||
let mut context = String::new();
|
let mut context = String::new();
|
||||||
let include_line_numbers = true;
|
let include_line_numbers = true;
|
||||||
|
|
||||||
for related_file in &data.included_files {
|
for related_file in &self.included_files {
|
||||||
writeln!(context, "<|file_sep|>{}", DiffPathFmt(&related_file.path)).unwrap();
|
writeln!(context, "<|file_sep|>{}", DiffPathFmt(&related_file.path)).unwrap();
|
||||||
|
|
||||||
if related_file.path == data.cursor_path {
|
if related_file.path == self.cursor_path {
|
||||||
write!(context, "<|fim_prefix|>").unwrap();
|
write!(context, "<|fim_prefix|>").unwrap();
|
||||||
write_excerpts(
|
write_excerpts(
|
||||||
&related_file.excerpts,
|
&related_file.excerpts,
|
||||||
&[(data.cursor_point, "<|fim_suffix|>")],
|
&[(self.cursor_point, "<|fim_suffix|>")],
|
||||||
related_file.max_row,
|
related_file.max_row,
|
||||||
include_line_numbers,
|
include_line_numbers,
|
||||||
&mut context,
|
&mut context,
|
||||||
@@ -993,83 +961,3 @@ impl MinimalQwenPrompt {
|
|||||||
context
|
context
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SeedCoder1120Prompt;
|
|
||||||
|
|
||||||
impl PromptFormatter for SeedCoder1120Prompt {
|
|
||||||
fn render(&self, data: &PromptData) -> String {
|
|
||||||
let edit_history = self.fmt_edit_history(data);
|
|
||||||
let context = self.fmt_context(data);
|
|
||||||
|
|
||||||
format!(
|
|
||||||
"# Edit History:\n{edit_history}\n\n{context}",
|
|
||||||
edit_history = edit_history,
|
|
||||||
context = context
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generation_params() -> GenerationParams {
|
|
||||||
GenerationParams {
|
|
||||||
temperature: Some(0.2),
|
|
||||||
top_p: Some(0.9),
|
|
||||||
stop: Some(vec!["<[end_of_sentence]>".into()]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SeedCoder1120Prompt {
|
|
||||||
fn fmt_edit_history(&self, data: &PromptData) -> String {
|
|
||||||
if data.events.is_empty() {
|
|
||||||
"(No edit history)\n\n".to_string()
|
|
||||||
} else {
|
|
||||||
let mut events_str = String::new();
|
|
||||||
push_events(&mut events_str, &data.events);
|
|
||||||
events_str
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fmt_context(&self, data: &PromptData) -> String {
|
|
||||||
let mut context = String::new();
|
|
||||||
let include_line_numbers = true;
|
|
||||||
|
|
||||||
for related_file in &data.included_files {
|
|
||||||
writeln!(context, "# Path: {}\n", DiffPathFmt(&related_file.path)).unwrap();
|
|
||||||
|
|
||||||
if related_file.path == data.cursor_path {
|
|
||||||
let fim_prompt = self.fmt_fim(&related_file, data.cursor_point);
|
|
||||||
context.push_str(&fim_prompt);
|
|
||||||
} else {
|
|
||||||
write_excerpts(
|
|
||||||
&related_file.excerpts,
|
|
||||||
&[],
|
|
||||||
related_file.max_row,
|
|
||||||
include_line_numbers,
|
|
||||||
&mut context,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
context
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fmt_fim(&self, file: &IncludedFile, cursor_point: Point) -> String {
|
|
||||||
let mut buf = String::new();
|
|
||||||
const FIM_SUFFIX: &str = "<[fim-suffix]>";
|
|
||||||
const FIM_PREFIX: &str = "<[fim-prefix]>";
|
|
||||||
const FIM_MIDDLE: &str = "<[fim-middle]>";
|
|
||||||
write!(buf, "{}", FIM_PREFIX).unwrap();
|
|
||||||
write_excerpts(
|
|
||||||
&file.excerpts,
|
|
||||||
&[(cursor_point, FIM_SUFFIX)],
|
|
||||||
file.max_row,
|
|
||||||
true,
|
|
||||||
&mut buf,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Swap prefix and suffix parts
|
|
||||||
let index = buf.find(FIM_SUFFIX).unwrap();
|
|
||||||
let prefix = &buf[..index];
|
|
||||||
let suffix = &buf[index..];
|
|
||||||
|
|
||||||
format!("{}{}{}", suffix, prefix, FIM_MIDDLE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ impl EditPredictionProvider for CodestralCompletionProvider {
|
|||||||
Self::api_key(cx).is_some()
|
Self::api_key(cx).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_refreshing(&self, _cx: &App) -> bool {
|
fn is_refreshing(&self) -> bool {
|
||||||
self.pending_request.is_some()
|
self.pending_request.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ scrypt = "0.11"
|
|||||||
# sea-orm and sea-orm-macros versions must match exactly.
|
# sea-orm and sea-orm-macros versions must match exactly.
|
||||||
sea-orm = { version = "=1.1.10", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] }
|
sea-orm = { version = "=1.1.10", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] }
|
||||||
sea-orm-macros = "=1.1.10"
|
sea-orm-macros = "=1.1.10"
|
||||||
|
semantic_version.workspace = true
|
||||||
semver.workspace = true
|
semver.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use axum::{
|
|||||||
};
|
};
|
||||||
use collections::{BTreeSet, HashMap};
|
use collections::{BTreeSet, HashMap};
|
||||||
use rpc::{ExtensionApiManifest, ExtensionProvides, GetExtensionsResponse};
|
use rpc::{ExtensionApiManifest, ExtensionProvides, GetExtensionsResponse};
|
||||||
use semver::Version as SemanticVersion;
|
use semantic_version::SemanticVersion;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::{sync::Arc, time::Duration};
|
use std::{sync::Arc, time::Duration};
|
||||||
@@ -108,8 +108,8 @@ struct GetExtensionUpdatesParams {
|
|||||||
ids: String,
|
ids: String,
|
||||||
min_schema_version: i32,
|
min_schema_version: i32,
|
||||||
max_schema_version: i32,
|
max_schema_version: i32,
|
||||||
min_wasm_api_version: semver::Version,
|
min_wasm_api_version: SemanticVersion,
|
||||||
max_wasm_api_version: semver::Version,
|
max_wasm_api_version: SemanticVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_extension_updates(
|
async fn get_extension_updates(
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ use sea_orm::{
|
|||||||
entity::prelude::*,
|
entity::prelude::*,
|
||||||
sea_query::{Alias, Expr, OnConflict},
|
sea_query::{Alias, Expr, OnConflict},
|
||||||
};
|
};
|
||||||
use semver::Version;
|
use semantic_version::SemanticVersion;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::ops::RangeInclusive;
|
use std::ops::RangeInclusive;
|
||||||
use std::{
|
use std::{
|
||||||
@@ -671,7 +671,7 @@ pub struct NewExtensionVersion {
|
|||||||
|
|
||||||
pub struct ExtensionVersionConstraints {
|
pub struct ExtensionVersionConstraints {
|
||||||
pub schema_versions: RangeInclusive<i32>,
|
pub schema_versions: RangeInclusive<i32>,
|
||||||
pub wasm_api_versions: RangeInclusive<semver::Version>,
|
pub wasm_api_versions: RangeInclusive<SemanticVersion>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LocalSettingsKind {
|
impl LocalSettingsKind {
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ impl Database {
|
|||||||
extensions: &[extension::Model],
|
extensions: &[extension::Model],
|
||||||
constraints: Option<&ExtensionVersionConstraints>,
|
constraints: Option<&ExtensionVersionConstraints>,
|
||||||
tx: &DatabaseTransaction,
|
tx: &DatabaseTransaction,
|
||||||
) -> Result<HashMap<ExtensionId, (extension_version::Model, Version)>> {
|
) -> Result<HashMap<ExtensionId, (extension_version::Model, SemanticVersion)>> {
|
||||||
let mut versions = extension_version::Entity::find()
|
let mut versions = extension_version::Entity::find()
|
||||||
.filter(
|
.filter(
|
||||||
extension_version::Column::ExtensionId
|
extension_version::Column::ExtensionId
|
||||||
@@ -79,10 +79,11 @@ impl Database {
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut max_versions =
|
let mut max_versions =
|
||||||
HashMap::<ExtensionId, (extension_version::Model, Version)>::default();
|
HashMap::<ExtensionId, (extension_version::Model, SemanticVersion)>::default();
|
||||||
while let Some(version) = versions.next().await {
|
while let Some(version) = versions.next().await {
|
||||||
let version = version?;
|
let version = version?;
|
||||||
let Some(extension_version) = Version::from_str(&version.version).log_err() else {
|
let Some(extension_version) = SemanticVersion::from_str(&version.version).log_err()
|
||||||
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -101,7 +102,7 @@ impl Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(wasm_api_version) = version.wasm_api_version.as_ref() {
|
if let Some(wasm_api_version) = version.wasm_api_version.as_ref() {
|
||||||
if let Some(version) = Version::from_str(wasm_api_version).log_err() {
|
if let Some(version) = SemanticVersion::from_str(wasm_api_version).log_err() {
|
||||||
if !constraints.wasm_api_versions.contains(&version) {
|
if !constraints.wasm_api_versions.contains(&version) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user