Compare commits
1 Commits
devcontain
...
editor/tog
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
350e355b15 |
@@ -1,55 +0,0 @@
|
||||
# Phase 2: Explore Repository
|
||||
|
||||
You are analyzing a codebase to understand its structure before reviewing documentation impact.
|
||||
|
||||
## Objective
|
||||
Produce a structured overview of the repository to inform subsequent documentation analysis.
|
||||
|
||||
## Instructions
|
||||
|
||||
1. **Identify Primary Languages and Frameworks**
|
||||
- Scan for Cargo.toml, package.json, or other manifest files
|
||||
- Note the primary language(s) and key dependencies
|
||||
|
||||
2. **Map Documentation Structure**
|
||||
- This project uses **mdBook** (https://rust-lang.github.io/mdBook/)
|
||||
- Documentation is in `docs/src/`
|
||||
- Table of contents: `docs/src/SUMMARY.md` (mdBook format: https://rust-lang.github.io/mdBook/format/summary.html)
|
||||
- Style guide: `docs/.rules`
|
||||
- Agent guidelines: `docs/AGENTS.md`
|
||||
- Formatting: Prettier (config in `docs/.prettierrc`)
|
||||
|
||||
3. **Identify Build and Tooling**
|
||||
- Note build systems (cargo, npm, etc.)
|
||||
- Identify documentation tooling (mdbook, etc.)
|
||||
|
||||
4. **Output Format**
|
||||
Produce a JSON summary:
|
||||
|
||||
```json
|
||||
{
|
||||
"primary_language": "Rust",
|
||||
"frameworks": ["GPUI"],
|
||||
"documentation": {
|
||||
"system": "mdBook",
|
||||
"location": "docs/src/",
|
||||
"toc_file": "docs/src/SUMMARY.md",
|
||||
"toc_format": "https://rust-lang.github.io/mdBook/format/summary.html",
|
||||
"style_guide": "docs/.rules",
|
||||
"agent_guidelines": "docs/AGENTS.md",
|
||||
"formatter": "prettier",
|
||||
"formatter_config": "docs/.prettierrc",
|
||||
"custom_preprocessor": "docs_preprocessor (handles {#kb action::Name} syntax)"
|
||||
},
|
||||
"key_directories": {
|
||||
"source": "crates/",
|
||||
"docs": "docs/src/",
|
||||
"extensions": "extensions/"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Constraints
|
||||
- Read-only: Do not modify any files
|
||||
- Focus on structure, not content details
|
||||
- Complete within 2 minutes
|
||||
@@ -1,57 +0,0 @@
|
||||
# Phase 3: Analyze Changes
|
||||
|
||||
You are analyzing code changes to understand their nature and scope.
|
||||
|
||||
## Objective
|
||||
Produce a clear, neutral summary of what changed in the codebase.
|
||||
|
||||
## Input
|
||||
You will receive:
|
||||
- List of changed files from the triggering commit/PR
|
||||
- Repository structure from Phase 2
|
||||
|
||||
## Instructions
|
||||
|
||||
1. **Categorize Changed Files**
|
||||
- Source code (which crates/modules)
|
||||
- Configuration
|
||||
- Tests
|
||||
- Documentation (already existing)
|
||||
- Other
|
||||
|
||||
2. **Analyze Each Change**
|
||||
- Review diffs for files likely to impact documentation
|
||||
- Focus on: public APIs, settings, keybindings, commands, user-visible behavior
|
||||
|
||||
3. **Identify What Did NOT Change**
|
||||
- Note stable interfaces or behaviors
|
||||
- Important for avoiding unnecessary documentation updates
|
||||
|
||||
4. **Output Format**
|
||||
Produce a markdown summary:
|
||||
|
||||
```markdown
|
||||
## Change Analysis
|
||||
|
||||
### Changed Files Summary
|
||||
| Category | Files | Impact Level |
|
||||
| --- | --- | --- |
|
||||
| Source - [crate] | file1.rs, file2.rs | High/Medium/Low |
|
||||
| Settings | settings.json | Medium |
|
||||
| Tests | test_*.rs | None |
|
||||
|
||||
### Behavioral Changes
|
||||
- **[Feature/Area]**: Description of what changed from user perspective
|
||||
- **[Feature/Area]**: Description...
|
||||
|
||||
### Unchanged Areas
|
||||
- [Area]: Confirmed no changes to [specific behavior]
|
||||
|
||||
### Files Requiring Deeper Review
|
||||
- `path/to/file.rs`: Reason for deeper review
|
||||
```
|
||||
|
||||
## Constraints
|
||||
- Read-only: Do not modify any files
|
||||
- Neutral tone: Describe what changed, not whether it's good/bad
|
||||
- Do not propose documentation changes yet
|
||||
@@ -1,76 +0,0 @@
|
||||
# Phase 4: Plan Documentation Impact
|
||||
|
||||
You are determining whether and how documentation should be updated based on code changes.
|
||||
|
||||
## Objective
|
||||
Produce a structured documentation plan that will guide Phase 5 execution.
|
||||
|
||||
## Documentation System
|
||||
This is an **mdBook** site (https://rust-lang.github.io/mdBook/):
|
||||
- `docs/src/SUMMARY.md` defines book structure per https://rust-lang.github.io/mdBook/format/summary.html
|
||||
- If adding new pages, they MUST be added to SUMMARY.md
|
||||
- Use `{#kb action::ActionName}` syntax for keybindings (custom preprocessor expands these)
|
||||
- Prettier formatting (80 char width) will be applied automatically
|
||||
|
||||
## Input
|
||||
You will receive:
|
||||
- Change analysis from Phase 3
|
||||
- Repository structure from Phase 2
|
||||
- Documentation guidelines from `docs/AGENTS.md`
|
||||
|
||||
## Instructions
|
||||
|
||||
1. **Review AGENTS.md**
|
||||
- Load and apply all rules from `docs/AGENTS.md`
|
||||
- Respect scope boundaries (in-scope vs out-of-scope)
|
||||
|
||||
2. **Evaluate Documentation Impact**
|
||||
For each behavioral change from Phase 3:
|
||||
- Does existing documentation cover this area?
|
||||
- Is the documentation now inaccurate or incomplete?
|
||||
- Classify per AGENTS.md "Change Classification" section
|
||||
|
||||
3. **Identify Specific Updates**
|
||||
For each required update:
|
||||
- Exact file path
|
||||
- Specific section or heading
|
||||
- Type of change (update existing, add new, deprecate)
|
||||
- Description of the change
|
||||
|
||||
4. **Flag Uncertainty**
|
||||
Explicitly mark:
|
||||
- Assumptions you're making
|
||||
- Areas where human confirmation is needed
|
||||
- Ambiguous requirements
|
||||
|
||||
5. **Output Format**
|
||||
Use the exact format specified in `docs/AGENTS.md` Phase 4 section:
|
||||
|
||||
```markdown
|
||||
## Documentation Impact Assessment
|
||||
|
||||
### Summary
|
||||
Brief description of code changes analyzed.
|
||||
|
||||
### Documentation Updates Required: [Yes/No]
|
||||
|
||||
### Planned Changes
|
||||
|
||||
#### 1. [File Path]
|
||||
- **Section**: [Section name or "New section"]
|
||||
- **Change Type**: [Update/Add/Deprecate]
|
||||
- **Reason**: Why this change is needed
|
||||
- **Description**: What will be added/modified
|
||||
|
||||
### Uncertainty Flags
|
||||
- [ ] [Description of any assumptions or areas needing confirmation]
|
||||
|
||||
### No Changes Needed
|
||||
- [List files reviewed but not requiring updates, with brief reason]
|
||||
```
|
||||
|
||||
## Constraints
|
||||
- Read-only: Do not modify any files
|
||||
- Conservative: When uncertain, flag for human review rather than planning changes
|
||||
- Scoped: Only plan changes that trace directly to code changes from Phase 3
|
||||
- No scope expansion: Do not plan "improvements" unrelated to triggering changes
|
||||
@@ -1,67 +0,0 @@
|
||||
# Phase 5: Apply Documentation Plan
|
||||
|
||||
You are executing a pre-approved documentation plan for an **mdBook** documentation site.
|
||||
|
||||
## Objective
|
||||
Implement exactly the changes specified in the documentation plan from Phase 4.
|
||||
|
||||
## Documentation System
|
||||
- **mdBook**: https://rust-lang.github.io/mdBook/
|
||||
- **SUMMARY.md**: Follows mdBook format (https://rust-lang.github.io/mdBook/format/summary.html)
|
||||
- **Prettier**: Will be run automatically after this phase (80 char line width)
|
||||
- **Custom preprocessor**: Use `{#kb action::ActionName}` for keybindings instead of hardcoding
|
||||
|
||||
## Input
|
||||
You will receive:
|
||||
- Documentation plan from Phase 4
|
||||
- Documentation guidelines from `docs/AGENTS.md`
|
||||
- Style rules from `docs/.rules`
|
||||
|
||||
## Instructions
|
||||
|
||||
1. **Validate Plan**
|
||||
- Confirm all planned files are within scope per AGENTS.md
|
||||
- Verify no out-of-scope files are targeted
|
||||
|
||||
2. **Execute Each Planned Change**
|
||||
For each item in "Planned Changes":
|
||||
- Navigate to the specified file
|
||||
- Locate the specified section
|
||||
- Apply the described change
|
||||
- Follow style rules from `docs/.rules`
|
||||
|
||||
3. **Style Compliance**
|
||||
Every edit must follow `docs/.rules`:
|
||||
- Second person, present tense
|
||||
- No hedging words ("simply", "just", "easily")
|
||||
- Proper keybinding format (`Cmd+Shift+P`)
|
||||
- Settings Editor first, JSON second
|
||||
- Correct terminology (folder not directory, etc.)
|
||||
|
||||
4. **Preserve Context**
|
||||
- Maintain surrounding content structure
|
||||
- Keep consistent heading levels
|
||||
- Preserve existing cross-references
|
||||
|
||||
## Constraints
|
||||
- Execute ONLY changes listed in the plan
|
||||
- Do not discover new documentation targets
|
||||
- Do not make stylistic improvements outside planned sections
|
||||
- Do not expand scope beyond what Phase 4 specified
|
||||
- If a planned change cannot be applied (file missing, section not found), skip and note it
|
||||
|
||||
## Output
|
||||
After applying changes, output a summary:
|
||||
|
||||
```markdown
|
||||
## Applied Changes
|
||||
|
||||
### Successfully Applied
|
||||
- `path/to/file.md`: [Brief description of change]
|
||||
|
||||
### Skipped (Could Not Apply)
|
||||
- `path/to/file.md`: [Reason - e.g., "Section not found"]
|
||||
|
||||
### Warnings
|
||||
- [Any issues encountered during application]
|
||||
```
|
||||
@@ -1,54 +0,0 @@
|
||||
# Phase 6: Summarize Changes
|
||||
|
||||
You are generating a summary of documentation updates for PR review.
|
||||
|
||||
## Objective
|
||||
Create a clear, reviewable summary of all documentation changes made.
|
||||
|
||||
## Input
|
||||
You will receive:
|
||||
- Applied changes report from Phase 5
|
||||
- Original change analysis from Phase 3
|
||||
- Git diff of documentation changes
|
||||
|
||||
## Instructions
|
||||
|
||||
1. **Gather Change Information**
|
||||
- List all modified documentation files
|
||||
- Identify the corresponding code changes that triggered each update
|
||||
|
||||
2. **Generate Summary**
|
||||
Use the format specified in `docs/AGENTS.md` Phase 6 section:
|
||||
|
||||
```markdown
|
||||
## Documentation Update Summary
|
||||
|
||||
### Changes Made
|
||||
| File | Change | Related Code |
|
||||
| --- | --- | --- |
|
||||
| docs/src/path.md | Brief description | PR #123 or commit SHA |
|
||||
|
||||
### Rationale
|
||||
Brief explanation of why these updates were made, linking back to the triggering code changes.
|
||||
|
||||
### Review Notes
|
||||
- Items reviewers should pay special attention to
|
||||
- Any uncertainty flags from Phase 4 that were addressed
|
||||
- Assumptions made during documentation
|
||||
```
|
||||
|
||||
3. **Add Context for Reviewers**
|
||||
- Highlight any changes that might be controversial
|
||||
- Note if any planned changes were skipped and why
|
||||
- Flag areas where reviewer expertise is especially needed
|
||||
|
||||
## Output Format
|
||||
The summary should be suitable for:
|
||||
- PR description body
|
||||
- Commit message (condensed version)
|
||||
- Team communication
|
||||
|
||||
## Constraints
|
||||
- Read-only (documentation changes already applied in Phase 5)
|
||||
- Factual: Describe what was done, not justify why it's good
|
||||
- Complete: Account for all changes, including skipped items
|
||||
@@ -1,67 +0,0 @@
|
||||
# Phase 7: Commit and Open PR
|
||||
|
||||
You are creating a git branch, committing documentation changes, and opening a PR.
|
||||
|
||||
## Objective
|
||||
Package documentation updates into a reviewable pull request.
|
||||
|
||||
## Input
|
||||
You will receive:
|
||||
- Summary from Phase 6
|
||||
- List of modified files
|
||||
|
||||
## Instructions
|
||||
|
||||
1. **Create Branch**
|
||||
```sh
|
||||
git checkout -b docs/auto-update-{date}
|
||||
```
|
||||
Use format: `docs/auto-update-YYYY-MM-DD` or `docs/auto-update-{short-sha}`
|
||||
|
||||
2. **Stage and Commit**
|
||||
- Stage only documentation files in `docs/src/`
|
||||
- Do not stage any other files
|
||||
|
||||
Commit message format:
|
||||
```
|
||||
docs: auto-update documentation for [brief description]
|
||||
|
||||
[Summary from Phase 6, condensed]
|
||||
|
||||
Triggered by: [commit SHA or PR reference]
|
||||
|
||||
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
|
||||
```
|
||||
|
||||
3. **Push Branch**
|
||||
```sh
|
||||
git push -u origin docs/auto-update-{date}
|
||||
```
|
||||
|
||||
4. **Create Pull Request**
|
||||
Use the Phase 6 summary as the PR body.
|
||||
|
||||
PR Title: `docs: [Brief description of documentation updates]`
|
||||
|
||||
Labels (if available): `documentation`, `automated`
|
||||
|
||||
Base branch: `main`
|
||||
|
||||
## Constraints
|
||||
- Do NOT auto-merge
|
||||
- Do NOT request specific reviewers (let CODEOWNERS handle it)
|
||||
- Do NOT modify files outside `docs/src/`
|
||||
- If no changes to commit, exit gracefully with message "No documentation changes to commit"
|
||||
|
||||
## Output
|
||||
```markdown
|
||||
## PR Created
|
||||
|
||||
- **Branch**: docs/auto-update-{date}
|
||||
- **PR URL**: https://github.com/zed-industries/zed/pull/XXXX
|
||||
- **Status**: Ready for review
|
||||
|
||||
### Commit
|
||||
- SHA: {commit-sha}
|
||||
- Files: {count} documentation files modified
|
||||
```
|
||||
1
.github/actionlint.yml
vendored
1
.github/actionlint.yml
vendored
@@ -25,7 +25,6 @@ self-hosted-runner:
|
||||
- namespace-profile-32x64-ubuntu-2204
|
||||
# Namespace Ubuntu 24.04 (like ubuntu-latest)
|
||||
- namespace-profile-2x4-ubuntu-2404
|
||||
- namespace-profile-8x32-ubuntu-2404
|
||||
# Namespace Limited Preview
|
||||
- namespace-profile-8x16-ubuntu-2004-arm-m4
|
||||
- namespace-profile-8x32-ubuntu-2004-arm-m4
|
||||
|
||||
12
.github/actions/build_docs/action.yml
vendored
12
.github/actions/build_docs/action.yml
vendored
@@ -19,18 +19,6 @@ runs:
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: ./script/linux
|
||||
|
||||
- name: Install mold linker
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: ./script/install-mold
|
||||
|
||||
- name: Download WASI SDK
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: ./script/download-wasi-sdk
|
||||
|
||||
- name: Generate action metadata
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: ./script/generate-action-metadata
|
||||
|
||||
- name: Check for broken links (in MD)
|
||||
uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1
|
||||
with:
|
||||
|
||||
85
.github/workflows/autofix_pr.yml
vendored
85
.github/workflows/autofix_pr.yml
vendored
@@ -9,23 +9,26 @@ on:
|
||||
description: pr_number
|
||||
required: true
|
||||
type: string
|
||||
run_clippy:
|
||||
description: run_clippy
|
||||
type: boolean
|
||||
default: 'true'
|
||||
jobs:
|
||||
run_autofix:
|
||||
runs-on: namespace-profile-16x32-ubuntu-2204
|
||||
steps:
|
||||
- name: steps::checkout_repo
|
||||
- id: get-app-token
|
||||
name: autofix_pr::run_autofix::authenticate_as_zippy
|
||||
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
|
||||
with:
|
||||
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
|
||||
private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
|
||||
- name: steps::checkout_repo_with_token
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
clean: false
|
||||
token: ${{ steps.get-app-token.outputs.token }}
|
||||
- name: autofix_pr::run_autofix::checkout_pr
|
||||
run: gh pr checkout ${{ inputs.pr_number }}
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
|
||||
- name: steps::setup_cargo_config
|
||||
run: |
|
||||
mkdir -p ./../.cargo
|
||||
@@ -54,79 +57,27 @@ jobs:
|
||||
- name: autofix_pr::run_autofix::run_cargo_fmt
|
||||
run: cargo fmt --all
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: autofix_pr::run_autofix::run_cargo_fix
|
||||
if: ${{ inputs.run_clippy }}
|
||||
run: cargo fix --workspace --release --all-targets --all-features --allow-dirty --allow-staged
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: autofix_pr::run_autofix::run_clippy_fix
|
||||
if: ${{ inputs.run_clippy }}
|
||||
run: cargo clippy --workspace --release --all-targets --all-features --fix --allow-dirty --allow-staged
|
||||
shell: bash -euxo pipefail {0}
|
||||
- id: create-patch
|
||||
name: autofix_pr::run_autofix::create_patch
|
||||
- name: autofix_pr::run_autofix::commit_and_push
|
||||
run: |
|
||||
if git diff --quiet; then
|
||||
echo "No changes to commit"
|
||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
git diff > autofix.patch
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
git add -A
|
||||
git commit -m "Autofix"
|
||||
git push
|
||||
fi
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: upload artifact autofix-patch
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
|
||||
with:
|
||||
name: autofix-patch
|
||||
path: autofix.patch
|
||||
if-no-files-found: ignore
|
||||
retention-days: '1'
|
||||
- name: steps::cleanup_cargo_config
|
||||
if: always()
|
||||
run: |
|
||||
rm -rf ./../.cargo
|
||||
shell: bash -euxo pipefail {0}
|
||||
outputs:
|
||||
has_changes: ${{ steps.create-patch.outputs.has_changes }}
|
||||
commit_changes:
|
||||
needs:
|
||||
- run_autofix
|
||||
if: needs.run_autofix.outputs.has_changes == 'true'
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- id: get-app-token
|
||||
name: steps::authenticate_as_zippy
|
||||
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
|
||||
with:
|
||||
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
|
||||
private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
|
||||
- name: steps::checkout_repo_with_token
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
clean: false
|
||||
token: ${{ steps.get-app-token.outputs.token }}
|
||||
- name: autofix_pr::commit_changes::checkout_pr
|
||||
run: gh pr checkout ${{ inputs.pr_number }}
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
|
||||
- name: autofix_pr::download_patch_artifact
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53
|
||||
with:
|
||||
name: autofix-patch
|
||||
- name: autofix_pr::commit_changes::apply_patch
|
||||
run: git apply autofix.patch
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: autofix_pr::commit_changes::commit_and_push
|
||||
run: |
|
||||
git commit -am "Autofix"
|
||||
git push
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GIT_COMMITTER_NAME: Zed Zippy
|
||||
GIT_COMMITTER_EMAIL: 234243425+zed-zippy[bot]@users.noreply.github.com
|
||||
GIT_AUTHOR_NAME: Zed Zippy
|
||||
GIT_AUTHOR_EMAIL: 234243425+zed-zippy[bot]@users.noreply.github.com
|
||||
GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ inputs.pr_number }}
|
||||
cancel-in-progress: true
|
||||
- name: steps::cleanup_cargo_config
|
||||
if: always()
|
||||
run: |
|
||||
rm -rf ./../.cargo
|
||||
shell: bash -euxo pipefail {0}
|
||||
|
||||
2
.github/workflows/cherry_pick.yml
vendored
2
.github/workflows/cherry_pick.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
with:
|
||||
clean: false
|
||||
- id: get-app-token
|
||||
name: steps::authenticate_as_zippy
|
||||
name: cherry_pick::run_cherry_pick::authenticate_as_zippy
|
||||
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
|
||||
with:
|
||||
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
|
||||
|
||||
@@ -34,7 +34,6 @@ jobs:
|
||||
CharlesChen0823
|
||||
chbk
|
||||
cppcoffee
|
||||
davidbarsky
|
||||
davewa
|
||||
ddoemonn
|
||||
djsauble
|
||||
|
||||
@@ -1,40 +1,29 @@
|
||||
name: "Close Stale Issues"
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 2 * * 5"
|
||||
- cron: "0 8 31 DEC *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
debug-only:
|
||||
description: "Run in dry-run mode (no changes made)"
|
||||
type: boolean
|
||||
default: false
|
||||
operations-per-run:
|
||||
description: "Max number of issues to process (default: 1000)"
|
||||
type: number
|
||||
default: 1000
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10
|
||||
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: >
|
||||
Hi there!
|
||||
Zed development moves fast and a significant number of bugs become outdated.
|
||||
If you can reproduce this bug on the latest stable Zed, please let us know by leaving a comment with the Zed version.
|
||||
If the bug doesn't appear for you anymore, feel free to close the issue yourself; otherwise, the bot will close it in a couple of weeks.
|
||||
Hi there! 👋
|
||||
|
||||
We're working to clean up our issue tracker by closing older bugs that might not be relevant anymore. If you are able to reproduce this issue in the latest version of Zed, please let us know by commenting on this issue, and it will be kept open. If you can't reproduce it, feel free to close the issue yourself. Otherwise, it will close automatically in 14 days.
|
||||
|
||||
Thanks for your help!
|
||||
close-issue-message: "This issue was closed due to inactivity. If you're still experiencing this problem, please leave a comment with your Zed version so that we can reopen the issue."
|
||||
close-issue-message: "This issue was closed due to inactivity. If you're still experiencing this problem, please open a new issue with a link to this issue."
|
||||
days-before-stale: 60
|
||||
days-before-close: 14
|
||||
only-issue-types: "Bug,Crash"
|
||||
operations-per-run: ${{ inputs.operations-per-run || 1000 }}
|
||||
operations-per-run: 1000
|
||||
ascending: true
|
||||
enable-statistics: true
|
||||
debug-only: ${{ inputs.debug-only }}
|
||||
stale-issue-label: "stale"
|
||||
exempt-issue-labels: "never stale"
|
||||
|
||||
264
.github/workflows/docs_automation.yml
vendored
264
.github/workflows/docs_automation.yml
vendored
@@ -1,264 +0,0 @@
|
||||
name: Documentation Automation
|
||||
|
||||
on:
|
||||
# push:
|
||||
# branches: [main]
|
||||
# paths:
|
||||
# - 'crates/**'
|
||||
# - 'extensions/**'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR number to analyze (gets full PR diff)'
|
||||
required: false
|
||||
type: string
|
||||
trigger_sha:
|
||||
description: 'Commit SHA to analyze (ignored if pr_number is set)'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
|
||||
DROID_MODEL: claude-opus-4-5-20251101
|
||||
|
||||
jobs:
|
||||
docs-automation:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Droid CLI
|
||||
id: install-droid
|
||||
run: |
|
||||
curl -fsSL https://app.factory.ai/cli | sh
|
||||
echo "${HOME}/.local/bin" >> "$GITHUB_PATH"
|
||||
echo "DROID_BIN=${HOME}/.local/bin/droid" >> "$GITHUB_ENV"
|
||||
# Verify installation
|
||||
"${HOME}/.local/bin/droid" --version
|
||||
|
||||
- name: Setup Node.js (for Prettier)
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install Prettier
|
||||
run: npm install -g prettier
|
||||
|
||||
- name: Get changed files
|
||||
id: changed
|
||||
run: |
|
||||
if [ -n "${{ inputs.pr_number }}" ]; then
|
||||
# Get full PR diff
|
||||
echo "Analyzing PR #${{ inputs.pr_number }}"
|
||||
echo "source=pr" >> "$GITHUB_OUTPUT"
|
||||
echo "ref=${{ inputs.pr_number }}" >> "$GITHUB_OUTPUT"
|
||||
gh pr diff "${{ inputs.pr_number }}" --name-only > /tmp/changed_files.txt
|
||||
elif [ -n "${{ inputs.trigger_sha }}" ]; then
|
||||
# Get single commit diff
|
||||
SHA="${{ inputs.trigger_sha }}"
|
||||
echo "Analyzing commit $SHA"
|
||||
echo "source=commit" >> "$GITHUB_OUTPUT"
|
||||
echo "ref=$SHA" >> "$GITHUB_OUTPUT"
|
||||
git diff --name-only "${SHA}^" "$SHA" > /tmp/changed_files.txt
|
||||
else
|
||||
# Default to current commit
|
||||
SHA="${{ github.sha }}"
|
||||
echo "Analyzing commit $SHA"
|
||||
echo "source=commit" >> "$GITHUB_OUTPUT"
|
||||
echo "ref=$SHA" >> "$GITHUB_OUTPUT"
|
||||
git diff --name-only "${SHA}^" "$SHA" > /tmp/changed_files.txt || git diff --name-only HEAD~1 HEAD > /tmp/changed_files.txt
|
||||
fi
|
||||
|
||||
echo "Changed files:"
|
||||
cat /tmp/changed_files.txt
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
# Phase 0: Guardrails are loaded via AGENTS.md in each phase
|
||||
|
||||
# Phase 2: Explore Repository (Read-Only - default)
|
||||
- name: "Phase 2: Explore Repository"
|
||||
id: phase2
|
||||
run: |
|
||||
"$DROID_BIN" exec \
|
||||
-m "$DROID_MODEL" \
|
||||
-f .factory/prompts/docs-automation/phase2-explore.md \
|
||||
> /tmp/phase2-output.txt 2>&1 || true
|
||||
echo "Repository exploration complete"
|
||||
cat /tmp/phase2-output.txt
|
||||
|
||||
# Phase 3: Analyze Changes (Read-Only - default)
|
||||
- name: "Phase 3: Analyze Changes"
|
||||
id: phase3
|
||||
run: |
|
||||
CHANGED_FILES=$(tr '\n' ' ' < /tmp/changed_files.txt)
|
||||
echo "Analyzing changes in: $CHANGED_FILES"
|
||||
|
||||
# Build prompt with context
|
||||
cat > /tmp/phase3-prompt.md << 'EOF'
|
||||
$(cat .factory/prompts/docs-automation/phase3-analyze.md)
|
||||
|
||||
## Context
|
||||
|
||||
### Changed Files
|
||||
$CHANGED_FILES
|
||||
|
||||
### Phase 2 Output
|
||||
$(cat /tmp/phase2-output.txt)
|
||||
EOF
|
||||
|
||||
"$DROID_BIN" exec \
|
||||
-m "$DROID_MODEL" \
|
||||
"$(cat .factory/prompts/docs-automation/phase3-analyze.md)
|
||||
|
||||
Changed files: $CHANGED_FILES" \
|
||||
> /tmp/phase3-output.md 2>&1 || true
|
||||
echo "Change analysis complete"
|
||||
cat /tmp/phase3-output.md
|
||||
|
||||
# Phase 4: Plan Documentation Impact (Read-Only - default)
|
||||
- name: "Phase 4: Plan Documentation Impact"
|
||||
id: phase4
|
||||
run: |
|
||||
"$DROID_BIN" exec \
|
||||
-m "$DROID_MODEL" \
|
||||
-f .factory/prompts/docs-automation/phase4-plan.md \
|
||||
> /tmp/phase4-plan.md 2>&1 || true
|
||||
echo "Documentation plan complete"
|
||||
cat /tmp/phase4-plan.md
|
||||
|
||||
# Check if updates are required
|
||||
if grep -q "NO_UPDATES_REQUIRED" /tmp/phase4-plan.md; then
|
||||
echo "updates_required=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "updates_required=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# Phase 5: Apply Plan (Write-Enabled with --auto medium)
|
||||
- name: "Phase 5: Apply Documentation Plan"
|
||||
id: phase5
|
||||
if: steps.phase4.outputs.updates_required == 'true'
|
||||
run: |
|
||||
"$DROID_BIN" exec \
|
||||
-m "$DROID_MODEL" \
|
||||
--auto medium \
|
||||
-f .factory/prompts/docs-automation/phase5-apply.md \
|
||||
> /tmp/phase5-report.md 2>&1 || true
|
||||
echo "Documentation updates applied"
|
||||
cat /tmp/phase5-report.md
|
||||
|
||||
# Phase 5b: Format with Prettier
|
||||
- name: "Phase 5b: Format with Prettier"
|
||||
id: phase5b
|
||||
if: steps.phase4.outputs.updates_required == 'true'
|
||||
run: |
|
||||
echo "Formatting documentation with Prettier..."
|
||||
cd docs && prettier --write src/
|
||||
|
||||
echo "Verifying Prettier formatting passes..."
|
||||
cd docs && prettier --check src/
|
||||
|
||||
echo "Prettier formatting complete"
|
||||
|
||||
# Phase 6: Summarize Changes (Read-Only - default)
|
||||
- name: "Phase 6: Summarize Changes"
|
||||
id: phase6
|
||||
if: steps.phase4.outputs.updates_required == 'true'
|
||||
run: |
|
||||
# Get git diff of docs
|
||||
git diff docs/src/ > /tmp/docs-diff.txt || true
|
||||
|
||||
"$DROID_BIN" exec \
|
||||
-m "$DROID_MODEL" \
|
||||
-f .factory/prompts/docs-automation/phase6-summarize.md \
|
||||
> /tmp/phase6-summary.md 2>&1 || true
|
||||
echo "Summary generated"
|
||||
cat /tmp/phase6-summary.md
|
||||
|
||||
# Phase 7: Commit and Open PR
|
||||
- name: "Phase 7: Create PR"
|
||||
id: phase7
|
||||
if: steps.phase4.outputs.updates_required == 'true'
|
||||
run: |
|
||||
# Check if there are actual changes
|
||||
if git diff --quiet docs/src/; then
|
||||
echo "No documentation changes detected"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Configure git
|
||||
git config user.name "factory-droid[bot]"
|
||||
git config user.email "138933559+factory-droid[bot]@users.noreply.github.com"
|
||||
|
||||
# Daily batch branch - one branch per day, multiple commits accumulate
|
||||
BRANCH_NAME="docs/auto-update-$(date +%Y-%m-%d)"
|
||||
|
||||
# Stash local changes from phase 5
|
||||
git stash push -m "docs-automation-changes" -- docs/src/
|
||||
|
||||
# Check if branch already exists on remote
|
||||
if git ls-remote --exit-code --heads origin "$BRANCH_NAME" > /dev/null 2>&1; then
|
||||
echo "Branch $BRANCH_NAME exists, checking out and updating..."
|
||||
git fetch origin "$BRANCH_NAME"
|
||||
git checkout -B "$BRANCH_NAME" "origin/$BRANCH_NAME"
|
||||
else
|
||||
echo "Creating new branch $BRANCH_NAME..."
|
||||
git checkout -b "$BRANCH_NAME"
|
||||
fi
|
||||
|
||||
# Apply stashed changes
|
||||
git stash pop || true
|
||||
|
||||
# Stage and commit
|
||||
git add docs/src/
|
||||
SUMMARY=$(head -50 < /tmp/phase6-summary.md)
|
||||
git commit -m "docs: auto-update documentation
|
||||
|
||||
${SUMMARY}
|
||||
|
||||
Triggered by: ${{ steps.changed.outputs.source }} ${{ steps.changed.outputs.ref }}
|
||||
|
||||
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>"
|
||||
|
||||
# Push
|
||||
git push -u origin "$BRANCH_NAME"
|
||||
|
||||
# Check if PR already exists for this branch
|
||||
EXISTING_PR=$(gh pr list --head "$BRANCH_NAME" --json number --jq '.[0].number' || echo "")
|
||||
|
||||
if [ -n "$EXISTING_PR" ]; then
|
||||
echo "PR #$EXISTING_PR already exists for branch $BRANCH_NAME, updated with new commit"
|
||||
else
|
||||
# Create new PR
|
||||
gh pr create \
|
||||
--title "docs: automated documentation update ($(date +%Y-%m-%d))" \
|
||||
--body-file /tmp/phase6-summary.md \
|
||||
--base main || true
|
||||
echo "PR created on branch: $BRANCH_NAME"
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
# Summary output
|
||||
- name: "Summary"
|
||||
if: always()
|
||||
run: |
|
||||
echo "## Documentation Automation Summary" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
if [ "${{ steps.phase4.outputs.updates_required }}" == "false" ]; then
|
||||
echo "No documentation updates required for this change." >> "$GITHUB_STEP_SUMMARY"
|
||||
elif [ -f /tmp/phase6-summary.md ]; then
|
||||
cat /tmp/phase6-summary.md >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "Workflow completed. Check individual phase outputs for details." >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
4
.github/workflows/extension_tests.yml
vendored
4
.github/workflows/extension_tests.yml
vendored
@@ -51,7 +51,7 @@ jobs:
|
||||
needs:
|
||||
- orchestrate
|
||||
if: needs.orchestrate.outputs.check_rust == 'true'
|
||||
runs-on: namespace-profile-4x8-ubuntu-2204
|
||||
runs-on: namespace-profile-16x32-ubuntu-2204
|
||||
steps:
|
||||
- name: steps::checkout_repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
needs:
|
||||
- orchestrate
|
||||
if: needs.orchestrate.outputs.check_extension == 'true'
|
||||
runs-on: namespace-profile-8x32-ubuntu-2404
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- name: steps::checkout_repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -472,17 +472,11 @@ jobs:
|
||||
if: startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- id: get-app-token
|
||||
name: steps::authenticate_as_zippy
|
||||
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
|
||||
with:
|
||||
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
|
||||
private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
|
||||
- name: gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false
|
||||
run: gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
notify_on_failure:
|
||||
needs:
|
||||
- upload_release_assets
|
||||
|
||||
11
.github/workflows/run_tests.yml
vendored
11
.github/workflows/run_tests.yml
vendored
@@ -74,12 +74,9 @@ jobs:
|
||||
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
|
||||
with:
|
||||
version: '9'
|
||||
- name: steps::prettier
|
||||
- name: ./script/prettier
|
||||
run: ./script/prettier
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::cargo_fmt
|
||||
run: cargo fmt --all -- --check
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: ./script/check-todos
|
||||
run: ./script/check-todos
|
||||
shell: bash -euxo pipefail {0}
|
||||
@@ -90,6 +87,9 @@ jobs:
|
||||
uses: crate-ci/typos@2d0ce569feab1f8752f1dde43cc2f2aa53236e06
|
||||
with:
|
||||
config: ./typos.toml
|
||||
- name: steps::cargo_fmt
|
||||
run: cargo fmt --all -- --check
|
||||
shell: bash -euxo pipefail {0}
|
||||
timeout-minutes: 60
|
||||
run_tests_windows:
|
||||
needs:
|
||||
@@ -353,9 +353,6 @@ jobs:
|
||||
- name: steps::download_wasi_sdk
|
||||
run: ./script/download-wasi-sdk
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: ./script/generate-action-metadata
|
||||
run: ./script/generate-action-metadata
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: run_tests::check_docs::install_mdbook
|
||||
uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08
|
||||
with:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -36,7 +36,6 @@
|
||||
DerivedData/
|
||||
Packages
|
||||
xcuserdata/
|
||||
crates/docs_preprocessor/actions.json
|
||||
|
||||
# Don't commit any secrets to the repo.
|
||||
.env
|
||||
|
||||
3
.mailmap
3
.mailmap
@@ -141,9 +141,6 @@ Uladzislau Kaminski <i@uladkaminski.com>
|
||||
Uladzislau Kaminski <i@uladkaminski.com> <uladzislau_kaminski@epam.com>
|
||||
Vitaly Slobodin <vitaliy.slobodin@gmail.com>
|
||||
Vitaly Slobodin <vitaliy.slobodin@gmail.com> <vitaly_slobodin@fastmail.com>
|
||||
Yara <davidsk@zed.dev>
|
||||
Yara <git@davidsk.dev>
|
||||
Yara <git@yara.blue>
|
||||
Will Bradley <williambbradley@gmail.com>
|
||||
Will Bradley <williambbradley@gmail.com> <will@zed.dev>
|
||||
WindSoilder <WindSoilder@outlook.com>
|
||||
|
||||
@@ -23,6 +23,7 @@ In particular we love PRs that are:
|
||||
|
||||
If you're looking for concrete ideas:
|
||||
|
||||
- [Curated board of issues](https://github.com/orgs/zed-industries/projects/69) suitable for everyone from first-time contributors to seasoned community champions.
|
||||
- [Triaged bugs with confirmed steps to reproduce](https://github.com/zed-industries/zed/issues?q=is%3Aissue%20state%3Aopen%20type%3ABug%20label%3Astate%3Areproducible).
|
||||
- [Area labels](https://github.com/zed-industries/zed/labels?q=area%3A*) to browse bugs in a specific part of the product you care about (after clicking on an area label, add type:Bug to the search).
|
||||
|
||||
|
||||
490
Cargo.lock
generated
490
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
25
Cargo.toml
25
Cargo.toml
@@ -39,7 +39,6 @@ members = [
|
||||
"crates/command_palette",
|
||||
"crates/command_palette_hooks",
|
||||
"crates/component",
|
||||
"crates/component_preview",
|
||||
"crates/context_server",
|
||||
"crates/copilot",
|
||||
"crates/crashes",
|
||||
@@ -193,13 +192,11 @@ members = [
|
||||
"crates/vercel",
|
||||
"crates/vim",
|
||||
"crates/vim_mode_setting",
|
||||
"crates/which_key",
|
||||
"crates/watch",
|
||||
"crates/web_search",
|
||||
"crates/web_search_providers",
|
||||
"crates/workspace",
|
||||
"crates/worktree",
|
||||
"crates/worktree_benchmarks",
|
||||
"crates/x_ai",
|
||||
"crates/zed",
|
||||
"crates/zed_actions",
|
||||
@@ -276,7 +273,6 @@ collections = { path = "crates/collections", version = "0.1.0" }
|
||||
command_palette = { path = "crates/command_palette" }
|
||||
command_palette_hooks = { path = "crates/command_palette_hooks" }
|
||||
component = { path = "crates/component" }
|
||||
component_preview = { path = "crates/component_preview" }
|
||||
context_server = { path = "crates/context_server" }
|
||||
copilot = { path = "crates/copilot" }
|
||||
crashes = { path = "crates/crashes" }
|
||||
@@ -419,7 +415,6 @@ util_macros = { path = "crates/util_macros" }
|
||||
vercel = { path = "crates/vercel" }
|
||||
vim = { path = "crates/vim" }
|
||||
vim_mode_setting = { path = "crates/vim_mode_setting" }
|
||||
which_key = { path = "crates/which_key" }
|
||||
|
||||
watch = { path = "crates/watch" }
|
||||
web_search = { path = "crates/web_search" }
|
||||
@@ -441,7 +436,7 @@ ztracing_macro = { path = "crates/ztracing_macro" }
|
||||
# External crates
|
||||
#
|
||||
|
||||
agent-client-protocol = { version = "=0.9.2", features = ["unstable"] }
|
||||
agent-client-protocol = { version = "=0.9.0", features = ["unstable"] }
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = "0.25.1-rc1"
|
||||
any_vec = "0.14"
|
||||
@@ -460,15 +455,15 @@ async-task = "4.7"
|
||||
async-trait = "0.1"
|
||||
async-tungstenite = "0.31.0"
|
||||
async_zip = { version = "0.0.18", features = ["deflate", "deflate64"] }
|
||||
aws-config = { version = "1.8.10", features = ["behavior-version-latest"] }
|
||||
aws-credential-types = { version = "1.2.8", features = [
|
||||
aws-config = { version = "1.6.1", features = ["behavior-version-latest"] }
|
||||
aws-credential-types = { version = "1.2.2", features = [
|
||||
"hardcoded-credentials",
|
||||
] }
|
||||
aws-sdk-bedrockruntime = { version = "1.112.0", features = [
|
||||
aws-sdk-bedrockruntime = { version = "1.80.0", features = [
|
||||
"behavior-version-latest",
|
||||
] }
|
||||
aws-smithy-runtime-api = { version = "1.9.2", features = ["http-1x", "client"] }
|
||||
aws-smithy-types = { version = "1.3.4", features = ["http-body-1-x"] }
|
||||
aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] }
|
||||
aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
|
||||
backtrace = "0.3"
|
||||
base64 = "0.22"
|
||||
bincode = "1.2.1"
|
||||
@@ -481,7 +476,6 @@ bytes = "1.0"
|
||||
cargo_metadata = "0.19"
|
||||
cargo_toml = "0.21"
|
||||
cfg-if = "1.0.3"
|
||||
chardetng = "0.1"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
ciborium = "0.2"
|
||||
circular-buffer = "1.0"
|
||||
@@ -505,7 +499,6 @@ dotenvy = "0.15.0"
|
||||
ec4rs = "1.1"
|
||||
emojis = "0.6.1"
|
||||
env_logger = "0.11"
|
||||
encoding_rs = "0.8"
|
||||
exec = "0.3.1"
|
||||
fancy-regex = "0.16.0"
|
||||
fork = "0.4.0"
|
||||
@@ -670,7 +663,7 @@ tokio-socks = { version = "0.5.2", default-features = false, features = ["future
|
||||
toml = "0.8"
|
||||
toml_edit = { version = "0.22", default-features = false, features = ["display", "parse", "serde"] }
|
||||
tower-http = "0.4.4"
|
||||
tree-sitter = { version = "0.26", features = ["wasm"] }
|
||||
tree-sitter = { version = "0.25.10", features = ["wasm"] }
|
||||
tree-sitter-bash = "0.25.1"
|
||||
tree-sitter-c = "0.23"
|
||||
tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "5cb9b693cfd7bfacab1d9ff4acac1a4150700609" }
|
||||
@@ -704,7 +697,7 @@ uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
|
||||
walkdir = "2.5"
|
||||
wasm-encoder = "0.221"
|
||||
wasmparser = "0.221"
|
||||
wasmtime = { version = "33", default-features = false, features = [
|
||||
wasmtime = { version = "29", default-features = false, features = [
|
||||
"async",
|
||||
"demangle",
|
||||
"runtime",
|
||||
@@ -713,7 +706,7 @@ wasmtime = { version = "33", default-features = false, features = [
|
||||
"incremental-cache",
|
||||
"parallel-compilation",
|
||||
] }
|
||||
wasmtime-wasi = "33"
|
||||
wasmtime-wasi = "29"
|
||||
wax = "0.6"
|
||||
which = "6.0.0"
|
||||
windows-core = "0.61"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax = docker/dockerfile:1.2
|
||||
|
||||
FROM rust:1.92-bookworm as builder
|
||||
FROM rust:1.91.1-bookworm as builder
|
||||
WORKDIR app
|
||||
COPY . .
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ Other platforms are not yet available:
|
||||
- [Building Zed for macOS](./docs/src/development/macos.md)
|
||||
- [Building Zed for Linux](./docs/src/development/linux.md)
|
||||
- [Building Zed for Windows](./docs/src/development/windows.md)
|
||||
- [Running Collaboration Locally](./docs/src/development/local-collaboration.md)
|
||||
|
||||
### Contributing
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ ai
|
||||
= @rtfeldman
|
||||
|
||||
audio
|
||||
= @yara-blue
|
||||
= @dvdsk
|
||||
|
||||
crashes
|
||||
= @p1n3appl3
|
||||
@@ -53,7 +53,7 @@ extension
|
||||
git
|
||||
= @cole-miller
|
||||
= @danilo-leal
|
||||
= @yara-blue
|
||||
= @dvdsk
|
||||
= @kubkon
|
||||
= @Anthony-Eid
|
||||
= @cameron1024
|
||||
@@ -76,7 +76,7 @@ languages
|
||||
|
||||
linux
|
||||
= @cole-miller
|
||||
= @yara-blue
|
||||
= @dvdsk
|
||||
= @p1n3appl3
|
||||
= @probably-neb
|
||||
= @smitbarmase
|
||||
@@ -92,7 +92,7 @@ multi_buffer
|
||||
= @SomeoneToIgnore
|
||||
|
||||
pickers
|
||||
= @yara-blue
|
||||
= @dvdsk
|
||||
= @p1n3appl3
|
||||
= @SomeoneToIgnore
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
"ctrl-alt-z": "edit_prediction::RatePredictions",
|
||||
"ctrl-alt-shift-i": "edit_prediction::ToggleMenu",
|
||||
"ctrl-alt-l": "lsp_tool::ToggleMenu",
|
||||
"ctrl-alt-shift-s": "workspace::ToggleWorktreeSecurity",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -227,7 +226,6 @@
|
||||
"ctrl-g": "search::SelectNextMatch",
|
||||
"ctrl-shift-g": "search::SelectPreviousMatch",
|
||||
"ctrl-k l": "agent::OpenRulesLibrary",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -241,7 +239,6 @@
|
||||
"ctrl-alt-l": "agent::OpenRulesLibrary",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"ctrl-alt-/": "agent::ToggleModelSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
"ctrl-shift-j": "agent::ToggleNavigationMenu",
|
||||
"ctrl-alt-i": "agent::ToggleOptionsMenu",
|
||||
"ctrl-alt-shift-n": "agent::ToggleNewThreadMenu",
|
||||
@@ -265,9 +262,9 @@
|
||||
{
|
||||
"context": "AgentPanel > Markdown",
|
||||
"bindings": {
|
||||
"copy": "markdown::CopyAsMarkdown",
|
||||
"ctrl-insert": "markdown::CopyAsMarkdown",
|
||||
"ctrl-c": "markdown::CopyAsMarkdown",
|
||||
"copy": "markdown::Copy",
|
||||
"ctrl-insert": "markdown::Copy",
|
||||
"ctrl-c": "markdown::Copy",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -285,6 +282,36 @@
|
||||
"ctrl-alt-t": "agent::NewThread",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor && !Picker > Editor && !use_modifier_to_send",
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"ctrl-enter": "agent::ChatWithFollow",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor && !Picker > Editor && use_modifier_to_send",
|
||||
"bindings": {
|
||||
"ctrl-enter": "agent::Chat",
|
||||
"enter": "editor::Newline",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "EditMessageEditor > Editor",
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-enter": "editor::Newline",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AgentFeedbackMessageEditor > Editor",
|
||||
"bindings": {
|
||||
@@ -299,25 +326,14 @@
|
||||
"ctrl-enter": "menu::Confirm",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-enter": "agent::ChatWithFollow",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"ctrl-shift-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > Editor && !use_modifier_to_send",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -325,7 +341,10 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-enter": "agent::Chat",
|
||||
"enter": "editor::Newline",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -792,7 +811,7 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "InlineAssistant",
|
||||
"context": "PromptEditor",
|
||||
"bindings": {
|
||||
"ctrl-[": "agent::CyclePreviousInlineAssist",
|
||||
"ctrl-]": "agent::CycleNextInlineAssist",
|
||||
@@ -881,10 +900,8 @@
|
||||
{
|
||||
"context": "GitPanel && ChangesList",
|
||||
"bindings": {
|
||||
"left": "git_panel::CollapseSelectedEntry",
|
||||
"right": "git_panel::ExpandSelectedEntry",
|
||||
"up": "git_panel::PreviousEntry",
|
||||
"down": "git_panel::NextEntry",
|
||||
"up": "menu::SelectPrevious",
|
||||
"down": "menu::SelectNext",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-y": "git::StageFile",
|
||||
"alt-shift-y": "git::UnstageFile",
|
||||
|
||||
@@ -51,7 +51,6 @@
|
||||
"ctrl-cmd-i": "edit_prediction::ToggleMenu",
|
||||
"ctrl-cmd-l": "lsp_tool::ToggleMenu",
|
||||
"ctrl-cmd-c": "editor::DisplayCursorNames",
|
||||
"ctrl-cmd-s": "workspace::ToggleWorktreeSecurity",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -266,8 +265,6 @@
|
||||
"cmd-g": "search::SelectNextMatch",
|
||||
"cmd-shift-g": "search::SelectPreviousMatch",
|
||||
"cmd-k l": "agent::OpenRulesLibrary",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
"cmd-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -282,7 +279,6 @@
|
||||
"cmd-alt-p": "agent::ManageProfiles",
|
||||
"cmd-i": "agent::ToggleProfileSelector",
|
||||
"cmd-alt-/": "agent::ToggleModelSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
"cmd-shift-j": "agent::ToggleNavigationMenu",
|
||||
"cmd-alt-m": "agent::ToggleOptionsMenu",
|
||||
"cmd-alt-shift-n": "agent::ToggleNewThreadMenu",
|
||||
@@ -307,7 +303,7 @@
|
||||
"context": "AgentPanel > Markdown",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-c": "markdown::CopyAsMarkdown",
|
||||
"cmd-c": "markdown::Copy",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -326,6 +322,39 @@
|
||||
"cmd-alt-t": "agent::NewThread",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor && !Picker > Editor && !use_modifier_to_send",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"cmd-enter": "agent::ChatWithFollow",
|
||||
"cmd-i": "agent::ToggleProfileSelector",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"cmd-shift-y": "agent::KeepAll",
|
||||
"cmd-shift-n": "agent::RejectAll",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor && !Picker > Editor && use_modifier_to_send",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-enter": "agent::Chat",
|
||||
"enter": "editor::Newline",
|
||||
"cmd-i": "agent::ToggleProfileSelector",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"cmd-shift-y": "agent::KeepAll",
|
||||
"cmd-shift-n": "agent::RejectAll",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "EditMessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-enter": "editor::Newline",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AgentFeedbackMessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
@@ -347,25 +376,15 @@
|
||||
"cmd-enter": "menu::Confirm",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"cmd-shift-y": "agent::KeepAll",
|
||||
"cmd-shift-n": "agent::RejectAll",
|
||||
"cmd-enter": "agent::ChatWithFollow",
|
||||
"cmd-shift-v": "agent::PasteRaw",
|
||||
"cmd-i": "agent::ToggleProfileSelector",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > Editor && !use_modifier_to_send",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"cmd-shift-y": "agent::KeepAll",
|
||||
"cmd-shift-n": "agent::RejectAll",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -373,7 +392,10 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-enter": "agent::Chat",
|
||||
"enter": "editor::Newline",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"cmd-shift-y": "agent::KeepAll",
|
||||
"cmd-shift-n": "agent::RejectAll",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -853,11 +875,10 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "InlineAssistant > Editor",
|
||||
"context": "PromptEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-alt-/": "agent::ToggleModelSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
"ctrl-[": "agent::CyclePreviousInlineAssist",
|
||||
"ctrl-]": "agent::CycleNextInlineAssist",
|
||||
"cmd-shift-enter": "inline_assistant::ThumbsUpResult",
|
||||
@@ -954,12 +975,10 @@
|
||||
"context": "GitPanel && ChangesList",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"up": "git_panel::PreviousEntry",
|
||||
"down": "git_panel::NextEntry",
|
||||
"cmd-up": "git_panel::FirstEntry",
|
||||
"cmd-down": "git_panel::LastEntry",
|
||||
"left": "git_panel::CollapseSelectedEntry",
|
||||
"right": "git_panel::ExpandSelectedEntry",
|
||||
"up": "menu::SelectPrevious",
|
||||
"down": "menu::SelectNext",
|
||||
"cmd-up": "menu::SelectFirst",
|
||||
"cmd-down": "menu::SelectLast",
|
||||
"enter": "menu::Confirm",
|
||||
"cmd-alt-y": "git::ToggleStaged",
|
||||
"space": "git::ToggleStaged",
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
"ctrl-shift-i": "edit_prediction::ToggleMenu",
|
||||
"shift-alt-l": "lsp_tool::ToggleMenu",
|
||||
"ctrl-shift-alt-c": "editor::DisplayCursorNames",
|
||||
"ctrl-shift-alt-s": "workspace::ToggleWorktreeSecurity",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -227,7 +226,6 @@
|
||||
"ctrl-g": "search::SelectNextMatch",
|
||||
"ctrl-shift-g": "search::SelectPreviousMatch",
|
||||
"ctrl-k l": "agent::OpenRulesLibrary",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -241,7 +239,6 @@
|
||||
"shift-alt-l": "agent::OpenRulesLibrary",
|
||||
"shift-alt-p": "agent::ManageProfiles",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
"shift-alt-/": "agent::ToggleModelSelector",
|
||||
"shift-alt-j": "agent::ToggleNavigationMenu",
|
||||
"shift-alt-i": "agent::ToggleOptionsMenu",
|
||||
@@ -268,7 +265,7 @@
|
||||
"context": "AgentPanel > Markdown",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-c": "markdown::CopyAsMarkdown",
|
||||
"ctrl-c": "markdown::Copy",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -287,6 +284,39 @@
|
||||
"ctrl-alt-t": "agent::NewThread",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor && !Picker > Editor && !use_modifier_to_send",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"ctrl-enter": "agent::ChatWithFollow",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"ctrl-shift-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor && !Picker > Editor && use_modifier_to_send",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-enter": "agent::Chat",
|
||||
"enter": "editor::Newline",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"ctrl-shift-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "EditMessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-enter": "editor::Newline",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AgentFeedbackMessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
@@ -302,25 +332,15 @@
|
||||
"ctrl-enter": "menu::Confirm",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-enter": "agent::ChatWithFollow",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"ctrl-shift-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > Editor && !use_modifier_to_send",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"ctrl-shift-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -328,7 +348,10 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-enter": "agent::Chat",
|
||||
"enter": "editor::Newline",
|
||||
"ctrl-shift-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -796,7 +819,7 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "InlineAssistant",
|
||||
"context": "PromptEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-[": "agent::CyclePreviousInlineAssist",
|
||||
@@ -881,10 +904,8 @@
|
||||
"context": "GitPanel && ChangesList",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"up": "git_panel::PreviousEntry",
|
||||
"down": "git_panel::NextEntry",
|
||||
"left": "git_panel::CollapseSelectedEntry",
|
||||
"right": "git_panel::ExpandSelectedEntry",
|
||||
"up": "menu::SelectPrevious",
|
||||
"down": "menu::SelectNext",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-y": "git::StageFile",
|
||||
"shift-alt-y": "git::UnstageFile",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "InlineAssistant > Editor",
|
||||
"context": "InlineAssistEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-backspace": "editor::Cancel",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "InlineAssistant > Editor",
|
||||
"context": "InlineAssistEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-shift-backspace": "editor::Cancel",
|
||||
|
||||
@@ -14,6 +14,7 @@ The section you'll need to rewrite is marked with <rewrite_this></rewrite_this>
|
||||
The context around the relevant section has been truncated (possibly in the middle of a line) for brevity.
|
||||
{{/if}}
|
||||
|
||||
{{#if rewrite_section}}
|
||||
And here's the section to rewrite based on that prompt again for reference:
|
||||
|
||||
<rewrite_this>
|
||||
@@ -32,6 +33,8 @@ Below are the diagnostic errors visible to the user. If the user requests probl
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
{{/if}}
|
||||
|
||||
Only make changes that are necessary to fulfill the prompt, leave everything else as-is. All surrounding {{content_type}} will be preserved.
|
||||
|
||||
Start at the indentation level in the original file in the rewritten {{content_type}}.
|
||||
|
||||
@@ -1178,10 +1178,6 @@
|
||||
"remove_trailing_whitespace_on_save": true,
|
||||
// Whether to start a new line with a comment when a previous line is a comment as well.
|
||||
"extend_comment_on_newline": true,
|
||||
// Whether to continue markdown lists when pressing enter.
|
||||
"extend_list_on_newline": true,
|
||||
// Whether to indent list items when pressing tab after a list marker.
|
||||
"indent_list_on_tab": true,
|
||||
// Removes any lines containing only whitespace at the end of the file and
|
||||
// ensures just one newline at the end.
|
||||
"ensure_final_newline_on_save": true,
|
||||
@@ -1325,14 +1321,6 @@
|
||||
"hidden_files": ["**/.*"],
|
||||
// Git gutter behavior configuration.
|
||||
"git": {
|
||||
// Global switch to enable or disable all git integration features.
|
||||
// If set to true, disables all git integration features.
|
||||
// If set to false, individual git integration features below will be independently enabled or disabled.
|
||||
"disable_git": false,
|
||||
// Whether to enable git status tracking.
|
||||
"enable_status": true,
|
||||
// Whether to enable git diff display.
|
||||
"enable_diff": true,
|
||||
// Control whether the git gutter is shown. May take 2 values:
|
||||
// 1. Show the gutter
|
||||
// "git_gutter": "tracked_files"
|
||||
@@ -1717,12 +1705,7 @@
|
||||
// }
|
||||
//
|
||||
"file_types": {
|
||||
"JSONC": [
|
||||
"**/.zed/*.json",
|
||||
"**/.vscode/**/*.json",
|
||||
"**/{zed,Zed}/{settings,keymap,tasks,debug}.json",
|
||||
"tsconfig*.json",
|
||||
],
|
||||
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json", "tsconfig*.json"],
|
||||
"Markdown": [".rules", ".cursorrules", ".windsurfrules", ".clinerules"],
|
||||
"Shell Script": [".env.*"],
|
||||
},
|
||||
@@ -2079,12 +2062,6 @@
|
||||
//
|
||||
// Default: true
|
||||
"restore_unsaved_buffers": true,
|
||||
// Whether or not to skip worktree trust checks.
|
||||
// When trusted, project settings are synchronized automatically,
|
||||
// language and MCP servers are downloaded and started automatically.
|
||||
//
|
||||
// Default: false
|
||||
"trust_all_worktrees": false,
|
||||
},
|
||||
// Zed's Prettier integration settings.
|
||||
// Allows to enable/disable formatting with Prettier
|
||||
@@ -2169,13 +2146,6 @@
|
||||
// The shape can be one of the following: "block", "bar", "underline", "hollow".
|
||||
"cursor_shape": {},
|
||||
},
|
||||
// Which-key popup settings
|
||||
"which_key": {
|
||||
// Whether to show the which-key popup when holding down key combinations.
|
||||
"enabled": false,
|
||||
// Delay in milliseconds before showing the which-key popup.
|
||||
"delay_ms": 1000,
|
||||
},
|
||||
// The server to connect to. If the environment variable
|
||||
// ZED_SERVER_URL is set, it will override this setting.
|
||||
"server_url": "https://zed.dev",
|
||||
|
||||
@@ -11,7 +11,6 @@ use language::language_settings::FormatOnSave;
|
||||
pub use mention::*;
|
||||
use project::lsp_store::{FormatTrigger, LspFormatTarget};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::to_string_pretty;
|
||||
use settings::Settings as _;
|
||||
use task::{Shell, ShellBuilder};
|
||||
pub use terminal::*;
|
||||
@@ -44,7 +43,6 @@ pub struct UserMessage {
|
||||
pub content: ContentBlock,
|
||||
pub chunks: Vec<acp::ContentBlock>,
|
||||
pub checkpoint: Option<Checkpoint>,
|
||||
pub indented: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -75,7 +73,6 @@ impl UserMessage {
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct AssistantMessage {
|
||||
pub chunks: Vec<AssistantMessageChunk>,
|
||||
pub indented: bool,
|
||||
}
|
||||
|
||||
impl AssistantMessage {
|
||||
@@ -126,14 +123,6 @@ pub enum AgentThreadEntry {
|
||||
}
|
||||
|
||||
impl AgentThreadEntry {
|
||||
pub fn is_indented(&self) -> bool {
|
||||
match self {
|
||||
Self::UserMessage(message) => message.indented,
|
||||
Self::AssistantMessage(message) => message.indented,
|
||||
Self::ToolCall(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_markdown(&self, cx: &App) -> String {
|
||||
match self {
|
||||
Self::UserMessage(message) => message.to_markdown(cx),
|
||||
@@ -193,7 +182,6 @@ pub struct ToolCall {
|
||||
pub locations: Vec<acp::ToolCallLocation>,
|
||||
pub resolved_locations: Vec<Option<AgentLocation>>,
|
||||
pub raw_input: Option<serde_json::Value>,
|
||||
pub raw_input_markdown: Option<Entity<Markdown>>,
|
||||
pub raw_output: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
@@ -224,11 +212,6 @@ impl ToolCall {
|
||||
}
|
||||
}
|
||||
|
||||
let raw_input_markdown = tool_call
|
||||
.raw_input
|
||||
.as_ref()
|
||||
.and_then(|input| markdown_for_raw_output(input, &language_registry, cx));
|
||||
|
||||
let result = Self {
|
||||
id: tool_call.tool_call_id,
|
||||
label: cx
|
||||
@@ -239,7 +222,6 @@ impl ToolCall {
|
||||
resolved_locations: Vec::default(),
|
||||
status,
|
||||
raw_input: tool_call.raw_input,
|
||||
raw_input_markdown,
|
||||
raw_output: tool_call.raw_output,
|
||||
};
|
||||
Ok(result)
|
||||
@@ -315,7 +297,6 @@ impl ToolCall {
|
||||
}
|
||||
|
||||
if let Some(raw_input) = raw_input {
|
||||
self.raw_input_markdown = markdown_for_raw_output(&raw_input, &language_registry, cx);
|
||||
self.raw_input = Some(raw_input);
|
||||
}
|
||||
|
||||
@@ -1203,16 +1184,6 @@ impl AcpThread {
|
||||
message_id: Option<UserMessageId>,
|
||||
chunk: acp::ContentBlock,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.push_user_content_block_with_indent(message_id, chunk, false, cx)
|
||||
}
|
||||
|
||||
pub fn push_user_content_block_with_indent(
|
||||
&mut self,
|
||||
message_id: Option<UserMessageId>,
|
||||
chunk: acp::ContentBlock,
|
||||
indented: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let language_registry = self.project.read(cx).languages().clone();
|
||||
let path_style = self.project.read(cx).path_style(cx);
|
||||
@@ -1223,10 +1194,8 @@ impl AcpThread {
|
||||
id,
|
||||
content,
|
||||
chunks,
|
||||
indented: existing_indented,
|
||||
..
|
||||
}) = last_entry
|
||||
&& *existing_indented == indented
|
||||
{
|
||||
*id = message_id.or(id.take());
|
||||
content.append(chunk.clone(), &language_registry, path_style, cx);
|
||||
@@ -1241,7 +1210,6 @@ impl AcpThread {
|
||||
content,
|
||||
chunks: vec![chunk],
|
||||
checkpoint: None,
|
||||
indented,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
@@ -1253,26 +1221,12 @@ impl AcpThread {
|
||||
chunk: acp::ContentBlock,
|
||||
is_thought: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.push_assistant_content_block_with_indent(chunk, is_thought, false, cx)
|
||||
}
|
||||
|
||||
pub fn push_assistant_content_block_with_indent(
|
||||
&mut self,
|
||||
chunk: acp::ContentBlock,
|
||||
is_thought: bool,
|
||||
indented: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let language_registry = self.project.read(cx).languages().clone();
|
||||
let path_style = self.project.read(cx).path_style(cx);
|
||||
let entries_len = self.entries.len();
|
||||
if let Some(last_entry) = self.entries.last_mut()
|
||||
&& let AgentThreadEntry::AssistantMessage(AssistantMessage {
|
||||
chunks,
|
||||
indented: existing_indented,
|
||||
}) = last_entry
|
||||
&& *existing_indented == indented
|
||||
&& let AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) = last_entry
|
||||
{
|
||||
let idx = entries_len - 1;
|
||||
cx.emit(AcpThreadEvent::EntryUpdated(idx));
|
||||
@@ -1301,7 +1255,6 @@ impl AcpThread {
|
||||
self.push_entry(
|
||||
AgentThreadEntry::AssistantMessage(AssistantMessage {
|
||||
chunks: vec![chunk],
|
||||
indented,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
@@ -1364,7 +1317,6 @@ impl AcpThread {
|
||||
locations: Vec::new(),
|
||||
resolved_locations: Vec::new(),
|
||||
raw_input: None,
|
||||
raw_input_markdown: None,
|
||||
raw_output: None,
|
||||
};
|
||||
self.push_entry(AgentThreadEntry::ToolCall(failed_tool_call), cx);
|
||||
@@ -1752,7 +1704,6 @@ impl AcpThread {
|
||||
content: block,
|
||||
chunks: message,
|
||||
checkpoint: None,
|
||||
indented: false,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
@@ -1993,42 +1944,37 @@ impl AcpThread {
|
||||
fn update_last_checkpoint(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||
let git_store = self.project.read(cx).git_store().clone();
|
||||
|
||||
let Some((_, message)) = self.last_user_message() else {
|
||||
let old_checkpoint = if let Some((_, message)) = self.last_user_message() {
|
||||
if let Some(checkpoint) = message.checkpoint.as_ref() {
|
||||
checkpoint.git_checkpoint.clone()
|
||||
} else {
|
||||
return Task::ready(Ok(()));
|
||||
}
|
||||
} else {
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
let Some(user_message_id) = message.id.clone() else {
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
let Some(checkpoint) = message.checkpoint.as_ref() else {
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
let old_checkpoint = checkpoint.git_checkpoint.clone();
|
||||
|
||||
let new_checkpoint = git_store.update(cx, |git, cx| git.checkpoint(cx));
|
||||
cx.spawn(async move |this, cx| {
|
||||
let Some(new_checkpoint) = new_checkpoint
|
||||
let new_checkpoint = new_checkpoint
|
||||
.await
|
||||
.context("failed to get new checkpoint")
|
||||
.log_err()
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let equal = git_store
|
||||
.update(cx, |git, cx| {
|
||||
git.compare_checkpoints(old_checkpoint.clone(), new_checkpoint, cx)
|
||||
})?
|
||||
.await
|
||||
.unwrap_or(true);
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
if let Some((ix, message)) = this.user_message_mut(&user_message_id) {
|
||||
if let Some(checkpoint) = message.checkpoint.as_mut() {
|
||||
checkpoint.show = !equal;
|
||||
cx.emit(AcpThreadEvent::EntryUpdated(ix));
|
||||
}
|
||||
}
|
||||
})?;
|
||||
.log_err();
|
||||
if let Some(new_checkpoint) = new_checkpoint {
|
||||
let equal = git_store
|
||||
.update(cx, |git, cx| {
|
||||
git.compare_checkpoints(old_checkpoint.clone(), new_checkpoint, cx)
|
||||
})?
|
||||
.await
|
||||
.unwrap_or(true);
|
||||
this.update(cx, |this, cx| {
|
||||
let (ix, message) = this.last_user_message().context("no user message")?;
|
||||
let checkpoint = message.checkpoint.as_mut().context("no checkpoint")?;
|
||||
checkpoint.show = !equal;
|
||||
cx.emit(AcpThreadEvent::EntryUpdated(ix));
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
@@ -2428,10 +2374,8 @@ fn markdown_for_raw_output(
|
||||
)
|
||||
})),
|
||||
value => Some(cx.new(|cx| {
|
||||
let pretty_json = to_string_pretty(value).unwrap_or_else(|_| value.to_string());
|
||||
|
||||
Markdown::new(
|
||||
format!("```json\n{}\n```", pretty_json).into(),
|
||||
format!("```json\n{}\n```", value).into(),
|
||||
Some(language_registry.clone()),
|
||||
None,
|
||||
cx,
|
||||
@@ -4074,67 +4018,4 @@ mod tests {
|
||||
"Should have exactly 2 terminals (the completed ones from before checkpoint)"
|
||||
);
|
||||
}
|
||||
|
||||
/// Tests that update_last_checkpoint correctly updates the original message's checkpoint
|
||||
/// even when a new user message is added while the async checkpoint comparison is in progress.
|
||||
///
|
||||
/// This is a regression test for a bug where update_last_checkpoint would fail with
|
||||
/// "no checkpoint" if a new user message (without a checkpoint) was added between when
|
||||
/// update_last_checkpoint started and when its async closure ran.
|
||||
#[gpui::test]
|
||||
async fn test_update_last_checkpoint_with_new_message_added(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/test"), json!({".git": {}, "file.txt": "content"}))
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [Path::new(path!("/test"))], cx).await;
|
||||
|
||||
let handler_done = Arc::new(AtomicBool::new(false));
|
||||
let handler_done_clone = handler_done.clone();
|
||||
let connection = Rc::new(FakeAgentConnection::new().on_user_message(
|
||||
move |_, _thread, _cx| {
|
||||
handler_done_clone.store(true, SeqCst);
|
||||
async move { Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) }.boxed_local()
|
||||
},
|
||||
));
|
||||
|
||||
let thread = cx
|
||||
.update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let send_future = thread.update(cx, |thread, cx| thread.send_raw("First message", cx));
|
||||
let send_task = cx.background_executor.spawn(send_future);
|
||||
|
||||
// Tick until handler completes, then a few more to let update_last_checkpoint start
|
||||
while !handler_done.load(SeqCst) {
|
||||
cx.executor().tick();
|
||||
}
|
||||
for _ in 0..5 {
|
||||
cx.executor().tick();
|
||||
}
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.push_entry(
|
||||
AgentThreadEntry::UserMessage(UserMessage {
|
||||
id: Some(UserMessageId::new()),
|
||||
content: ContentBlock::Empty,
|
||||
chunks: vec!["Injected message (no checkpoint)".into()],
|
||||
checkpoint: None,
|
||||
indented: false,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
let result = send_task.await;
|
||||
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"send should succeed even when new message added during update_last_checkpoint: {:?}",
|
||||
result.err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,21 +204,12 @@ pub trait AgentModelSelector: 'static {
|
||||
}
|
||||
}
|
||||
|
||||
/// Icon for a model in the model selector.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum AgentModelIcon {
|
||||
/// A built-in icon from Zed's icon set.
|
||||
Named(IconName),
|
||||
/// Path to a custom SVG icon file.
|
||||
Path(SharedString),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AgentModelInfo {
|
||||
pub id: acp::ModelId,
|
||||
pub name: SharedString,
|
||||
pub description: Option<SharedString>,
|
||||
pub icon: Option<AgentModelIcon>,
|
||||
pub icon: Option<IconName>,
|
||||
}
|
||||
|
||||
impl From<acp::ModelInfo> for AgentModelInfo {
|
||||
@@ -248,10 +239,6 @@ impl AgentModelList {
|
||||
AgentModelList::Grouped(groups) => groups.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_flat(&self) -> bool {
|
||||
matches!(self, AgentModelList::Flat(_))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
|
||||
@@ -6,7 +6,7 @@ use futures::{FutureExt, StreamExt, channel::mpsc};
|
||||
use gpui::{
|
||||
App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity,
|
||||
};
|
||||
use language::{Anchor, Buffer, BufferEvent, Point, ToPoint};
|
||||
use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint};
|
||||
use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};
|
||||
use std::{cmp, ops::Range, sync::Arc};
|
||||
use text::{Edit, Patch, Rope};
|
||||
@@ -150,7 +150,7 @@ impl ActionLog {
|
||||
if buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.is_some_and(|file| file.disk_state().is_deleted())
|
||||
.is_some_and(|file| file.disk_state() == DiskState::Deleted)
|
||||
{
|
||||
// If the buffer had been edited by a tool, but it got
|
||||
// deleted externally, we want to stop tracking it.
|
||||
@@ -162,7 +162,7 @@ impl ActionLog {
|
||||
if buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.is_some_and(|file| !file.disk_state().is_deleted())
|
||||
.is_some_and(|file| file.disk_state() != DiskState::Deleted)
|
||||
{
|
||||
// If the buffer had been deleted by a tool, but it got
|
||||
// resurrected externally, we want to clear the edits we
|
||||
@@ -769,7 +769,7 @@ impl ActionLog {
|
||||
tracked.version != buffer.version
|
||||
&& buffer
|
||||
.file()
|
||||
.is_some_and(|file| !file.disk_state().is_deleted())
|
||||
.is_some_and(|file| file.disk_state() != DiskState::Deleted)
|
||||
})
|
||||
.map(|(buffer, _)| buffer)
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ mod legacy_thread;
|
||||
mod native_agent_server;
|
||||
pub mod outline;
|
||||
mod templates;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod thread;
|
||||
mod tools;
|
||||
|
||||
use context_server::ContextServerId;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use db::*;
|
||||
pub use history_store::*;
|
||||
pub use native_agent_server::NativeAgentServer;
|
||||
@@ -18,11 +18,11 @@ pub use templates::*;
|
||||
pub use thread::*;
|
||||
pub use tools::*;
|
||||
|
||||
use acp_thread::{AcpThread, AgentModelSelector, UserMessageId};
|
||||
use acp_thread::{AcpThread, AgentModelSelector};
|
||||
use agent_client_protocol as acp;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::{HashMap, HashSet, IndexMap};
|
||||
use collections::{HashSet, IndexMap};
|
||||
use fs::Fs;
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
use futures::future::Shared;
|
||||
@@ -30,7 +30,7 @@ use futures::{StreamExt, future};
|
||||
use gpui::{
|
||||
App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity,
|
||||
};
|
||||
use language_model::{IconOrSvg, LanguageModel, LanguageModelProvider, LanguageModelRegistry};
|
||||
use language_model::{LanguageModel, LanguageModelProvider, LanguageModelRegistry};
|
||||
use project::{Project, ProjectItem, ProjectPath, Worktree};
|
||||
use prompt_store::{
|
||||
ProjectContext, PromptStore, RULES_FILE_NAMES, RulesFileContext, UserRulesContext,
|
||||
@@ -39,6 +39,7 @@ use prompt_store::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{LanguageModelSelection, update_settings_file};
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
@@ -93,7 +94,7 @@ impl LanguageModels {
|
||||
fn refresh_list(&mut self, cx: &App) {
|
||||
let providers = LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.visible_providers()
|
||||
.providers()
|
||||
.into_iter()
|
||||
.filter(|provider| provider.is_authenticated(cx))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -153,10 +154,7 @@ impl LanguageModels {
|
||||
id: Self::model_id(model),
|
||||
name: model.name().0,
|
||||
description: None,
|
||||
icon: Some(match provider.icon() {
|
||||
IconOrSvg::Svg(path) => acp_thread::AgentModelIcon::Path(path),
|
||||
IconOrSvg::Icon(name) => acp_thread::AgentModelIcon::Named(name),
|
||||
}),
|
||||
icon: Some(provider.icon()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +165,7 @@ impl LanguageModels {
|
||||
fn authenticate_all_language_model_providers(cx: &mut App) -> Task<()> {
|
||||
let authenticate_all_providers = LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.visible_providers()
|
||||
.providers()
|
||||
.iter()
|
||||
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -254,24 +252,12 @@ impl NativeAgent {
|
||||
.await;
|
||||
|
||||
cx.new(|cx| {
|
||||
let context_server_store = project.read(cx).context_server_store();
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(context_server_store.clone(), cx));
|
||||
|
||||
let mut subscriptions = vec![
|
||||
cx.subscribe(&project, Self::handle_project_event),
|
||||
cx.subscribe(
|
||||
&LanguageModelRegistry::global(cx),
|
||||
Self::handle_models_updated_event,
|
||||
),
|
||||
cx.subscribe(
|
||||
&context_server_store,
|
||||
Self::handle_context_server_store_updated,
|
||||
),
|
||||
cx.subscribe(
|
||||
&context_server_registry,
|
||||
Self::handle_context_server_registry_event,
|
||||
),
|
||||
];
|
||||
if let Some(prompt_store) = prompt_store.as_ref() {
|
||||
subscriptions.push(cx.subscribe(prompt_store, Self::handle_prompts_updated_event))
|
||||
@@ -280,14 +266,16 @@ impl NativeAgent {
|
||||
let (project_context_needs_refresh_tx, project_context_needs_refresh_rx) =
|
||||
watch::channel(());
|
||||
Self {
|
||||
sessions: HashMap::default(),
|
||||
sessions: HashMap::new(),
|
||||
history,
|
||||
project_context: cx.new(|_| project_context),
|
||||
project_context_needs_refresh: project_context_needs_refresh_tx,
|
||||
_maintain_project_context: cx.spawn(async move |this, cx| {
|
||||
Self::maintain_project_context(this, project_context_needs_refresh_rx, cx).await
|
||||
}),
|
||||
context_server_registry,
|
||||
context_server_registry: cx.new(|cx| {
|
||||
ContextServerRegistry::new(project.read(cx).context_server_store(), cx)
|
||||
}),
|
||||
templates,
|
||||
models: LanguageModels::new(cx),
|
||||
project,
|
||||
@@ -356,9 +344,6 @@ impl NativeAgent {
|
||||
pending_save: Task::ready(()),
|
||||
},
|
||||
);
|
||||
|
||||
self.update_available_commands(cx);
|
||||
|
||||
acp_thread
|
||||
}
|
||||
|
||||
@@ -429,7 +414,10 @@ impl NativeAgent {
|
||||
.into_iter()
|
||||
.flat_map(|(contents, prompt_metadata)| match contents {
|
||||
Ok(contents) => Some(UserRulesContext {
|
||||
uuid: prompt_metadata.id.as_user()?,
|
||||
uuid: match prompt_metadata.id {
|
||||
prompt_store::PromptId::User { uuid } => uuid,
|
||||
prompt_store::PromptId::EditWorkflow => return None,
|
||||
},
|
||||
title: prompt_metadata.title.map(|title| title.to_string()),
|
||||
contents,
|
||||
}),
|
||||
@@ -623,99 +611,6 @@ impl NativeAgent {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_context_server_store_updated(
|
||||
&mut self,
|
||||
_store: Entity<project::context_server_store::ContextServerStore>,
|
||||
_event: &project::context_server_store::Event,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.update_available_commands(cx);
|
||||
}
|
||||
|
||||
fn handle_context_server_registry_event(
|
||||
&mut self,
|
||||
_registry: Entity<ContextServerRegistry>,
|
||||
event: &ContextServerRegistryEvent,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
ContextServerRegistryEvent::ToolsChanged => {}
|
||||
ContextServerRegistryEvent::PromptsChanged => {
|
||||
self.update_available_commands(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_available_commands(&self, cx: &mut Context<Self>) {
|
||||
let available_commands = self.build_available_commands(cx);
|
||||
for session in self.sessions.values() {
|
||||
if let Some(acp_thread) = session.acp_thread.upgrade() {
|
||||
acp_thread.update(cx, |thread, cx| {
|
||||
thread
|
||||
.handle_session_update(
|
||||
acp::SessionUpdate::AvailableCommandsUpdate(
|
||||
acp::AvailableCommandsUpdate::new(available_commands.clone()),
|
||||
),
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_available_commands(&self, cx: &App) -> Vec<acp::AvailableCommand> {
|
||||
let registry = self.context_server_registry.read(cx);
|
||||
|
||||
let mut prompt_name_counts: HashMap<&str, usize> = HashMap::default();
|
||||
for context_server_prompt in registry.prompts() {
|
||||
*prompt_name_counts
|
||||
.entry(context_server_prompt.prompt.name.as_str())
|
||||
.or_insert(0) += 1;
|
||||
}
|
||||
|
||||
registry
|
||||
.prompts()
|
||||
.flat_map(|context_server_prompt| {
|
||||
let prompt = &context_server_prompt.prompt;
|
||||
|
||||
let should_prefix = prompt_name_counts
|
||||
.get(prompt.name.as_str())
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
> 1;
|
||||
|
||||
let name = if should_prefix {
|
||||
format!("{}.{}", context_server_prompt.server_id, prompt.name)
|
||||
} else {
|
||||
prompt.name.clone()
|
||||
};
|
||||
|
||||
let mut command = acp::AvailableCommand::new(
|
||||
name,
|
||||
prompt.description.clone().unwrap_or_default(),
|
||||
);
|
||||
|
||||
match prompt.arguments.as_deref() {
|
||||
Some([arg]) => {
|
||||
let hint = format!("<{}>", arg.name);
|
||||
|
||||
command = command.input(acp::AvailableCommandInput::Unstructured(
|
||||
acp::UnstructuredCommandInput::new(hint),
|
||||
));
|
||||
}
|
||||
Some([]) | None => {}
|
||||
Some(_) => {
|
||||
// skip >1 argument commands since we don't support them yet
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Some(command)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn load_thread(
|
||||
&mut self,
|
||||
id: acp::SessionId,
|
||||
@@ -814,102 +709,6 @@ impl NativeAgent {
|
||||
history.update(cx, |history, cx| history.reload(cx)).ok();
|
||||
});
|
||||
}
|
||||
|
||||
fn send_mcp_prompt(
|
||||
&self,
|
||||
message_id: UserMessageId,
|
||||
session_id: agent_client_protocol::SessionId,
|
||||
prompt_name: String,
|
||||
server_id: ContextServerId,
|
||||
arguments: HashMap<String, String>,
|
||||
original_content: Vec<acp::ContentBlock>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<acp::PromptResponse>> {
|
||||
let server_store = self.context_server_registry.read(cx).server_store().clone();
|
||||
let path_style = self.project.read(cx).path_style(cx);
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let prompt =
|
||||
crate::get_prompt(&server_store, &server_id, &prompt_name, arguments, cx).await?;
|
||||
|
||||
let (acp_thread, thread) = this.update(cx, |this, _cx| {
|
||||
let session = this
|
||||
.sessions
|
||||
.get(&session_id)
|
||||
.context("Failed to get session")?;
|
||||
anyhow::Ok((session.acp_thread.clone(), session.thread.clone()))
|
||||
})??;
|
||||
|
||||
let mut last_is_user = true;
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.push_acp_user_block(
|
||||
message_id,
|
||||
original_content.into_iter().skip(1),
|
||||
path_style,
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
|
||||
for message in prompt.messages {
|
||||
let context_server::types::PromptMessage { role, content } = message;
|
||||
let block = mcp_message_content_to_acp_content_block(content);
|
||||
|
||||
match role {
|
||||
context_server::types::Role::User => {
|
||||
let id = acp_thread::UserMessageId::new();
|
||||
|
||||
acp_thread.update(cx, |acp_thread, cx| {
|
||||
acp_thread.push_user_content_block_with_indent(
|
||||
Some(id.clone()),
|
||||
block.clone(),
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.push_acp_user_block(id, [block], path_style, cx);
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
}
|
||||
context_server::types::Role::Assistant => {
|
||||
acp_thread.update(cx, |acp_thread, cx| {
|
||||
acp_thread.push_assistant_content_block_with_indent(
|
||||
block.clone(),
|
||||
false,
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.push_acp_agent_block(block, cx);
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
}
|
||||
}
|
||||
|
||||
last_is_user = role == context_server::types::Role::User;
|
||||
}
|
||||
|
||||
let response_stream = thread.update(cx, |thread, cx| {
|
||||
if last_is_user {
|
||||
thread.send_existing(cx)
|
||||
} else {
|
||||
// Resume if MCP prompt did not end with a user message
|
||||
thread.resume(cx)
|
||||
}
|
||||
})??;
|
||||
|
||||
cx.update(|cx| {
|
||||
NativeAgentConnection::handle_thread_events(response_stream, acp_thread, cx)
|
||||
})?
|
||||
.await
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper struct that implements the AgentConnection trait
|
||||
@@ -1044,39 +843,6 @@ impl NativeAgentConnection {
|
||||
}
|
||||
}
|
||||
|
||||
struct Command<'a> {
|
||||
prompt_name: &'a str,
|
||||
arg_value: &'a str,
|
||||
explicit_server_id: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> Command<'a> {
|
||||
fn parse(prompt: &'a [acp::ContentBlock]) -> Option<Self> {
|
||||
let acp::ContentBlock::Text(text_content) = prompt.first()? else {
|
||||
return None;
|
||||
};
|
||||
let text = text_content.text.trim();
|
||||
let command = text.strip_prefix('/')?;
|
||||
let (command, arg_value) = command
|
||||
.split_once(char::is_whitespace)
|
||||
.unwrap_or((command, ""));
|
||||
|
||||
if let Some((server_id, prompt_name)) = command.split_once('.') {
|
||||
Some(Self {
|
||||
prompt_name,
|
||||
arg_value,
|
||||
explicit_server_id: Some(server_id),
|
||||
})
|
||||
} else {
|
||||
Some(Self {
|
||||
prompt_name: command,
|
||||
arg_value,
|
||||
explicit_server_id: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NativeAgentModelSelector {
|
||||
session_id: acp::SessionId,
|
||||
connection: NativeAgentConnection,
|
||||
@@ -1242,47 +1008,6 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||
let session_id = params.session_id.clone();
|
||||
log::info!("Received prompt request for session: {}", session_id);
|
||||
log::debug!("Prompt blocks count: {}", params.prompt.len());
|
||||
|
||||
if let Some(parsed_command) = Command::parse(¶ms.prompt) {
|
||||
let registry = self.0.read(cx).context_server_registry.read(cx);
|
||||
|
||||
let explicit_server_id = parsed_command
|
||||
.explicit_server_id
|
||||
.map(|server_id| ContextServerId(server_id.into()));
|
||||
|
||||
if let Some(prompt) =
|
||||
registry.find_prompt(explicit_server_id.as_ref(), parsed_command.prompt_name)
|
||||
{
|
||||
let arguments = if !parsed_command.arg_value.is_empty()
|
||||
&& let Some(arg_name) = prompt
|
||||
.prompt
|
||||
.arguments
|
||||
.as_ref()
|
||||
.and_then(|args| args.first())
|
||||
.map(|arg| arg.name.clone())
|
||||
{
|
||||
HashMap::from_iter([(arg_name, parsed_command.arg_value.to_string())])
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let prompt_name = prompt.prompt.name.clone();
|
||||
let server_id = prompt.server_id.clone();
|
||||
|
||||
return self.0.update(cx, |agent, cx| {
|
||||
agent.send_mcp_prompt(
|
||||
id,
|
||||
session_id.clone(),
|
||||
prompt_name,
|
||||
server_id,
|
||||
arguments,
|
||||
params.prompt,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
let path_style = self.0.read(cx).project.read(cx).path_style(cx);
|
||||
|
||||
self.run_turn(session_id, cx, move |thread, cx| {
|
||||
@@ -1629,9 +1354,7 @@ mod internal_tests {
|
||||
id: acp::ModelId::new("fake/fake"),
|
||||
name: "Fake".into(),
|
||||
description: None,
|
||||
icon: Some(acp_thread::AgentModelIcon::Named(
|
||||
ui::IconName::ZedAssistant
|
||||
)),
|
||||
icon: Some(ui::IconName::ZedAssistant),
|
||||
}]
|
||||
)])
|
||||
);
|
||||
@@ -1881,35 +1604,3 @@ mod internal_tests {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn mcp_message_content_to_acp_content_block(
|
||||
content: context_server::types::MessageContent,
|
||||
) -> acp::ContentBlock {
|
||||
match content {
|
||||
context_server::types::MessageContent::Text {
|
||||
text,
|
||||
annotations: _,
|
||||
} => text.into(),
|
||||
context_server::types::MessageContent::Image {
|
||||
data,
|
||||
mime_type,
|
||||
annotations: _,
|
||||
} => acp::ContentBlock::Image(acp::ImageContent::new(data, mime_type)),
|
||||
context_server::types::MessageContent::Audio {
|
||||
data,
|
||||
mime_type,
|
||||
annotations: _,
|
||||
} => acp::ContentBlock::Audio(acp::AudioContent::new(data, mime_type)),
|
||||
context_server::types::MessageContent::Resource {
|
||||
resource,
|
||||
annotations: _,
|
||||
} => {
|
||||
let mut link =
|
||||
acp::ResourceLink::new(resource.uri.to_string(), resource.uri.to_string());
|
||||
if let Some(mime_type) = resource.mime_type {
|
||||
link = link.mime_type(mime_type);
|
||||
}
|
||||
acp::ContentBlock::ResourceLink(link)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,10 +216,14 @@ impl HistoryStore {
|
||||
}
|
||||
|
||||
pub fn reload(&self, cx: &mut Context<Self>) {
|
||||
let database_connection = ThreadsDatabase::connect(cx);
|
||||
let database_future = ThreadsDatabase::connect(cx);
|
||||
cx.spawn(async move |this, cx| {
|
||||
let database = database_connection.await;
|
||||
let threads = database.map_err(|err| anyhow!(err))?.list_threads().await?;
|
||||
let threads = database_future
|
||||
.await
|
||||
.map_err(|err| anyhow!(err))?
|
||||
.list_threads()
|
||||
.await?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
if this.recently_opened_entries.len() < MAX_RECENTLY_OPENED_ENTRIES {
|
||||
for thread in threads
|
||||
@@ -340,8 +344,7 @@ impl HistoryStore {
|
||||
fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<VecDeque<HistoryEntryId>>> {
|
||||
cx.background_spawn(async move {
|
||||
if cfg!(any(feature = "test-support", test)) {
|
||||
log::warn!("history store does not persist in tests");
|
||||
return Ok(VecDeque::new());
|
||||
anyhow::bail!("history store does not persist in tests");
|
||||
}
|
||||
let json = KEY_VALUE_STORE
|
||||
.read_kvp(RECENTLY_OPENED_THREADS_KEY)?
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
use std::{any::Any, path::Path, rc::Rc, sync::Arc};
|
||||
|
||||
use agent_client_protocol as acp;
|
||||
use agent_servers::{AgentServer, AgentServerDelegate};
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::Result;
|
||||
use collections::HashSet;
|
||||
use fs::Fs;
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use prompt_store::PromptStore;
|
||||
use settings::{LanguageModelSelection, Settings as _, update_settings_file};
|
||||
|
||||
use crate::{HistoryStore, NativeAgent, NativeAgentConnection, templates::Templates};
|
||||
|
||||
@@ -75,38 +71,6 @@ impl AgentServer for NativeAgentServer {
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
|
||||
self
|
||||
}
|
||||
|
||||
fn favorite_model_ids(&self, cx: &mut App) -> HashSet<acp::ModelId> {
|
||||
AgentSettings::get_global(cx).favorite_model_ids()
|
||||
}
|
||||
|
||||
fn toggle_favorite_model(
|
||||
&self,
|
||||
model_id: acp::ModelId,
|
||||
should_be_favorite: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &App,
|
||||
) {
|
||||
let selection = model_id_to_selection(&model_id);
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
let agent = settings.agent.get_or_insert_default();
|
||||
if should_be_favorite {
|
||||
agent.add_favorite_model(selection.clone());
|
||||
} else {
|
||||
agent.remove_favorite_model(&selection);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a ModelId (e.g. "anthropic/claude-3-5-sonnet") to a LanguageModelSelection.
|
||||
fn model_id_to_selection(model_id: &acp::ModelId) -> LanguageModelSelection {
|
||||
let id = model_id.0.as_ref();
|
||||
let (provider, model) = id.split_once('/').unwrap_or(("", id));
|
||||
LanguageModelSelection {
|
||||
provider: provider.to_owned().into(),
|
||||
model: model.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -2809,181 +2809,3 @@ fn setup_context_server(
|
||||
cx.run_until_parked();
|
||||
mcp_tool_calls_rx
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_tokens_before_message(cx: &mut TestAppContext) {
|
||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
|
||||
// First message
|
||||
let message_1_id = UserMessageId::new();
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(message_1_id.clone(), ["First message"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Before any response, tokens_before_message should return None for first message
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert_eq!(
|
||||
thread.tokens_before_message(&message_1_id),
|
||||
None,
|
||||
"First message should have no tokens before it"
|
||||
);
|
||||
});
|
||||
|
||||
// Complete first message with usage
|
||||
fake_model.send_last_completion_stream_text_chunk("Response 1");
|
||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
|
||||
language_model::TokenUsage {
|
||||
input_tokens: 100,
|
||||
output_tokens: 50,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
},
|
||||
));
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
// First message still has no tokens before it
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert_eq!(
|
||||
thread.tokens_before_message(&message_1_id),
|
||||
None,
|
||||
"First message should still have no tokens before it after response"
|
||||
);
|
||||
});
|
||||
|
||||
// Second message
|
||||
let message_2_id = UserMessageId::new();
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(message_2_id.clone(), ["Second message"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Second message should have first message's input tokens before it
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert_eq!(
|
||||
thread.tokens_before_message(&message_2_id),
|
||||
Some(100),
|
||||
"Second message should have 100 tokens before it (from first request)"
|
||||
);
|
||||
});
|
||||
|
||||
// Complete second message
|
||||
fake_model.send_last_completion_stream_text_chunk("Response 2");
|
||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
|
||||
language_model::TokenUsage {
|
||||
input_tokens: 250, // Total for this request (includes previous context)
|
||||
output_tokens: 75,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
},
|
||||
));
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Third message
|
||||
let message_3_id = UserMessageId::new();
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(message_3_id.clone(), ["Third message"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Third message should have second message's input tokens (250) before it
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert_eq!(
|
||||
thread.tokens_before_message(&message_3_id),
|
||||
Some(250),
|
||||
"Third message should have 250 tokens before it (from second request)"
|
||||
);
|
||||
// Second message should still have 100
|
||||
assert_eq!(
|
||||
thread.tokens_before_message(&message_2_id),
|
||||
Some(100),
|
||||
"Second message should still have 100 tokens before it"
|
||||
);
|
||||
// First message still has none
|
||||
assert_eq!(
|
||||
thread.tokens_before_message(&message_1_id),
|
||||
None,
|
||||
"First message should still have no tokens before it"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_tokens_before_message_after_truncate(cx: &mut TestAppContext) {
|
||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
|
||||
// Set up three messages with responses
|
||||
let message_1_id = UserMessageId::new();
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(message_1_id.clone(), ["Message 1"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
fake_model.send_last_completion_stream_text_chunk("Response 1");
|
||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
|
||||
language_model::TokenUsage {
|
||||
input_tokens: 100,
|
||||
output_tokens: 50,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
},
|
||||
));
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
let message_2_id = UserMessageId::new();
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(message_2_id.clone(), ["Message 2"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
fake_model.send_last_completion_stream_text_chunk("Response 2");
|
||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
|
||||
language_model::TokenUsage {
|
||||
input_tokens: 250,
|
||||
output_tokens: 75,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
},
|
||||
));
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Verify initial state
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert_eq!(thread.tokens_before_message(&message_2_id), Some(100));
|
||||
});
|
||||
|
||||
// Truncate at message 2 (removes message 2 and everything after)
|
||||
thread
|
||||
.update(cx, |thread, cx| thread.truncate(message_2_id.clone(), cx))
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
// After truncation, message_2_id no longer exists, so lookup should return None
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert_eq!(
|
||||
thread.tokens_before_message(&message_2_id),
|
||||
None,
|
||||
"After truncation, message 2 no longer exists"
|
||||
);
|
||||
// Message 1 still exists but has no tokens before it
|
||||
assert_eq!(
|
||||
thread.tokens_before_message(&message_1_id),
|
||||
None,
|
||||
"First message still has no tokens before it"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -108,13 +108,7 @@ impl Message {
|
||||
|
||||
pub fn to_request(&self) -> Vec<LanguageModelRequestMessage> {
|
||||
match self {
|
||||
Message::User(message) => {
|
||||
if message.content.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
vec![message.to_request()]
|
||||
}
|
||||
}
|
||||
Message::User(message) => vec![message.to_request()],
|
||||
Message::Agent(message) => message.to_request(),
|
||||
Message::Resume => vec![LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
@@ -1095,28 +1089,6 @@ impl Thread {
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the total input token count as of the message before the given message.
|
||||
///
|
||||
/// Returns `None` if:
|
||||
/// - `target_id` is the first message (no previous message)
|
||||
/// - The previous message hasn't received a response yet (no usage data)
|
||||
/// - `target_id` is not found in the messages
|
||||
pub fn tokens_before_message(&self, target_id: &UserMessageId) -> Option<u64> {
|
||||
let mut previous_user_message_id: Option<&UserMessageId> = None;
|
||||
|
||||
for message in &self.messages {
|
||||
if let Message::User(user_msg) = message {
|
||||
if &user_msg.id == target_id {
|
||||
let prev_id = previous_user_message_id?;
|
||||
let usage = self.request_token_usage.get(prev_id)?;
|
||||
return Some(usage.input_tokens);
|
||||
}
|
||||
previous_user_message_id = Some(&user_msg.id);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Look up the active profile and resolve its preferred model if one is configured.
|
||||
fn resolve_profile_model(
|
||||
profile_id: &AgentProfileId,
|
||||
@@ -1169,6 +1141,11 @@ impl Thread {
|
||||
where
|
||||
T: Into<UserMessageContent>,
|
||||
{
|
||||
let model = self.model().context("No language model configured")?;
|
||||
|
||||
log::info!("Thread::send called with model: {}", model.name().0);
|
||||
self.advance_prompt_id();
|
||||
|
||||
let content = content.into_iter().map(Into::into).collect::<Vec<_>>();
|
||||
log::debug!("Thread::send content: {:?}", content);
|
||||
|
||||
@@ -1176,59 +1153,10 @@ impl Thread {
|
||||
.push(Message::User(UserMessage { id, content }));
|
||||
cx.notify();
|
||||
|
||||
self.send_existing(cx)
|
||||
}
|
||||
|
||||
pub fn send_existing(
|
||||
&mut self,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>> {
|
||||
let model = self.model().context("No language model configured")?;
|
||||
|
||||
log::info!("Thread::send called with model: {}", model.name().0);
|
||||
self.advance_prompt_id();
|
||||
|
||||
log::debug!("Total messages in thread: {}", self.messages.len());
|
||||
self.run_turn(cx)
|
||||
}
|
||||
|
||||
pub fn push_acp_user_block(
|
||||
&mut self,
|
||||
id: UserMessageId,
|
||||
blocks: impl IntoIterator<Item = acp::ContentBlock>,
|
||||
path_style: PathStyle,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let content = blocks
|
||||
.into_iter()
|
||||
.map(|block| UserMessageContent::from_content_block(block, path_style))
|
||||
.collect::<Vec<_>>();
|
||||
self.messages
|
||||
.push(Message::User(UserMessage { id, content }));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn push_acp_agent_block(&mut self, block: acp::ContentBlock, cx: &mut Context<Self>) {
|
||||
let text = match block {
|
||||
acp::ContentBlock::Text(text_content) => text_content.text,
|
||||
acp::ContentBlock::Image(_) => "[image]".to_string(),
|
||||
acp::ContentBlock::Audio(_) => "[audio]".to_string(),
|
||||
acp::ContentBlock::ResourceLink(resource_link) => resource_link.uri,
|
||||
acp::ContentBlock::Resource(resource) => match resource.resource {
|
||||
acp::EmbeddedResourceResource::TextResourceContents(resource) => resource.uri,
|
||||
acp::EmbeddedResourceResource::BlobResourceContents(resource) => resource.uri,
|
||||
_ => "[resource]".to_string(),
|
||||
},
|
||||
_ => "[unknown]".to_string(),
|
||||
};
|
||||
|
||||
self.messages.push(Message::Agent(AgentMessage {
|
||||
content: vec![AgentMessageContent::Text(text)],
|
||||
..Default::default()
|
||||
}));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
#[cfg(feature = "eval")]
|
||||
pub fn proceed(
|
||||
&mut self,
|
||||
@@ -1725,10 +1653,6 @@ impl Thread {
|
||||
self.pending_summary_generation.is_some()
|
||||
}
|
||||
|
||||
pub fn is_generating_title(&self) -> bool {
|
||||
self.pending_title_generation.is_some()
|
||||
}
|
||||
|
||||
pub fn summary(&mut self, cx: &mut Context<Self>) -> Shared<Task<Option<SharedString>>> {
|
||||
if let Some(summary) = self.summary.as_ref() {
|
||||
return Task::ready(Some(summary.clone())).shared();
|
||||
@@ -1796,7 +1720,7 @@ impl Thread {
|
||||
task
|
||||
}
|
||||
|
||||
pub fn generate_title(&mut self, cx: &mut Context<Self>) {
|
||||
fn generate_title(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(model) = self.summarization_model.clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -2,24 +2,12 @@ use crate::{AgentToolOutput, AnyAgentTool, ToolCallEventStream};
|
||||
use agent_client_protocol::ToolKind;
|
||||
use anyhow::{Result, anyhow, bail};
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use context_server::{ContextServerId, client::NotificationSubscription};
|
||||
use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task};
|
||||
use context_server::ContextServerId;
|
||||
use gpui::{App, Context, Entity, SharedString, Task};
|
||||
use project::context_server_store::{ContextServerStatus, ContextServerStore};
|
||||
use std::sync::Arc;
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct ContextServerPrompt {
|
||||
pub server_id: ContextServerId,
|
||||
pub prompt: context_server::types::Prompt,
|
||||
}
|
||||
|
||||
pub enum ContextServerRegistryEvent {
|
||||
ToolsChanged,
|
||||
PromptsChanged,
|
||||
}
|
||||
|
||||
impl EventEmitter<ContextServerRegistryEvent> for ContextServerRegistry {}
|
||||
|
||||
pub struct ContextServerRegistry {
|
||||
server_store: Entity<ContextServerStore>,
|
||||
registered_servers: HashMap<ContextServerId, RegisteredContextServer>,
|
||||
@@ -28,10 +16,7 @@ pub struct ContextServerRegistry {
|
||||
|
||||
struct RegisteredContextServer {
|
||||
tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
|
||||
prompts: BTreeMap<SharedString, ContextServerPrompt>,
|
||||
load_tools: Task<Result<()>>,
|
||||
load_prompts: Task<Result<()>>,
|
||||
_tools_updated_subscription: Option<NotificationSubscription>,
|
||||
}
|
||||
|
||||
impl ContextServerRegistry {
|
||||
@@ -43,7 +28,6 @@ impl ContextServerRegistry {
|
||||
};
|
||||
for server in server_store.read(cx).running_servers() {
|
||||
this.reload_tools_for_server(server.id(), cx);
|
||||
this.reload_prompts_for_server(server.id(), cx);
|
||||
}
|
||||
this
|
||||
}
|
||||
@@ -72,88 +56,6 @@ impl ContextServerRegistry {
|
||||
.map(|(id, server)| (id, &server.tools))
|
||||
}
|
||||
|
||||
pub fn prompts(&self) -> impl Iterator<Item = &ContextServerPrompt> {
|
||||
self.registered_servers
|
||||
.values()
|
||||
.flat_map(|server| server.prompts.values())
|
||||
}
|
||||
|
||||
pub fn find_prompt(
|
||||
&self,
|
||||
server_id: Option<&ContextServerId>,
|
||||
name: &str,
|
||||
) -> Option<&ContextServerPrompt> {
|
||||
if let Some(server_id) = server_id {
|
||||
self.registered_servers
|
||||
.get(server_id)
|
||||
.and_then(|server| server.prompts.get(name))
|
||||
} else {
|
||||
self.registered_servers
|
||||
.values()
|
||||
.find_map(|server| server.prompts.get(name))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn server_store(&self) -> &Entity<ContextServerStore> {
|
||||
&self.server_store
|
||||
}
|
||||
|
||||
fn get_or_register_server(
|
||||
&mut self,
|
||||
server_id: &ContextServerId,
|
||||
cx: &mut Context<Self>,
|
||||
) -> &mut RegisteredContextServer {
|
||||
self.registered_servers
|
||||
.entry(server_id.clone())
|
||||
.or_insert_with(|| Self::init_registered_server(server_id, &self.server_store, cx))
|
||||
}
|
||||
|
||||
fn init_registered_server(
|
||||
server_id: &ContextServerId,
|
||||
server_store: &Entity<ContextServerStore>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> RegisteredContextServer {
|
||||
let tools_updated_subscription = server_store
|
||||
.read(cx)
|
||||
.get_running_server(server_id)
|
||||
.and_then(|server| {
|
||||
let client = server.client()?;
|
||||
|
||||
if !client.capable(context_server::protocol::ServerCapability::Tools) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let server_id = server.id();
|
||||
let this = cx.entity().downgrade();
|
||||
|
||||
Some(client.on_notification(
|
||||
"notifications/tools/list_changed",
|
||||
Box::new(move |_params, cx: AsyncApp| {
|
||||
let server_id = server_id.clone();
|
||||
let this = this.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
log::info!(
|
||||
"Received tools/list_changed notification for server {}",
|
||||
server_id
|
||||
);
|
||||
this.reload_tools_for_server(server_id, cx);
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
}),
|
||||
))
|
||||
});
|
||||
|
||||
RegisteredContextServer {
|
||||
tools: BTreeMap::default(),
|
||||
prompts: BTreeMap::default(),
|
||||
load_tools: Task::ready(Ok(())),
|
||||
load_prompts: Task::ready(Ok(())),
|
||||
_tools_updated_subscription: tools_updated_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
fn reload_tools_for_server(&mut self, server_id: ContextServerId, cx: &mut Context<Self>) {
|
||||
let Some(server) = self.server_store.read(cx).get_running_server(&server_id) else {
|
||||
return;
|
||||
@@ -161,12 +63,17 @@ impl ContextServerRegistry {
|
||||
let Some(client) = server.client() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !client.capable(context_server::protocol::ServerCapability::Tools) {
|
||||
return;
|
||||
}
|
||||
|
||||
let registered_server = self.get_or_register_server(&server_id, cx);
|
||||
let registered_server =
|
||||
self.registered_servers
|
||||
.entry(server_id.clone())
|
||||
.or_insert(RegisteredContextServer {
|
||||
tools: BTreeMap::default(),
|
||||
load_tools: Task::ready(Ok(())),
|
||||
});
|
||||
registered_server.load_tools = cx.spawn(async move |this, cx| {
|
||||
let response = client
|
||||
.request::<context_server::types::requests::ListTools>(())
|
||||
@@ -187,49 +94,6 @@ impl ContextServerRegistry {
|
||||
));
|
||||
registered_server.tools.insert(tool.name(), tool);
|
||||
}
|
||||
cx.emit(ContextServerRegistryEvent::ToolsChanged);
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn reload_prompts_for_server(&mut self, server_id: ContextServerId, cx: &mut Context<Self>) {
|
||||
let Some(server) = self.server_store.read(cx).get_running_server(&server_id) else {
|
||||
return;
|
||||
};
|
||||
let Some(client) = server.client() else {
|
||||
return;
|
||||
};
|
||||
if !client.capable(context_server::protocol::ServerCapability::Prompts) {
|
||||
return;
|
||||
}
|
||||
|
||||
let registered_server = self.get_or_register_server(&server_id, cx);
|
||||
|
||||
registered_server.load_prompts = cx.spawn(async move |this, cx| {
|
||||
let response = client
|
||||
.request::<context_server::types::requests::PromptsList>(())
|
||||
.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
let Some(registered_server) = this.registered_servers.get_mut(&server_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
registered_server.prompts.clear();
|
||||
if let Some(response) = response.log_err() {
|
||||
for prompt in response.prompts {
|
||||
let name: SharedString = prompt.name.clone().into();
|
||||
registered_server.prompts.insert(
|
||||
name,
|
||||
ContextServerPrompt {
|
||||
server_id: server_id.clone(),
|
||||
prompt,
|
||||
},
|
||||
);
|
||||
}
|
||||
cx.emit(ContextServerRegistryEvent::PromptsChanged);
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
@@ -248,17 +112,9 @@ impl ContextServerRegistry {
|
||||
ContextServerStatus::Starting => {}
|
||||
ContextServerStatus::Running => {
|
||||
self.reload_tools_for_server(server_id.clone(), cx);
|
||||
self.reload_prompts_for_server(server_id.clone(), cx);
|
||||
}
|
||||
ContextServerStatus::Stopped | ContextServerStatus::Error(_) => {
|
||||
if let Some(registered_server) = self.registered_servers.remove(server_id) {
|
||||
if !registered_server.tools.is_empty() {
|
||||
cx.emit(ContextServerRegistryEvent::ToolsChanged);
|
||||
}
|
||||
if !registered_server.prompts.is_empty() {
|
||||
cx.emit(ContextServerRegistryEvent::PromptsChanged);
|
||||
}
|
||||
}
|
||||
self.registered_servers.remove(server_id);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -395,39 +251,3 @@ impl AnyAgentTool for ContextServerTool {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_prompt(
|
||||
server_store: &Entity<ContextServerStore>,
|
||||
server_id: &ContextServerId,
|
||||
prompt_name: &str,
|
||||
arguments: HashMap<String, String>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Task<Result<context_server::types::PromptsGetResponse>> {
|
||||
let server = match cx.update(|cx| server_store.read(cx).get_running_server(server_id)) {
|
||||
Ok(server) => server,
|
||||
Err(error) => return Task::ready(Err(error)),
|
||||
};
|
||||
let Some(server) = server else {
|
||||
return Task::ready(Err(anyhow::anyhow!("Context server not found")));
|
||||
};
|
||||
|
||||
let Some(protocol) = server.client() else {
|
||||
return Task::ready(Err(anyhow::anyhow!("Context server not initialized")));
|
||||
};
|
||||
|
||||
let prompt_name = prompt_name.to_string();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let response = protocol
|
||||
.request::<context_server::types::requests::PromptsGet>(
|
||||
context_server::types::PromptsGetParams {
|
||||
name: prompt_name,
|
||||
arguments: (!arguments.is_empty()).then(|| arguments),
|
||||
meta: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(response)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ mod codex;
|
||||
mod custom;
|
||||
mod gemini;
|
||||
|
||||
use collections::HashSet;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod e2e_tests;
|
||||
|
||||
@@ -58,19 +56,9 @@ impl AgentServerDelegate {
|
||||
pub trait AgentServer: Send {
|
||||
fn logo(&self) -> ui::IconName;
|
||||
fn name(&self) -> SharedString;
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: Option<&Path>,
|
||||
delegate: AgentServerDelegate,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>>;
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
|
||||
|
||||
fn default_mode(&self, _cx: &mut App) -> Option<agent_client_protocol::SessionModeId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_default_mode(
|
||||
&self,
|
||||
_mode_id: Option<agent_client_protocol::SessionModeId>,
|
||||
@@ -91,18 +79,14 @@ pub trait AgentServer: Send {
|
||||
) {
|
||||
}
|
||||
|
||||
fn favorite_model_ids(&self, _cx: &mut App) -> HashSet<agent_client_protocol::ModelId> {
|
||||
HashSet::default()
|
||||
}
|
||||
|
||||
fn toggle_favorite_model(
|
||||
fn connect(
|
||||
&self,
|
||||
_model_id: agent_client_protocol::ModelId,
|
||||
_should_be_favorite: bool,
|
||||
_fs: Arc<dyn Fs>,
|
||||
_cx: &App,
|
||||
) {
|
||||
}
|
||||
root_dir: Option<&Path>,
|
||||
delegate: AgentServerDelegate,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>>;
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
|
||||
}
|
||||
|
||||
impl dyn AgentServer {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use agent_client_protocol as acp;
|
||||
use collections::HashSet;
|
||||
use fs::Fs;
|
||||
use settings::{SettingsStore, update_settings_file};
|
||||
use std::path::Path;
|
||||
@@ -73,48 +72,6 @@ impl AgentServer for ClaudeCode {
|
||||
});
|
||||
}
|
||||
|
||||
fn favorite_model_ids(&self, cx: &mut App) -> HashSet<acp::ModelId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).claude.clone()
|
||||
});
|
||||
|
||||
settings
|
||||
.as_ref()
|
||||
.map(|s| {
|
||||
s.favorite_models
|
||||
.iter()
|
||||
.map(|id| acp::ModelId::new(id.clone()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn toggle_favorite_model(
|
||||
&self,
|
||||
model_id: acp::ModelId,
|
||||
should_be_favorite: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &App,
|
||||
) {
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
let favorite_models = &mut settings
|
||||
.agent_servers
|
||||
.get_or_insert_default()
|
||||
.claude
|
||||
.get_or_insert_default()
|
||||
.favorite_models;
|
||||
|
||||
let model_id_str = model_id.to_string();
|
||||
if should_be_favorite {
|
||||
if !favorite_models.contains(&model_id_str) {
|
||||
favorite_models.push(model_id_str);
|
||||
}
|
||||
} else {
|
||||
favorite_models.retain(|id| id != &model_id_str);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: Option<&Path>,
|
||||
|
||||
@@ -5,7 +5,6 @@ use std::{any::Any, path::Path};
|
||||
use acp_thread::AgentConnection;
|
||||
use agent_client_protocol as acp;
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::HashSet;
|
||||
use fs::Fs;
|
||||
use gpui::{App, AppContext as _, SharedString, Task};
|
||||
use project::agent_server_store::{AllAgentServersSettings, CODEX_NAME};
|
||||
@@ -74,48 +73,6 @@ impl AgentServer for Codex {
|
||||
});
|
||||
}
|
||||
|
||||
fn favorite_model_ids(&self, cx: &mut App) -> HashSet<acp::ModelId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).codex.clone()
|
||||
});
|
||||
|
||||
settings
|
||||
.as_ref()
|
||||
.map(|s| {
|
||||
s.favorite_models
|
||||
.iter()
|
||||
.map(|id| acp::ModelId::new(id.clone()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn toggle_favorite_model(
|
||||
&self,
|
||||
model_id: acp::ModelId,
|
||||
should_be_favorite: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &App,
|
||||
) {
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
let favorite_models = &mut settings
|
||||
.agent_servers
|
||||
.get_or_insert_default()
|
||||
.codex
|
||||
.get_or_insert_default()
|
||||
.favorite_models;
|
||||
|
||||
let model_id_str = model_id.to_string();
|
||||
if should_be_favorite {
|
||||
if !favorite_models.contains(&model_id_str) {
|
||||
favorite_models.push(model_id_str);
|
||||
}
|
||||
} else {
|
||||
favorite_models.retain(|id| id != &model_id_str);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: Option<&Path>,
|
||||
|
||||
@@ -2,7 +2,6 @@ use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
|
||||
use acp_thread::AgentConnection;
|
||||
use agent_client_protocol as acp;
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::HashSet;
|
||||
use fs::Fs;
|
||||
use gpui::{App, AppContext as _, SharedString, Task};
|
||||
use project::agent_server_store::{AllAgentServersSettings, ExternalAgentServerName};
|
||||
@@ -55,7 +54,6 @@ impl AgentServer for CustomAgentServer {
|
||||
.or_insert_with(|| settings::CustomAgentServerSettings::Extension {
|
||||
default_model: None,
|
||||
default_mode: None,
|
||||
favorite_models: Vec::new(),
|
||||
});
|
||||
|
||||
match settings {
|
||||
@@ -92,7 +90,6 @@ impl AgentServer for CustomAgentServer {
|
||||
.or_insert_with(|| settings::CustomAgentServerSettings::Extension {
|
||||
default_model: None,
|
||||
default_mode: None,
|
||||
favorite_models: Vec::new(),
|
||||
});
|
||||
|
||||
match settings {
|
||||
@@ -104,66 +101,6 @@ impl AgentServer for CustomAgentServer {
|
||||
});
|
||||
}
|
||||
|
||||
fn favorite_model_ids(&self, cx: &mut App) -> HashSet<acp::ModelId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
.custom
|
||||
.get(&self.name())
|
||||
.cloned()
|
||||
});
|
||||
|
||||
settings
|
||||
.as_ref()
|
||||
.map(|s| {
|
||||
s.favorite_models()
|
||||
.iter()
|
||||
.map(|id| acp::ModelId::new(id.clone()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn toggle_favorite_model(
|
||||
&self,
|
||||
model_id: acp::ModelId,
|
||||
should_be_favorite: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &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,
|
||||
favorite_models: Vec::new(),
|
||||
});
|
||||
|
||||
let favorite_models = match settings {
|
||||
settings::CustomAgentServerSettings::Custom {
|
||||
favorite_models, ..
|
||||
}
|
||||
| settings::CustomAgentServerSettings::Extension {
|
||||
favorite_models, ..
|
||||
} => favorite_models,
|
||||
};
|
||||
|
||||
let model_id_str = model_id.to_string();
|
||||
if should_be_favorite {
|
||||
if !favorite_models.contains(&model_id_str) {
|
||||
favorite_models.push(model_id_str);
|
||||
}
|
||||
} else {
|
||||
favorite_models.retain(|id| id != &model_id_str);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: Option<&Path>,
|
||||
|
||||
@@ -460,7 +460,6 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
|
||||
ignore_system_version: None,
|
||||
default_mode: None,
|
||||
default_model: None,
|
||||
favorite_models: vec![],
|
||||
}),
|
||||
gemini: Some(crate::gemini::tests::local_command().into()),
|
||||
codex: Some(BuiltinAgentServerSettings {
|
||||
@@ -470,7 +469,6 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
|
||||
ignore_system_version: None,
|
||||
default_mode: None,
|
||||
default_model: None,
|
||||
favorite_models: vec![],
|
||||
}),
|
||||
custom: collections::HashMap::default(),
|
||||
},
|
||||
|
||||
@@ -12,7 +12,6 @@ workspace = true
|
||||
path = "src/agent_settings.rs"
|
||||
|
||||
[dependencies]
|
||||
agent-client-protocol.workspace = true
|
||||
anyhow.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
|
||||
@@ -2,8 +2,7 @@ mod agent_profile;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent_client_protocol::ModelId;
|
||||
use collections::{HashSet, IndexMap};
|
||||
use collections::IndexMap;
|
||||
use gpui::{App, Pixels, px};
|
||||
use language_model::LanguageModel;
|
||||
use project::DisableAiSettings;
|
||||
@@ -34,7 +33,6 @@ pub struct AgentSettings {
|
||||
pub commit_message_model: Option<LanguageModelSelection>,
|
||||
pub thread_summary_model: Option<LanguageModelSelection>,
|
||||
pub inline_alternatives: Vec<LanguageModelSelection>,
|
||||
pub favorite_models: Vec<LanguageModelSelection>,
|
||||
pub default_profile: AgentProfileId,
|
||||
pub default_view: DefaultAgentView,
|
||||
pub profiles: IndexMap<AgentProfileId, AgentProfileSettings>,
|
||||
@@ -98,13 +96,6 @@ impl AgentSettings {
|
||||
pub fn set_message_editor_max_lines(&self) -> usize {
|
||||
self.message_editor_min_lines * 2
|
||||
}
|
||||
|
||||
pub fn favorite_model_ids(&self) -> HashSet<ModelId> {
|
||||
self.favorite_models
|
||||
.iter()
|
||||
.map(|sel| ModelId::new(format!("{}/{}", sel.provider.0, sel.model)))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
|
||||
@@ -173,7 +164,6 @@ impl Settings for AgentSettings {
|
||||
commit_message_model: agent.commit_message_model,
|
||||
thread_summary_model: agent.thread_summary_model,
|
||||
inline_alternatives: agent.inline_alternatives.unwrap_or_default(),
|
||||
favorite_models: agent.favorite_models,
|
||||
default_profile: AgentProfileId(agent.default_profile.unwrap()),
|
||||
default_view: agent.default_view.unwrap(),
|
||||
profiles: agent
|
||||
|
||||
@@ -13,7 +13,7 @@ path = "src/agent_ui.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["assistant_text_thread/test-support", "eval_utils", "gpui/test-support", "language/test-support", "reqwest_client", "workspace/test-support", "agent/test-support"]
|
||||
test-support = ["assistant_text_thread/test-support", "eval_utils", "gpui/test-support", "language/test-support", "reqwest_client", "workspace/test-support"]
|
||||
unit-eval = []
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -31,10 +31,10 @@ use rope::Point;
|
||||
use settings::Settings;
|
||||
use std::{cell::RefCell, fmt::Write, rc::Rc, sync::Arc};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{ContextMenu, prelude::*};
|
||||
use ui::prelude::*;
|
||||
use util::{ResultExt, debug_panic};
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_actions::agent::{Chat, PasteRaw};
|
||||
use zed_actions::agent::Chat;
|
||||
|
||||
pub struct MessageEditor {
|
||||
mention_set: Entity<MentionSet>,
|
||||
@@ -132,21 +132,6 @@ impl MessageEditor {
|
||||
placement: Some(ContextMenuPlacement::Above),
|
||||
});
|
||||
editor.register_addon(MessageEditorAddon::new());
|
||||
|
||||
editor.set_custom_context_menu(|editor, _point, window, cx| {
|
||||
let has_selection = editor.has_non_empty_selection(&editor.display_snapshot(cx));
|
||||
|
||||
Some(ContextMenu::build(window, cx, |menu, _, _| {
|
||||
menu.action("Cut", Box::new(editor::actions::Cut))
|
||||
.action_disabled_when(
|
||||
!has_selection,
|
||||
"Copy",
|
||||
Box::new(editor::actions::Copy),
|
||||
)
|
||||
.action("Paste", Box::new(editor::actions::Paste))
|
||||
}))
|
||||
});
|
||||
|
||||
editor
|
||||
});
|
||||
let mention_set =
|
||||
@@ -558,9 +543,6 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let editor_clipboard_selections = cx
|
||||
.read_from_clipboard()
|
||||
.and_then(|item| item.entries().first().cloned())
|
||||
@@ -571,127 +553,133 @@ impl MessageEditor {
|
||||
_ => None,
|
||||
});
|
||||
|
||||
// Insert creases for pasted clipboard selections that:
|
||||
// 1. Contain exactly one selection
|
||||
// 2. Have an associated file path
|
||||
// 3. Span multiple lines (not single-line selections)
|
||||
// 4. Belong to a file that exists in the current project
|
||||
let should_insert_creases = util::maybe!({
|
||||
let selections = editor_clipboard_selections.as_ref()?;
|
||||
if selections.len() > 1 {
|
||||
return Some(false);
|
||||
}
|
||||
let selection = selections.first()?;
|
||||
let file_path = selection.file_path.as_ref()?;
|
||||
let line_range = selection.line_range.as_ref()?;
|
||||
let has_file_context = editor_clipboard_selections
|
||||
.as_ref()
|
||||
.is_some_and(|selections| {
|
||||
selections
|
||||
.iter()
|
||||
.any(|sel| sel.file_path.is_some() && sel.line_range.is_some())
|
||||
});
|
||||
|
||||
if line_range.start() == line_range.end() {
|
||||
return Some(false);
|
||||
}
|
||||
|
||||
Some(
|
||||
workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.project_path_for_absolute_path(file_path, cx)
|
||||
.is_some(),
|
||||
)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
if should_insert_creases && let Some(selections) = editor_clipboard_selections {
|
||||
cx.stop_propagation();
|
||||
let insertion_target = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.selections
|
||||
.newest_anchor()
|
||||
.start
|
||||
.text_anchor;
|
||||
|
||||
let project = workspace.read(cx).project().clone();
|
||||
for selection in selections {
|
||||
if let (Some(file_path), Some(line_range)) =
|
||||
(selection.file_path, selection.line_range)
|
||||
{
|
||||
let crease_text =
|
||||
acp_thread::selection_name(Some(file_path.as_ref()), &line_range);
|
||||
|
||||
let mention_uri = MentionUri::Selection {
|
||||
abs_path: Some(file_path.clone()),
|
||||
line_range: line_range.clone(),
|
||||
};
|
||||
|
||||
let mention_text = mention_uri.as_link().to_string();
|
||||
let (excerpt_id, text_anchor, content_len) =
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let buffer = editor.buffer().read(cx);
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
let (excerpt_id, _, buffer_snapshot) = snapshot.as_singleton().unwrap();
|
||||
let text_anchor = insertion_target.bias_left(&buffer_snapshot);
|
||||
|
||||
editor.insert(&mention_text, window, cx);
|
||||
editor.insert(" ", window, cx);
|
||||
|
||||
(*excerpt_id, text_anchor, mention_text.len())
|
||||
});
|
||||
|
||||
let Some((crease_id, tx)) = insert_crease_for_mention(
|
||||
excerpt_id,
|
||||
text_anchor,
|
||||
content_len,
|
||||
crease_text.into(),
|
||||
mention_uri.icon_path(cx),
|
||||
None,
|
||||
self.editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
drop(tx);
|
||||
|
||||
let mention_task = cx
|
||||
.spawn({
|
||||
let project = project.clone();
|
||||
async move |_, cx| {
|
||||
let project_path = project
|
||||
.update(cx, |project, cx| {
|
||||
project.project_path_for_absolute_path(&file_path, cx)
|
||||
})
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| "project path not found".to_string())?;
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx))
|
||||
.map_err(|e| e.to_string())?
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
buffer
|
||||
.update(cx, |buffer, cx| {
|
||||
let start = Point::new(*line_range.start(), 0)
|
||||
.min(buffer.max_point());
|
||||
let end = Point::new(*line_range.end() + 1, 0)
|
||||
.min(buffer.max_point());
|
||||
let content = buffer.text_for_range(start..end).collect();
|
||||
Mention::Text {
|
||||
content,
|
||||
tracked_buffers: vec![cx.entity()],
|
||||
}
|
||||
})
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
})
|
||||
.shared();
|
||||
|
||||
self.mention_set.update(cx, |mention_set, _cx| {
|
||||
mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task)
|
||||
});
|
||||
if has_file_context {
|
||||
if let Some((workspace, selections)) =
|
||||
self.workspace.upgrade().zip(editor_clipboard_selections)
|
||||
{
|
||||
let Some(first_selection) = selections.first() else {
|
||||
return;
|
||||
};
|
||||
if let Some(file_path) = &first_selection.file_path {
|
||||
// In case someone pastes selections from another window
|
||||
// with a different project, we don't want to insert the
|
||||
// crease (containing the absolute path) since the agent
|
||||
// cannot access files outside the project.
|
||||
let is_in_project = workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.project_path_for_absolute_path(file_path, cx)
|
||||
.is_some();
|
||||
if !is_in_project {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
cx.stop_propagation();
|
||||
let insertion_target = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.selections
|
||||
.newest_anchor()
|
||||
.start
|
||||
.text_anchor;
|
||||
|
||||
let project = workspace.read(cx).project().clone();
|
||||
for selection in selections {
|
||||
if let (Some(file_path), Some(line_range)) =
|
||||
(selection.file_path, selection.line_range)
|
||||
{
|
||||
let crease_text =
|
||||
acp_thread::selection_name(Some(file_path.as_ref()), &line_range);
|
||||
|
||||
let mention_uri = MentionUri::Selection {
|
||||
abs_path: Some(file_path.clone()),
|
||||
line_range: line_range.clone(),
|
||||
};
|
||||
|
||||
let mention_text = mention_uri.as_link().to_string();
|
||||
let (excerpt_id, text_anchor, content_len) =
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let buffer = editor.buffer().read(cx);
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
let (excerpt_id, _, buffer_snapshot) =
|
||||
snapshot.as_singleton().unwrap();
|
||||
let text_anchor = insertion_target.bias_left(&buffer_snapshot);
|
||||
|
||||
editor.insert(&mention_text, window, cx);
|
||||
editor.insert(" ", window, cx);
|
||||
|
||||
(*excerpt_id, text_anchor, mention_text.len())
|
||||
});
|
||||
|
||||
let Some((crease_id, tx)) = insert_crease_for_mention(
|
||||
excerpt_id,
|
||||
text_anchor,
|
||||
content_len,
|
||||
crease_text.into(),
|
||||
mention_uri.icon_path(cx),
|
||||
None,
|
||||
self.editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
drop(tx);
|
||||
|
||||
let mention_task = cx
|
||||
.spawn({
|
||||
let project = project.clone();
|
||||
async move |_, cx| {
|
||||
let project_path = project
|
||||
.update(cx, |project, cx| {
|
||||
project.project_path_for_absolute_path(&file_path, cx)
|
||||
})
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| "project path not found".to_string())?;
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer(project_path, cx)
|
||||
})
|
||||
.map_err(|e| e.to_string())?
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
buffer
|
||||
.update(cx, |buffer, cx| {
|
||||
let start = Point::new(*line_range.start(), 0)
|
||||
.min(buffer.max_point());
|
||||
let end = Point::new(*line_range.end() + 1, 0)
|
||||
.min(buffer.max_point());
|
||||
let content =
|
||||
buffer.text_for_range(start..end).collect();
|
||||
Mention::Text {
|
||||
content,
|
||||
tracked_buffers: vec![cx.entity()],
|
||||
}
|
||||
})
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
})
|
||||
.shared();
|
||||
|
||||
self.mention_set.update(cx, |mention_set, _cx| {
|
||||
mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task)
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if self.prompt_capabilities.borrow().image
|
||||
@@ -702,13 +690,6 @@ impl MessageEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let editor = self.editor.clone();
|
||||
window.defer(cx, move |window, cx| {
|
||||
editor.update(cx, |editor, cx| editor.paste(&Paste, window, cx));
|
||||
});
|
||||
}
|
||||
|
||||
pub fn insert_dragged_files(
|
||||
&mut self,
|
||||
paths: Vec<project::ProjectPath>,
|
||||
@@ -986,7 +967,6 @@ impl Render for MessageEditor {
|
||||
.on_action(cx.listener(Self::chat))
|
||||
.on_action(cx.listener(Self::chat_with_follow))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::paste_raw))
|
||||
.capture_action(cx.listener(Self::paste))
|
||||
.flex_1()
|
||||
.child({
|
||||
@@ -1385,7 +1365,7 @@ mod tests {
|
||||
cx,
|
||||
);
|
||||
});
|
||||
message_editor.read(cx).focus_handle(cx).focus(window, cx);
|
||||
message_editor.read(cx).focus_handle(cx).focus(window);
|
||||
message_editor.read(cx).editor().clone()
|
||||
});
|
||||
|
||||
@@ -1607,7 +1587,7 @@ mod tests {
|
||||
cx,
|
||||
);
|
||||
});
|
||||
message_editor.read(cx).focus_handle(cx).focus(window, cx);
|
||||
message_editor.read(cx).focus_handle(cx).focus(window);
|
||||
let editor = message_editor.read(cx).editor().clone();
|
||||
(message_editor, editor)
|
||||
});
|
||||
@@ -2335,7 +2315,7 @@ mod tests {
|
||||
cx,
|
||||
);
|
||||
});
|
||||
message_editor.read(cx).focus_handle(cx).focus(window, cx);
|
||||
message_editor.read(cx).focus_handle(cx).focus(window);
|
||||
let editor = message_editor.read(cx).editor().clone();
|
||||
(message_editor, editor)
|
||||
});
|
||||
|
||||
@@ -186,17 +186,6 @@ impl Render for ModeSelector {
|
||||
move |_window, cx| {
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(Label::new("Toggle Mode Menu"))
|
||||
.child(KeyBinding::for_action_in(
|
||||
&ToggleProfileSelector,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.pb_1()
|
||||
@@ -211,6 +200,17 @@ impl Render for ModeSelector {
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(Label::new("Toggle Mode Menu"))
|
||||
.child(KeyBinding::for_action_in(
|
||||
&ToggleProfileSelector,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
use std::{cmp::Reverse, rc::Rc, sync::Arc};
|
||||
|
||||
use acp_thread::{AgentModelIcon, AgentModelInfo, AgentModelList, AgentModelSelector};
|
||||
use agent_client_protocol::ModelId;
|
||||
use acp_thread::{AgentModelInfo, AgentModelList, AgentModelSelector};
|
||||
use agent_servers::AgentServer;
|
||||
use anyhow::Result;
|
||||
use collections::{HashSet, IndexMap};
|
||||
use collections::IndexMap;
|
||||
use fs::Fs;
|
||||
use futures::FutureExt;
|
||||
use fuzzy::{StringMatchCandidate, match_strings};
|
||||
use gpui::{
|
||||
Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, Task,
|
||||
WeakEntity,
|
||||
Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, FocusHandle, Task, WeakEntity,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use settings::SettingsStore;
|
||||
use ui::{DocumentationAside, DocumentationEdge, DocumentationSide, IntoElement, prelude::*};
|
||||
use ui::{DocumentationAside, DocumentationEdge, DocumentationSide, prelude::*};
|
||||
use util::ResultExt;
|
||||
use zed_actions::agent::OpenSettings;
|
||||
|
||||
@@ -42,7 +38,7 @@ pub fn acp_model_selector(
|
||||
|
||||
enum AcpModelPickerEntry {
|
||||
Separator(SharedString),
|
||||
Model(AgentModelInfo, bool),
|
||||
Model(AgentModelInfo),
|
||||
}
|
||||
|
||||
pub struct AcpModelPickerDelegate {
|
||||
@@ -54,9 +50,7 @@ pub struct AcpModelPickerDelegate {
|
||||
selected_index: usize,
|
||||
selected_description: Option<(usize, SharedString, bool)>,
|
||||
selected_model: Option<AgentModelInfo>,
|
||||
favorites: HashSet<ModelId>,
|
||||
_refresh_models_task: Task<()>,
|
||||
_settings_subscription: Subscription,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
@@ -104,19 +98,6 @@ impl AcpModelPickerDelegate {
|
||||
})
|
||||
};
|
||||
|
||||
let agent_server_for_subscription = agent_server.clone();
|
||||
let settings_subscription =
|
||||
cx.observe_global_in::<SettingsStore>(window, move |picker, window, cx| {
|
||||
// Only refresh if the favorites actually changed to avoid redundant work
|
||||
// when other settings are modified (e.g., user editing settings.json)
|
||||
let new_favorites = agent_server_for_subscription.favorite_model_ids(cx);
|
||||
if new_favorites != picker.delegate.favorites {
|
||||
picker.delegate.favorites = new_favorites;
|
||||
picker.refresh(window, cx);
|
||||
}
|
||||
});
|
||||
let favorites = agent_server.favorite_model_ids(cx);
|
||||
|
||||
Self {
|
||||
selector,
|
||||
agent_server,
|
||||
@@ -126,9 +107,7 @@ impl AcpModelPickerDelegate {
|
||||
selected_model: None,
|
||||
selected_index: 0,
|
||||
selected_description: None,
|
||||
favorites,
|
||||
_refresh_models_task: refresh_models_task,
|
||||
_settings_subscription: settings_subscription,
|
||||
focus_handle,
|
||||
}
|
||||
}
|
||||
@@ -136,64 +115,6 @@ impl AcpModelPickerDelegate {
|
||||
pub fn active_model(&self) -> Option<&AgentModelInfo> {
|
||||
self.selected_model.as_ref()
|
||||
}
|
||||
|
||||
pub fn favorites_count(&self) -> usize {
|
||||
self.favorites.len()
|
||||
}
|
||||
|
||||
pub fn cycle_favorite_models(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
if self.favorites.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(models) = &self.models else {
|
||||
return;
|
||||
};
|
||||
|
||||
let all_models: Vec<&AgentModelInfo> = match models {
|
||||
AgentModelList::Flat(list) => list.iter().collect(),
|
||||
AgentModelList::Grouped(index_map) => index_map.values().flatten().collect(),
|
||||
};
|
||||
|
||||
let favorite_models: Vec<_> = all_models
|
||||
.into_iter()
|
||||
.filter(|model| self.favorites.contains(&model.id))
|
||||
.unique_by(|model| &model.id)
|
||||
.collect();
|
||||
|
||||
if favorite_models.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let current_id = self.selected_model.as_ref().map(|m| &m.id);
|
||||
|
||||
let current_index_in_favorites = current_id
|
||||
.and_then(|id| favorite_models.iter().position(|m| &m.id == id))
|
||||
.unwrap_or(usize::MAX);
|
||||
|
||||
let next_index = if current_index_in_favorites == usize::MAX {
|
||||
0
|
||||
} else {
|
||||
(current_index_in_favorites + 1) % favorite_models.len()
|
||||
};
|
||||
|
||||
let next_model = favorite_models[next_index].clone();
|
||||
|
||||
self.selector
|
||||
.select_model(next_model.id.clone(), cx)
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
self.selected_model = Some(next_model);
|
||||
|
||||
// Keep the picker selection aligned with the newly-selected model
|
||||
if let Some(new_index) = self.filtered_entries.iter().position(|entry| {
|
||||
matches!(entry, AcpModelPickerEntry::Model(model_info, _) if self.selected_model.as_ref().is_some_and(|selected| model_info.id == selected.id))
|
||||
}) {
|
||||
self.set_selected_index(new_index, window, cx);
|
||||
} else {
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for AcpModelPickerDelegate {
|
||||
@@ -219,7 +140,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
) -> bool {
|
||||
match self.filtered_entries.get(ix) {
|
||||
Some(AcpModelPickerEntry::Model(_, _)) => true,
|
||||
Some(AcpModelPickerEntry::Model(_)) => true,
|
||||
Some(AcpModelPickerEntry::Separator(_)) | None => false,
|
||||
}
|
||||
}
|
||||
@@ -234,8 +155,6 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let favorites = self.favorites.clone();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let filtered_models = match this
|
||||
.read_with(cx, |this, cx| {
|
||||
@@ -252,7 +171,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.delegate.filtered_entries =
|
||||
info_list_to_picker_entries(filtered_models, &favorites);
|
||||
info_list_to_picker_entries(filtered_models).collect();
|
||||
// Finds the currently selected model in the list
|
||||
let new_index = this
|
||||
.delegate
|
||||
@@ -260,7 +179,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
.as_ref()
|
||||
.and_then(|selected| {
|
||||
this.delegate.filtered_entries.iter().position(|entry| {
|
||||
if let AcpModelPickerEntry::Model(model_info, _) = entry {
|
||||
if let AcpModelPickerEntry::Model(model_info) = entry {
|
||||
model_info.id == selected.id
|
||||
} else {
|
||||
false
|
||||
@@ -276,7 +195,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
if let Some(AcpModelPickerEntry::Model(model_info, _)) =
|
||||
if let Some(AcpModelPickerEntry::Model(model_info)) =
|
||||
self.filtered_entries.get(self.selected_index)
|
||||
{
|
||||
if window.modifiers().secondary() {
|
||||
@@ -314,7 +233,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
is_focused: bool,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
@@ -322,54 +241,32 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
AcpModelPickerEntry::Separator(title) => {
|
||||
Some(ModelSelectorHeader::new(title, ix > 1).into_any_element())
|
||||
}
|
||||
AcpModelPickerEntry::Model(model_info, is_favorite) => {
|
||||
AcpModelPickerEntry::Model(model_info) => {
|
||||
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 is_favorite = *is_favorite;
|
||||
let handle_action_click = {
|
||||
let model_id = model_info.id.clone();
|
||||
let fs = self.fs.clone();
|
||||
let agent_server = self.agent_server.clone();
|
||||
|
||||
cx.listener(move |_, _, _, cx| {
|
||||
agent_server.toggle_favorite_model(
|
||||
model_id.clone(),
|
||||
!is_favorite,
|
||||
fs.clone(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
};
|
||||
|
||||
Some(
|
||||
div()
|
||||
.id(("model-picker-menu-child", ix))
|
||||
.when_some(model_info.description.clone(), |this, description| {
|
||||
this.on_hover(cx.listener(move |menu, hovered, _, cx| {
|
||||
if *hovered {
|
||||
menu.delegate.selected_description =
|
||||
Some((ix, description.clone(), is_default));
|
||||
} else if matches!(menu.delegate.selected_description, Some((id, _, _)) if id == ix) {
|
||||
menu.delegate.selected_description = None;
|
||||
}
|
||||
cx.notify();
|
||||
}))
|
||||
this
|
||||
.on_hover(cx.listener(move |menu, hovered, _, cx| {
|
||||
if *hovered {
|
||||
menu.delegate.selected_description = Some((ix, description.clone(), is_default));
|
||||
} else if matches!(menu.delegate.selected_description, Some((id, _, _)) if id == ix) {
|
||||
menu.delegate.selected_description = None;
|
||||
}
|
||||
cx.notify();
|
||||
}))
|
||||
})
|
||||
.child(
|
||||
ModelSelectorListItem::new(ix, model_info.name.clone())
|
||||
.map(|this| match &model_info.icon {
|
||||
Some(AgentModelIcon::Path(path)) => this.icon_path(path.clone()),
|
||||
Some(AgentModelIcon::Named(icon)) => this.icon(*icon),
|
||||
None => this,
|
||||
})
|
||||
.is_focused(is_focused)
|
||||
.is_selected(is_selected)
|
||||
.is_focused(selected)
|
||||
.is_favorite(is_favorite)
|
||||
.on_toggle_favorite(handle_action_click),
|
||||
.when_some(model_info.icon, |this, icon| this.icon(icon)),
|
||||
)
|
||||
.into_any_element(),
|
||||
.into_any_element()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -417,51 +314,18 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
|
||||
fn info_list_to_picker_entries(
|
||||
model_list: AgentModelList,
|
||||
favorites: &HashSet<ModelId>,
|
||||
) -> Vec<AcpModelPickerEntry> {
|
||||
let mut entries = Vec::new();
|
||||
|
||||
let all_models: Vec<_> = match &model_list {
|
||||
AgentModelList::Flat(list) => list.iter().collect(),
|
||||
AgentModelList::Grouped(index_map) => index_map.values().flatten().collect(),
|
||||
};
|
||||
|
||||
let favorite_models: Vec<_> = all_models
|
||||
.iter()
|
||||
.filter(|m| favorites.contains(&m.id))
|
||||
.unique_by(|m| &m.id)
|
||||
.collect();
|
||||
|
||||
let has_favorites = !favorite_models.is_empty();
|
||||
if has_favorites {
|
||||
entries.push(AcpModelPickerEntry::Separator("Favorite".into()));
|
||||
for model in favorite_models {
|
||||
entries.push(AcpModelPickerEntry::Model((*model).clone(), true));
|
||||
}
|
||||
}
|
||||
|
||||
) -> impl Iterator<Item = AcpModelPickerEntry> {
|
||||
match model_list {
|
||||
AgentModelList::Flat(list) => {
|
||||
if has_favorites {
|
||||
entries.push(AcpModelPickerEntry::Separator("All".into()));
|
||||
}
|
||||
for model in list {
|
||||
let is_favorite = favorites.contains(&model.id);
|
||||
entries.push(AcpModelPickerEntry::Model(model, is_favorite));
|
||||
}
|
||||
itertools::Either::Left(list.into_iter().map(AcpModelPickerEntry::Model))
|
||||
}
|
||||
AgentModelList::Grouped(index_map) => {
|
||||
for (group_name, models) in index_map {
|
||||
entries.push(AcpModelPickerEntry::Separator(group_name.0));
|
||||
for model in models {
|
||||
let is_favorite = favorites.contains(&model.id);
|
||||
entries.push(AcpModelPickerEntry::Model(model, is_favorite));
|
||||
}
|
||||
}
|
||||
itertools::Either::Right(index_map.into_iter().flat_map(|(group_name, models)| {
|
||||
std::iter::once(AcpModelPickerEntry::Separator(group_name.0))
|
||||
.chain(models.into_iter().map(AcpModelPickerEntry::Model))
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
entries
|
||||
}
|
||||
|
||||
async fn fuzzy_search(
|
||||
@@ -583,33 +447,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_favorites(models: Vec<&str>) -> HashSet<ModelId> {
|
||||
models
|
||||
.into_iter()
|
||||
.map(|m| ModelId::new(m.to_string()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_entry_model_ids(entries: &[AcpModelPickerEntry]) -> Vec<&str> {
|
||||
entries
|
||||
.iter()
|
||||
.filter_map(|entry| match entry {
|
||||
AcpModelPickerEntry::Model(info, _) => Some(info.id.0.as_ref()),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_entry_labels(entries: &[AcpModelPickerEntry]) -> Vec<&str> {
|
||||
entries
|
||||
.iter()
|
||||
.map(|entry| match entry {
|
||||
AcpModelPickerEntry::Model(info, _) => info.id.0.as_ref(),
|
||||
AcpModelPickerEntry::Separator(s) => &s,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_fuzzy_match(cx: &mut TestAppContext) {
|
||||
let models = create_model_list(vec![
|
||||
@@ -649,185 +486,4 @@ mod tests {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_favorites_section_appears_when_favorites_exist(_cx: &mut TestAppContext) {
|
||||
let models = create_model_list(vec![
|
||||
("zed", vec!["zed/claude", "zed/gemini"]),
|
||||
("openai", vec!["openai/gpt-5"]),
|
||||
]);
|
||||
let favorites = create_favorites(vec!["zed/gemini"]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
|
||||
assert!(matches!(
|
||||
entries.first(),
|
||||
Some(AcpModelPickerEntry::Separator(s)) if s == "Favorite"
|
||||
));
|
||||
|
||||
let model_ids = get_entry_model_ids(&entries);
|
||||
assert_eq!(model_ids[0], "zed/gemini");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_no_favorites_section_when_no_favorites(_cx: &mut TestAppContext) {
|
||||
let models = create_model_list(vec![("zed", vec!["zed/claude", "zed/gemini"])]);
|
||||
let favorites = create_favorites(vec![]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
|
||||
assert!(matches!(
|
||||
entries.first(),
|
||||
Some(AcpModelPickerEntry::Separator(s)) if s == "zed"
|
||||
));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_models_have_correct_actions(_cx: &mut TestAppContext) {
|
||||
let models = create_model_list(vec![
|
||||
("zed", vec!["zed/claude", "zed/gemini"]),
|
||||
("openai", vec!["openai/gpt-5"]),
|
||||
]);
|
||||
let favorites = create_favorites(vec!["zed/claude"]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
|
||||
for entry in &entries {
|
||||
if let AcpModelPickerEntry::Model(info, is_favorite) = entry {
|
||||
if info.id.0.as_ref() == "zed/claude" {
|
||||
assert!(is_favorite, "zed/claude should be a favorite");
|
||||
} else {
|
||||
assert!(!is_favorite, "{} should not be a favorite", info.id.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_favorites_appear_in_both_sections(_cx: &mut TestAppContext) {
|
||||
let models = create_model_list(vec![
|
||||
("zed", vec!["zed/claude", "zed/gemini"]),
|
||||
("openai", vec!["openai/gpt-5", "openai/gpt-4"]),
|
||||
]);
|
||||
let favorites = create_favorites(vec!["zed/gemini", "openai/gpt-5"]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
let model_ids = get_entry_model_ids(&entries);
|
||||
|
||||
assert_eq!(model_ids[0], "zed/gemini");
|
||||
assert_eq!(model_ids[1], "openai/gpt-5");
|
||||
|
||||
assert!(model_ids[2..].contains(&"zed/gemini"));
|
||||
assert!(model_ids[2..].contains(&"openai/gpt-5"));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_favorites_are_not_duplicated_when_repeated_in_other_sections(_cx: &mut TestAppContext) {
|
||||
let models = create_model_list(vec![
|
||||
("Recommended", vec!["zed/claude", "anthropic/claude"]),
|
||||
("Zed", vec!["zed/claude", "zed/gpt-5"]),
|
||||
("Antropic", vec!["anthropic/claude"]),
|
||||
("OpenAI", vec!["openai/gpt-5"]),
|
||||
]);
|
||||
|
||||
let favorites = create_favorites(vec!["zed/claude"]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
let labels = get_entry_labels(&entries);
|
||||
|
||||
assert_eq!(
|
||||
labels,
|
||||
vec![
|
||||
"Favorite",
|
||||
"zed/claude",
|
||||
"Recommended",
|
||||
"zed/claude",
|
||||
"anthropic/claude",
|
||||
"Zed",
|
||||
"zed/claude",
|
||||
"zed/gpt-5",
|
||||
"Antropic",
|
||||
"anthropic/claude",
|
||||
"OpenAI",
|
||||
"openai/gpt-5"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_flat_model_list_with_favorites(_cx: &mut TestAppContext) {
|
||||
let models = AgentModelList::Flat(vec![
|
||||
acp_thread::AgentModelInfo {
|
||||
id: acp::ModelId::new("zed/claude".to_string()),
|
||||
name: "Claude".into(),
|
||||
description: None,
|
||||
icon: None,
|
||||
},
|
||||
acp_thread::AgentModelInfo {
|
||||
id: acp::ModelId::new("zed/gemini".to_string()),
|
||||
name: "Gemini".into(),
|
||||
description: None,
|
||||
icon: None,
|
||||
},
|
||||
]);
|
||||
let favorites = create_favorites(vec!["zed/gemini"]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
|
||||
assert!(matches!(
|
||||
entries.first(),
|
||||
Some(AcpModelPickerEntry::Separator(s)) if s == "Favorite"
|
||||
));
|
||||
|
||||
assert!(entries.iter().any(|e| matches!(
|
||||
e,
|
||||
AcpModelPickerEntry::Separator(s) if s == "All"
|
||||
)));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_favorites_count_returns_correct_count(_cx: &mut TestAppContext) {
|
||||
let empty_favorites: HashSet<ModelId> = HashSet::default();
|
||||
assert_eq!(empty_favorites.len(), 0);
|
||||
|
||||
let one_favorite = create_favorites(vec!["model-a"]);
|
||||
assert_eq!(one_favorite.len(), 1);
|
||||
|
||||
let multiple_favorites = create_favorites(vec!["model-a", "model-b", "model-c"]);
|
||||
assert_eq!(multiple_favorites.len(), 3);
|
||||
|
||||
let with_duplicates = create_favorites(vec!["model-a", "model-a", "model-b"]);
|
||||
assert_eq!(with_duplicates.len(), 2);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_is_favorite_flag_set_correctly_in_entries(_cx: &mut TestAppContext) {
|
||||
let models = AgentModelList::Flat(vec![
|
||||
acp_thread::AgentModelInfo {
|
||||
id: acp::ModelId::new("favorite-model".to_string()),
|
||||
name: "Favorite".into(),
|
||||
description: None,
|
||||
icon: None,
|
||||
},
|
||||
acp_thread::AgentModelInfo {
|
||||
id: acp::ModelId::new("regular-model".to_string()),
|
||||
name: "Regular".into(),
|
||||
description: None,
|
||||
icon: None,
|
||||
},
|
||||
]);
|
||||
let favorites = create_favorites(vec!["favorite-model"]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
|
||||
for entry in &entries {
|
||||
if let AcpModelPickerEntry::Model(info, is_favorite) = entry {
|
||||
if info.id.0.as_ref() == "favorite-model" {
|
||||
assert!(*is_favorite, "favorite-model should have is_favorite=true");
|
||||
} else if info.id.0.as_ref() == "regular-model" {
|
||||
assert!(!*is_favorite, "regular-model should have is_favorite=false");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use acp_thread::{AgentModelIcon, AgentModelInfo, AgentModelSelector};
|
||||
use acp_thread::{AgentModelInfo, AgentModelSelector};
|
||||
use agent_servers::AgentServer;
|
||||
use fs::Fs;
|
||||
use gpui::{Entity, FocusHandle};
|
||||
use picker::popover_menu::PickerPopoverMenu;
|
||||
use ui::{ButtonLike, PopoverMenuHandle, TintColor, Tooltip, prelude::*};
|
||||
use ui::{
|
||||
ButtonLike, Context, IntoElement, PopoverMenuHandle, SharedString, TintColor, Tooltip, Window,
|
||||
prelude::*,
|
||||
};
|
||||
use zed_actions::agent::ToggleModelSelector;
|
||||
|
||||
use crate::acp::{AcpModelSelector, model_selector::acp_model_selector};
|
||||
use crate::ui::ModelSelectorTooltip;
|
||||
|
||||
pub struct AcpModelSelectorPopover {
|
||||
selector: Entity<AcpModelSelector>,
|
||||
@@ -19,7 +23,7 @@ pub struct AcpModelSelectorPopover {
|
||||
impl AcpModelSelectorPopover {
|
||||
pub(crate) fn new(
|
||||
selector: Rc<dyn AgentModelSelector>,
|
||||
agent_server: Rc<dyn agent_servers::AgentServer>,
|
||||
agent_server: Rc<dyn AgentServer>,
|
||||
fs: Arc<dyn Fs>,
|
||||
menu_handle: PopoverMenuHandle<AcpModelSelector>,
|
||||
focus_handle: FocusHandle,
|
||||
@@ -50,24 +54,17 @@ impl AcpModelSelectorPopover {
|
||||
pub fn active_model<'a>(&self, cx: &'a App) -> Option<&'a AgentModelInfo> {
|
||||
self.selector.read(cx).delegate.active_model()
|
||||
}
|
||||
|
||||
pub fn cycle_favorite_models(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.selector.update(cx, |selector, cx| {
|
||||
selector.delegate.cycle_favorite_models(window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AcpModelSelectorPopover {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let selector = self.selector.read(cx);
|
||||
let model = selector.delegate.active_model();
|
||||
let model = self.selector.read(cx).delegate.active_model();
|
||||
let model_name = model
|
||||
.as_ref()
|
||||
.map(|model| model.name.clone())
|
||||
.unwrap_or_else(|| SharedString::from("Select a Model"));
|
||||
|
||||
let model_icon = model.as_ref().and_then(|model| model.icon.clone());
|
||||
let model_icon = model.as_ref().and_then(|model| model.icon);
|
||||
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
@@ -77,29 +74,12 @@ impl Render for AcpModelSelectorPopover {
|
||||
(Color::Muted, IconName::ChevronDown)
|
||||
};
|
||||
|
||||
let show_cycle_row = selector.delegate.favorites_count() > 1;
|
||||
|
||||
let tooltip = Tooltip::element({
|
||||
move |_, _cx| {
|
||||
ModelSelectorTooltip::new(focus_handle.clone())
|
||||
.show_cycle_row(show_cycle_row)
|
||||
.into_any_element()
|
||||
}
|
||||
});
|
||||
|
||||
PickerPopoverMenu::new(
|
||||
self.selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.when_some(model_icon, |this, icon| {
|
||||
this.child(
|
||||
match icon {
|
||||
AgentModelIcon::Path(path) => Icon::from_external_svg(path),
|
||||
AgentModelIcon::Named(icon_name) => Icon::new(icon_name),
|
||||
}
|
||||
.color(color)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
this.child(Icon::new(icon).color(color).size(IconSize::XSmall))
|
||||
})
|
||||
.child(
|
||||
Label::new(model_name)
|
||||
@@ -108,7 +88,9 @@ impl Render for AcpModelSelectorPopover {
|
||||
.ml_0p5(),
|
||||
)
|
||||
.child(Icon::new(icon).color(Color::Muted).size(IconSize::XSmall)),
|
||||
tooltip,
|
||||
move |_window, cx| {
|
||||
Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
|
||||
},
|
||||
gpui::Corner::BottomRight,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::acp::AcpThreadView;
|
||||
use crate::{AgentPanel, RemoveHistory, RemoveSelectedThread};
|
||||
use agent::{HistoryEntry, HistoryStore};
|
||||
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta, Utc};
|
||||
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
@@ -402,22 +402,7 @@ impl AcpThreadHistory {
|
||||
let selected = ix == self.selected_index;
|
||||
let hovered = Some(ix) == self.hovered_index;
|
||||
let timestamp = entry.updated_at().timestamp();
|
||||
|
||||
let display_text = match format {
|
||||
EntryTimeFormat::DateAndTime => {
|
||||
let entry_time = entry.updated_at();
|
||||
let now = Utc::now();
|
||||
let duration = now.signed_duration_since(entry_time);
|
||||
let days = duration.num_days();
|
||||
|
||||
format!("{}d", days)
|
||||
}
|
||||
EntryTimeFormat::TimeOnly => format.format_timestamp(timestamp, self.local_timezone),
|
||||
};
|
||||
|
||||
let title = entry.title().clone();
|
||||
let full_date =
|
||||
EntryTimeFormat::DateAndTime.format_timestamp(timestamp, self.local_timezone);
|
||||
let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
@@ -438,14 +423,11 @@ impl AcpThreadHistory {
|
||||
.truncate(),
|
||||
)
|
||||
.child(
|
||||
Label::new(display_text)
|
||||
Label::new(thread_timestamp)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
),
|
||||
)
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::with_meta(title.clone(), None, full_date.clone(), cx)
|
||||
})
|
||||
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
|
||||
if *is_hovered {
|
||||
this.hovered_index = Some(ix);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,8 +22,7 @@ use gpui::{
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
IconOrSvg, LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry,
|
||||
ZED_CLOUD_PROVIDER_ID,
|
||||
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use language_models::AllLanguageModelSettings;
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
@@ -118,7 +117,7 @@ impl AgentConfiguration {
|
||||
}
|
||||
|
||||
fn build_provider_configuration_views(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let providers = LanguageModelRegistry::read_global(cx).visible_providers();
|
||||
let providers = LanguageModelRegistry::read_global(cx).providers();
|
||||
for provider in providers {
|
||||
self.add_provider_configuration_view(&provider, window, cx);
|
||||
}
|
||||
@@ -262,12 +261,9 @@ impl AgentConfiguration {
|
||||
.w_full()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
match provider.icon() {
|
||||
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
|
||||
IconOrSvg::Icon(name) => Icon::new(name),
|
||||
}
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
Icon::new(provider.icon())
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -420,7 +416,7 @@ impl AgentConfiguration {
|
||||
&mut self,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let providers = LanguageModelRegistry::read_global(cx).visible_providers();
|
||||
let providers = LanguageModelRegistry::read_global(cx).providers();
|
||||
|
||||
let popover_menu = PopoverMenu::new("add-provider-popover")
|
||||
.trigger(
|
||||
@@ -1370,7 +1366,6 @@ async fn open_new_agent_servers_entry_in_settings_editor(
|
||||
env: Some(HashMap::default()),
|
||||
default_mode: None,
|
||||
default_model: None,
|
||||
favorite_models: vec![],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -446,17 +446,17 @@ impl AddLlmProviderModal {
|
||||
})
|
||||
}
|
||||
|
||||
fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
|
||||
window.focus_next(cx);
|
||||
fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, _: &mut Context<Self>) {
|
||||
window.focus_next();
|
||||
}
|
||||
|
||||
fn on_tab_prev(
|
||||
&mut self,
|
||||
_: &menu::SelectPrevious,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
_: &mut Context<Self>,
|
||||
) {
|
||||
window.focus_prev(cx);
|
||||
window.focus_prev();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,7 +493,7 @@ impl Render for AddLlmProviderModal {
|
||||
.on_action(cx.listener(Self::on_tab))
|
||||
.on_action(cx.listener(Self::on_tab_prev))
|
||||
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
|
||||
this.focus_handle(cx).focus(window, cx);
|
||||
this.focus_handle(cx).focus(window);
|
||||
}))
|
||||
.child(
|
||||
Modal::new("configure-context-server", None)
|
||||
|
||||
@@ -831,7 +831,7 @@ impl Render for ConfigureContextServerModal {
|
||||
}),
|
||||
)
|
||||
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
|
||||
this.focus_handle(cx).focus(window, cx);
|
||||
this.focus_handle(cx).focus(window);
|
||||
}))
|
||||
.child(
|
||||
Modal::new("configure-context-server", None)
|
||||
|
||||
@@ -156,7 +156,7 @@ impl ManageProfilesModal {
|
||||
cx.observe_global_in::<SettingsStore>(window, |this, window, cx| {
|
||||
if matches!(this.mode, Mode::ChooseProfile(_)) {
|
||||
this.mode = Mode::choose_profile(window, cx);
|
||||
this.focus_handle(cx).focus(window, cx);
|
||||
this.focus_handle(cx).focus(window);
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
@@ -173,7 +173,7 @@ impl ManageProfilesModal {
|
||||
|
||||
fn choose_profile(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.mode = Mode::choose_profile(window, cx);
|
||||
self.focus_handle(cx).focus(window, cx);
|
||||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
fn new_profile(
|
||||
@@ -191,7 +191,7 @@ impl ManageProfilesModal {
|
||||
name_editor,
|
||||
base_profile_id,
|
||||
});
|
||||
self.focus_handle(cx).focus(window, cx);
|
||||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
pub fn view_profile(
|
||||
@@ -209,7 +209,7 @@ impl ManageProfilesModal {
|
||||
delete_profile: NavigableEntry::focusable(cx),
|
||||
cancel_item: NavigableEntry::focusable(cx),
|
||||
});
|
||||
self.focus_handle(cx).focus(window, cx);
|
||||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
fn configure_default_model(
|
||||
@@ -222,6 +222,7 @@ impl ManageProfilesModal {
|
||||
let profile_id_for_closure = profile_id.clone();
|
||||
|
||||
let model_picker = cx.new(|cx| {
|
||||
let fs = fs.clone();
|
||||
let profile_id = profile_id_for_closure.clone();
|
||||
|
||||
language_model_selector(
|
||||
@@ -249,36 +250,22 @@ impl ManageProfilesModal {
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
let fs = fs.clone();
|
||||
move |model, cx| {
|
||||
let provider = model.provider_id().0.to_string();
|
||||
let model_id = model.id().0.to_string();
|
||||
let profile_id = profile_id.clone();
|
||||
move |model, cx| {
|
||||
let provider = model.provider_id().0.to_string();
|
||||
let model_id = model.id().0.to_string();
|
||||
let profile_id = profile_id.clone();
|
||||
|
||||
update_settings_file(fs.clone(), cx, move |settings, _cx| {
|
||||
let agent_settings = settings.agent.get_or_insert_default();
|
||||
if let Some(profiles) = agent_settings.profiles.as_mut() {
|
||||
if let Some(profile) = profiles.get_mut(profile_id.0.as_ref()) {
|
||||
profile.default_model = Some(LanguageModelSelection {
|
||||
provider: LanguageModelProviderSetting(provider.clone()),
|
||||
model: model_id.clone(),
|
||||
});
|
||||
}
|
||||
update_settings_file(fs.clone(), cx, move |settings, _cx| {
|
||||
let agent_settings = settings.agent.get_or_insert_default();
|
||||
if let Some(profiles) = agent_settings.profiles.as_mut() {
|
||||
if let Some(profile) = profiles.get_mut(profile_id.0.as_ref()) {
|
||||
profile.default_model = Some(LanguageModelSelection {
|
||||
provider: LanguageModelProviderSetting(provider.clone()),
|
||||
model: model_id.clone(),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
let fs = fs.clone();
|
||||
move |model, should_be_favorite, cx| {
|
||||
crate::favorite_models::toggle_in_settings(
|
||||
model,
|
||||
should_be_favorite,
|
||||
fs.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
false, // Do not use popover styles for the model picker
|
||||
self.focus_handle.clone(),
|
||||
@@ -300,7 +287,7 @@ impl ManageProfilesModal {
|
||||
model_picker,
|
||||
_subscription: dismiss_subscription,
|
||||
};
|
||||
self.focus_handle(cx).focus(window, cx);
|
||||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
fn configure_mcp_tools(
|
||||
@@ -336,7 +323,7 @@ impl ManageProfilesModal {
|
||||
tool_picker,
|
||||
_subscription: dismiss_subscription,
|
||||
};
|
||||
self.focus_handle(cx).focus(window, cx);
|
||||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
fn configure_builtin_tools(
|
||||
@@ -377,7 +364,7 @@ impl ManageProfilesModal {
|
||||
tool_picker,
|
||||
_subscription: dismiss_subscription,
|
||||
};
|
||||
self.focus_handle(cx).focus(window, cx);
|
||||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
fn confirm(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -951,7 +938,7 @@ impl Render for ManageProfilesModal {
|
||||
.on_action(cx.listener(|this, _: &menu::Cancel, window, cx| this.cancel(window, cx)))
|
||||
.on_action(cx.listener(|this, _: &menu::Confirm, window, cx| this.confirm(window, cx)))
|
||||
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
|
||||
this.focus_handle(cx).focus(window, cx);
|
||||
this.focus_handle(cx).focus(window);
|
||||
}))
|
||||
.on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
|
||||
.child(match &self.mode {
|
||||
|
||||
@@ -17,7 +17,7 @@ use gpui::{
|
||||
Global, SharedString, Subscription, Task, WeakEntity, Window, prelude::*,
|
||||
};
|
||||
|
||||
use language::{Buffer, Capability, OffsetRangeExt, Point};
|
||||
use language::{Buffer, Capability, DiskState, OffsetRangeExt, Point};
|
||||
use multi_buffer::PathKey;
|
||||
use project::{Project, ProjectItem, ProjectPath};
|
||||
use settings::{Settings, SettingsStore};
|
||||
@@ -192,7 +192,7 @@ impl AgentDiffPane {
|
||||
&& buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.is_some_and(|file| file.disk_state().is_deleted())
|
||||
.is_some_and(|file| file.disk_state() == DiskState::Deleted)
|
||||
{
|
||||
editor.fold_buffer(snapshot.text.remote_id(), cx)
|
||||
}
|
||||
@@ -212,10 +212,10 @@ impl AgentDiffPane {
|
||||
.focus_handle(cx)
|
||||
.contains_focused(window, cx)
|
||||
{
|
||||
self.focus_handle.focus(window, cx);
|
||||
self.focus_handle.focus(window);
|
||||
} else if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.focus_handle(cx).focus(window, cx);
|
||||
editor.focus_handle(cx).focus(window);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -874,12 +874,12 @@ impl AgentDiffToolbar {
|
||||
match active_item {
|
||||
AgentDiffToolbarItem::Pane(agent_diff) => {
|
||||
if let Some(agent_diff) = agent_diff.upgrade() {
|
||||
agent_diff.focus_handle(cx).focus(window, cx);
|
||||
agent_diff.focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
AgentDiffToolbarItem::Editor { editor, .. } => {
|
||||
if let Some(editor) = editor.upgrade() {
|
||||
editor.read(cx).focus_handle(cx).focus(window, cx);
|
||||
editor.read(cx).focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
use crate::{
|
||||
ModelUsageContext,
|
||||
language_model_selector::{LanguageModelSelector, language_model_selector},
|
||||
ui::ModelSelectorTooltip,
|
||||
};
|
||||
use fs::Fs;
|
||||
use gpui::{Entity, FocusHandle, SharedString};
|
||||
use language_model::IconOrSvg;
|
||||
use picker::popover_menu::PickerPopoverMenu;
|
||||
use settings::update_settings_file;
|
||||
use std::sync::Arc;
|
||||
use ui::{ButtonLike, PopoverMenuHandle, TintColor, Tooltip, prelude::*};
|
||||
use zed_actions::agent::ToggleModelSelector;
|
||||
|
||||
pub struct AgentModelSelector {
|
||||
selector: Entity<LanguageModelSelector>,
|
||||
@@ -30,39 +29,26 @@ impl AgentModelSelector {
|
||||
|
||||
Self {
|
||||
selector: cx.new(move |cx| {
|
||||
let fs = fs.clone();
|
||||
language_model_selector(
|
||||
{
|
||||
let model_context = model_usage_context.clone();
|
||||
move |cx| model_context.configured_model(cx)
|
||||
},
|
||||
{
|
||||
let fs = fs.clone();
|
||||
move |model, cx| {
|
||||
let provider = model.provider_id().0.to_string();
|
||||
let model_id = model.id().0.to_string();
|
||||
match &model_usage_context {
|
||||
ModelUsageContext::InlineAssistant => {
|
||||
update_settings_file(fs.clone(), cx, move |settings, _cx| {
|
||||
settings
|
||||
.agent
|
||||
.get_or_insert_default()
|
||||
.set_inline_assistant_model(provider.clone(), model_id);
|
||||
});
|
||||
}
|
||||
move |model, cx| {
|
||||
let provider = model.provider_id().0.to_string();
|
||||
let model_id = model.id().0.to_string();
|
||||
match &model_usage_context {
|
||||
ModelUsageContext::InlineAssistant => {
|
||||
update_settings_file(fs.clone(), cx, move |settings, _cx| {
|
||||
settings
|
||||
.agent
|
||||
.get_or_insert_default()
|
||||
.set_inline_assistant_model(provider.clone(), model_id);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
let fs = fs.clone();
|
||||
move |model, should_be_favorite, cx| {
|
||||
crate::favorite_models::toggle_in_settings(
|
||||
model,
|
||||
should_be_favorite,
|
||||
fs.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
},
|
||||
true, // Use popover styles for picker
|
||||
focus_handle_clone,
|
||||
window,
|
||||
@@ -81,12 +67,6 @@ impl AgentModelSelector {
|
||||
pub fn active_model(&self, cx: &App) -> Option<language_model::ConfiguredModel> {
|
||||
self.selector.read(cx).delegate.active_model(cx)
|
||||
}
|
||||
|
||||
pub fn cycle_favorite_models(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.selector.update(cx, |selector, cx| {
|
||||
selector.delegate.cycle_favorite_models(window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AgentModelSelector {
|
||||
@@ -104,30 +84,13 @@ impl Render for AgentModelSelector {
|
||||
Color::Muted
|
||||
};
|
||||
|
||||
let show_cycle_row = self.selector.read(cx).delegate.favorites_count() > 1;
|
||||
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
let tooltip = Tooltip::element({
|
||||
move |_, _cx| {
|
||||
ModelSelectorTooltip::new(focus_handle.clone())
|
||||
.show_cycle_row(show_cycle_row)
|
||||
.into_any_element()
|
||||
}
|
||||
});
|
||||
|
||||
PickerPopoverMenu::new(
|
||||
self.selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
.when_some(provider_icon, |this, icon| {
|
||||
this.child(
|
||||
match icon {
|
||||
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
|
||||
IconOrSvg::Icon(name) => Icon::new(name),
|
||||
}
|
||||
.color(color)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
this.child(Icon::new(icon).color(color).size(IconSize::XSmall))
|
||||
})
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.child(
|
||||
@@ -139,9 +102,11 @@ impl Render for AgentModelSelector {
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(color)
|
||||
.size(IconSize::XSmall),
|
||||
.size(IconSize::Small),
|
||||
),
|
||||
tooltip,
|
||||
move |_window, cx| {
|
||||
Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
|
||||
},
|
||||
gpui::Corner::TopRight,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::{ops::Range, path::Path, rc::Rc, sync::Arc, time::Duration};
|
||||
|
||||
use acp_thread::AcpThread;
|
||||
use agent::{ContextServerRegistry, DbThreadMetadata, HistoryEntry, HistoryStore};
|
||||
use agent_servers::AgentServer;
|
||||
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
||||
use project::{
|
||||
ExternalAgentServerName,
|
||||
@@ -288,7 +287,7 @@ impl ActiveView {
|
||||
}
|
||||
}
|
||||
|
||||
fn native_agent(
|
||||
pub fn native_agent(
|
||||
fs: Arc<dyn Fs>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
history_store: Entity<agent::HistoryStore>,
|
||||
@@ -443,7 +442,6 @@ pub struct AgentPanel {
|
||||
pending_serialization: Option<Task<Result<()>>>,
|
||||
onboarding: Entity<AgentPanelOnboarding>,
|
||||
selected_agent: AgentType,
|
||||
show_trust_workspace_message: bool,
|
||||
}
|
||||
|
||||
impl AgentPanel {
|
||||
@@ -694,7 +692,6 @@ impl AgentPanel {
|
||||
history_store,
|
||||
selected_agent: AgentType::default(),
|
||||
loading: false,
|
||||
show_trust_workspace_message: false,
|
||||
};
|
||||
|
||||
// Initial sync of agent servers from extensions
|
||||
@@ -822,7 +819,7 @@ impl AgentPanel {
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
text_thread_editor.focus_handle(cx).focus(window, cx);
|
||||
text_thread_editor.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
fn external_thread(
|
||||
@@ -888,21 +885,36 @@ impl AgentPanel {
|
||||
};
|
||||
|
||||
let server = ext_agent.server(fs, history);
|
||||
this.update_in(cx, |agent_panel, window, cx| {
|
||||
agent_panel._external_thread(
|
||||
server,
|
||||
resume_thread,
|
||||
summarize_thread,
|
||||
workspace,
|
||||
project,
|
||||
loading,
|
||||
ext_agent,
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
let selected_agent = ext_agent.into();
|
||||
if this.selected_agent != selected_agent {
|
||||
this.selected_agent = selected_agent;
|
||||
this.serialize(cx);
|
||||
}
|
||||
|
||||
let thread_view = cx.new(|cx| {
|
||||
crate::acp::AcpThreadView::new(
|
||||
server,
|
||||
resume_thread,
|
||||
summarize_thread,
|
||||
workspace.clone(),
|
||||
project,
|
||||
this.history_store.clone(),
|
||||
this.prompt_store.clone(),
|
||||
!loading,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
this.set_active_view(
|
||||
ActiveView::ExternalAgentThread { thread_view },
|
||||
!loading,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
@@ -935,7 +947,7 @@ impl AgentPanel {
|
||||
if let Some(thread_view) = self.active_thread_view() {
|
||||
thread_view.update(cx, |view, cx| {
|
||||
view.expand_message_editor(&ExpandMessageEditor, window, cx);
|
||||
view.focus_handle(cx).focus(window, cx);
|
||||
view.focus_handle(cx).focus(window);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1016,12 +1028,12 @@ impl AgentPanel {
|
||||
|
||||
match &self.active_view {
|
||||
ActiveView::ExternalAgentThread { thread_view } => {
|
||||
thread_view.focus_handle(cx).focus(window, cx);
|
||||
thread_view.focus_handle(cx).focus(window);
|
||||
}
|
||||
ActiveView::TextThread {
|
||||
text_thread_editor, ..
|
||||
} => {
|
||||
text_thread_editor.focus_handle(cx).focus(window, cx);
|
||||
text_thread_editor.focus_handle(cx).focus(window);
|
||||
}
|
||||
ActiveView::History | ActiveView::Configuration => {}
|
||||
}
|
||||
@@ -1169,7 +1181,7 @@ impl AgentPanel {
|
||||
Self::handle_agent_configuration_event,
|
||||
));
|
||||
|
||||
configuration.focus_handle(cx).focus(window, cx);
|
||||
configuration.focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1305,7 +1317,7 @@ impl AgentPanel {
|
||||
}
|
||||
|
||||
if focus {
|
||||
self.focus_handle(cx).focus(window, cx);
|
||||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1465,47 +1477,6 @@ impl AgentPanel {
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn _external_thread(
|
||||
&mut self,
|
||||
server: Rc<dyn AgentServer>,
|
||||
resume_thread: Option<DbThreadMetadata>,
|
||||
summarize_thread: Option<DbThreadMetadata>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
loading: bool,
|
||||
ext_agent: ExternalAgent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let selected_agent = AgentType::from(ext_agent);
|
||||
if self.selected_agent != selected_agent {
|
||||
self.selected_agent = selected_agent;
|
||||
self.serialize(cx);
|
||||
}
|
||||
|
||||
let thread_view = cx.new(|cx| {
|
||||
crate::acp::AcpThreadView::new(
|
||||
server,
|
||||
resume_thread,
|
||||
summarize_thread,
|
||||
workspace.clone(),
|
||||
project,
|
||||
self.history_store.clone(),
|
||||
self.prompt_store.clone(),
|
||||
!loading,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
self.set_active_view(
|
||||
ActiveView::ExternalAgentThread { thread_view },
|
||||
!loading,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for AgentPanel {
|
||||
@@ -1620,19 +1591,14 @@ impl AgentPanel {
|
||||
|
||||
let content = match &self.active_view {
|
||||
ActiveView::ExternalAgentThread { thread_view } => {
|
||||
let is_generating_title = thread_view
|
||||
.read(cx)
|
||||
.as_native_thread(cx)
|
||||
.map_or(false, |t| t.read(cx).is_generating_title());
|
||||
|
||||
if let Some(title_editor) = thread_view.read(cx).title_editor() {
|
||||
let container = div()
|
||||
div()
|
||||
.w_full()
|
||||
.on_action({
|
||||
let thread_view = thread_view.downgrade();
|
||||
move |_: &menu::Confirm, window, cx| {
|
||||
if let Some(thread_view) = thread_view.upgrade() {
|
||||
thread_view.focus_handle(cx).focus(window, cx);
|
||||
thread_view.focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1640,25 +1606,12 @@ impl AgentPanel {
|
||||
let thread_view = thread_view.downgrade();
|
||||
move |_: &editor::actions::Cancel, window, cx| {
|
||||
if let Some(thread_view) = thread_view.upgrade() {
|
||||
thread_view.focus_handle(cx).focus(window, cx);
|
||||
thread_view.focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
})
|
||||
.child(title_editor);
|
||||
|
||||
if is_generating_title {
|
||||
container
|
||||
.with_animation(
|
||||
"generating_title",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.4, 0.8)),
|
||||
|div, delta| div.opacity(delta),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
container.into_any_element()
|
||||
}
|
||||
.child(title_editor)
|
||||
.into_any_element()
|
||||
} else {
|
||||
Label::new(thread_view.read(cx).title(cx))
|
||||
.color(Color::Muted)
|
||||
@@ -1688,13 +1641,6 @@ impl AgentPanel {
|
||||
Label::new(LOADING_SUMMARY_PLACEHOLDER)
|
||||
.truncate()
|
||||
.color(Color::Muted)
|
||||
.with_animation(
|
||||
"generating_title",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.4, 0.8)),
|
||||
|label, delta| label.alpha(delta),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
@@ -1738,25 +1684,6 @@ impl AgentPanel {
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn handle_regenerate_thread_title(thread_view: Entity<AcpThreadView>, cx: &mut App) {
|
||||
thread_view.update(cx, |thread_view, cx| {
|
||||
if let Some(thread) = thread_view.as_native_thread(cx) {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.generate_title(cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_regenerate_text_thread_title(
|
||||
text_thread_editor: Entity<TextThreadEditor>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
text_thread_editor.update(cx, |text_thread_editor, cx| {
|
||||
text_thread_editor.regenerate_summary(cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn render_panel_options_menu(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
@@ -1776,35 +1703,6 @@ impl AgentPanel {
|
||||
|
||||
let selected_agent = self.selected_agent.clone();
|
||||
|
||||
let text_thread_view = match &self.active_view {
|
||||
ActiveView::TextThread {
|
||||
text_thread_editor, ..
|
||||
} => Some(text_thread_editor.clone()),
|
||||
_ => None,
|
||||
};
|
||||
let text_thread_with_messages = match &self.active_view {
|
||||
ActiveView::TextThread {
|
||||
text_thread_editor, ..
|
||||
} => text_thread_editor
|
||||
.read(cx)
|
||||
.text_thread()
|
||||
.read(cx)
|
||||
.messages(cx)
|
||||
.any(|message| message.role == language_model::Role::Assistant),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
let thread_view = match &self.active_view {
|
||||
ActiveView::ExternalAgentThread { thread_view } => Some(thread_view.clone()),
|
||||
_ => None,
|
||||
};
|
||||
let thread_with_messages = match &self.active_view {
|
||||
ActiveView::ExternalAgentThread { thread_view } => {
|
||||
thread_view.read(cx).has_user_submitted_prompt(cx)
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
PopoverMenu::new("agent-options-menu")
|
||||
.trigger_with_tooltip(
|
||||
IconButton::new("agent-options-menu", IconName::Ellipsis)
|
||||
@@ -1827,7 +1725,6 @@ impl AgentPanel {
|
||||
move |window, cx| {
|
||||
Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
|
||||
menu = menu.context(focus_handle.clone());
|
||||
|
||||
if let Some(usage) = usage {
|
||||
menu = menu
|
||||
.header_with_link("Prompt Usage", "Manage", account_url.clone())
|
||||
@@ -1865,38 +1762,6 @@ impl AgentPanel {
|
||||
.separator()
|
||||
}
|
||||
|
||||
if thread_with_messages | text_thread_with_messages {
|
||||
menu = menu.header("Current Thread");
|
||||
|
||||
if let Some(text_thread_view) = text_thread_view.as_ref() {
|
||||
menu = menu
|
||||
.entry("Regenerate Thread Title", None, {
|
||||
let text_thread_view = text_thread_view.clone();
|
||||
move |_, cx| {
|
||||
Self::handle_regenerate_text_thread_title(
|
||||
text_thread_view.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})
|
||||
.separator();
|
||||
}
|
||||
|
||||
if let Some(thread_view) = thread_view.as_ref() {
|
||||
menu = menu
|
||||
.entry("Regenerate Thread Title", None, {
|
||||
let thread_view = thread_view.clone();
|
||||
move |_, cx| {
|
||||
Self::handle_regenerate_thread_title(
|
||||
thread_view.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})
|
||||
.separator();
|
||||
}
|
||||
}
|
||||
|
||||
menu = menu
|
||||
.header("MCP Servers")
|
||||
.action(
|
||||
@@ -2428,7 +2293,7 @@ impl AgentPanel {
|
||||
let history_is_empty = self.history_store.read(cx).is_empty(cx);
|
||||
|
||||
let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
|
||||
.visible_providers()
|
||||
.providers()
|
||||
.iter()
|
||||
.any(|provider| {
|
||||
provider.is_authenticated(cx)
|
||||
@@ -2692,38 +2557,6 @@ impl AgentPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_workspace_trust_message(&self, cx: &Context<Self>) -> Option<impl IntoElement> {
|
||||
if !self.show_trust_workspace_message {
|
||||
return None;
|
||||
}
|
||||
|
||||
let description = "To protect your system, third-party code—like MCP servers—won't run until you mark this workspace as safe.";
|
||||
|
||||
Some(
|
||||
Callout::new()
|
||||
.icon(IconName::Warning)
|
||||
.severity(Severity::Warning)
|
||||
.border_position(ui::BorderPosition::Bottom)
|
||||
.title("You're in Restricted Mode")
|
||||
.description(description)
|
||||
.actions_slot(
|
||||
Button::new("open-trust-modal", "Configure Project Trust")
|
||||
.label_size(LabelSize::Small)
|
||||
.style(ButtonStyle::Outlined)
|
||||
.on_click({
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.show_worktree_trust_security_modal(true, window, cx)
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn key_context(&self) -> KeyContext {
|
||||
let mut key_context = KeyContext::new_with_defaults();
|
||||
key_context.add("AgentPanel");
|
||||
@@ -2776,7 +2609,6 @@ impl Render for AgentPanel {
|
||||
}
|
||||
}))
|
||||
.child(self.render_toolbar(window, cx))
|
||||
.children(self.render_workspace_trust_message(cx))
|
||||
.children(self.render_onboarding(window, cx))
|
||||
.map(|parent| match &self.active_view {
|
||||
ActiveView::ExternalAgentThread { thread_view, .. } => parent
|
||||
|
||||
@@ -7,7 +7,6 @@ mod buffer_codegen;
|
||||
mod completion_provider;
|
||||
mod context;
|
||||
mod context_server_configuration;
|
||||
mod favorite_models;
|
||||
mod inline_assistant;
|
||||
mod inline_prompt_editor;
|
||||
mod language_model_selector;
|
||||
@@ -68,8 +67,6 @@ actions!(
|
||||
ToggleProfileSelector,
|
||||
/// Cycles through available session modes.
|
||||
CycleModeSelector,
|
||||
/// Cycles through favorited models in the ACP model selector.
|
||||
CycleFavoriteModels,
|
||||
/// Expands the message editor to full size.
|
||||
ExpandMessageEditor,
|
||||
/// Removes all thread history.
|
||||
@@ -348,8 +345,7 @@ fn init_language_model_settings(cx: &mut App) {
|
||||
|_, event: &language_model::Event, cx| match event {
|
||||
language_model::Event::ProviderStateChanged(_)
|
||||
| language_model::Event::AddedProvider(_)
|
||||
| language_model::Event::RemovedProvider(_)
|
||||
| language_model::Event::ProvidersChanged => {
|
||||
| language_model::Event::RemovedProvider(_) => {
|
||||
update_active_language_model_from_settings(cx);
|
||||
}
|
||||
_ => {}
|
||||
@@ -461,7 +457,6 @@ mod tests {
|
||||
commit_message_model: None,
|
||||
thread_summary_model: None,
|
||||
inline_alternatives: vec![],
|
||||
favorite_models: vec![],
|
||||
default_profile: AgentProfileId::default(),
|
||||
default_view: DefaultAgentView::Thread,
|
||||
profiles: Default::default(),
|
||||
|
||||
@@ -75,9 +75,6 @@ pub struct BufferCodegen {
|
||||
session_id: Uuid,
|
||||
}
|
||||
|
||||
pub const REWRITE_SECTION_TOOL_NAME: &str = "rewrite_section";
|
||||
pub const FAILURE_MESSAGE_TOOL_NAME: &str = "failure_message";
|
||||
|
||||
impl BufferCodegen {
|
||||
pub fn new(
|
||||
buffer: Entity<MultiBuffer>,
|
||||
@@ -444,8 +441,7 @@ impl CodegenAlternative {
|
||||
})
|
||||
.boxed_local()
|
||||
};
|
||||
self.generation =
|
||||
self.handle_stream(model, /* strip_invalid_spans: */ true, stream, cx);
|
||||
self.generation = self.handle_stream(model, stream, cx);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -525,12 +521,12 @@ impl CodegenAlternative {
|
||||
|
||||
let tools = vec![
|
||||
LanguageModelRequestTool {
|
||||
name: REWRITE_SECTION_TOOL_NAME.to_string(),
|
||||
name: "rewrite_section".to_string(),
|
||||
description: "Replaces text in <rewrite_this></rewrite_this> tags with your replacement_text.".to_string(),
|
||||
input_schema: language_model::tool_schema::root_schema_for::<RewriteSectionInput>(tool_input_format).to_value(),
|
||||
},
|
||||
LanguageModelRequestTool {
|
||||
name: FAILURE_MESSAGE_TOOL_NAME.to_string(),
|
||||
name: "failure_message".to_string(),
|
||||
description: "Use this tool to provide a message to the user when you're unable to complete a task.".to_string(),
|
||||
input_schema: language_model::tool_schema::root_schema_for::<FailureMessageInput>(tool_input_format).to_value(),
|
||||
},
|
||||
@@ -633,7 +629,6 @@ impl CodegenAlternative {
|
||||
pub fn handle_stream(
|
||||
&mut self,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
strip_invalid_spans: bool,
|
||||
stream: impl 'static + Future<Output = Result<LanguageModelTextStream>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<()> {
|
||||
@@ -718,16 +713,10 @@ impl CodegenAlternative {
|
||||
let mut response_latency = None;
|
||||
let request_start = Instant::now();
|
||||
let diff = async {
|
||||
let raw_stream = stream?.stream.map_err(|error| error.into());
|
||||
|
||||
let stripped;
|
||||
let mut chunks: Pin<Box<dyn Stream<Item = Result<String>> + Send>> =
|
||||
if strip_invalid_spans {
|
||||
stripped = StripInvalidSpans::new(raw_stream);
|
||||
Box::pin(stripped)
|
||||
} else {
|
||||
Box::pin(raw_stream)
|
||||
};
|
||||
let chunks = StripInvalidSpans::new(
|
||||
stream?.stream.map_err(|error| error.into()),
|
||||
);
|
||||
futures::pin_mut!(chunks);
|
||||
|
||||
let mut diff = StreamingDiff::new(selected_text.to_string());
|
||||
let mut line_diff = LineDiff::default();
|
||||
@@ -1170,7 +1159,7 @@ impl CodegenAlternative {
|
||||
let process_tool_use = move |tool_use: LanguageModelToolUse| -> Option<ToolUseOutput> {
|
||||
let mut chars_read_so_far = chars_read_so_far.lock();
|
||||
match tool_use.name.as_ref() {
|
||||
REWRITE_SECTION_TOOL_NAME => {
|
||||
"rewrite_section" => {
|
||||
let Ok(input) =
|
||||
serde_json::from_value::<RewriteSectionInput>(tool_use.input)
|
||||
else {
|
||||
@@ -1183,7 +1172,7 @@ impl CodegenAlternative {
|
||||
description: None,
|
||||
})
|
||||
}
|
||||
FAILURE_MESSAGE_TOOL_NAME => {
|
||||
"failure_message" => {
|
||||
let Ok(mut input) =
|
||||
serde_json::from_value::<FailureMessageInput>(tool_use.input)
|
||||
else {
|
||||
@@ -1318,12 +1307,7 @@ impl CodegenAlternative {
|
||||
|
||||
let Some(task) = codegen
|
||||
.update(cx, move |codegen, cx| {
|
||||
codegen.handle_stream(
|
||||
model,
|
||||
/* strip_invalid_spans: */ false,
|
||||
async { Ok(language_model_text_stream) },
|
||||
cx,
|
||||
)
|
||||
codegen.handle_stream(model, async { Ok(language_model_text_stream) }, cx)
|
||||
})
|
||||
.ok()
|
||||
else {
|
||||
@@ -1496,10 +1480,7 @@ mod tests {
|
||||
use indoc::indoc;
|
||||
use language::{Buffer, Point};
|
||||
use language_model::fake_provider::FakeLanguageModel;
|
||||
use language_model::{
|
||||
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelRegistry,
|
||||
LanguageModelToolUse, StopReason, TokenUsage,
|
||||
};
|
||||
use language_model::{LanguageModelRegistry, TokenUsage};
|
||||
use languages::rust_lang;
|
||||
use rand::prelude::*;
|
||||
use settings::SettingsStore;
|
||||
@@ -1811,51 +1792,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// When not streaming tool calls, we strip backticks as part of parsing the model's
|
||||
// plain text response. This is a regression test for a bug where we stripped
|
||||
// backticks incorrectly.
|
||||
#[gpui::test]
|
||||
async fn test_allows_model_to_output_backticks(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let text = "- Improved; `cmd+click` behavior. Now requires `cmd` to be pressed before the click starts or it doesn't run. ([#44579](https://github.com/zed-industries/zed/pull/44579); thanks [Zachiah](https://github.com/Zachiah))";
|
||||
let buffer = cx.new(|cx| Buffer::local("", cx));
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let range = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(0, 0))..snapshot.anchor_after(Point::new(0, 0))
|
||||
});
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let codegen = cx.new(|cx| {
|
||||
CodegenAlternative::new(
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
true,
|
||||
prompt_builder,
|
||||
Uuid::new_v4(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let events_tx = simulate_tool_based_completion(&codegen, cx);
|
||||
let chunk_len = text.find('`').unwrap();
|
||||
events_tx
|
||||
.unbounded_send(rewrite_tool_use("tool_1", &text[..chunk_len], false))
|
||||
.unwrap();
|
||||
events_tx
|
||||
.unbounded_send(rewrite_tool_use("tool_2", &text, true))
|
||||
.unwrap();
|
||||
events_tx
|
||||
.unbounded_send(LanguageModelCompletionEvent::Stop(StopReason::EndTurn))
|
||||
.unwrap();
|
||||
drop(events_tx);
|
||||
cx.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
text
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_strip_invalid_spans_from_codeblock() {
|
||||
assert_chunks("Lorem ipsum dolor", "Lorem ipsum dolor").await;
|
||||
@@ -1910,7 +1846,6 @@ mod tests {
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
codegen.generation = codegen.handle_stream(
|
||||
model,
|
||||
/* strip_invalid_spans: */ false,
|
||||
future::ready(Ok(LanguageModelTextStream {
|
||||
message_id: None,
|
||||
stream: chunks_rx.map(Ok).boxed(),
|
||||
@@ -1921,39 +1856,4 @@ mod tests {
|
||||
});
|
||||
chunks_tx
|
||||
}
|
||||
|
||||
fn simulate_tool_based_completion(
|
||||
codegen: &Entity<CodegenAlternative>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> mpsc::UnboundedSender<LanguageModelCompletionEvent> {
|
||||
let (events_tx, events_rx) = mpsc::unbounded();
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
let completion_stream = Task::ready(Ok(events_rx.map(Ok).boxed()
|
||||
as BoxStream<
|
||||
'static,
|
||||
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
|
||||
>));
|
||||
codegen.generation = codegen.handle_completion(model, completion_stream, cx);
|
||||
});
|
||||
events_tx
|
||||
}
|
||||
|
||||
fn rewrite_tool_use(
|
||||
id: &str,
|
||||
replacement_text: &str,
|
||||
is_complete: bool,
|
||||
) -> LanguageModelCompletionEvent {
|
||||
let input = RewriteSectionInput {
|
||||
replacement_text: replacement_text.into(),
|
||||
};
|
||||
LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
|
||||
id: id.into(),
|
||||
name: REWRITE_SECTION_TOOL_NAME.into(),
|
||||
raw_input: serde_json::to_string(&input).unwrap(),
|
||||
input: serde_json::to_value(&input).unwrap(),
|
||||
is_input_complete: is_complete,
|
||||
thought_signature: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ use project::{
|
||||
Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse,
|
||||
PathMatchCandidateSet, Project, ProjectPath, Symbol, WorktreeId,
|
||||
};
|
||||
use prompt_store::{PromptStore, UserPromptId};
|
||||
use prompt_store::{PromptId, PromptStore, UserPromptId};
|
||||
use rope::Point;
|
||||
use text::{Anchor, ToPoint as _};
|
||||
use ui::prelude::*;
|
||||
@@ -1585,10 +1585,13 @@ pub(crate) fn search_rules(
|
||||
if metadata.default {
|
||||
None
|
||||
} else {
|
||||
Some(RulesContextEntry {
|
||||
prompt_id: metadata.id.as_user()?,
|
||||
title: metadata.title?,
|
||||
})
|
||||
match metadata.id {
|
||||
PromptId::EditWorkflow => None,
|
||||
PromptId::User { uuid } => Some(RulesContextEntry {
|
||||
prompt_id: uuid,
|
||||
title: metadata.title?,
|
||||
}),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use fs::Fs;
|
||||
use language_model::LanguageModel;
|
||||
use settings::{LanguageModelSelection, update_settings_file};
|
||||
use ui::App;
|
||||
|
||||
fn language_model_to_selection(model: &Arc<dyn LanguageModel>) -> LanguageModelSelection {
|
||||
LanguageModelSelection {
|
||||
provider: model.provider_id().to_string().into(),
|
||||
model: model.id().0.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_in_settings(
|
||||
model: Arc<dyn LanguageModel>,
|
||||
should_be_favorite: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let selection = language_model_to_selection(&model);
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
let agent = settings.agent.get_or_insert_default();
|
||||
if should_be_favorite {
|
||||
agent.add_favorite_model(selection.clone());
|
||||
} else {
|
||||
agent.remove_favorite_model(&selection);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1197,7 +1197,7 @@ impl InlineAssistant {
|
||||
|
||||
assist
|
||||
.editor
|
||||
.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx), cx))
|
||||
.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx)))
|
||||
.ok();
|
||||
}
|
||||
|
||||
@@ -1209,7 +1209,7 @@ impl InlineAssistant {
|
||||
if let Some(decorations) = assist.decorations.as_ref() {
|
||||
decorations.prompt_editor.update(cx, |prompt_editor, cx| {
|
||||
prompt_editor.editor.update(cx, |editor, cx| {
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor.select_all(&SelectAll, window, cx);
|
||||
})
|
||||
});
|
||||
@@ -1259,26 +1259,28 @@ impl InlineAssistant {
|
||||
let bottom = top + 1.0;
|
||||
(top, bottom)
|
||||
});
|
||||
let height_in_lines = editor.visible_line_count().unwrap_or(0.);
|
||||
let vertical_scroll_margin = editor.vertical_scroll_margin() as ScrollOffset;
|
||||
let scroll_target_top = (scroll_target_range.0 - vertical_scroll_margin)
|
||||
// Don't scroll up too far in the case of a large vertical_scroll_margin.
|
||||
.max(scroll_target_range.0 - height_in_lines / 2.0);
|
||||
let scroll_target_bottom = (scroll_target_range.1 + vertical_scroll_margin)
|
||||
// Don't scroll down past where the top would still be visible.
|
||||
.min(scroll_target_top + height_in_lines);
|
||||
let mut scroll_target_top = scroll_target_range.0;
|
||||
let mut scroll_target_bottom = scroll_target_range.1;
|
||||
|
||||
scroll_target_top -= editor.vertical_scroll_margin() as ScrollOffset;
|
||||
scroll_target_bottom += editor.vertical_scroll_margin() as ScrollOffset;
|
||||
|
||||
let height_in_lines = editor.visible_line_count().unwrap_or(0.);
|
||||
let scroll_top = editor.scroll_position(cx).y;
|
||||
let scroll_bottom = scroll_top + height_in_lines;
|
||||
|
||||
if scroll_target_top < scroll_top {
|
||||
editor.set_scroll_position(point(0., scroll_target_top), window, cx);
|
||||
} else if scroll_target_bottom > scroll_bottom {
|
||||
editor.set_scroll_position(
|
||||
point(0., scroll_target_bottom - height_in_lines),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
if (scroll_target_bottom - scroll_target_top) <= height_in_lines {
|
||||
editor.set_scroll_position(
|
||||
point(0., scroll_target_bottom - height_in_lines),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
editor.set_scroll_position(point(0., scroll_target_top), window, cx);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2269,36 +2271,6 @@ pub mod evals {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(not(feature = "unit-eval"), ignore)]
|
||||
fn eval_empty_buffer() {
|
||||
run_eval(
|
||||
20,
|
||||
1.0,
|
||||
"Write a Python hello, world program".to_string(),
|
||||
"ˇ".to_string(),
|
||||
|output| match output {
|
||||
InlineAssistantOutput::Success {
|
||||
full_buffer_text, ..
|
||||
} => {
|
||||
if full_buffer_text.is_empty() {
|
||||
EvalOutput::failed("expected some output".to_string())
|
||||
} else {
|
||||
EvalOutput::passed(format!("Produced {full_buffer_text}"))
|
||||
}
|
||||
}
|
||||
o @ InlineAssistantOutput::Failure { .. } => EvalOutput::failed(format!(
|
||||
"Assistant output does not match expected output: {:?}",
|
||||
o
|
||||
)),
|
||||
o @ InlineAssistantOutput::Malformed { .. } => EvalOutput::failed(format!(
|
||||
"Assistant output does not match expected output: {:?}",
|
||||
o
|
||||
)),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn run_eval(
|
||||
iterations: usize,
|
||||
expected_pass_ratio: f32,
|
||||
|
||||
@@ -40,9 +40,7 @@ use crate::completion_provider::{
|
||||
use crate::mention_set::paste_images_as_context;
|
||||
use crate::mention_set::{MentionSet, crease_for_mention};
|
||||
use crate::terminal_codegen::TerminalCodegen;
|
||||
use crate::{
|
||||
CycleFavoriteModels, CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext,
|
||||
};
|
||||
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext};
|
||||
|
||||
actions!(inline_assistant, [ThumbsUpResult, ThumbsDownResult]);
|
||||
|
||||
@@ -150,7 +148,7 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
.into_any_element();
|
||||
|
||||
v_flex()
|
||||
.key_context("InlineAssistant")
|
||||
.key_context("PromptEditor")
|
||||
.capture_action(cx.listener(Self::paste))
|
||||
.block_mouse_except_scroll()
|
||||
.size_full()
|
||||
@@ -164,6 +162,10 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
h_flex()
|
||||
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
|
||||
this.model_selector
|
||||
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
|
||||
}))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::move_up))
|
||||
@@ -172,15 +174,6 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
.on_action(cx.listener(Self::thumbs_down))
|
||||
.capture_action(cx.listener(Self::cycle_prev))
|
||||
.capture_action(cx.listener(Self::cycle_next))
|
||||
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
|
||||
this.model_selector
|
||||
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
|
||||
this.model_selector.update(cx, |model_selector, cx| {
|
||||
model_selector.cycle_favorite_models(window, cx);
|
||||
});
|
||||
}))
|
||||
.child(
|
||||
WithRemSize::new(ui_font_size)
|
||||
.h_full()
|
||||
@@ -364,7 +357,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
creases = insert_message_creases(&mut editor, &existing_creases, window, cx);
|
||||
|
||||
if focus {
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
}
|
||||
editor
|
||||
});
|
||||
@@ -851,73 +844,26 @@ impl<T: 'static> PromptEditor<T> {
|
||||
|
||||
if show_rating_buttons {
|
||||
buttons.push(
|
||||
h_flex()
|
||||
.pl_1()
|
||||
.gap_1()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(
|
||||
IconButton::new("thumbs-up", IconName::ThumbsUp)
|
||||
.shape(IconButtonShape::Square)
|
||||
.map(|this| {
|
||||
if rated {
|
||||
this.disabled(true)
|
||||
.icon_color(Color::Disabled)
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Good Result",
|
||||
None,
|
||||
"You already rated this result",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
} else {
|
||||
this.icon_color(Color::Muted).tooltip(
|
||||
move |_, cx| {
|
||||
Tooltip::for_action(
|
||||
"Good Result",
|
||||
&ThumbsUpResult,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.thumbs_up(&ThumbsUpResult, window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("thumbs-down", IconName::ThumbsDown)
|
||||
.shape(IconButtonShape::Square)
|
||||
.map(|this| {
|
||||
if rated {
|
||||
this.disabled(true)
|
||||
.icon_color(Color::Disabled)
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Bad Result",
|
||||
None,
|
||||
"You already rated this result",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
} else {
|
||||
this.icon_color(Color::Muted).tooltip(
|
||||
move |_, cx| {
|
||||
Tooltip::for_action(
|
||||
"Bad Result",
|
||||
&ThumbsDownResult,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.thumbs_down(&ThumbsDownResult, window, cx);
|
||||
})),
|
||||
)
|
||||
IconButton::new("thumbs-down", IconName::ThumbsDown)
|
||||
.icon_color(if rated { Color::Muted } else { Color::Default })
|
||||
.shape(IconButtonShape::Square)
|
||||
.disabled(rated)
|
||||
.tooltip(Tooltip::text("Bad result"))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.thumbs_down(&ThumbsDownResult, window, cx);
|
||||
}))
|
||||
.into_any_element(),
|
||||
);
|
||||
|
||||
buttons.push(
|
||||
IconButton::new("thumbs-up", IconName::ThumbsUp)
|
||||
.icon_color(if rated { Color::Muted } else { Color::Default })
|
||||
.shape(IconButtonShape::Square)
|
||||
.disabled(rated)
|
||||
.tooltip(Tooltip::text("Good result"))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.thumbs_up(&ThumbsUpResult, window, cx);
|
||||
}))
|
||||
.into_any_element(),
|
||||
);
|
||||
}
|
||||
@@ -981,21 +927,10 @@ impl<T: 'static> PromptEditor<T> {
|
||||
}
|
||||
|
||||
fn render_close_button(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
let focus_handle = self.editor.focus_handle(cx);
|
||||
|
||||
IconButton::new("cancel", IconName::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip({
|
||||
move |_window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Close Assistant",
|
||||
&editor::actions::Cancel,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.tooltip(Tooltip::text("Close Assistant"))
|
||||
.on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -1109,6 +1044,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
let colors = cx.theme().colors();
|
||||
|
||||
div()
|
||||
.key_context("InlineAssistEditor")
|
||||
.size_full()
|
||||
.p_2()
|
||||
.pl_1()
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
use std::{cmp::Reverse, sync::Arc};
|
||||
|
||||
use agent_settings::AgentSettings;
|
||||
use collections::{HashMap, HashSet, IndexMap};
|
||||
use collections::IndexMap;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
||||
use gpui::{
|
||||
Action, AnyElement, App, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, Task,
|
||||
};
|
||||
use language_model::{
|
||||
AuthenticateError, ConfiguredModel, IconOrSvg, LanguageModel, LanguageModelId,
|
||||
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry,
|
||||
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId,
|
||||
LanguageModelRegistry,
|
||||
};
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use settings::Settings;
|
||||
use ui::prelude::*;
|
||||
use zed_actions::agent::OpenSettings;
|
||||
|
||||
@@ -20,14 +18,12 @@ use crate::ui::{ModelSelectorFooter, ModelSelectorHeader, ModelSelectorListItem}
|
||||
|
||||
type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &mut App) + 'static>;
|
||||
type GetActiveModel = Arc<dyn Fn(&App) -> Option<ConfiguredModel> + 'static>;
|
||||
type OnToggleFavorite = Arc<dyn Fn(Arc<dyn LanguageModel>, bool, &mut App) + 'static>;
|
||||
|
||||
pub type LanguageModelSelector = Picker<LanguageModelPickerDelegate>;
|
||||
|
||||
pub fn language_model_selector(
|
||||
get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
|
||||
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
|
||||
on_toggle_favorite: impl Fn(Arc<dyn LanguageModel>, bool, &mut App) + 'static,
|
||||
popover_styles: bool,
|
||||
focus_handle: FocusHandle,
|
||||
window: &mut Window,
|
||||
@@ -36,7 +32,6 @@ pub fn language_model_selector(
|
||||
let delegate = LanguageModelPickerDelegate::new(
|
||||
get_active_model,
|
||||
on_model_changed,
|
||||
on_toggle_favorite,
|
||||
popover_styles,
|
||||
focus_handle,
|
||||
window,
|
||||
@@ -54,17 +49,7 @@ pub fn language_model_selector(
|
||||
}
|
||||
|
||||
fn all_models(cx: &App) -> GroupedModels {
|
||||
let lm_registry = LanguageModelRegistry::global(cx).read(cx);
|
||||
let providers = lm_registry.visible_providers();
|
||||
|
||||
let mut favorites_index = FavoritesIndex::default();
|
||||
|
||||
for sel in &AgentSettings::get_global(cx).favorite_models {
|
||||
favorites_index
|
||||
.entry(sel.provider.0.clone().into())
|
||||
.or_default()
|
||||
.insert(sel.model.clone().into());
|
||||
}
|
||||
let providers = LanguageModelRegistry::global(cx).read(cx).providers();
|
||||
|
||||
let recommended = providers
|
||||
.iter()
|
||||
@@ -72,7 +57,10 @@ fn all_models(cx: &App) -> GroupedModels {
|
||||
provider
|
||||
.recommended_models(cx)
|
||||
.into_iter()
|
||||
.map(|model| ModelInfo::new(&**provider, model, &favorites_index))
|
||||
.map(|model| ModelInfo {
|
||||
model,
|
||||
icon: provider.icon(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -82,44 +70,25 @@ fn all_models(cx: &App) -> GroupedModels {
|
||||
provider
|
||||
.provided_models(cx)
|
||||
.into_iter()
|
||||
.map(|model| ModelInfo::new(&**provider, model, &favorites_index))
|
||||
.map(|model| ModelInfo {
|
||||
model,
|
||||
icon: provider.icon(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
GroupedModels::new(all, recommended)
|
||||
}
|
||||
|
||||
type FavoritesIndex = HashMap<LanguageModelProviderId, HashSet<LanguageModelId>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ModelInfo {
|
||||
model: Arc<dyn LanguageModel>,
|
||||
icon: IconOrSvg,
|
||||
is_favorite: bool,
|
||||
}
|
||||
|
||||
impl ModelInfo {
|
||||
fn new(
|
||||
provider: &dyn LanguageModelProvider,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
favorites_index: &FavoritesIndex,
|
||||
) -> Self {
|
||||
let is_favorite = favorites_index
|
||||
.get(&provider.id())
|
||||
.map_or(false, |set| set.contains(&model.id()));
|
||||
|
||||
Self {
|
||||
model,
|
||||
icon: provider.icon(),
|
||||
is_favorite,
|
||||
}
|
||||
}
|
||||
icon: IconName,
|
||||
}
|
||||
|
||||
pub struct LanguageModelPickerDelegate {
|
||||
on_model_changed: OnModelChanged,
|
||||
get_active_model: GetActiveModel,
|
||||
on_toggle_favorite: OnToggleFavorite,
|
||||
all_models: Arc<GroupedModels>,
|
||||
filtered_entries: Vec<LanguageModelPickerEntry>,
|
||||
selected_index: usize,
|
||||
@@ -133,7 +102,6 @@ impl LanguageModelPickerDelegate {
|
||||
fn new(
|
||||
get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
|
||||
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
|
||||
on_toggle_favorite: impl Fn(Arc<dyn LanguageModel>, bool, &mut App) + 'static,
|
||||
popover_styles: bool,
|
||||
focus_handle: FocusHandle,
|
||||
window: &mut Window,
|
||||
@@ -149,7 +117,6 @@ impl LanguageModelPickerDelegate {
|
||||
selected_index: Self::get_active_model_index(&entries, get_active_model(cx)),
|
||||
filtered_entries: entries,
|
||||
get_active_model: Arc::new(get_active_model),
|
||||
on_toggle_favorite: Arc::new(on_toggle_favorite),
|
||||
_authenticate_all_providers_task: Self::authenticate_all_providers(cx),
|
||||
_subscriptions: vec![cx.subscribe_in(
|
||||
&LanguageModelRegistry::global(cx),
|
||||
@@ -203,7 +170,7 @@ impl LanguageModelPickerDelegate {
|
||||
fn authenticate_all_providers(cx: &mut App) -> Task<()> {
|
||||
let authenticate_all_providers = LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.visible_providers()
|
||||
.providers()
|
||||
.iter()
|
||||
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -249,61 +216,15 @@ impl LanguageModelPickerDelegate {
|
||||
pub fn active_model(&self, cx: &App) -> Option<ConfiguredModel> {
|
||||
(self.get_active_model)(cx)
|
||||
}
|
||||
|
||||
pub fn favorites_count(&self) -> usize {
|
||||
self.all_models.favorites.len()
|
||||
}
|
||||
|
||||
pub fn cycle_favorite_models(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
if self.all_models.favorites.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let active_model = (self.get_active_model)(cx);
|
||||
let active_provider_id = active_model.as_ref().map(|m| m.provider.id());
|
||||
let active_model_id = active_model.as_ref().map(|m| m.model.id());
|
||||
|
||||
let current_index = self
|
||||
.all_models
|
||||
.favorites
|
||||
.iter()
|
||||
.position(|info| {
|
||||
Some(info.model.provider_id()) == active_provider_id
|
||||
&& Some(info.model.id()) == active_model_id
|
||||
})
|
||||
.unwrap_or(usize::MAX);
|
||||
|
||||
let next_index = if current_index == usize::MAX {
|
||||
0
|
||||
} else {
|
||||
(current_index + 1) % self.all_models.favorites.len()
|
||||
};
|
||||
|
||||
let next_model = self.all_models.favorites[next_index].model.clone();
|
||||
|
||||
(self.on_model_changed)(next_model, cx);
|
||||
|
||||
// Align the picker selection with the newly-active model
|
||||
let new_index =
|
||||
Self::get_active_model_index(&self.filtered_entries, (self.get_active_model)(cx));
|
||||
self.set_selected_index(new_index, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
struct GroupedModels {
|
||||
favorites: Vec<ModelInfo>,
|
||||
recommended: Vec<ModelInfo>,
|
||||
all: IndexMap<LanguageModelProviderId, Vec<ModelInfo>>,
|
||||
}
|
||||
|
||||
impl GroupedModels {
|
||||
pub fn new(all: Vec<ModelInfo>, recommended: Vec<ModelInfo>) -> Self {
|
||||
let favorites = all
|
||||
.iter()
|
||||
.filter(|info| info.is_favorite)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
let mut all_by_provider: IndexMap<_, Vec<ModelInfo>> = IndexMap::default();
|
||||
for model in all {
|
||||
let provider = model.model.provider_id();
|
||||
@@ -315,7 +236,6 @@ impl GroupedModels {
|
||||
}
|
||||
|
||||
Self {
|
||||
favorites,
|
||||
recommended,
|
||||
all: all_by_provider,
|
||||
}
|
||||
@@ -324,18 +244,13 @@ impl GroupedModels {
|
||||
fn entries(&self) -> Vec<LanguageModelPickerEntry> {
|
||||
let mut entries = Vec::new();
|
||||
|
||||
if !self.favorites.is_empty() {
|
||||
entries.push(LanguageModelPickerEntry::Separator("Favorite".into()));
|
||||
for info in &self.favorites {
|
||||
entries.push(LanguageModelPickerEntry::Model(info.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
if !self.recommended.is_empty() {
|
||||
entries.push(LanguageModelPickerEntry::Separator("Recommended".into()));
|
||||
for info in &self.recommended {
|
||||
entries.push(LanguageModelPickerEntry::Model(info.clone()));
|
||||
}
|
||||
entries.extend(
|
||||
self.recommended
|
||||
.iter()
|
||||
.map(|info| LanguageModelPickerEntry::Model(info.clone())),
|
||||
);
|
||||
}
|
||||
|
||||
for models in self.all.values() {
|
||||
@@ -345,11 +260,12 @@ impl GroupedModels {
|
||||
entries.push(LanguageModelPickerEntry::Separator(
|
||||
models[0].model.provider_name().0,
|
||||
));
|
||||
for info in models {
|
||||
entries.push(LanguageModelPickerEntry::Model(info.clone()));
|
||||
}
|
||||
entries.extend(
|
||||
models
|
||||
.iter()
|
||||
.map(|info| LanguageModelPickerEntry::Model(info.clone())),
|
||||
);
|
||||
}
|
||||
|
||||
entries
|
||||
}
|
||||
}
|
||||
@@ -478,7 +394,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
|
||||
let configured_providers = language_model_registry
|
||||
.read(cx)
|
||||
.visible_providers()
|
||||
.providers()
|
||||
.into_iter()
|
||||
.filter(|provider| provider.is_authenticated(cx))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -545,7 +461,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
is_focused: bool,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
@@ -561,26 +477,11 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
let is_selected = Some(model_info.model.provider_id()) == active_provider_id
|
||||
&& Some(model_info.model.id()) == active_model_id;
|
||||
|
||||
let is_favorite = model_info.is_favorite;
|
||||
let handle_action_click = {
|
||||
let model = model_info.model.clone();
|
||||
let on_toggle_favorite = self.on_toggle_favorite.clone();
|
||||
cx.listener(move |picker, _, window, cx| {
|
||||
on_toggle_favorite(model.clone(), !is_favorite, cx);
|
||||
picker.refresh(window, cx);
|
||||
})
|
||||
};
|
||||
|
||||
Some(
|
||||
ModelSelectorListItem::new(ix, model_info.model.name().0)
|
||||
.map(|this| match &model_info.icon {
|
||||
IconOrSvg::Icon(icon_name) => this.icon(*icon_name),
|
||||
IconOrSvg::Svg(icon_path) => this.icon_path(icon_path.clone()),
|
||||
})
|
||||
.is_focused(is_focused)
|
||||
.is_selected(is_selected)
|
||||
.is_focused(selected)
|
||||
.is_favorite(is_favorite)
|
||||
.on_toggle_favorite(handle_action_click)
|
||||
.icon(model_info.icon)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
@@ -592,12 +493,12 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<gpui::AnyElement> {
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
if !self.popover_styles {
|
||||
return None;
|
||||
}
|
||||
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
Some(ModelSelectorFooter::new(OpenSettings.boxed_clone(), focus_handle).into_any_element())
|
||||
}
|
||||
}
|
||||
@@ -697,24 +598,11 @@ mod tests {
|
||||
}
|
||||
|
||||
fn create_models(model_specs: Vec<(&str, &str)>) -> Vec<ModelInfo> {
|
||||
create_models_with_favorites(model_specs, vec![])
|
||||
}
|
||||
|
||||
fn create_models_with_favorites(
|
||||
model_specs: Vec<(&str, &str)>,
|
||||
favorites: Vec<(&str, &str)>,
|
||||
) -> Vec<ModelInfo> {
|
||||
model_specs
|
||||
.into_iter()
|
||||
.map(|(provider, name)| {
|
||||
let is_favorite = favorites
|
||||
.iter()
|
||||
.any(|(fav_provider, fav_name)| *fav_provider == provider && *fav_name == name);
|
||||
ModelInfo {
|
||||
model: Arc::new(TestLanguageModel::new(name, provider)),
|
||||
icon: IconOrSvg::Icon(IconName::Ai),
|
||||
is_favorite,
|
||||
}
|
||||
.map(|(provider, name)| ModelInfo {
|
||||
model: Arc::new(TestLanguageModel::new(name, provider)),
|
||||
icon: IconName::Ai,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -852,93 +740,4 @@ mod tests {
|
||||
vec!["zed/claude", "zed/gemini", "copilot/claude"],
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_favorites_section_appears_when_favorites_exist(_cx: &mut TestAppContext) {
|
||||
let recommended_models = create_models(vec![("zed", "claude")]);
|
||||
let all_models = create_models_with_favorites(
|
||||
vec![("zed", "claude"), ("zed", "gemini"), ("openai", "gpt-4")],
|
||||
vec![("zed", "gemini")],
|
||||
);
|
||||
|
||||
let grouped_models = GroupedModels::new(all_models, recommended_models);
|
||||
let entries = grouped_models.entries();
|
||||
|
||||
assert!(matches!(
|
||||
entries.first(),
|
||||
Some(LanguageModelPickerEntry::Separator(s)) if s == "Favorite"
|
||||
));
|
||||
|
||||
assert_models_eq(grouped_models.favorites, vec!["zed/gemini"]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_no_favorites_section_when_no_favorites(_cx: &mut TestAppContext) {
|
||||
let recommended_models = create_models(vec![("zed", "claude")]);
|
||||
let all_models = create_models(vec![("zed", "claude"), ("zed", "gemini")]);
|
||||
|
||||
let grouped_models = GroupedModels::new(all_models, recommended_models);
|
||||
let entries = grouped_models.entries();
|
||||
|
||||
assert!(matches!(
|
||||
entries.first(),
|
||||
Some(LanguageModelPickerEntry::Separator(s)) if s == "Recommended"
|
||||
));
|
||||
|
||||
assert!(grouped_models.favorites.is_empty());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_models_have_correct_actions(_cx: &mut TestAppContext) {
|
||||
let recommended_models =
|
||||
create_models_with_favorites(vec![("zed", "claude")], vec![("zed", "claude")]);
|
||||
let all_models = create_models_with_favorites(
|
||||
vec![("zed", "claude"), ("zed", "gemini"), ("openai", "gpt-4")],
|
||||
vec![("zed", "claude")],
|
||||
);
|
||||
|
||||
let grouped_models = GroupedModels::new(all_models, recommended_models);
|
||||
let entries = grouped_models.entries();
|
||||
|
||||
for entry in &entries {
|
||||
if let LanguageModelPickerEntry::Model(info) = entry {
|
||||
if info.model.telemetry_id() == "zed/claude" {
|
||||
assert!(info.is_favorite, "zed/claude should be a favorite");
|
||||
} else {
|
||||
assert!(
|
||||
!info.is_favorite,
|
||||
"{} should not be a favorite",
|
||||
info.model.telemetry_id()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_favorites_appear_in_other_sections(_cx: &mut TestAppContext) {
|
||||
let favorites = vec![("zed", "gemini"), ("openai", "gpt-4")];
|
||||
|
||||
let recommended_models =
|
||||
create_models_with_favorites(vec![("zed", "claude")], favorites.clone());
|
||||
|
||||
let all_models = create_models_with_favorites(
|
||||
vec![
|
||||
("zed", "claude"),
|
||||
("zed", "gemini"),
|
||||
("openai", "gpt-4"),
|
||||
("openai", "gpt-3.5"),
|
||||
],
|
||||
favorites,
|
||||
);
|
||||
|
||||
let grouped_models = GroupedModels::new(all_models, recommended_models);
|
||||
|
||||
assert_models_eq(grouped_models.favorites, vec!["zed/gemini", "openai/gpt-4"]);
|
||||
assert_models_eq(grouped_models.recommended, vec!["zed/claude"]);
|
||||
assert_models_eq(
|
||||
grouped_models.all.values().flatten().cloned().collect(),
|
||||
vec!["zed/claude", "zed/gemini", "openai/gpt-4", "openai/gpt-3.5"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,9 +191,6 @@ impl Render for ProfileSelector {
|
||||
let container = || h_flex().gap_1().justify_between();
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(container().child(Label::new("Toggle Profile Menu")).child(
|
||||
KeyBinding::for_action_in(&ToggleProfileSelector, &focus_handle, cx),
|
||||
))
|
||||
.child(
|
||||
container()
|
||||
.pb_1()
|
||||
@@ -206,6 +203,9 @@ impl Render for ProfileSelector {
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.child(container().child(Label::new("Toggle Profile Menu")).child(
|
||||
KeyBinding::for_action_in(&ToggleProfileSelector, &focus_handle, cx),
|
||||
))
|
||||
.into_any()
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -127,7 +127,7 @@ impl TerminalInlineAssistant {
|
||||
if let Some(prompt_editor) = assist.prompt_editor.as_ref() {
|
||||
prompt_editor.update(cx, |this, cx| {
|
||||
this.editor.update(cx, |editor, cx| {
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor.select_all(&SelectAll, window, cx);
|
||||
});
|
||||
});
|
||||
@@ -292,7 +292,7 @@ impl TerminalInlineAssistant {
|
||||
.terminal
|
||||
.update(cx, |this, cx| {
|
||||
this.clear_block_below_cursor(cx);
|
||||
this.focus_handle(cx).focus(window, cx);
|
||||
this.focus_handle(cx).focus(window);
|
||||
})
|
||||
.log_err();
|
||||
|
||||
@@ -369,7 +369,7 @@ impl TerminalInlineAssistant {
|
||||
.terminal
|
||||
.update(cx, |this, cx| {
|
||||
this.clear_block_below_cursor(cx);
|
||||
this.focus_handle(cx).focus(window, cx);
|
||||
this.focus_handle(cx).focus(window);
|
||||
})
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
language_model_selector::{LanguageModelSelector, language_model_selector},
|
||||
ui::{BurnModeTooltip, ModelSelectorTooltip},
|
||||
ui::BurnModeTooltip,
|
||||
};
|
||||
use agent_settings::CompletionMode;
|
||||
use anyhow::Result;
|
||||
@@ -33,8 +33,7 @@ use language::{
|
||||
language_settings::{SoftWrap, all_language_settings},
|
||||
};
|
||||
use language_model::{
|
||||
ConfigurationError, IconOrSvg, LanguageModelExt, LanguageModelImage, LanguageModelRegistry,
|
||||
Role,
|
||||
ConfigurationError, LanguageModelExt, LanguageModelImage, LanguageModelRegistry, Role,
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use picker::{Picker, popover_menu::PickerPopoverMenu};
|
||||
@@ -72,9 +71,7 @@ use workspace::{
|
||||
pane,
|
||||
searchable::{SearchEvent, SearchableItem},
|
||||
};
|
||||
use zed_actions::agent::{AddSelectionToThread, PasteRaw, ToggleModelSelector};
|
||||
|
||||
use crate::CycleFavoriteModels;
|
||||
use zed_actions::agent::{AddSelectionToThread, ToggleModelSelector};
|
||||
|
||||
use crate::{slash_command::SlashCommandCompletionProvider, slash_command_picker};
|
||||
use assistant_text_thread::{
|
||||
@@ -307,31 +304,17 @@ impl TextThreadEditor {
|
||||
language_model_selector: cx.new(|cx| {
|
||||
language_model_selector(
|
||||
|cx| LanguageModelRegistry::read_global(cx).default_model(),
|
||||
{
|
||||
let fs = fs.clone();
|
||||
move |model, cx| {
|
||||
update_settings_file(fs.clone(), cx, move |settings, _| {
|
||||
let provider = model.provider_id().0.to_string();
|
||||
let model = model.id().0.to_string();
|
||||
settings.agent.get_or_insert_default().set_model(
|
||||
LanguageModelSelection {
|
||||
provider: LanguageModelProviderSetting(provider),
|
||||
model,
|
||||
},
|
||||
)
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
let fs = fs.clone();
|
||||
move |model, should_be_favorite, cx| {
|
||||
crate::favorite_models::toggle_in_settings(
|
||||
model,
|
||||
should_be_favorite,
|
||||
fs.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
move |model, cx| {
|
||||
update_settings_file(fs.clone(), cx, move |settings, _| {
|
||||
let provider = model.provider_id().0.to_string();
|
||||
let model = model.id().0.to_string();
|
||||
settings.agent.get_or_insert_default().set_model(
|
||||
LanguageModelSelection {
|
||||
provider: LanguageModelProviderSetting(provider),
|
||||
model,
|
||||
},
|
||||
)
|
||||
});
|
||||
},
|
||||
true, // Use popover styles for picker
|
||||
focus_handle,
|
||||
@@ -1342,7 +1325,7 @@ impl TextThreadEditor {
|
||||
if let Some((text, _)) = Self::get_selection_or_code_block(&context_editor_view, cx) {
|
||||
active_editor_view.update(cx, |editor, cx| {
|
||||
editor.insert(&text, window, cx);
|
||||
editor.focus_handle(cx).focus(window, cx);
|
||||
editor.focus_handle(cx).focus(window);
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1699,9 +1682,6 @@ impl TextThreadEditor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let editor_clipboard_selections = cx
|
||||
.read_from_clipboard()
|
||||
.and_then(|item| item.entries().first().cloned())
|
||||
@@ -1712,101 +1692,84 @@ impl TextThreadEditor {
|
||||
_ => None,
|
||||
});
|
||||
|
||||
// Insert creases for pasted clipboard selections that:
|
||||
// 1. Contain exactly one selection
|
||||
// 2. Have an associated file path
|
||||
// 3. Span multiple lines (not single-line selections)
|
||||
// 4. Belong to a file that exists in the current project
|
||||
let should_insert_creases = util::maybe!({
|
||||
let selections = editor_clipboard_selections.as_ref()?;
|
||||
if selections.len() > 1 {
|
||||
return Some(false);
|
||||
}
|
||||
let selection = selections.first()?;
|
||||
let file_path = selection.file_path.as_ref()?;
|
||||
let line_range = selection.line_range.as_ref()?;
|
||||
let has_file_context = editor_clipboard_selections
|
||||
.as_ref()
|
||||
.is_some_and(|selections| {
|
||||
selections
|
||||
.iter()
|
||||
.any(|sel| sel.file_path.is_some() && sel.line_range.is_some())
|
||||
});
|
||||
|
||||
if line_range.start() == line_range.end() {
|
||||
return Some(false);
|
||||
}
|
||||
if has_file_context {
|
||||
if let Some(clipboard_item) = cx.read_from_clipboard() {
|
||||
if let Some(ClipboardEntry::String(clipboard_text)) =
|
||||
clipboard_item.entries().first()
|
||||
{
|
||||
if let Some(selections) = editor_clipboard_selections {
|
||||
cx.stop_propagation();
|
||||
|
||||
Some(
|
||||
workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.project_path_for_absolute_path(file_path, cx)
|
||||
.is_some(),
|
||||
)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
let text = clipboard_text.text();
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let mut current_offset = 0;
|
||||
let weak_editor = cx.entity().downgrade();
|
||||
|
||||
if should_insert_creases && let Some(clipboard_item) = cx.read_from_clipboard() {
|
||||
if let Some(ClipboardEntry::String(clipboard_text)) = clipboard_item.entries().first() {
|
||||
if let Some(selections) = editor_clipboard_selections {
|
||||
cx.stop_propagation();
|
||||
for selection in selections {
|
||||
if let (Some(file_path), Some(line_range)) =
|
||||
(selection.file_path, selection.line_range)
|
||||
{
|
||||
let selected_text =
|
||||
&text[current_offset..current_offset + selection.len];
|
||||
let fence = assistant_slash_commands::codeblock_fence_for_path(
|
||||
file_path.to_str(),
|
||||
Some(line_range.clone()),
|
||||
);
|
||||
let formatted_text = format!("{fence}{selected_text}\n```");
|
||||
|
||||
let text = clipboard_text.text();
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let mut current_offset = 0;
|
||||
let weak_editor = cx.entity().downgrade();
|
||||
let insert_point = editor
|
||||
.selections
|
||||
.newest::<Point>(&editor.display_snapshot(cx))
|
||||
.head();
|
||||
let start_row = MultiBufferRow(insert_point.row);
|
||||
|
||||
for selection in selections {
|
||||
if let (Some(file_path), Some(line_range)) =
|
||||
(selection.file_path, selection.line_range)
|
||||
{
|
||||
let selected_text =
|
||||
&text[current_offset..current_offset + selection.len];
|
||||
let fence = assistant_slash_commands::codeblock_fence_for_path(
|
||||
file_path.to_str(),
|
||||
Some(line_range.clone()),
|
||||
);
|
||||
let formatted_text = format!("{fence}{selected_text}\n```");
|
||||
editor.insert(&formatted_text, window, cx);
|
||||
|
||||
let insert_point = editor
|
||||
.selections
|
||||
.newest::<Point>(&editor.display_snapshot(cx))
|
||||
.head();
|
||||
let start_row = MultiBufferRow(insert_point.row);
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let anchor_before = snapshot.anchor_after(insert_point);
|
||||
let anchor_after = editor
|
||||
.selections
|
||||
.newest_anchor()
|
||||
.head()
|
||||
.bias_left(&snapshot);
|
||||
|
||||
editor.insert(&formatted_text, window, cx);
|
||||
editor.insert("\n", window, cx);
|
||||
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let anchor_before = snapshot.anchor_after(insert_point);
|
||||
let anchor_after = editor
|
||||
.selections
|
||||
.newest_anchor()
|
||||
.head()
|
||||
.bias_left(&snapshot);
|
||||
let crease_text = acp_thread::selection_name(
|
||||
Some(file_path.as_ref()),
|
||||
&line_range,
|
||||
);
|
||||
|
||||
editor.insert("\n", window, cx);
|
||||
let fold_placeholder = quote_selection_fold_placeholder(
|
||||
crease_text,
|
||||
weak_editor.clone(),
|
||||
);
|
||||
let crease = Crease::inline(
|
||||
anchor_before..anchor_after,
|
||||
fold_placeholder,
|
||||
render_quote_selection_output_toggle,
|
||||
|_, _, _, _| Empty.into_any(),
|
||||
);
|
||||
editor.insert_creases(vec![crease], cx);
|
||||
editor.fold_at(start_row, window, cx);
|
||||
|
||||
let crease_text = acp_thread::selection_name(
|
||||
Some(file_path.as_ref()),
|
||||
&line_range,
|
||||
);
|
||||
|
||||
let fold_placeholder = quote_selection_fold_placeholder(
|
||||
crease_text,
|
||||
weak_editor.clone(),
|
||||
);
|
||||
let crease = Crease::inline(
|
||||
anchor_before..anchor_after,
|
||||
fold_placeholder,
|
||||
render_quote_selection_output_toggle,
|
||||
|_, _, _, _| Empty.into_any(),
|
||||
);
|
||||
editor.insert_creases(vec![crease], cx);
|
||||
editor.fold_at(start_row, window, cx);
|
||||
|
||||
current_offset += selection.len;
|
||||
if !selection.is_entire_line && current_offset < text.len() {
|
||||
current_offset += 1;
|
||||
current_offset += selection.len;
|
||||
if !selection.is_entire_line && current_offset < text.len() {
|
||||
current_offset += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1965,12 +1928,6 @@ impl TextThreadEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.paste(&editor::actions::Paste, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn update_image_blocks(&mut self, cx: &mut Context<Self>) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
@@ -2232,41 +2189,18 @@ impl TextThreadEditor {
|
||||
.default_model()
|
||||
.map(|default| default.provider);
|
||||
|
||||
let provider_icon = active_provider
|
||||
.as_ref()
|
||||
.map(|p| p.icon())
|
||||
.unwrap_or(IconOrSvg::Icon(IconName::Ai));
|
||||
let provider_icon = match active_provider {
|
||||
Some(provider) => provider.icon(),
|
||||
None => IconName::Ai,
|
||||
};
|
||||
|
||||
let focus_handle = self.editor().focus_handle(cx);
|
||||
|
||||
let (color, icon) = if self.language_model_selector_menu_handle.is_deployed() {
|
||||
(Color::Accent, IconName::ChevronUp)
|
||||
} else {
|
||||
(Color::Muted, IconName::ChevronDown)
|
||||
};
|
||||
|
||||
let provider_icon_element = match provider_icon {
|
||||
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
|
||||
IconOrSvg::Icon(name) => Icon::new(name),
|
||||
}
|
||||
.color(color)
|
||||
.size(IconSize::XSmall);
|
||||
|
||||
let show_cycle_row = self
|
||||
.language_model_selector
|
||||
.read(cx)
|
||||
.delegate
|
||||
.favorites_count()
|
||||
> 1;
|
||||
|
||||
let tooltip = Tooltip::element({
|
||||
move |_, _cx| {
|
||||
ModelSelectorTooltip::new(focus_handle.clone())
|
||||
.show_cycle_row(show_cycle_row)
|
||||
.into_any_element()
|
||||
}
|
||||
});
|
||||
|
||||
PickerPopoverMenu::new(
|
||||
self.language_model_selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
@@ -2274,7 +2208,7 @@ impl TextThreadEditor {
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(provider_icon_element)
|
||||
.child(Icon::new(provider_icon).color(color).size(IconSize::XSmall))
|
||||
.child(
|
||||
Label::new(model_name)
|
||||
.color(color)
|
||||
@@ -2283,7 +2217,9 @@ impl TextThreadEditor {
|
||||
)
|
||||
.child(Icon::new(icon).color(color).size(IconSize::XSmall)),
|
||||
),
|
||||
tooltip,
|
||||
move |_window, cx| {
|
||||
Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
|
||||
},
|
||||
gpui::Corner::BottomRight,
|
||||
cx,
|
||||
)
|
||||
@@ -2636,7 +2572,6 @@ impl Render for TextThreadEditor {
|
||||
.capture_action(cx.listener(TextThreadEditor::copy))
|
||||
.capture_action(cx.listener(TextThreadEditor::cut))
|
||||
.capture_action(cx.listener(TextThreadEditor::paste))
|
||||
.on_action(cx.listener(TextThreadEditor::paste_raw))
|
||||
.capture_action(cx.listener(TextThreadEditor::cycle_message_role))
|
||||
.capture_action(cx.listener(TextThreadEditor::confirm_command))
|
||||
.on_action(cx.listener(TextThreadEditor::assist))
|
||||
@@ -2644,11 +2579,6 @@ impl Render for TextThreadEditor {
|
||||
.on_action(move |_: &ToggleModelSelector, window, cx| {
|
||||
language_model_selector.toggle(window, cx);
|
||||
})
|
||||
.on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
|
||||
this.language_model_selector.update(cx, |selector, cx| {
|
||||
selector.delegate.cycle_favorite_models(window, cx);
|
||||
});
|
||||
}))
|
||||
.size_full()
|
||||
.child(
|
||||
div()
|
||||
|
||||
@@ -222,8 +222,8 @@ impl Render for AcpOnboardingModal {
|
||||
acp_onboarding_event!("Canceled", trigger = "Action");
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
|
||||
this.focus_handle.focus(window, cx);
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
|
||||
this.focus_handle.focus(window);
|
||||
}))
|
||||
.child(illustration)
|
||||
.child(
|
||||
|
||||
@@ -230,8 +230,8 @@ impl Render for ClaudeCodeOnboardingModal {
|
||||
claude_code_onboarding_event!("Canceled", trigger = "Action");
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
|
||||
this.focus_handle.focus(window, cx);
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
|
||||
this.focus_handle.focus(window);
|
||||
}))
|
||||
.child(illustration)
|
||||
.child(
|
||||
|
||||
@@ -27,7 +27,7 @@ impl RenderOnce for HoldForDefault {
|
||||
PlatformStyle::platform(),
|
||||
None,
|
||||
Some(TextSize::Default.rems(cx).into()),
|
||||
false,
|
||||
true,
|
||||
)))
|
||||
.child(div().map(|this| {
|
||||
if self.is_default {
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
use gpui::{Action, ClickEvent, FocusHandle, prelude::*};
|
||||
use ui::{ElevationIndex, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*};
|
||||
use zed_actions::agent::ToggleModelSelector;
|
||||
|
||||
use crate::CycleFavoriteModels;
|
||||
|
||||
enum ModelIcon {
|
||||
Name(IconName),
|
||||
Path(SharedString),
|
||||
}
|
||||
use gpui::{Action, FocusHandle, prelude::*};
|
||||
use ui::{KeyBinding, ListItem, ListItemSpacing, prelude::*};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ModelSelectorHeader {
|
||||
@@ -47,11 +39,9 @@ impl RenderOnce for ModelSelectorHeader {
|
||||
pub struct ModelSelectorListItem {
|
||||
index: usize,
|
||||
title: SharedString,
|
||||
icon: Option<ModelIcon>,
|
||||
icon: Option<IconName>,
|
||||
is_selected: bool,
|
||||
is_focused: bool,
|
||||
is_favorite: bool,
|
||||
on_toggle_favorite: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
||||
}
|
||||
|
||||
impl ModelSelectorListItem {
|
||||
@@ -62,18 +52,11 @@ impl ModelSelectorListItem {
|
||||
icon: None,
|
||||
is_selected: false,
|
||||
is_focused: false,
|
||||
is_favorite: false,
|
||||
on_toggle_favorite: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon(mut self, icon: IconName) -> Self {
|
||||
self.icon = Some(ModelIcon::Name(icon));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn icon_path(mut self, path: SharedString) -> Self {
|
||||
self.icon = Some(ModelIcon::Path(path));
|
||||
self.icon = Some(icon);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -86,19 +69,6 @@ impl ModelSelectorListItem {
|
||||
self.is_focused = is_focused;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_favorite(mut self, is_favorite: bool) -> Self {
|
||||
self.is_favorite = is_favorite;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_toggle_favorite(
|
||||
mut self,
|
||||
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_toggle_favorite = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ModelSelectorListItem {
|
||||
@@ -109,8 +79,6 @@ impl RenderOnce for ModelSelectorListItem {
|
||||
Color::Muted
|
||||
};
|
||||
|
||||
let is_favorite = self.is_favorite;
|
||||
|
||||
ListItem::new(self.index)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
@@ -121,35 +89,19 @@ impl RenderOnce for ModelSelectorListItem {
|
||||
.gap_1p5()
|
||||
.when_some(self.icon, |this, icon| {
|
||||
this.child(
|
||||
match icon {
|
||||
ModelIcon::Name(icon_name) => Icon::new(icon_name),
|
||||
ModelIcon::Path(icon_path) => Icon::from_external_svg(icon_path),
|
||||
}
|
||||
.color(model_icon_color)
|
||||
.size(IconSize::Small),
|
||||
Icon::new(icon)
|
||||
.color(model_icon_color)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
})
|
||||
.child(Label::new(self.title).truncate()),
|
||||
)
|
||||
.end_slot(div().pr_2().when(self.is_selected, |this| {
|
||||
this.child(Icon::new(IconName::Check).color(Color::Accent))
|
||||
}))
|
||||
.end_hover_slot(div().pr_1p5().when_some(self.on_toggle_favorite, {
|
||||
|this, handle_click| {
|
||||
let (icon, color, tooltip) = if is_favorite {
|
||||
(IconName::StarFilled, Color::Accent, "Unfavorite Model")
|
||||
} else {
|
||||
(IconName::Star, Color::Default, "Favorite Model")
|
||||
};
|
||||
this.child(
|
||||
IconButton::new(("toggle-favorite", self.index), icon)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.icon_color(color)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text(tooltip))
|
||||
.on_click(move |event, window, cx| (handle_click)(event, window, cx)),
|
||||
)
|
||||
}
|
||||
this.child(
|
||||
Icon::new(IconName::Check)
|
||||
.color(Color::Accent)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -193,57 +145,3 @@ impl RenderOnce for ModelSelectorFooter {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ModelSelectorTooltip {
|
||||
focus_handle: FocusHandle,
|
||||
show_cycle_row: bool,
|
||||
}
|
||||
|
||||
impl ModelSelectorTooltip {
|
||||
pub fn new(focus_handle: FocusHandle) -> Self {
|
||||
Self {
|
||||
focus_handle,
|
||||
show_cycle_row: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_cycle_row(mut self, show: bool) -> Self {
|
||||
self.show_cycle_row = show;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ModelSelectorTooltip {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(Label::new("Change Model"))
|
||||
.child(KeyBinding::for_action_in(
|
||||
&ToggleModelSelector,
|
||||
&self.focus_handle,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.when(self.show_cycle_row, |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.pt_1()
|
||||
.gap_2()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.justify_between()
|
||||
.child(Label::new("Cycle Favorited Models"))
|
||||
.child(KeyBinding::for_action_in(
|
||||
&CycleFavoriteModels,
|
||||
&self.focus_handle,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,8 +83,8 @@ impl Render for AgentOnboardingModal {
|
||||
agent_onboarding_event!("Canceled", trigger = "Action");
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
|
||||
this.focus_handle.focus(window, cx);
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
|
||||
this.focus_handle.focus(window);
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
|
||||
@@ -12,10 +12,6 @@ workspace = true
|
||||
path = "src/agent_ui_v2.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["agent/test-support"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
agent.workspace = true
|
||||
agent_servers.workspace = true
|
||||
@@ -42,6 +38,3 @@ time_format.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
agent = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -1 +1 @@
|
||||
../../LICENSE-GPL
|
||||
LICENSE-GPL
|
||||
@@ -1,5 +1,5 @@
|
||||
use agent::{HistoryEntry, HistoryStore};
|
||||
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta, Utc};
|
||||
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
@@ -411,22 +411,7 @@ impl AcpThreadHistory {
|
||||
let selected = ix == self.selected_index;
|
||||
let hovered = Some(ix) == self.hovered_index;
|
||||
let timestamp = entry.updated_at().timestamp();
|
||||
|
||||
let display_text = match format {
|
||||
EntryTimeFormat::DateAndTime => {
|
||||
let entry_time = entry.updated_at();
|
||||
let now = Utc::now();
|
||||
let duration = now.signed_duration_since(entry_time);
|
||||
let days = duration.num_days();
|
||||
|
||||
format!("{}d", days)
|
||||
}
|
||||
EntryTimeFormat::TimeOnly => format.format_timestamp(timestamp, self.local_timezone),
|
||||
};
|
||||
|
||||
let title = entry.title().clone();
|
||||
let full_date =
|
||||
EntryTimeFormat::DateAndTime.format_timestamp(timestamp, self.local_timezone);
|
||||
let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
@@ -447,14 +432,11 @@ impl AcpThreadHistory {
|
||||
.truncate(),
|
||||
)
|
||||
.child(
|
||||
Label::new(display_text)
|
||||
Label::new(thread_timestamp)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
),
|
||||
)
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::with_meta(title.clone(), None, full_date.clone(), cx)
|
||||
})
|
||||
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
|
||||
if *is_hovered {
|
||||
this.hovered_index = Some(ix);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use gpui::{Action, IntoElement, ParentElement, RenderOnce, point};
|
||||
use language_model::{IconOrSvg, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
|
||||
use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
|
||||
use ui::{Divider, List, ListBulletItem, prelude::*};
|
||||
|
||||
pub struct ApiKeysWithProviders {
|
||||
configured_providers: Vec<(IconOrSvg, SharedString)>,
|
||||
configured_providers: Vec<(IconName, SharedString)>,
|
||||
}
|
||||
|
||||
impl ApiKeysWithProviders {
|
||||
@@ -13,8 +13,7 @@ impl ApiKeysWithProviders {
|
||||
|this: &mut Self, _registry, event: &language_model::Event, cx| match event {
|
||||
language_model::Event::ProviderStateChanged(_)
|
||||
| language_model::Event::AddedProvider(_)
|
||||
| language_model::Event::RemovedProvider(_)
|
||||
| language_model::Event::ProvidersChanged => {
|
||||
| language_model::Event::RemovedProvider(_) => {
|
||||
this.configured_providers = Self::compute_configured_providers(cx)
|
||||
}
|
||||
_ => {}
|
||||
@@ -27,9 +26,9 @@ impl ApiKeysWithProviders {
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_configured_providers(cx: &App) -> Vec<(IconOrSvg, SharedString)> {
|
||||
fn compute_configured_providers(cx: &App) -> Vec<(IconName, SharedString)> {
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.visible_providers()
|
||||
.providers()
|
||||
.iter()
|
||||
.filter(|provider| {
|
||||
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
|
||||
@@ -48,14 +47,7 @@ impl Render for ApiKeysWithProviders {
|
||||
.map(|(icon, name)| {
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
match icon {
|
||||
IconOrSvg::Icon(icon_name) => Icon::new(icon_name),
|
||||
IconOrSvg::Svg(icon_path) => Icon::from_external_svg(icon_path),
|
||||
}
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
|
||||
.child(Label::new(name))
|
||||
});
|
||||
div()
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{AgentPanelOnboardingCard, ApiKeysWithoutProviders, ZedAiOnboarding};
|
||||
pub struct AgentPanelOnboarding {
|
||||
user_store: Entity<UserStore>,
|
||||
client: Arc<Client>,
|
||||
has_configured_providers: bool,
|
||||
configured_providers: Vec<(IconName, SharedString)>,
|
||||
continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||
}
|
||||
|
||||
@@ -27,9 +27,8 @@ impl AgentPanelOnboarding {
|
||||
|this: &mut Self, _registry, event: &language_model::Event, cx| match event {
|
||||
language_model::Event::ProviderStateChanged(_)
|
||||
| language_model::Event::AddedProvider(_)
|
||||
| language_model::Event::RemovedProvider(_)
|
||||
| language_model::Event::ProvidersChanged => {
|
||||
this.has_configured_providers = Self::has_configured_providers(cx)
|
||||
| language_model::Event::RemovedProvider(_) => {
|
||||
this.configured_providers = Self::compute_available_providers(cx)
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
@@ -39,16 +38,20 @@ impl AgentPanelOnboarding {
|
||||
Self {
|
||||
user_store,
|
||||
client,
|
||||
has_configured_providers: Self::has_configured_providers(cx),
|
||||
configured_providers: Self::compute_available_providers(cx),
|
||||
continue_with_zed_ai: Arc::new(continue_with_zed_ai),
|
||||
}
|
||||
}
|
||||
|
||||
fn has_configured_providers(cx: &App) -> bool {
|
||||
fn compute_available_providers(cx: &App) -> Vec<(IconName, SharedString)> {
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.visible_providers()
|
||||
.providers()
|
||||
.iter()
|
||||
.any(|provider| provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID)
|
||||
.filter(|provider| {
|
||||
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
|
||||
})
|
||||
.map(|provider| (provider.icon(), provider.name().0))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +81,7 @@ impl Render for AgentPanelOnboarding {
|
||||
}),
|
||||
)
|
||||
.map(|this| {
|
||||
if enrolled_in_trial || is_pro_user || self.has_configured_providers {
|
||||
if enrolled_in_trial || is_pro_user || !self.configured_providers.is_empty() {
|
||||
this
|
||||
} else {
|
||||
this.child(ApiKeysWithoutProviders::new())
|
||||
|
||||
@@ -1052,71 +1052,6 @@ pub fn parse_prompt_too_long(message: &str) -> Option<u64> {
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Request body for the token counting API.
|
||||
/// Similar to `Request` but without `max_tokens` since it's not needed for counting.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CountTokensRequest {
|
||||
pub model: String,
|
||||
pub messages: Vec<Message>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub system: Option<StringOrContents>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub tools: Vec<Tool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub thinking: Option<Thinking>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub tool_choice: Option<ToolChoice>,
|
||||
}
|
||||
|
||||
/// Response from the token counting API.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CountTokensResponse {
|
||||
pub input_tokens: u64,
|
||||
}
|
||||
|
||||
/// Count the number of tokens in a message without creating it.
|
||||
pub async fn count_tokens(
|
||||
client: &dyn HttpClient,
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
request: CountTokensRequest,
|
||||
) -> Result<CountTokensResponse, AnthropicError> {
|
||||
let uri = format!("{api_url}/v1/messages/count_tokens");
|
||||
|
||||
let request_builder = HttpRequest::builder()
|
||||
.method(Method::POST)
|
||||
.uri(uri)
|
||||
.header("Anthropic-Version", "2023-06-01")
|
||||
.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 http_request = request_builder
|
||||
.body(AsyncBody::from(serialized_request))
|
||||
.map_err(AnthropicError::BuildRequestBody)?;
|
||||
|
||||
let mut response = client
|
||||
.send(http_request)
|
||||
.await
|
||||
.map_err(AnthropicError::HttpSend)?;
|
||||
|
||||
let rate_limits = RateLimitInfo::from_headers(response.headers());
|
||||
|
||||
if response.status().is_success() {
|
||||
let mut body = String::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_string(&mut body)
|
||||
.await
|
||||
.map_err(AnthropicError::ReadResponse)?;
|
||||
|
||||
serde_json::from_str(&body).map_err(AnthropicError::DeserializeResponse)
|
||||
} else {
|
||||
Err(handle_error_response(response, rate_limits).await)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_match_window_exceeded() {
|
||||
let error = ApiError {
|
||||
|
||||
@@ -87,7 +87,7 @@ pub async fn stream_completion(
|
||||
Ok(None) => None,
|
||||
Err(err) => Some((
|
||||
Err(BedrockError::ClientError(anyhow!(
|
||||
"{}",
|
||||
"{:?}",
|
||||
aws_sdk_bedrockruntime::error::DisplayErrorContext(err)
|
||||
))),
|
||||
stream,
|
||||
|
||||
@@ -314,12 +314,6 @@ impl BufferDiffSnapshot {
|
||||
self.inner.hunks.is_empty()
|
||||
}
|
||||
|
||||
pub fn base_text_string(&self) -> Option<String> {
|
||||
self.inner
|
||||
.base_text_exists
|
||||
.then(|| self.inner.base_text.text())
|
||||
}
|
||||
|
||||
pub fn secondary_diff(&self) -> Option<&BufferDiffSnapshot> {
|
||||
self.secondary_diff.as_deref()
|
||||
}
|
||||
@@ -622,11 +616,13 @@ impl BufferDiffInner {
|
||||
secondary: Option<&'a Self>,
|
||||
) -> impl 'a + Iterator<Item = DiffHunk> {
|
||||
let range = range.to_offset(buffer);
|
||||
println!(" >>> range = {range:?}");
|
||||
|
||||
let mut cursor = self
|
||||
.hunks
|
||||
.filter::<_, DiffHunkSummary>(buffer, move |summary| {
|
||||
let summary_range = summary.buffer_range.to_offset(buffer);
|
||||
println!(" >>> summary_range = {:?}", summary_range);
|
||||
let before_start = summary_range.end < range.start;
|
||||
let after_end = summary_range.start > range.end;
|
||||
!before_start && !after_end
|
||||
@@ -1165,34 +1161,6 @@ impl BufferDiff {
|
||||
new_index_text
|
||||
}
|
||||
|
||||
pub fn stage_or_unstage_all_hunks(
|
||||
&mut self,
|
||||
stage: bool,
|
||||
buffer: &text::BufferSnapshot,
|
||||
file_exists: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let hunks = self
|
||||
.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx)
|
||||
.collect::<Vec<_>>();
|
||||
let Some(secondary) = self.secondary_diff.as_ref() else {
|
||||
return;
|
||||
};
|
||||
self.inner.stage_or_unstage_hunks_impl(
|
||||
&secondary.read(cx).inner,
|
||||
stage,
|
||||
&hunks,
|
||||
buffer,
|
||||
file_exists,
|
||||
);
|
||||
if let Some((first, last)) = hunks.first().zip(hunks.last()) {
|
||||
let changed_range = first.buffer_range.start..last.buffer_range.end;
|
||||
cx.emit(BufferDiffEvent::DiffChanged {
|
||||
changed_range: Some(changed_range),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn range_to_hunk_range(
|
||||
&self,
|
||||
range: Range<Anchor>,
|
||||
@@ -2189,7 +2157,7 @@ mod tests {
|
||||
let range = diff_1.inner.compare(&empty_diff.inner, &buffer).unwrap();
|
||||
assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0));
|
||||
|
||||
// Edit does affects the diff because it recalculates word diffs.
|
||||
// Edit does not affect the diff.
|
||||
buffer.edit_via_marked_text(
|
||||
&"
|
||||
one
|
||||
@@ -2204,14 +2172,7 @@ mod tests {
|
||||
.unindent(),
|
||||
);
|
||||
let diff_2 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
|
||||
assert_eq!(
|
||||
Point::new(4, 0)..Point::new(5, 0),
|
||||
diff_2
|
||||
.inner
|
||||
.compare(&diff_1.inner, &buffer)
|
||||
.unwrap()
|
||||
.to_point(&buffer)
|
||||
);
|
||||
assert_eq!(None, diff_2.inner.compare(&diff_1.inner, &buffer));
|
||||
|
||||
// Edit turns a deletion hunk into a modification.
|
||||
buffer.edit_via_marked_text(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use edit_prediction_context::{EditPredictionExcerpt, EditPredictionExcerptOptions};
|
||||
use edit_prediction_types::{EditPrediction, EditPredictionDelegate};
|
||||
use edit_prediction_types::{Direction, EditPrediction, EditPredictionDelegate};
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::{App, Context, Entity, Task};
|
||||
use http_client::HttpClient;
|
||||
@@ -300,6 +300,16 @@ impl EditPredictionDelegate for CodestralEditPredictionDelegate {
|
||||
}));
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
&mut self,
|
||||
_buffer: Entity<Buffer>,
|
||||
_cursor_position: Anchor,
|
||||
_direction: Direction,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
// Codestral doesn't support multiple completions, so cycling does nothing
|
||||
}
|
||||
|
||||
fn accept(&mut self, _cx: &mut Context<Self>) {
|
||||
log::debug!("Codestral: Completion accepted");
|
||||
self.pending_request = None;
|
||||
|
||||
@@ -113,7 +113,7 @@ impl CopilotSweAgentBot {
|
||||
const USER_ID: i32 = 198982749;
|
||||
/// The alias of the GitHub copilot user. Although https://api.github.com/users/copilot
|
||||
/// yields a 404, GitHub still refers to the copilot bot user as @Copilot in some cases.
|
||||
const NAME_ALIAS: &'static str = "Copilot";
|
||||
const NAME_ALIAS: &'static str = "copilot";
|
||||
|
||||
/// Returns the `created_at` timestamp for the Dependabot bot user.
|
||||
fn created_at() -> &'static NaiveDateTime {
|
||||
|
||||
@@ -6745,13 +6745,8 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
// Split pane to the right
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
pane.split(
|
||||
workspace::SplitDirection::Right,
|
||||
workspace::SplitMode::default(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.split(workspace::SplitDirection::Right, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let right_pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
@@ -4,7 +4,6 @@ use collections::{HashMap, HashSet};
|
||||
|
||||
use dap::{Capabilities, adapters::DebugTaskDefinition, transport::RequestHandling};
|
||||
use debugger_ui::debugger_panel::DebugPanel;
|
||||
use editor::{Editor, EditorMode, MultiBuffer};
|
||||
use extension::ExtensionHostProxy;
|
||||
use fs::{FakeFs, Fs as _, RemoveOptions};
|
||||
use futures::StreamExt as _;
|
||||
@@ -13,30 +12,22 @@ use http_client::BlockedHttpClient;
|
||||
use language::{
|
||||
FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
|
||||
language_settings::{Formatter, FormatterList, language_settings},
|
||||
rust_lang, tree_sitter_typescript,
|
||||
tree_sitter_typescript,
|
||||
};
|
||||
use node_runtime::NodeRuntime;
|
||||
use project::{
|
||||
ProjectPath,
|
||||
debugger::session::ThreadId,
|
||||
lsp_store::{FormatTrigger, LspFormatTarget},
|
||||
trusted_worktrees::{PathTrust, TrustedWorktrees},
|
||||
};
|
||||
use remote::RemoteClient;
|
||||
use remote_server::{HeadlessAppState, HeadlessProject};
|
||||
use rpc::proto;
|
||||
use serde_json::json;
|
||||
use settings::{
|
||||
InlayHintSettingsContent, LanguageServerFormatterSpecifier, PrettierSettingsContent,
|
||||
SettingsStore,
|
||||
};
|
||||
use settings::{LanguageServerFormatterSpecifier, PrettierSettingsContent, SettingsStore};
|
||||
use std::{
|
||||
path::Path,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
},
|
||||
time::Duration,
|
||||
sync::{Arc, atomic::AtomicUsize},
|
||||
};
|
||||
use task::TcpArgumentsTemplate;
|
||||
use util::{path, rel_path::rel_path};
|
||||
@@ -99,14 +90,13 @@ async fn test_sharing_an_ssh_remote_project(
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
|
||||
let (project_a, worktree_id) = client_a
|
||||
.build_ssh_project(path!("/code/project1"), client_ssh, false, cx_a)
|
||||
.build_ssh_project(path!("/code/project1"), client_ssh, cx_a)
|
||||
.await;
|
||||
|
||||
// While the SSH worktree is being scanned, user A shares the remote project.
|
||||
@@ -260,14 +250,13 @@ async fn test_ssh_collaboration_git_branches(
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
|
||||
let (project_a, _) = client_a
|
||||
.build_ssh_project("/project", client_ssh, false, cx_a)
|
||||
.build_ssh_project("/project", client_ssh, cx_a)
|
||||
.await;
|
||||
|
||||
// While the SSH worktree is being scanned, user A shares the remote project.
|
||||
@@ -465,14 +454,13 @@ async fn test_ssh_collaboration_formatting_with_prettier(
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
|
||||
let (project_a, worktree_id) = client_a
|
||||
.build_ssh_project(path!("/project"), client_ssh, false, cx_a)
|
||||
.build_ssh_project(path!("/project"), client_ssh, cx_a)
|
||||
.await;
|
||||
|
||||
// While the SSH worktree is being scanned, user A shares the remote project.
|
||||
@@ -627,7 +615,6 @@ async fn test_remote_server_debugger(
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -640,7 +627,7 @@ async fn test_remote_server_debugger(
|
||||
command_palette_hooks::init(cx);
|
||||
});
|
||||
let (project_a, _) = client_a
|
||||
.build_ssh_project(path!("/code"), client_ssh.clone(), false, cx_a)
|
||||
.build_ssh_project(path!("/code"), client_ssh.clone(), cx_a)
|
||||
.await;
|
||||
|
||||
let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
||||
@@ -736,7 +723,6 @@ async fn test_slow_adapter_startup_retries(
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -749,7 +735,7 @@ async fn test_slow_adapter_startup_retries(
|
||||
command_palette_hooks::init(cx);
|
||||
});
|
||||
let (project_a, _) = client_a
|
||||
.build_ssh_project(path!("/code"), client_ssh.clone(), false, cx_a)
|
||||
.build_ssh_project(path!("/code"), client_ssh.clone(), cx_a)
|
||||
.await;
|
||||
|
||||
let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
||||
@@ -852,261 +838,3 @@ async fn test_slow_adapter_startup_retries(
|
||||
|
||||
shutdown_session.await.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
||||
use project::trusted_worktrees::RemoteHostLocation;
|
||||
|
||||
cx_a.update(|cx| {
|
||||
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
||||
project::trusted_worktrees::init(HashMap::default(), None, None, cx);
|
||||
});
|
||||
server_cx.update(|cx| {
|
||||
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
||||
project::trusted_worktrees::init(HashMap::default(), None, None, cx);
|
||||
});
|
||||
|
||||
let mut server = TestServer::start(cx_a.executor().clone()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
|
||||
let server_name = "override-rust-analyzer";
|
||||
let lsp_inlay_hint_request_count = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
|
||||
let remote_fs = FakeFs::new(server_cx.executor());
|
||||
remote_fs
|
||||
.insert_tree(
|
||||
path!("/projects"),
|
||||
json!({
|
||||
"project_a": {
|
||||
".zed": {
|
||||
"settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
|
||||
},
|
||||
"main.rs": "fn main() {}"
|
||||
},
|
||||
"project_b": { "lib.rs": "pub fn lib() {}" }
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
server_cx.update(HeadlessProject::init);
|
||||
let remote_http_client = Arc::new(BlockedHttpClient);
|
||||
let node = NodeRuntime::unavailable();
|
||||
let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
|
||||
languages.add(rust_lang());
|
||||
|
||||
let capabilities = lsp::ServerCapabilities {
|
||||
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
|
||||
..lsp::ServerCapabilities::default()
|
||||
};
|
||||
let mut fake_language_servers = languages.register_fake_lsp(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
name: server_name,
|
||||
capabilities: capabilities.clone(),
|
||||
initializer: Some(Box::new({
|
||||
let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
|
||||
move |fake_server| {
|
||||
let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
|
||||
fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
|
||||
move |_params, _| {
|
||||
lsp_inlay_hint_request_count.fetch_add(1, Ordering::Release);
|
||||
async move {
|
||||
Ok(Some(vec![lsp::InlayHint {
|
||||
position: lsp::Position::new(0, 0),
|
||||
label: lsp::InlayHintLabel::String("hint".to_string()),
|
||||
kind: None,
|
||||
text_edits: None,
|
||||
tooltip: None,
|
||||
padding_left: None,
|
||||
padding_right: None,
|
||||
data: None,
|
||||
}]))
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
})),
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
|
||||
let _headless_project = server_cx.new(|cx| {
|
||||
HeadlessProject::new(
|
||||
HeadlessAppState {
|
||||
session: server_ssh,
|
||||
fs: remote_fs.clone(),
|
||||
http_client: remote_http_client,
|
||||
node_runtime: node,
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
true,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
|
||||
let (project_a, worktree_id_a) = client_a
|
||||
.build_ssh_project(path!("/projects/project_a"), client_ssh.clone(), true, cx_a)
|
||||
.await;
|
||||
|
||||
cx_a.update(|cx| {
|
||||
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
||||
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
let language_settings = &mut settings.project.all_languages.defaults;
|
||||
language_settings.inlay_hints = Some(InlayHintSettingsContent {
|
||||
enabled: Some(true),
|
||||
..InlayHintSettingsContent::default()
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
project_a
|
||||
.update(cx_a, |project, cx| {
|
||||
project.languages().add(rust_lang());
|
||||
project.languages().register_fake_lsp_adapter(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
name: server_name,
|
||||
capabilities,
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
project.find_or_create_worktree(path!("/projects/project_b"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx_a.run_until_parked();
|
||||
|
||||
let worktree_ids = project_a.read_with(cx_a, |project, cx| {
|
||||
project
|
||||
.worktrees(cx)
|
||||
.map(|wt| wt.read(cx).id())
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
assert_eq!(worktree_ids.len(), 2);
|
||||
|
||||
let remote_host = project_a.read_with(cx_a, |project, cx| {
|
||||
project
|
||||
.remote_connection_options(cx)
|
||||
.map(RemoteHostLocation::from)
|
||||
});
|
||||
|
||||
let trusted_worktrees =
|
||||
cx_a.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
|
||||
|
||||
let can_trust_a =
|
||||
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[0], cx));
|
||||
let can_trust_b =
|
||||
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[1], cx));
|
||||
assert!(!can_trust_a, "project_a should be restricted initially");
|
||||
assert!(!can_trust_b, "project_b should be restricted initially");
|
||||
|
||||
let worktree_store = project_a.read_with(cx_a, |project, _| project.worktree_store());
|
||||
let has_restricted = trusted_worktrees.read_with(cx_a, |store, cx| {
|
||||
store.has_restricted_worktrees(&worktree_store, cx)
|
||||
});
|
||||
assert!(has_restricted, "should have restricted worktrees");
|
||||
|
||||
let buffer_before_approval = project_a
|
||||
.update(cx_a, |project, cx| {
|
||||
project.open_buffer((worktree_id_a, rel_path("main.rs")), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (editor, cx_a) = cx_a.add_window_view(|window, cx| {
|
||||
Editor::new(
|
||||
EditorMode::full(),
|
||||
cx.new(|cx| MultiBuffer::singleton(buffer_before_approval.clone(), cx)),
|
||||
Some(project_a.clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx_a.run_until_parked();
|
||||
let fake_language_server = fake_language_servers.next();
|
||||
|
||||
cx_a.read(|cx| {
|
||||
let file = buffer_before_approval.read(cx).file();
|
||||
assert_eq!(
|
||||
language_settings(Some("Rust".into()), file, cx).language_servers,
|
||||
["...".to_string()],
|
||||
"remote .zed/settings.json must not sync before trust approval"
|
||||
)
|
||||
});
|
||||
|
||||
editor.update_in(cx_a, |editor, window, cx| {
|
||||
editor.handle_input("1", window, cx);
|
||||
});
|
||||
cx_a.run_until_parked();
|
||||
cx_a.executor().advance_clock(Duration::from_secs(1));
|
||||
assert_eq!(
|
||||
lsp_inlay_hint_request_count.load(Ordering::Acquire),
|
||||
0,
|
||||
"inlay hints must not be queried before trust approval"
|
||||
);
|
||||
|
||||
trusted_worktrees.update(cx_a, |store, cx| {
|
||||
store.trust(
|
||||
HashSet::from_iter([PathTrust::Worktree(worktree_ids[0])]),
|
||||
remote_host.clone(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx_a.run_until_parked();
|
||||
|
||||
cx_a.read(|cx| {
|
||||
let file = buffer_before_approval.read(cx).file();
|
||||
assert_eq!(
|
||||
language_settings(Some("Rust".into()), file, cx).language_servers,
|
||||
["override-rust-analyzer".to_string()],
|
||||
"remote .zed/settings.json should sync after trust approval"
|
||||
)
|
||||
});
|
||||
let _fake_language_server = fake_language_server.await.unwrap();
|
||||
editor.update_in(cx_a, |editor, window, cx| {
|
||||
editor.handle_input("1", window, cx);
|
||||
});
|
||||
cx_a.run_until_parked();
|
||||
cx_a.executor().advance_clock(Duration::from_secs(1));
|
||||
assert!(
|
||||
lsp_inlay_hint_request_count.load(Ordering::Acquire) > 0,
|
||||
"inlay hints should be queried after trust approval"
|
||||
);
|
||||
|
||||
let can_trust_a =
|
||||
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[0], cx));
|
||||
let can_trust_b =
|
||||
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[1], cx));
|
||||
assert!(can_trust_a, "project_a should be trusted after trust()");
|
||||
assert!(!can_trust_b, "project_b should still be restricted");
|
||||
|
||||
trusted_worktrees.update(cx_a, |store, cx| {
|
||||
store.trust(
|
||||
HashSet::from_iter([PathTrust::Worktree(worktree_ids[1])]),
|
||||
remote_host.clone(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let can_trust_a =
|
||||
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[0], cx));
|
||||
let can_trust_b =
|
||||
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[1], cx));
|
||||
assert!(can_trust_a, "project_a should remain trusted");
|
||||
assert!(can_trust_b, "project_b should now be trusted");
|
||||
|
||||
let has_restricted_after = trusted_worktrees.read_with(cx_a, |store, cx| {
|
||||
store.has_restricted_worktrees(&worktree_store, cx)
|
||||
});
|
||||
assert!(
|
||||
!has_restricted_after,
|
||||
"should have no restricted worktrees after trusting both"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -761,7 +761,6 @@ impl TestClient {
|
||||
&self,
|
||||
root_path: impl AsRef<Path>,
|
||||
ssh: Entity<RemoteClient>,
|
||||
init_worktree_trust: bool,
|
||||
cx: &mut TestAppContext,
|
||||
) -> (Entity<Project>, WorktreeId) {
|
||||
let project = cx.update(|cx| {
|
||||
@@ -772,7 +771,6 @@ impl TestClient {
|
||||
self.app_state.user_store.clone(),
|
||||
self.app_state.languages.clone(),
|
||||
self.app_state.fs.clone(),
|
||||
init_worktree_trust,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -841,7 +839,6 @@ impl TestClient {
|
||||
self.app_state.languages.clone(),
|
||||
self.app_state.fs.clone(),
|
||||
None,
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1252,7 +1252,7 @@ impl CollabPanel {
|
||||
context_menu
|
||||
});
|
||||
|
||||
window.focus(&context_menu.focus_handle(cx), cx);
|
||||
window.focus(&context_menu.focus_handle(cx));
|
||||
let subscription = cx.subscribe_in(
|
||||
&context_menu,
|
||||
window,
|
||||
@@ -1424,7 +1424,7 @@ impl CollabPanel {
|
||||
context_menu
|
||||
});
|
||||
|
||||
window.focus(&context_menu.focus_handle(cx), cx);
|
||||
window.focus(&context_menu.focus_handle(cx));
|
||||
let subscription = cx.subscribe_in(
|
||||
&context_menu,
|
||||
window,
|
||||
@@ -1487,7 +1487,7 @@ impl CollabPanel {
|
||||
})
|
||||
});
|
||||
|
||||
window.focus(&context_menu.focus_handle(cx), cx);
|
||||
window.focus(&context_menu.focus_handle(cx));
|
||||
let subscription = cx.subscribe_in(
|
||||
&context_menu,
|
||||
window,
|
||||
@@ -1521,9 +1521,9 @@ impl CollabPanel {
|
||||
if cx.stop_active_drag(window) {
|
||||
return;
|
||||
} else if self.take_editing_state(window, cx) {
|
||||
window.focus(&self.filter_editor.focus_handle(cx), cx);
|
||||
window.focus(&self.filter_editor.focus_handle(cx));
|
||||
} else if !self.reset_filter_editor_text(window, cx) {
|
||||
self.focus_handle.focus(window, cx);
|
||||
self.focus_handle.focus(window);
|
||||
}
|
||||
|
||||
if self.context_menu.is_some() {
|
||||
@@ -1826,7 +1826,7 @@ impl CollabPanel {
|
||||
});
|
||||
self.update_entries(false, cx);
|
||||
self.select_channel_editor();
|
||||
window.focus(&self.channel_name_editor.focus_handle(cx), cx);
|
||||
window.focus(&self.channel_name_editor.focus_handle(cx));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -1851,7 +1851,7 @@ impl CollabPanel {
|
||||
});
|
||||
self.update_entries(false, cx);
|
||||
self.select_channel_editor();
|
||||
window.focus(&self.channel_name_editor.focus_handle(cx), cx);
|
||||
window.focus(&self.channel_name_editor.focus_handle(cx));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -1900,7 +1900,7 @@ impl CollabPanel {
|
||||
editor.set_text(channel.name.clone(), window, cx);
|
||||
editor.select_all(&Default::default(), window, cx);
|
||||
});
|
||||
window.focus(&self.channel_name_editor.focus_handle(cx), cx);
|
||||
window.focus(&self.channel_name_editor.focus_handle(cx));
|
||||
self.update_entries(false, cx);
|
||||
self.select_channel_editor();
|
||||
}
|
||||
|
||||
@@ -642,7 +642,7 @@ impl ChannelModalDelegate {
|
||||
});
|
||||
menu
|
||||
});
|
||||
window.focus(&context_menu.focus_handle(cx), cx);
|
||||
window.focus(&context_menu.focus_handle(cx));
|
||||
let subscription = cx.subscribe_in(
|
||||
&context_menu,
|
||||
window,
|
||||
|
||||
@@ -588,7 +588,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
let action = command.action;
|
||||
window.focus(&self.previous_focus_handle, cx);
|
||||
window.focus(&self.previous_focus_handle);
|
||||
self.dismissed(window, cx);
|
||||
window.dispatch_action(action, cx);
|
||||
}
|
||||
@@ -784,7 +784,7 @@ mod tests {
|
||||
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
|
||||
editor.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx), cx))
|
||||
editor.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx)))
|
||||
});
|
||||
|
||||
cx.simulate_keystrokes("cmd-shift-p");
|
||||
@@ -855,7 +855,7 @@ mod tests {
|
||||
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
|
||||
editor.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx), cx))
|
||||
editor.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx)))
|
||||
});
|
||||
|
||||
// Test normalize (trimming whitespace and double colons)
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
[package]
|
||||
name = "component_preview"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/component_preview.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
preview = []
|
||||
test-support = ["db/test-support"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
component.workspace = true
|
||||
db.workspace = true
|
||||
fs.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
node_runtime.workspace = true
|
||||
notifications.workspace = true
|
||||
project.workspace = true
|
||||
release_channel.workspace = true
|
||||
reqwest_client.workspace = true
|
||||
session.workspace = true
|
||||
settings.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
ui_input.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[[example]]
|
||||
name = "component_preview"
|
||||
path = "examples/component_preview.rs"
|
||||
required-features = ["preview"]
|
||||
@@ -1 +0,0 @@
|
||||
LICENSE-GPL
|
||||
@@ -1,18 +0,0 @@
|
||||
//! Component Preview Example
|
||||
//!
|
||||
//! Run with: `cargo run -p component_preview --example component_preview --features="preview"`
|
||||
//!
|
||||
//! To use this in other projects, add the following to your `Cargo.toml`:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! component_preview = { path = "../component_preview", features = ["preview"] }
|
||||
//!
|
||||
//! [[example]]
|
||||
//! name = "component_preview"
|
||||
//! path = "examples/component_preview.rs"
|
||||
//! ```
|
||||
|
||||
fn main() {
|
||||
component_preview::run_component_preview();
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
/// Run the component preview application.
|
||||
///
|
||||
/// This initializes the application with minimal required infrastructure
|
||||
/// and opens a workspace with the ComponentPreview item.
|
||||
#[cfg(feature = "preview")]
|
||||
pub fn run_component_preview() {
|
||||
use fs::RealFs;
|
||||
use gpui::{
|
||||
AppContext as _, Application, Bounds, KeyBinding, WindowBounds, WindowOptions, actions,
|
||||
size,
|
||||
};
|
||||
|
||||
use client::{Client, UserStore};
|
||||
use language::LanguageRegistry;
|
||||
use node_runtime::NodeRuntime;
|
||||
use project::Project;
|
||||
use reqwest_client::ReqwestClient;
|
||||
use session::{AppSession, Session};
|
||||
use std::sync::Arc;
|
||||
use ui::{App, px};
|
||||
use workspace::{AppState, Workspace, WorkspaceStore};
|
||||
|
||||
use crate::{ComponentPreview, init};
|
||||
|
||||
actions!(zed, [Quit]);
|
||||
|
||||
fn quit(_: &Quit, cx: &mut App) {
|
||||
cx.quit();
|
||||
}
|
||||
|
||||
Application::new().run(|cx| {
|
||||
component::init();
|
||||
|
||||
cx.on_action(quit);
|
||||
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
|
||||
let version = release_channel::AppVersion::load(env!("CARGO_PKG_VERSION"), None, None);
|
||||
release_channel::init(version, cx);
|
||||
|
||||
let http_client =
|
||||
ReqwestClient::user_agent("component_preview").expect("Failed to create HTTP client");
|
||||
cx.set_http_client(Arc::new(http_client));
|
||||
|
||||
let fs = Arc::new(RealFs::new(None, cx.background_executor().clone()));
|
||||
<dyn fs::Fs>::set_global(fs.clone(), cx);
|
||||
|
||||
settings::init(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
|
||||
let languages = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
|
||||
let client = Client::production(cx);
|
||||
client::init(&client, cx);
|
||||
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
|
||||
let session_id = uuid::Uuid::new_v4().to_string();
|
||||
let session = cx.background_executor().block(Session::new(session_id));
|
||||
let session = cx.new(|cx| AppSession::new(session, cx));
|
||||
let node_runtime = NodeRuntime::unavailable();
|
||||
|
||||
let app_state = Arc::new(AppState {
|
||||
languages,
|
||||
client,
|
||||
user_store,
|
||||
workspace_store,
|
||||
fs,
|
||||
build_window_options: |_, _| Default::default(),
|
||||
node_runtime,
|
||||
session,
|
||||
});
|
||||
AppState::set_global(Arc::downgrade(&app_state), cx);
|
||||
|
||||
workspace::init(app_state.clone(), cx);
|
||||
init(app_state.clone(), cx);
|
||||
|
||||
let size = size(px(1200.), px(800.));
|
||||
let bounds = Bounds::centered(None, size, cx);
|
||||
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
{
|
||||
move |window, cx| {
|
||||
let app_state = app_state;
|
||||
theme::setup_ui_font(window, cx);
|
||||
|
||||
let project = Project::local(
|
||||
app_state.client.clone(),
|
||||
app_state.node_runtime.clone(),
|
||||
app_state.user_store.clone(),
|
||||
app_state.languages.clone(),
|
||||
app_state.fs.clone(),
|
||||
None,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
|
||||
let workspace = cx.new(|cx| {
|
||||
Workspace::new(
|
||||
Default::default(),
|
||||
project.clone(),
|
||||
app_state.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let weak_workspace = cx.entity().downgrade();
|
||||
let language_registry = app_state.languages.clone();
|
||||
let user_store = app_state.user_store.clone();
|
||||
|
||||
let component_preview = cx.new(|cx| {
|
||||
ComponentPreview::new(
|
||||
weak_workspace,
|
||||
project,
|
||||
language_registry,
|
||||
user_store,
|
||||
None,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.expect("Failed to create component preview")
|
||||
});
|
||||
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(component_preview),
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
workspace
|
||||
}
|
||||
},
|
||||
)
|
||||
.expect("Failed to open component preview window");
|
||||
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
@@ -29,7 +29,6 @@ schemars.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
slotmap.workspace = true
|
||||
smol.workspace = true
|
||||
tempfile.workspace = true
|
||||
url = { workspace = true, features = ["serde"] }
|
||||
|
||||
@@ -6,7 +6,6 @@ use parking_lot::Mutex;
|
||||
use postage::barrier;
|
||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||
use serde_json::{Value, value::RawValue};
|
||||
use slotmap::SlotMap;
|
||||
use smol::channel;
|
||||
use std::{
|
||||
fmt,
|
||||
@@ -51,7 +50,7 @@ pub(crate) struct Client {
|
||||
next_id: AtomicI32,
|
||||
outbound_tx: channel::Sender<String>,
|
||||
name: Arc<str>,
|
||||
subscription_set: Arc<Mutex<NotificationSubscriptionSet>>,
|
||||
notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
|
||||
response_handlers: Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[allow(dead_code)]
|
||||
@@ -192,20 +191,21 @@ impl Client {
|
||||
let (outbound_tx, outbound_rx) = channel::unbounded::<String>();
|
||||
let (output_done_tx, output_done_rx) = barrier::channel();
|
||||
|
||||
let subscription_set = Arc::new(Mutex::new(NotificationSubscriptionSet::default()));
|
||||
let notification_handlers =
|
||||
Arc::new(Mutex::new(HashMap::<_, NotificationHandler>::default()));
|
||||
let response_handlers =
|
||||
Arc::new(Mutex::new(Some(HashMap::<_, ResponseHandler>::default())));
|
||||
let request_handlers = Arc::new(Mutex::new(HashMap::<_, RequestHandler>::default()));
|
||||
|
||||
let receive_input_task = cx.spawn({
|
||||
let subscription_set = subscription_set.clone();
|
||||
let notification_handlers = notification_handlers.clone();
|
||||
let response_handlers = response_handlers.clone();
|
||||
let request_handlers = request_handlers.clone();
|
||||
let transport = transport.clone();
|
||||
async move |cx| {
|
||||
Self::handle_input(
|
||||
transport,
|
||||
subscription_set,
|
||||
notification_handlers,
|
||||
request_handlers,
|
||||
response_handlers,
|
||||
cx,
|
||||
@@ -236,7 +236,7 @@ impl Client {
|
||||
|
||||
Ok(Self {
|
||||
server_id,
|
||||
subscription_set,
|
||||
notification_handlers,
|
||||
response_handlers,
|
||||
name: server_name,
|
||||
next_id: Default::default(),
|
||||
@@ -257,7 +257,7 @@ impl Client {
|
||||
/// to pending requests) and notifications (which trigger registered handlers).
|
||||
async fn handle_input(
|
||||
transport: Arc<dyn Transport>,
|
||||
subscription_set: Arc<Mutex<NotificationSubscriptionSet>>,
|
||||
notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
|
||||
request_handlers: Arc<Mutex<HashMap<&'static str, RequestHandler>>>,
|
||||
response_handlers: Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
|
||||
cx: &mut AsyncApp,
|
||||
@@ -282,11 +282,10 @@ impl Client {
|
||||
handler(Ok(message.to_string()));
|
||||
}
|
||||
} else if let Ok(notification) = serde_json::from_str::<AnyNotification>(&message) {
|
||||
subscription_set.lock().notify(
|
||||
¬ification.method,
|
||||
notification.params.unwrap_or(Value::Null),
|
||||
cx,
|
||||
)
|
||||
let mut notification_handlers = notification_handlers.lock();
|
||||
if let Some(handler) = notification_handlers.get_mut(notification.method.as_str()) {
|
||||
handler(notification.params.unwrap_or(Value::Null), cx.clone());
|
||||
}
|
||||
} else {
|
||||
log::error!("Unhandled JSON from context_server: {}", message);
|
||||
}
|
||||
@@ -452,18 +451,12 @@ impl Client {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn on_notification(
|
||||
&self,
|
||||
method: &'static str,
|
||||
f: Box<dyn 'static + Send + FnMut(Value, AsyncApp)>,
|
||||
) -> NotificationSubscription {
|
||||
let mut notification_subscriptions = self.subscription_set.lock();
|
||||
|
||||
NotificationSubscription {
|
||||
id: notification_subscriptions.add_handler(method, f),
|
||||
set: self.subscription_set.clone(),
|
||||
}
|
||||
) {
|
||||
self.notification_handlers.lock().insert(method, f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,73 +485,3 @@ impl fmt::Debug for Client {
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
slotmap::new_key_type! {
|
||||
struct NotificationSubscriptionId;
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct NotificationSubscriptionSet {
|
||||
// we have very few subscriptions at the moment
|
||||
methods: Vec<(&'static str, Vec<NotificationSubscriptionId>)>,
|
||||
handlers: SlotMap<NotificationSubscriptionId, NotificationHandler>,
|
||||
}
|
||||
|
||||
impl NotificationSubscriptionSet {
|
||||
#[must_use]
|
||||
fn add_handler(
|
||||
&mut self,
|
||||
method: &'static str,
|
||||
handler: NotificationHandler,
|
||||
) -> NotificationSubscriptionId {
|
||||
let id = self.handlers.insert(handler);
|
||||
if let Some((_, handler_ids)) = self
|
||||
.methods
|
||||
.iter_mut()
|
||||
.find(|(probe_method, _)| method == *probe_method)
|
||||
{
|
||||
debug_assert!(
|
||||
handler_ids.len() < 20,
|
||||
"Too many MCP handlers for {}. Consider using a different data structure.",
|
||||
method
|
||||
);
|
||||
|
||||
handler_ids.push(id);
|
||||
} else {
|
||||
self.methods.push((method, vec![id]));
|
||||
};
|
||||
id
|
||||
}
|
||||
|
||||
fn notify(&mut self, method: &str, payload: Value, cx: &mut AsyncApp) {
|
||||
let Some((_, handler_ids)) = self
|
||||
.methods
|
||||
.iter_mut()
|
||||
.find(|(probe_method, _)| method == *probe_method)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
for handler_id in handler_ids {
|
||||
if let Some(handler) = self.handlers.get_mut(*handler_id) {
|
||||
handler(payload.clone(), cx.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NotificationSubscription {
|
||||
id: NotificationSubscriptionId,
|
||||
set: Arc<Mutex<NotificationSubscriptionSet>>,
|
||||
}
|
||||
|
||||
impl Drop for NotificationSubscription {
|
||||
fn drop(&mut self) {
|
||||
let mut set = self.set.lock();
|
||||
set.handlers.remove(self.id);
|
||||
set.methods.retain_mut(|(_, handler_ids)| {
|
||||
handler_ids.retain(|id| *id != self.id);
|
||||
!handler_ids.is_empty()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,22 @@ impl ContextServer {
|
||||
self.initialize(self.new_client(cx)?).await
|
||||
}
|
||||
|
||||
/// Starts the context server, making sure handlers are registered before initialization happens
|
||||
pub async fn start_with_handlers(
|
||||
&self,
|
||||
notification_handlers: Vec<(
|
||||
&'static str,
|
||||
Box<dyn 'static + Send + FnMut(serde_json::Value, AsyncApp)>,
|
||||
)>,
|
||||
cx: &AsyncApp,
|
||||
) -> Result<()> {
|
||||
let client = self.new_client(cx)?;
|
||||
for (method, handler) in notification_handlers {
|
||||
client.on_notification(method, handler);
|
||||
}
|
||||
self.initialize(client).await
|
||||
}
|
||||
|
||||
fn new_client(&self, cx: &AsyncApp) -> Result<Client> {
|
||||
Ok(match &self.configuration {
|
||||
ContextServerTransport::Stdio(command, working_directory) => Client::stdio(
|
||||
|
||||
@@ -12,7 +12,7 @@ use futures::channel::oneshot;
|
||||
use gpui::AsyncApp;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::client::{Client, NotificationSubscription};
|
||||
use crate::client::Client;
|
||||
use crate::types::{self, Notification, Request};
|
||||
|
||||
pub struct ModelContextProtocol {
|
||||
@@ -119,7 +119,7 @@ impl InitializedContextServerProtocol {
|
||||
&self,
|
||||
method: &'static str,
|
||||
f: Box<dyn 'static + Send + FnMut(Value, AsyncApp)>,
|
||||
) -> NotificationSubscription {
|
||||
self.inner.on_notification(method, f)
|
||||
) {
|
||||
self.inner.on_notification(method, f);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user