Compare commits
188 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
872b2b3510 | ||
|
|
83966115a8 | ||
|
|
554382a87e | ||
|
|
9677da9a60 | ||
|
|
9e5546edc5 | ||
|
|
c7203f7a60 | ||
|
|
c87f12ee42 | ||
|
|
084d01b5f0 | ||
|
|
ed7360206f | ||
|
|
9f599466b5 | ||
|
|
dffda91838 | ||
|
|
6b3412232f | ||
|
|
963cb2c200 | ||
|
|
0c7d639cbd | ||
|
|
189b5ff3a2 | ||
|
|
a3bf58d393 | ||
|
|
d844f5db30 | ||
|
|
fc45f6e042 | ||
|
|
843be766df | ||
|
|
469ad070b0 | ||
|
|
392b6184bf | ||
|
|
fc89e19098 | ||
|
|
f53b01d5a2 | ||
|
|
bf1c8819d9 | ||
|
|
3247264288 | ||
|
|
6d947b7746 | ||
|
|
db221ca72d | ||
|
|
1d006a8cb0 | ||
|
|
aaab9f6960 | ||
|
|
209cf0a48f | ||
|
|
260691c99c | ||
|
|
9e88f3f33c | ||
|
|
2cad6c8ef1 | ||
|
|
bc24ffe863 | ||
|
|
1e4a970ae2 | ||
|
|
3e656a0911 | ||
|
|
57ea23d161 | ||
|
|
a50c5b2c10 | ||
|
|
f1b723973b | ||
|
|
a7ce677ac3 | ||
|
|
ed67f246cb | ||
|
|
93f29326c4 | ||
|
|
85f4681299 | ||
|
|
741c5d5010 | ||
|
|
f03987fb68 | ||
|
|
ca47822667 | ||
|
|
a34fe06bb1 | ||
|
|
0ce484e66c | ||
|
|
251033f88f | ||
|
|
9f90c1a1b7 | ||
|
|
d43cc46288 | ||
|
|
fdb8e71b43 | ||
|
|
6bc433ed43 | ||
|
|
1281f4672c | ||
|
|
ed705c0cbc | ||
|
|
8980333e23 | ||
|
|
acee48bfda | ||
|
|
71298e6949 | ||
|
|
07ada58466 | ||
|
|
dd521a96fb | ||
|
|
f9d9721b93 | ||
|
|
cff3ac6f93 | ||
|
|
746b76488c | ||
|
|
397fcf6083 | ||
|
|
9adb3e1daa | ||
|
|
1469d94683 | ||
|
|
3b626c8ac1 | ||
|
|
3dc0614dba | ||
|
|
045e154915 | ||
|
|
dc72e1c4ba | ||
|
|
0884305e43 | ||
|
|
83449293b6 | ||
|
|
213cb30445 | ||
|
|
4b56fec971 | ||
|
|
32621dc5de | ||
|
|
215ac50bc8 | ||
|
|
a5540a08fb | ||
|
|
3e8c25f5a9 | ||
|
|
7f0842e3a6 | ||
|
|
6dad419cd5 | ||
|
|
0facdfa5ca | ||
|
|
58461377ca | ||
|
|
42d5f7e73e | ||
|
|
5395197619 | ||
|
|
1d76539d28 | ||
|
|
e5eb26e8d6 | ||
|
|
a86b0ab2e0 | ||
|
|
5fb220a19a | ||
|
|
12dbbdd1d3 | ||
|
|
6dfabddbb4 | ||
|
|
895213a94d | ||
|
|
1c576ccf82 | ||
|
|
3f4da03d38 | ||
|
|
ff71f4d46d | ||
|
|
71f4dc2481 | ||
|
|
b091cc4d9a | ||
|
|
8e5d33ebc6 | ||
|
|
99224ccc75 | ||
|
|
56646e6bc3 | ||
|
|
bb2f037407 | ||
|
|
07db88a327 | ||
|
|
e61f9081d4 | ||
|
|
1bc3fa8154 | ||
|
|
22916311cd | ||
|
|
1edd050baf | ||
|
|
b53f661515 | ||
|
|
4ef5d2c814 | ||
|
|
bfe3c66c3e | ||
|
|
361b8e0ba9 | ||
|
|
d7e41f74fb | ||
|
|
e05dcecac4 | ||
|
|
32600f255a | ||
|
|
a7e07010e5 | ||
|
|
ea34cc5324 | ||
|
|
a7d43063d4 | ||
|
|
8001877df2 | ||
|
|
b603372f44 | ||
|
|
7427924405 | ||
|
|
ae44c3c881 | ||
|
|
4e0471cf66 | ||
|
|
62d36b22fd | ||
|
|
69f6eeaa3a | ||
|
|
1dc5de4592 | ||
|
|
b9aef75f2d | ||
|
|
95ae388c0c | ||
|
|
1ac170e663 | ||
|
|
3104482c6c | ||
|
|
7ee56e1a18 | ||
|
|
f2495a6f98 | ||
|
|
6d776c3157 | ||
|
|
596826f741 | ||
|
|
e44529ed7b | ||
|
|
e052127e1c | ||
|
|
0531035b86 | ||
|
|
05ce34eea4 | ||
|
|
63c4406137 | ||
|
|
3f67c5220d | ||
|
|
435d4c5f24 | ||
|
|
e0ff995e2d | ||
|
|
6976208e21 | ||
|
|
6055b45ee1 | ||
|
|
88f90c12ed | ||
|
|
0d74f982a5 | ||
|
|
ca90b8555d | ||
|
|
8516d81e13 | ||
|
|
af589ff25f | ||
|
|
d2bbfbb3bf | ||
|
|
413f4ea49c | ||
|
|
1b6d588413 | ||
|
|
334ca21857 | ||
|
|
f58278aaf4 | ||
|
|
e10b9b70ef | ||
|
|
098adf3bdd | ||
|
|
a85c508f69 | ||
|
|
2a713c546b | ||
|
|
f937c1931f | ||
|
|
7a62f01ea5 | ||
|
|
2d071b0cb6 | ||
|
|
bd2b0de231 | ||
|
|
886de8f54b | ||
|
|
7a783a91cc | ||
|
|
f9462da2f7 | ||
|
|
61dd6a8f31 | ||
|
|
abb199c85e | ||
|
|
cebbf77491 | ||
|
|
0180f3e72a | ||
|
|
5488a19221 | ||
|
|
bb1198e7d6 | ||
|
|
69fe27f45e | ||
|
|
469da2fd07 | ||
|
|
4f87822133 | ||
|
|
9a69d89f88 | ||
|
|
54f360ace1 | ||
|
|
b2a0b78ece | ||
|
|
f1ca2f9f31 | ||
|
|
4b34adedd2 | ||
|
|
df48294caa | ||
|
|
cdc5cc348f | ||
|
|
0f7f540138 | ||
|
|
184001b33b | ||
|
|
225a2a8a20 | ||
|
|
ea37057814 | ||
|
|
77cdef3596 | ||
|
|
05108c50fd | ||
|
|
07538ff08e | ||
|
|
9073a2666c | ||
|
|
843a35a1a9 | ||
|
|
aff93f2f6c |
55
.factory/prompts/docs-automation/phase2-explore.md
Normal file
55
.factory/prompts/docs-automation/phase2-explore.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# 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
|
||||
57
.factory/prompts/docs-automation/phase3-analyze.md
Normal file
57
.factory/prompts/docs-automation/phase3-analyze.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# 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
|
||||
76
.factory/prompts/docs-automation/phase4-plan.md
Normal file
76
.factory/prompts/docs-automation/phase4-plan.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# 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
|
||||
67
.factory/prompts/docs-automation/phase5-apply.md
Normal file
67
.factory/prompts/docs-automation/phase5-apply.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# 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]
|
||||
```
|
||||
54
.factory/prompts/docs-automation/phase6-summarize.md
Normal file
54
.factory/prompts/docs-automation/phase6-summarize.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# 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
|
||||
67
.factory/prompts/docs-automation/phase7-commit.md
Normal file
67
.factory/prompts/docs-automation/phase7-commit.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# 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,6 +25,7 @@ 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,6 +19,18 @@ 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:
|
||||
|
||||
4
.github/workflows/autofix_pr.yml
vendored
4
.github/workflows/autofix_pr.yml
vendored
@@ -54,6 +54,10 @@ 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
|
||||
|
||||
@@ -33,18 +33,19 @@ jobs:
|
||||
bobbymannino
|
||||
CharlesChen0823
|
||||
chbk
|
||||
cppcoffee
|
||||
davidbarsky
|
||||
davewa
|
||||
davidbarsky
|
||||
ddoemonn
|
||||
djsauble
|
||||
errmayank
|
||||
fantacell
|
||||
fdncred
|
||||
findrakecil
|
||||
FloppyDisco
|
||||
gko
|
||||
huacnlee
|
||||
imumesh18
|
||||
injust
|
||||
jacobtread
|
||||
jansol
|
||||
jeffreyguenther
|
||||
@@ -56,21 +57,23 @@ jobs:
|
||||
marius851000
|
||||
mikebronner
|
||||
ognevny
|
||||
PKief
|
||||
playdohface
|
||||
RemcoSmitsDev
|
||||
romaninsh
|
||||
rxptr
|
||||
Simek
|
||||
someone13574
|
||||
sourcefrog
|
||||
suxiaoshao
|
||||
Takk8IS
|
||||
tartarughina
|
||||
thedadams
|
||||
tidely
|
||||
timvermeulen
|
||||
valentinegb
|
||||
versecafe
|
||||
vitallium
|
||||
warrenjokinen
|
||||
WhySoBad
|
||||
ya7010
|
||||
Zertsov
|
||||
|
||||
@@ -1,29 +1,40 @@
|
||||
name: "Close Stale Issues"
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 8 31 DEC *"
|
||||
- cron: "0 2 * * 5"
|
||||
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@5bef64f19d7facfb25b37b414482c7164d639639 # v9
|
||||
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: >
|
||||
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.
|
||||
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.
|
||||
|
||||
Thanks for your help!
|
||||
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."
|
||||
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."
|
||||
days-before-stale: 60
|
||||
days-before-close: 14
|
||||
only-issue-types: "Bug,Crash"
|
||||
operations-per-run: 1000
|
||||
operations-per-run: ${{ inputs.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
Normal file
264
.github/workflows/docs_automation.yml
vendored
Normal file
@@ -0,0 +1,264 @@
|
||||
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
|
||||
6
.github/workflows/extension_bump.yml
vendored
6
.github/workflows/extension_bump.yml
vendored
@@ -66,7 +66,7 @@ jobs:
|
||||
if: |-
|
||||
(github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') &&
|
||||
(inputs.force-bump == 'true' || needs.check_bump_needed.outputs.needs_bump == 'true')
|
||||
runs-on: namespace-profile-8x16-ubuntu-2204
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- id: generate-token
|
||||
name: extension_bump::generate_token
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
with:
|
||||
clean: false
|
||||
- name: extension_bump::install_bump_2_version
|
||||
run: pip install bump2version
|
||||
run: pip install bump2version --break-system-packages
|
||||
shell: bash -euxo pipefail {0}
|
||||
- id: bump-version
|
||||
name: extension_bump::bump_version
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
needs:
|
||||
- check_bump_needed
|
||||
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.check_bump_needed.outputs.needs_bump == 'false'
|
||||
runs-on: namespace-profile-8x16-ubuntu-2204
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- id: generate-token
|
||||
name: extension_bump::generate_token
|
||||
|
||||
2
.github/workflows/extension_release.yml
vendored
2
.github/workflows/extension_release.yml
vendored
@@ -13,7 +13,7 @@ on:
|
||||
jobs:
|
||||
create_release:
|
||||
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
|
||||
runs-on: namespace-profile-8x16-ubuntu-2204
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- id: generate-token
|
||||
name: extension_bump::generate_token
|
||||
|
||||
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-16x32-ubuntu-2204
|
||||
runs-on: namespace-profile-4x8-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-2x4-ubuntu-2404
|
||||
runs-on: namespace-profile-8x32-ubuntu-2404
|
||||
steps:
|
||||
- name: steps::checkout_repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
|
||||
106
.github/workflows/extension_workflow_rollout.yml
vendored
Normal file
106
.github/workflows/extension_workflow_rollout.yml
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
# Generated from xtask::workflows::extension_workflow_rollout
|
||||
# Rebuild with `cargo xtask workflows`.
|
||||
name: extension_workflow_rollout
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
jobs:
|
||||
fetch_extension_repos:
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- id: list-repos
|
||||
name: extension_workflow_rollout::fetch_extension_repos::get_repositories
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const repos = await github.paginate(github.rest.repos.listForOrg, {
|
||||
org: 'zed-extensions',
|
||||
type: 'public',
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const filteredRepos = repos
|
||||
.filter(repo => !repo.archived)
|
||||
.filter(repo => repo.name !== 'workflows' && repo.name !== 'material-icon-theme')
|
||||
.map(repo => repo.name);
|
||||
|
||||
console.log(`Found ${filteredRepos.length} extension repos`);
|
||||
return filteredRepos;
|
||||
result-encoding: json
|
||||
outputs:
|
||||
repos: ${{ steps.list-repos.outputs.result }}
|
||||
timeout-minutes: 5
|
||||
rollout_workflows_to_extension:
|
||||
needs:
|
||||
- fetch_extension_repos
|
||||
if: needs.fetch_extension_repos.outputs.repos != '[]'
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
strategy:
|
||||
matrix:
|
||||
repo: ${{ fromJson(needs.fetch_extension_repos.outputs.repos) }}
|
||||
fail-fast: false
|
||||
max-parallel: 5
|
||||
steps:
|
||||
- id: generate-token
|
||||
name: extension_bump::generate_token
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
|
||||
private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
|
||||
owner: zed-extensions
|
||||
repositories: ${{ matrix.repo }}
|
||||
permission-pull-requests: write
|
||||
permission-contents: write
|
||||
permission-workflows: write
|
||||
- name: checkout_zed_repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
clean: false
|
||||
path: zed
|
||||
- name: steps::checkout_repo_with_token
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
clean: false
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
repository: zed-extensions/${{ matrix.repo }}
|
||||
path: extension
|
||||
- name: extension_workflow_rollout::rollout_workflows_to_extension::copy_workflow_files
|
||||
run: |
|
||||
mkdir -p extension/.github/workflows
|
||||
cp zed/extensions/workflows/shared/*.yml extension/.github/workflows/
|
||||
shell: bash -euxo pipefail {0}
|
||||
- id: short-sha
|
||||
name: extension_workflow_rollout::rollout_workflows_to_extension::get_short_sha
|
||||
run: |
|
||||
echo "sha_short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
|
||||
shell: bash -euxo pipefail {0}
|
||||
working-directory: zed
|
||||
- id: create-pr
|
||||
name: extension_workflow_rollout::rollout_workflows_to_extension::create_pull_request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
path: extension
|
||||
title: Update CI workflows to `zed@${{ steps.short-sha.outputs.sha_short }}`
|
||||
body: |
|
||||
This PR updates the CI workflow files from the main Zed repository
|
||||
based on the commit zed-industries/zed@${{ github.sha }}
|
||||
commit-message: Update CI workflows to `zed@${{ steps.short-sha.outputs.sha_short }}`
|
||||
branch: update-workflows
|
||||
committer: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>
|
||||
author: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>
|
||||
base: main
|
||||
delete-branch: true
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
sign-commits: true
|
||||
- name: extension_workflow_rollout::rollout_workflows_to_extension::enable_auto_merge
|
||||
run: |
|
||||
PR_NUMBER="${{ steps.create-pr.outputs.pull-request-number }}"
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
cd extension
|
||||
gh pr merge "$PR_NUMBER" --auto --squash
|
||||
fi
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||
timeout-minutes: 10
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
|
||||
jobs:
|
||||
handle-good-first-issue:
|
||||
if: github.event.label.name == 'good first issue' && github.repository_owner == 'zed-industries'
|
||||
if: github.event.label.name == '.contrib/good first issue' && github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -74,12 +74,6 @@ jobs:
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::trigger_autofix
|
||||
if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
|
||||
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=true
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: steps::cargo_install_nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: steps::clear_target_dir_if_large
|
||||
|
||||
17
.github/workflows/run_tests.yml
vendored
17
.github/workflows/run_tests.yml
vendored
@@ -74,18 +74,12 @@ jobs:
|
||||
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
|
||||
with:
|
||||
version: '9'
|
||||
- name: ./script/prettier
|
||||
- name: steps::prettier
|
||||
run: ./script/prettier
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::cargo_fmt
|
||||
run: cargo fmt --all -- --check
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::trigger_autofix
|
||||
if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
|
||||
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=false
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: ./script/check-todos
|
||||
run: ./script/check-todos
|
||||
shell: bash -euxo pipefail {0}
|
||||
@@ -166,12 +160,6 @@ jobs:
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::trigger_autofix
|
||||
if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
|
||||
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=true
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: steps::cargo_install_nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -365,6 +353,9 @@ 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,6 +36,7 @@
|
||||
DerivedData/
|
||||
Packages
|
||||
xcuserdata/
|
||||
crates/docs_preprocessor/actions.json
|
||||
|
||||
# Don't commit any secrets to the repo.
|
||||
.env
|
||||
|
||||
@@ -23,7 +23,6 @@ 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).
|
||||
|
||||
|
||||
97
Cargo.lock
generated
97
Cargo.lock
generated
@@ -226,9 +226,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-client-protocol"
|
||||
version = "0.9.0"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2ffe7d502c1e451aafc5aff655000f84d09c9af681354ac0012527009b1af13"
|
||||
checksum = "d3e527d7dfe0f334313d42d1d9318f0a79665f6f21c440d0798f230a77a7ed6c"
|
||||
dependencies = [
|
||||
"agent-client-protocol-schema",
|
||||
"anyhow",
|
||||
@@ -243,9 +243,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-client-protocol-schema"
|
||||
version = "0.10.0"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8af81cc2d5c3f9c04f73db452efd058333735ba9d51c2cf7ef33c9fee038e7e6"
|
||||
checksum = "6903a00e8ac822f9bacac59a1932754d7387c72ebb7c9c7439ad021505591da4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"derive_more 2.0.1",
|
||||
@@ -268,6 +268,7 @@ dependencies = [
|
||||
"client",
|
||||
"collections",
|
||||
"env_logger 0.11.8",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
@@ -793,7 +794,7 @@ dependencies = [
|
||||
"url",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols 0.32.9",
|
||||
"wayland-protocols",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
@@ -2380,7 +2381,6 @@ dependencies = [
|
||||
name = "buffer_diff"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clock",
|
||||
"ctor",
|
||||
"futures 0.3.31",
|
||||
@@ -3525,6 +3525,33 @@ dependencies = [
|
||||
"theme",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "component_preview"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"collections",
|
||||
"component",
|
||||
"db",
|
||||
"fs",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
"node_runtime",
|
||||
"notifications",
|
||||
"project",
|
||||
"release_channel",
|
||||
"reqwest_client",
|
||||
"session",
|
||||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
"ui_input",
|
||||
"uuid",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "compression-codecs"
|
||||
version = "0.4.31"
|
||||
@@ -5021,8 +5048,6 @@ name = "docs_preprocessor"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"command_palette",
|
||||
"gpui",
|
||||
"mdbook",
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -5031,7 +5056,6 @@ dependencies = [
|
||||
"task",
|
||||
"theme",
|
||||
"util",
|
||||
"zed",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
@@ -5188,6 +5212,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"arrayvec",
|
||||
"brotli",
|
||||
"buffer_diff",
|
||||
"client",
|
||||
"clock",
|
||||
"cloud_api_types",
|
||||
@@ -5225,7 +5250,9 @@ dependencies = [
|
||||
"strum 0.27.2",
|
||||
"telemetry",
|
||||
"telemetry_events",
|
||||
"text",
|
||||
"thiserror 2.0.17",
|
||||
"time",
|
||||
"ui",
|
||||
"util",
|
||||
"uuid",
|
||||
@@ -5330,8 +5357,10 @@ dependencies = [
|
||||
"anyhow",
|
||||
"buffer_diff",
|
||||
"client",
|
||||
"clock",
|
||||
"cloud_llm_client",
|
||||
"codestral",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"copilot",
|
||||
"edit_prediction",
|
||||
@@ -5340,18 +5369,20 @@ dependencies = [
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"git",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"language",
|
||||
"log",
|
||||
"language_model",
|
||||
"lsp",
|
||||
"markdown",
|
||||
"menu",
|
||||
"multi_buffer",
|
||||
"paths",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"semver",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"supermaven",
|
||||
@@ -5364,6 +5395,7 @@ dependencies = [
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
"zeta_prompt",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7370,7 +7402,7 @@ dependencies = [
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-cursor",
|
||||
"wayland-protocols 0.31.2",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols-plasma",
|
||||
"wayland-protocols-wlr",
|
||||
"windows 0.61.3",
|
||||
@@ -8932,6 +8964,8 @@ dependencies = [
|
||||
"credentials_provider",
|
||||
"deepseek",
|
||||
"editor",
|
||||
"extension",
|
||||
"extension_host",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"google_ai",
|
||||
@@ -12571,6 +12605,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"language",
|
||||
"menu",
|
||||
"notifications",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"rayon",
|
||||
@@ -12648,6 +12683,8 @@ dependencies = [
|
||||
"paths",
|
||||
"rope",
|
||||
"serde",
|
||||
"strum 0.27.2",
|
||||
"tempfile",
|
||||
"text",
|
||||
"util",
|
||||
"uuid",
|
||||
@@ -18927,18 +18964,6 @@ dependencies = [
|
||||
"xcursor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols"
|
||||
version = "0.31.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols"
|
||||
version = "0.32.9"
|
||||
@@ -18953,14 +18978,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols-plasma"
|
||||
version = "0.2.0"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479"
|
||||
checksum = "a07a14257c077ab3279987c4f8bb987851bf57081b93710381daea94f2c2c032"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols 0.31.2",
|
||||
"wayland-protocols",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
@@ -18973,7 +18998,7 @@ dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols 0.32.9",
|
||||
"wayland-protocols",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
@@ -20275,6 +20300,16 @@ dependencies = [
|
||||
"zlog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "worktree_benchmarks"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"fs",
|
||||
"gpui",
|
||||
"settings",
|
||||
"worktree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.1"
|
||||
@@ -20643,6 +20678,7 @@ dependencies = [
|
||||
"collections",
|
||||
"command_palette",
|
||||
"component",
|
||||
"component_preview",
|
||||
"copilot",
|
||||
"crashes",
|
||||
"dap",
|
||||
@@ -20748,7 +20784,6 @@ dependencies = [
|
||||
"tree-sitter-md",
|
||||
"tree-sitter-rust",
|
||||
"ui",
|
||||
"ui_input",
|
||||
"ui_prompt",
|
||||
"url",
|
||||
"urlencoding",
|
||||
@@ -20928,7 +20963,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_glsl"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
@@ -20942,7 +20977,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_proto"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.7.0",
|
||||
]
|
||||
|
||||
@@ -39,6 +39,7 @@ members = [
|
||||
"crates/command_palette",
|
||||
"crates/command_palette_hooks",
|
||||
"crates/component",
|
||||
"crates/component_preview",
|
||||
"crates/context_server",
|
||||
"crates/copilot",
|
||||
"crates/crashes",
|
||||
@@ -198,6 +199,7 @@ members = [
|
||||
"crates/web_search_providers",
|
||||
"crates/workspace",
|
||||
"crates/worktree",
|
||||
"crates/worktree_benchmarks",
|
||||
"crates/x_ai",
|
||||
"crates/zed",
|
||||
"crates/zed_actions",
|
||||
@@ -274,6 +276,7 @@ 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" }
|
||||
@@ -438,7 +441,7 @@ ztracing_macro = { path = "crates/ztracing_macro" }
|
||||
# External crates
|
||||
#
|
||||
|
||||
agent-client-protocol = { version = "=0.9.0", features = ["unstable"] }
|
||||
agent-client-protocol = { version = "=0.9.2", features = ["unstable"] }
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = "0.25.1-rc1"
|
||||
any_vec = "0.14"
|
||||
|
||||
@@ -20,7 +20,6 @@ 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
|
||||
|
||||
|
||||
@@ -55,6 +55,13 @@
|
||||
"down": "menu::SelectNext",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "menu",
|
||||
"bindings": {
|
||||
"right": "menu::SelectChild",
|
||||
"left": "menu::SelectParent",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
@@ -227,6 +234,7 @@
|
||||
"ctrl-g": "search::SelectNextMatch",
|
||||
"ctrl-shift-g": "search::SelectPreviousMatch",
|
||||
"ctrl-k l": "agent::OpenRulesLibrary",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -240,6 +248,7 @@
|
||||
"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",
|
||||
@@ -252,7 +261,6 @@
|
||||
"ctrl-y": "agent::AllowOnce",
|
||||
"ctrl-alt-y": "agent::AllowAlways",
|
||||
"ctrl-alt-z": "agent::RejectOnce",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -284,36 +292,6 @@
|
||||
"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": {
|
||||
@@ -328,14 +306,25 @@
|
||||
"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",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -343,11 +332,7 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-enter": "agent::Chat",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
"enter": "editor::Newline",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -814,7 +799,7 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "PromptEditor",
|
||||
"context": "InlineAssistant",
|
||||
"bindings": {
|
||||
"ctrl-[": "agent::CyclePreviousInlineAssist",
|
||||
"ctrl-]": "agent::CycleNextInlineAssist",
|
||||
|
||||
@@ -54,6 +54,13 @@
|
||||
"ctrl-cmd-s": "workspace::ToggleWorktreeSecurity",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "menu",
|
||||
"bindings": {
|
||||
"right": "menu::SelectChild",
|
||||
"left": "menu::SelectParent",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"use_key_equivalents": true,
|
||||
@@ -267,6 +274,7 @@
|
||||
"cmd-shift-g": "search::SelectPreviousMatch",
|
||||
"cmd-k l": "agent::OpenRulesLibrary",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
"cmd-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -281,6 +289,7 @@
|
||||
"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",
|
||||
@@ -293,7 +302,6 @@
|
||||
"cmd-y": "agent::AllowOnce",
|
||||
"cmd-alt-y": "agent::AllowAlways",
|
||||
"cmd-alt-z": "agent::RejectOnce",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -325,39 +333,6 @@
|
||||
"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,
|
||||
@@ -379,16 +354,25 @@
|
||||
"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",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -396,11 +380,7 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-enter": "agent::Chat",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"cmd-shift-y": "agent::KeepAll",
|
||||
"cmd-shift-n": "agent::RejectAll",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
"enter": "editor::Newline",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -880,7 +860,7 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "PromptEditor",
|
||||
"context": "InlineAssistant > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-alt-/": "agent::ToggleModelSelector",
|
||||
|
||||
@@ -54,6 +54,13 @@
|
||||
"down": "menu::SelectNext",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "menu",
|
||||
"bindings": {
|
||||
"right": "menu::SelectChild",
|
||||
"left": "menu::SelectParent",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"use_key_equivalents": true,
|
||||
@@ -227,6 +234,7 @@
|
||||
"ctrl-g": "search::SelectNextMatch",
|
||||
"ctrl-shift-g": "search::SelectPreviousMatch",
|
||||
"ctrl-k l": "agent::OpenRulesLibrary",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -240,6 +248,7 @@
|
||||
"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",
|
||||
@@ -253,7 +262,6 @@
|
||||
"shift-alt-a": "agent::AllowOnce",
|
||||
"ctrl-alt-y": "agent::AllowAlways",
|
||||
"shift-alt-z": "agent::RejectOnce",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -286,39 +294,6 @@
|
||||
"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,
|
||||
@@ -334,16 +309,25 @@
|
||||
"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",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -351,11 +335,7 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-enter": "agent::Chat",
|
||||
"ctrl-shift-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
"enter": "editor::Newline",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -823,7 +803,7 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "PromptEditor",
|
||||
"context": "InlineAssistant",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-[": "agent::CyclePreviousInlineAssist",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "InlineAssistEditor",
|
||||
"context": "InlineAssistant > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-backspace": "editor::Cancel",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "InlineAssistEditor",
|
||||
"context": "InlineAssistant > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-shift-backspace": "editor::Cancel",
|
||||
|
||||
@@ -1178,6 +1178,10 @@
|
||||
"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,
|
||||
@@ -1321,6 +1325,14 @@
|
||||
"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"
|
||||
|
||||
@@ -11,6 +11,7 @@ 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::*;
|
||||
@@ -192,6 +193,7 @@ 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>,
|
||||
}
|
||||
|
||||
@@ -222,6 +224,11 @@ 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
|
||||
@@ -232,6 +239,7 @@ impl ToolCall {
|
||||
resolved_locations: Vec::default(),
|
||||
status,
|
||||
raw_input: tool_call.raw_input,
|
||||
raw_input_markdown,
|
||||
raw_output: tool_call.raw_output,
|
||||
};
|
||||
Ok(result)
|
||||
@@ -307,6 +315,7 @@ 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);
|
||||
}
|
||||
|
||||
@@ -875,6 +884,7 @@ pub enum AcpThreadEvent {
|
||||
Refusal,
|
||||
AvailableCommandsUpdated(Vec<acp::AvailableCommand>),
|
||||
ModeUpdated(acp::SessionModeId),
|
||||
ConfigOptionsUpdated(Vec<acp::SessionConfigOption>),
|
||||
}
|
||||
|
||||
impl EventEmitter<AcpThreadEvent> for AcpThread {}
|
||||
@@ -1184,6 +1194,10 @@ impl AcpThread {
|
||||
current_mode_id,
|
||||
..
|
||||
}) => cx.emit(AcpThreadEvent::ModeUpdated(current_mode_id)),
|
||||
acp::SessionUpdate::ConfigOptionUpdate(acp::ConfigOptionUpdate {
|
||||
config_options,
|
||||
..
|
||||
}) => cx.emit(AcpThreadEvent::ConfigOptionsUpdated(config_options)),
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
@@ -1355,6 +1369,7 @@ 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);
|
||||
@@ -1983,37 +1998,42 @@ 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 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 {
|
||||
let Some((_, message)) = self.last_user_message() 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 new_checkpoint = new_checkpoint
|
||||
let Some(new_checkpoint) = new_checkpoint
|
||||
.await
|
||||
.context("failed to get new checkpoint")
|
||||
.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(())
|
||||
})??;
|
||||
}
|
||||
.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));
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
@@ -2413,8 +2433,10 @@ 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```", value).into(),
|
||||
format!("```json\n{}\n```", pretty_json).into(),
|
||||
Some(language_registry.clone()),
|
||||
None,
|
||||
cx,
|
||||
@@ -4057,4 +4079,67 @@ 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +86,14 @@ pub trait AgentConnection {
|
||||
None
|
||||
}
|
||||
|
||||
fn session_config_options(
|
||||
&self,
|
||||
_session_id: &acp::SessionId,
|
||||
_cx: &App,
|
||||
) -> Option<Rc<dyn AgentSessionConfigOptions>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
|
||||
}
|
||||
|
||||
@@ -125,6 +133,26 @@ pub trait AgentSessionModes {
|
||||
fn set_mode(&self, mode: acp::SessionModeId, cx: &mut App) -> Task<Result<()>>;
|
||||
}
|
||||
|
||||
pub trait AgentSessionConfigOptions {
|
||||
/// Get all current config options with their state
|
||||
fn config_options(&self) -> Vec<acp::SessionConfigOption>;
|
||||
|
||||
/// Set a config option value
|
||||
/// Returns the full updated list of config options
|
||||
fn set_config_option(
|
||||
&self,
|
||||
config_id: acp::SessionConfigId,
|
||||
value: acp::SessionConfigValueId,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Vec<acp::SessionConfigOption>>>;
|
||||
|
||||
/// Whenever the config options are updated the receiver will be notified.
|
||||
/// Optional for agents that don't update their config options dynamically.
|
||||
fn watch(&self, _cx: &mut App) -> Option<watch::Receiver<()>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AuthRequired {
|
||||
pub description: Option<String>,
|
||||
@@ -202,12 +230,15 @@ pub trait AgentModelSelector: 'static {
|
||||
fn should_render_footer(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this selector supports the favorites feature.
|
||||
/// Only the native agent uses the model ID format that maps to settings.
|
||||
fn supports_favorites(&self) -> bool {
|
||||
false
|
||||
}
|
||||
/// 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)]
|
||||
@@ -215,7 +246,7 @@ pub struct AgentModelInfo {
|
||||
pub id: acp::ModelId,
|
||||
pub name: SharedString,
|
||||
pub description: Option<SharedString>,
|
||||
pub icon: Option<IconName>,
|
||||
pub icon: Option<AgentModelIcon>,
|
||||
}
|
||||
|
||||
impl From<acp::ModelInfo> for AgentModelInfo {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use anyhow::Result;
|
||||
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
|
||||
use buffer_diff::BufferDiff;
|
||||
use editor::{MultiBuffer, PathKey, multibuffer_context_lines};
|
||||
use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task};
|
||||
use itertools::Itertools;
|
||||
use language::{
|
||||
Anchor, Buffer, Capability, LanguageRegistry, OffsetRangeExt as _, Point, Rope, TextBuffer,
|
||||
Anchor, Buffer, Capability, LanguageRegistry, OffsetRangeExt as _, Point, TextBuffer,
|
||||
};
|
||||
use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
|
||||
use util::ResultExt;
|
||||
@@ -49,15 +49,15 @@ impl Diff {
|
||||
.update(cx, |multibuffer, cx| {
|
||||
let hunk_ranges = {
|
||||
let buffer = buffer.read(cx);
|
||||
let diff = diff.read(cx);
|
||||
diff.hunks_intersecting_range(
|
||||
Anchor::min_for_buffer(buffer.remote_id())
|
||||
..Anchor::max_for_buffer(buffer.remote_id()),
|
||||
buffer,
|
||||
cx,
|
||||
)
|
||||
.map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer))
|
||||
.collect::<Vec<_>>()
|
||||
diff.read(cx)
|
||||
.snapshot(cx)
|
||||
.hunks_intersecting_range(
|
||||
Anchor::min_for_buffer(buffer.remote_id())
|
||||
..Anchor::max_for_buffer(buffer.remote_id()),
|
||||
buffer,
|
||||
)
|
||||
.map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
multibuffer.set_excerpts_for_path(
|
||||
@@ -86,17 +86,9 @@ impl Diff {
|
||||
|
||||
pub fn new(buffer: Entity<Buffer>, cx: &mut Context<Self>) -> Self {
|
||||
let buffer_text_snapshot = buffer.read(cx).text_snapshot();
|
||||
let base_text_snapshot = buffer.read(cx).snapshot();
|
||||
let base_text = base_text_snapshot.text();
|
||||
debug_assert_eq!(buffer_text_snapshot.text(), base_text);
|
||||
let buffer_diff = cx.new(|cx| {
|
||||
let mut diff = BufferDiff::new_unchanged(&buffer_text_snapshot, base_text_snapshot);
|
||||
let snapshot = diff.snapshot(cx);
|
||||
let secondary_diff = cx.new(|cx| {
|
||||
let mut diff = BufferDiff::new(&buffer_text_snapshot, cx);
|
||||
diff.set_snapshot(snapshot, &buffer_text_snapshot, cx);
|
||||
diff
|
||||
});
|
||||
let mut diff = BufferDiff::new_unchanged(&buffer_text_snapshot, cx);
|
||||
let secondary_diff = cx.new(|cx| BufferDiff::new_unchanged(&buffer_text_snapshot, cx));
|
||||
diff.set_secondary_diff(secondary_diff);
|
||||
diff
|
||||
});
|
||||
@@ -109,7 +101,7 @@ impl Diff {
|
||||
|
||||
Self::Pending(PendingDiff {
|
||||
multibuffer,
|
||||
base_text: Arc::new(base_text),
|
||||
base_text: Arc::from(buffer_text_snapshot.text().as_str()),
|
||||
_subscription: cx.observe(&buffer, |this, _, cx| {
|
||||
if let Diff::Pending(diff) = this {
|
||||
diff.update(cx);
|
||||
@@ -176,7 +168,7 @@ impl Diff {
|
||||
new_buffer,
|
||||
..
|
||||
}) => {
|
||||
base_text.as_str() != old_text
|
||||
base_text.as_ref() != old_text
|
||||
|| !new_buffer.read(cx).as_rope().chunks().equals_str(new_text)
|
||||
}
|
||||
Diff::Finalized(FinalizedDiff {
|
||||
@@ -184,7 +176,7 @@ impl Diff {
|
||||
new_buffer,
|
||||
..
|
||||
}) => {
|
||||
base_text.as_str() != old_text
|
||||
base_text.as_ref() != old_text
|
||||
|| !new_buffer.read(cx).as_rope().chunks().equals_str(new_text)
|
||||
}
|
||||
}
|
||||
@@ -193,7 +185,7 @@ impl Diff {
|
||||
|
||||
pub struct PendingDiff {
|
||||
multibuffer: Entity<MultiBuffer>,
|
||||
base_text: Arc<String>,
|
||||
base_text: Arc<str>,
|
||||
new_buffer: Entity<Buffer>,
|
||||
diff: Entity<BufferDiff>,
|
||||
revealed_ranges: Vec<Range<Anchor>>,
|
||||
@@ -208,21 +200,22 @@ impl PendingDiff {
|
||||
let base_text = self.base_text.clone();
|
||||
self.update_diff = cx.spawn(async move |diff, cx| {
|
||||
let text_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot())?;
|
||||
let diff_snapshot = BufferDiff::update_diff(
|
||||
buffer_diff.clone(),
|
||||
text_snapshot.clone(),
|
||||
Some(base_text),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
let language = buffer.read_with(cx, |buffer, _| buffer.language().cloned())?;
|
||||
let update = buffer_diff
|
||||
.update(cx, |diff, cx| {
|
||||
diff.update_diff(
|
||||
text_snapshot.clone(),
|
||||
Some(base_text.clone()),
|
||||
false,
|
||||
language,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await;
|
||||
buffer_diff.update(cx, |diff, cx| {
|
||||
diff.set_snapshot(diff_snapshot.clone(), &text_snapshot, cx);
|
||||
diff.set_snapshot(update.clone(), &text_snapshot, cx);
|
||||
diff.secondary_diff().unwrap().update(cx, |diff, cx| {
|
||||
diff.set_snapshot(diff_snapshot.clone(), &text_snapshot, cx);
|
||||
diff.set_snapshot(update, &text_snapshot, cx);
|
||||
});
|
||||
})?;
|
||||
diff.update(cx, |diff, cx| {
|
||||
@@ -319,13 +312,14 @@ impl PendingDiff {
|
||||
|
||||
fn excerpt_ranges(&self, cx: &App) -> Vec<Range<Point>> {
|
||||
let buffer = self.new_buffer.read(cx);
|
||||
let diff = self.diff.read(cx);
|
||||
let mut ranges = diff
|
||||
let mut ranges = self
|
||||
.diff
|
||||
.read(cx)
|
||||
.snapshot(cx)
|
||||
.hunks_intersecting_range(
|
||||
Anchor::min_for_buffer(buffer.remote_id())
|
||||
..Anchor::max_for_buffer(buffer.remote_id()),
|
||||
buffer,
|
||||
cx,
|
||||
)
|
||||
.map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -357,60 +351,47 @@ impl PendingDiff {
|
||||
|
||||
pub struct FinalizedDiff {
|
||||
path: String,
|
||||
base_text: Arc<String>,
|
||||
base_text: Arc<str>,
|
||||
new_buffer: Entity<Buffer>,
|
||||
multibuffer: Entity<MultiBuffer>,
|
||||
_update_diff: Task<Result<()>>,
|
||||
}
|
||||
|
||||
async fn build_buffer_diff(
|
||||
old_text: Arc<String>,
|
||||
old_text: Arc<str>,
|
||||
buffer: &Entity<Buffer>,
|
||||
language_registry: Option<Arc<LanguageRegistry>>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Entity<BufferDiff>> {
|
||||
let language = cx.update(|cx| buffer.read(cx).language().cloned())?;
|
||||
let buffer = cx.update(|cx| buffer.read(cx).snapshot())?;
|
||||
|
||||
let old_text_rope = cx
|
||||
.background_spawn({
|
||||
let old_text = old_text.clone();
|
||||
async move { Rope::from(old_text.as_str()) }
|
||||
})
|
||||
.await;
|
||||
let base_buffer = cx
|
||||
.update(|cx| {
|
||||
Buffer::build_snapshot(
|
||||
old_text_rope,
|
||||
buffer.language().cloned(),
|
||||
language_registry,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await;
|
||||
let secondary_diff = cx.new(|cx| BufferDiff::new(&buffer, cx))?;
|
||||
|
||||
let diff_snapshot = cx
|
||||
.update(|cx| {
|
||||
BufferDiffSnapshot::new_with_base_buffer(
|
||||
let update = secondary_diff
|
||||
.update(cx, |secondary_diff, cx| {
|
||||
secondary_diff.update_diff(
|
||||
buffer.text.clone(),
|
||||
Some(old_text),
|
||||
base_buffer,
|
||||
true,
|
||||
language.clone(),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await;
|
||||
|
||||
let secondary_diff = cx.new(|cx| {
|
||||
let mut diff = BufferDiff::new(&buffer, cx);
|
||||
diff.set_snapshot(diff_snapshot.clone(), &buffer, cx);
|
||||
diff
|
||||
secondary_diff.update(cx, |secondary_diff, cx| {
|
||||
secondary_diff.language_changed(language.clone(), language_registry.clone(), cx);
|
||||
secondary_diff.set_snapshot(update.clone(), &buffer, cx);
|
||||
})?;
|
||||
|
||||
cx.new(|cx| {
|
||||
let mut diff = BufferDiff::new(&buffer.text, cx);
|
||||
diff.set_snapshot(diff_snapshot, &buffer, cx);
|
||||
let diff = cx.new(|cx| BufferDiff::new(&buffer, cx))?;
|
||||
diff.update(cx, |diff, cx| {
|
||||
diff.language_changed(language, language_registry, cx);
|
||||
diff.set_snapshot(update.clone(), &buffer, cx);
|
||||
diff.set_secondary_diff(secondary_diff);
|
||||
diff
|
||||
})
|
||||
})?;
|
||||
Ok(diff)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -4,22 +4,20 @@ use std::{
|
||||
fmt::Display,
|
||||
rc::{Rc, Weak},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use agent_client_protocol as acp;
|
||||
use collections::HashMap;
|
||||
use gpui::{
|
||||
App, ClipboardItem, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment,
|
||||
ListState, StyleRefinement, Subscription, Task, TextStyleRefinement, Window, actions, list,
|
||||
prelude::*,
|
||||
App, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment, ListState,
|
||||
StyleRefinement, Subscription, Task, TextStyleRefinement, Window, actions, list, prelude::*,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use markdown::{CodeBlockRenderer, Markdown, MarkdownElement, MarkdownStyle};
|
||||
use project::Project;
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{Tooltip, WithScrollbar, prelude::*};
|
||||
use ui::{CopyButton, Tooltip, WithScrollbar, prelude::*};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{
|
||||
Item, ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||
@@ -544,15 +542,11 @@ impl Render for AcpTools {
|
||||
|
||||
pub struct AcpToolsToolbarItemView {
|
||||
acp_tools: Option<Entity<AcpTools>>,
|
||||
just_copied: bool,
|
||||
}
|
||||
|
||||
impl AcpToolsToolbarItemView {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
acp_tools: None,
|
||||
just_copied: false,
|
||||
}
|
||||
Self { acp_tools: None }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -572,37 +566,14 @@ impl Render for AcpToolsToolbarItemView {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child({
|
||||
let acp_tools = acp_tools.clone();
|
||||
IconButton::new(
|
||||
"copy_all_messages",
|
||||
if self.just_copied {
|
||||
IconName::Check
|
||||
} else {
|
||||
IconName::Copy
|
||||
},
|
||||
)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text(if self.just_copied {
|
||||
"Copied!"
|
||||
} else {
|
||||
"Copy All Messages"
|
||||
}))
|
||||
.disabled(!has_messages)
|
||||
.on_click(cx.listener(move |this, _, _window, cx| {
|
||||
if let Some(content) = acp_tools.read(cx).serialize_observed_messages() {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(content));
|
||||
let message = acp_tools
|
||||
.read(cx)
|
||||
.serialize_observed_messages()
|
||||
.unwrap_or_default();
|
||||
|
||||
this.just_copied = true;
|
||||
cx.spawn(async move |this, cx| {
|
||||
cx.background_executor().timer(Duration::from_secs(2)).await;
|
||||
this.update(cx, |this, cx| {
|
||||
this.just_copied = false;
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}))
|
||||
CopyButton::new(message)
|
||||
.tooltip_label("Copy All Messages")
|
||||
.disabled(!has_messages)
|
||||
})
|
||||
.child(
|
||||
IconButton::new("clear_messages", IconName::Trash)
|
||||
|
||||
@@ -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, DiskState, Point, ToPoint};
|
||||
use language::{Anchor, Buffer, BufferEvent, 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() == DiskState::Deleted)
|
||||
.is_some_and(|file| file.disk_state().is_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() != DiskState::Deleted)
|
||||
.is_some_and(|file| !file.disk_state().is_deleted())
|
||||
{
|
||||
// If the buffer had been deleted by a tool, but it got
|
||||
// resurrected externally, we want to clear the edits we
|
||||
@@ -262,7 +262,7 @@ impl ActionLog {
|
||||
);
|
||||
}
|
||||
|
||||
(Arc::new(base_text.to_string()), base_text)
|
||||
(Arc::from(base_text.to_string().as_str()), base_text)
|
||||
}
|
||||
});
|
||||
|
||||
@@ -302,7 +302,7 @@ impl ActionLog {
|
||||
.context("buffer not tracked")?;
|
||||
let old_unreviewed_edits = tracked_buffer.unreviewed_edits.clone();
|
||||
let agent_diff_base = tracked_buffer.diff_base.clone();
|
||||
let git_diff_base = git_diff.read(cx).base_text().as_rope().clone();
|
||||
let git_diff_base = git_diff.read(cx).base_text(cx).as_rope().clone();
|
||||
let buffer_text = tracked_buffer.snapshot.as_rope().clone();
|
||||
anyhow::Ok(cx.background_spawn(async move {
|
||||
let mut old_unreviewed_edits = old_unreviewed_edits.into_iter().peekable();
|
||||
@@ -352,7 +352,7 @@ impl ActionLog {
|
||||
}
|
||||
|
||||
(
|
||||
Arc::new(new_agent_diff_base.to_string()),
|
||||
Arc::from(new_agent_diff_base.to_string().as_str()),
|
||||
new_agent_diff_base,
|
||||
)
|
||||
}))
|
||||
@@ -374,11 +374,11 @@ impl ActionLog {
|
||||
this: &WeakEntity<ActionLog>,
|
||||
buffer: &Entity<Buffer>,
|
||||
buffer_snapshot: text::BufferSnapshot,
|
||||
new_base_text: Arc<String>,
|
||||
new_base_text: Arc<str>,
|
||||
new_diff_base: Rope,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<()> {
|
||||
let (diff, language, language_registry) = this.read_with(cx, |this, cx| {
|
||||
let (diff, language) = this.read_with(cx, |this, cx| {
|
||||
let tracked_buffer = this
|
||||
.tracked_buffers
|
||||
.get(buffer)
|
||||
@@ -386,25 +386,28 @@ impl ActionLog {
|
||||
anyhow::Ok((
|
||||
tracked_buffer.diff.clone(),
|
||||
buffer.read(cx).language().cloned(),
|
||||
buffer.read(cx).language_registry(),
|
||||
))
|
||||
})??;
|
||||
let diff_snapshot = BufferDiff::update_diff(
|
||||
diff.clone(),
|
||||
buffer_snapshot.clone(),
|
||||
Some(new_base_text),
|
||||
true,
|
||||
false,
|
||||
language,
|
||||
language_registry,
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let update = diff.update(cx, |diff, cx| {
|
||||
diff.update_diff(
|
||||
buffer_snapshot.clone(),
|
||||
Some(new_base_text),
|
||||
true,
|
||||
language,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let mut unreviewed_edits = Patch::default();
|
||||
if let Ok(diff_snapshot) = diff_snapshot {
|
||||
if let Ok(update) = update {
|
||||
let update = update.await;
|
||||
|
||||
let diff_snapshot = diff.update(cx, |diff, cx| {
|
||||
diff.set_snapshot(update.clone(), &buffer_snapshot, cx);
|
||||
diff.snapshot(cx)
|
||||
})?;
|
||||
|
||||
unreviewed_edits = cx
|
||||
.background_spawn({
|
||||
let diff_snapshot = diff_snapshot.clone();
|
||||
let buffer_snapshot = buffer_snapshot.clone();
|
||||
let new_diff_base = new_diff_base.clone();
|
||||
async move {
|
||||
@@ -431,10 +434,6 @@ impl ActionLog {
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
diff.update(cx, |diff, cx| {
|
||||
diff.set_snapshot(diff_snapshot, &buffer_snapshot, cx);
|
||||
})?;
|
||||
}
|
||||
this.update(cx, |this, cx| {
|
||||
let tracked_buffer = this
|
||||
@@ -769,7 +768,7 @@ impl ActionLog {
|
||||
tracked.version != buffer.version
|
||||
&& buffer
|
||||
.file()
|
||||
.is_some_and(|file| file.disk_state() != DiskState::Deleted)
|
||||
.is_some_and(|file| !file.disk_state().is_deleted())
|
||||
})
|
||||
.map(|(buffer, _)| buffer)
|
||||
}
|
||||
@@ -975,7 +974,8 @@ impl TrackedBuffer {
|
||||
fn has_edits(&self, cx: &App) -> bool {
|
||||
self.diff
|
||||
.read(cx)
|
||||
.hunks(self.buffer.read(cx), cx)
|
||||
.snapshot(cx)
|
||||
.hunks(self.buffer.read(cx))
|
||||
.next()
|
||||
.is_some()
|
||||
}
|
||||
@@ -2388,13 +2388,14 @@ mod tests {
|
||||
(
|
||||
buffer,
|
||||
diff.read(cx)
|
||||
.hunks(&snapshot, cx)
|
||||
.snapshot(cx)
|
||||
.hunks(&snapshot)
|
||||
.map(|hunk| HunkStatus {
|
||||
diff_status: hunk.status().kind,
|
||||
range: hunk.range,
|
||||
old_text: diff
|
||||
.read(cx)
|
||||
.base_text()
|
||||
.base_text(cx)
|
||||
.text_for_range(hunk.diff_base_byte_range)
|
||||
.collect(),
|
||||
})
|
||||
|
||||
@@ -30,7 +30,7 @@ use futures::{StreamExt, future};
|
||||
use gpui::{
|
||||
App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity,
|
||||
};
|
||||
use language_model::{LanguageModel, LanguageModelProvider, LanguageModelRegistry};
|
||||
use language_model::{IconOrSvg, LanguageModel, LanguageModelProvider, LanguageModelRegistry};
|
||||
use project::{Project, ProjectItem, ProjectPath, Worktree};
|
||||
use prompt_store::{
|
||||
ProjectContext, PromptStore, RULES_FILE_NAMES, RulesFileContext, UserRulesContext,
|
||||
@@ -93,7 +93,7 @@ impl LanguageModels {
|
||||
fn refresh_list(&mut self, cx: &App) {
|
||||
let providers = LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.providers()
|
||||
.visible_providers()
|
||||
.into_iter()
|
||||
.filter(|provider| provider.is_authenticated(cx))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -153,7 +153,10 @@ impl LanguageModels {
|
||||
id: Self::model_id(model),
|
||||
name: model.name().0,
|
||||
description: None,
|
||||
icon: Some(provider.icon()),
|
||||
icon: Some(match provider.icon() {
|
||||
IconOrSvg::Svg(path) => acp_thread::AgentModelIcon::Path(path),
|
||||
IconOrSvg::Icon(name) => acp_thread::AgentModelIcon::Named(name),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +167,7 @@ impl LanguageModels {
|
||||
fn authenticate_all_language_model_providers(cx: &mut App) -> Task<()> {
|
||||
let authenticate_all_providers = LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.providers()
|
||||
.visible_providers()
|
||||
.iter()
|
||||
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -426,7 +429,7 @@ impl NativeAgent {
|
||||
.into_iter()
|
||||
.flat_map(|(contents, prompt_metadata)| match contents {
|
||||
Ok(contents) => Some(UserRulesContext {
|
||||
uuid: prompt_metadata.id.user_id()?,
|
||||
uuid: prompt_metadata.id.as_user()?,
|
||||
title: prompt_metadata.title.map(|title| title.to_string()),
|
||||
contents,
|
||||
}),
|
||||
@@ -1164,10 +1167,6 @@ impl acp_thread::AgentModelSelector for NativeAgentModelSelector {
|
||||
fn should_render_footer(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn supports_favorites(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||
@@ -1630,7 +1629,9 @@ mod internal_tests {
|
||||
id: acp::ModelId::new("fake/fake"),
|
||||
name: "Fake".into(),
|
||||
description: None,
|
||||
icon: Some(ui::IconName::ZedAssistant),
|
||||
icon: Some(acp_thread::AgentModelIcon::Named(
|
||||
ui::IconName::ZedAssistant
|
||||
)),
|
||||
}]
|
||||
)])
|
||||
);
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
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};
|
||||
|
||||
@@ -71,6 +75,38 @@ 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)]
|
||||
|
||||
@@ -5,7 +5,7 @@ use futures::StreamExt;
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use language::{OffsetRangeExt, ParseStatus, Point};
|
||||
use project::{
|
||||
Project, WorktreeSettings,
|
||||
Project, SearchResults, WorktreeSettings,
|
||||
search::{SearchQuery, SearchResult},
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
@@ -176,14 +176,17 @@ impl AgentTool for GrepTool {
|
||||
|
||||
let project = self.project.downgrade();
|
||||
cx.spawn(async move |cx| {
|
||||
futures::pin_mut!(results);
|
||||
// Keep the search alive for the duration of result iteration. Dropping this task is the
|
||||
// cancellation mechanism; we intentionally do not detach it.
|
||||
let SearchResults {rx, _task_handle} = results;
|
||||
futures::pin_mut!(rx);
|
||||
|
||||
let mut output = String::new();
|
||||
let mut skips_remaining = input.offset;
|
||||
let mut matches_found = 0;
|
||||
let mut has_more_matches = false;
|
||||
|
||||
'outer: while let Some(SearchResult::Buffer { buffer, ranges }) = results.next().await {
|
||||
'outer: while let Some(SearchResult::Buffer { buffer, ranges }) = rx.next().await {
|
||||
if ranges.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ acp_tools.workspace = true
|
||||
acp_thread.workspace = true
|
||||
action_log.workspace = true
|
||||
agent-client-protocol.workspace = true
|
||||
feature_flags.workspace = true
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
client.workspace = true
|
||||
|
||||
@@ -4,6 +4,7 @@ use action_log::ActionLog;
|
||||
use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
|
||||
use anyhow::anyhow;
|
||||
use collections::HashMap;
|
||||
use feature_flags::{AcpBetaFeatureFlag, FeatureFlagAppExt as _};
|
||||
use futures::AsyncBufReadExt as _;
|
||||
use futures::io::BufReader;
|
||||
use project::Project;
|
||||
@@ -12,8 +13,10 @@ use serde::Deserialize;
|
||||
use settings::Settings as _;
|
||||
use task::ShellBuilder;
|
||||
use util::ResultExt as _;
|
||||
use util::process::Child;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::process::Stdio;
|
||||
use std::{any::Any, cell::RefCell};
|
||||
use std::{path::Path, rc::Rc};
|
||||
use thiserror::Error;
|
||||
@@ -38,20 +41,37 @@ pub struct AcpConnection {
|
||||
agent_capabilities: acp::AgentCapabilities,
|
||||
default_mode: Option<acp::SessionModeId>,
|
||||
default_model: Option<acp::ModelId>,
|
||||
default_config_options: HashMap<String, String>,
|
||||
root_dir: PathBuf,
|
||||
// NB: Don't move this into the wait_task, since we need to ensure the process is
|
||||
// killed on drop (setting kill_on_drop on the command seems to not always work).
|
||||
child: smol::process::Child,
|
||||
child: Child,
|
||||
_io_task: Task<Result<(), acp::Error>>,
|
||||
_wait_task: Task<Result<()>>,
|
||||
_stderr_task: Task<Result<()>>,
|
||||
}
|
||||
|
||||
struct ConfigOptions {
|
||||
config_options: Rc<RefCell<Vec<acp::SessionConfigOption>>>,
|
||||
tx: Rc<RefCell<watch::Sender<()>>>,
|
||||
rx: watch::Receiver<()>,
|
||||
}
|
||||
|
||||
impl ConfigOptions {
|
||||
fn new(config_options: Rc<RefCell<Vec<acp::SessionConfigOption>>>) -> Self {
|
||||
let (tx, rx) = watch::channel(());
|
||||
Self {
|
||||
config_options,
|
||||
tx: Rc::new(RefCell::new(tx)),
|
||||
rx,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AcpSession {
|
||||
thread: WeakEntity<AcpThread>,
|
||||
suppress_abort_err: bool,
|
||||
models: Option<Rc<RefCell<acp::SessionModelState>>>,
|
||||
session_modes: Option<Rc<RefCell<acp::SessionModeState>>>,
|
||||
config_options: Option<ConfigOptions>,
|
||||
}
|
||||
|
||||
pub async fn connect(
|
||||
@@ -60,6 +80,7 @@ pub async fn connect(
|
||||
root_dir: &Path,
|
||||
default_mode: Option<acp::SessionModeId>,
|
||||
default_model: Option<acp::ModelId>,
|
||||
default_config_options: HashMap<String, String>,
|
||||
is_remote: bool,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Rc<dyn AgentConnection>> {
|
||||
@@ -69,6 +90,7 @@ pub async fn connect(
|
||||
root_dir,
|
||||
default_mode,
|
||||
default_model,
|
||||
default_config_options,
|
||||
is_remote,
|
||||
cx,
|
||||
)
|
||||
@@ -85,22 +107,19 @@ impl AcpConnection {
|
||||
root_dir: &Path,
|
||||
default_mode: Option<acp::SessionModeId>,
|
||||
default_model: Option<acp::ModelId>,
|
||||
default_config_options: HashMap<String, String>,
|
||||
is_remote: bool,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Self> {
|
||||
let shell = cx.update(|cx| TerminalSettings::get(None, cx).shell.clone())?;
|
||||
let builder = ShellBuilder::new(&shell, cfg!(windows)).non_interactive();
|
||||
let mut child =
|
||||
builder.build_command(Some(command.path.display().to_string()), &command.args);
|
||||
child
|
||||
.envs(command.env.iter().flatten())
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped());
|
||||
builder.build_std_command(Some(command.path.display().to_string()), &command.args);
|
||||
child.envs(command.env.iter().flatten());
|
||||
if !is_remote {
|
||||
child.current_dir(root_dir);
|
||||
}
|
||||
let mut child = child.spawn()?;
|
||||
let mut child = Child::spawn(child, Stdio::piped(), Stdio::piped(), Stdio::piped())?;
|
||||
|
||||
let stdout = child.stdout.take().context("Failed to take stdout")?;
|
||||
let stdin = child.stdin.take().context("Failed to take stdin")?;
|
||||
@@ -217,6 +236,7 @@ impl AcpConnection {
|
||||
agent_capabilities: response.agent_capabilities,
|
||||
default_mode,
|
||||
default_model,
|
||||
default_config_options,
|
||||
_io_task: io_task,
|
||||
_wait_task: wait_task,
|
||||
_stderr_task: stderr_task,
|
||||
@@ -235,7 +255,6 @@ impl AcpConnection {
|
||||
|
||||
impl Drop for AcpConnection {
|
||||
fn drop(&mut self) {
|
||||
// See the comment on the child field.
|
||||
self.child.kill().log_err();
|
||||
}
|
||||
}
|
||||
@@ -256,6 +275,7 @@ impl AgentConnection for AcpConnection {
|
||||
let sessions = self.sessions.clone();
|
||||
let default_mode = self.default_mode.clone();
|
||||
let default_model = self.default_model.clone();
|
||||
let default_config_options = self.default_config_options.clone();
|
||||
let cwd = cwd.to_path_buf();
|
||||
let context_server_store = project.read(cx).context_server_store().read(cx);
|
||||
let mcp_servers = if project.read(cx).is_local() {
|
||||
@@ -322,8 +342,21 @@ impl AgentConnection for AcpConnection {
|
||||
}
|
||||
})?;
|
||||
|
||||
let modes = response.modes.map(|modes| Rc::new(RefCell::new(modes)));
|
||||
let models = response.models.map(|models| Rc::new(RefCell::new(models)));
|
||||
let use_config_options = cx.update(|cx| cx.has_flag::<AcpBetaFeatureFlag>())?;
|
||||
|
||||
// Config options take precedence over legacy modes/models
|
||||
let (modes, models, config_options) = if use_config_options && let Some(opts) = response.config_options {
|
||||
(
|
||||
None,
|
||||
None,
|
||||
Some(Rc::new(RefCell::new(opts))),
|
||||
)
|
||||
} else {
|
||||
// Fall back to legacy modes/models
|
||||
let modes = response.modes.map(|modes| Rc::new(RefCell::new(modes)));
|
||||
let models = response.models.map(|models| Rc::new(RefCell::new(models)));
|
||||
(modes, models, None)
|
||||
};
|
||||
|
||||
if let Some(default_mode) = default_mode {
|
||||
if let Some(modes) = modes.as_ref() {
|
||||
@@ -411,6 +444,92 @@ impl AgentConnection for AcpConnection {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(config_opts) = config_options.as_ref() {
|
||||
let defaults_to_apply: Vec<_> = {
|
||||
let config_opts_ref = config_opts.borrow();
|
||||
config_opts_ref
|
||||
.iter()
|
||||
.filter_map(|config_option| {
|
||||
let default_value = default_config_options.get(&*config_option.id.0)?;
|
||||
|
||||
let is_valid = match &config_option.kind {
|
||||
acp::SessionConfigKind::Select(select) => match &select.options {
|
||||
acp::SessionConfigSelectOptions::Ungrouped(options) => {
|
||||
options.iter().any(|opt| &*opt.value.0 == default_value.as_str())
|
||||
}
|
||||
acp::SessionConfigSelectOptions::Grouped(groups) => groups
|
||||
.iter()
|
||||
.any(|g| g.options.iter().any(|opt| &*opt.value.0 == default_value.as_str())),
|
||||
_ => false,
|
||||
},
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if is_valid {
|
||||
let initial_value = match &config_option.kind {
|
||||
acp::SessionConfigKind::Select(select) => {
|
||||
Some(select.current_value.clone())
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
Some((config_option.id.clone(), default_value.clone(), initial_value))
|
||||
} else {
|
||||
log::warn!(
|
||||
"`{}` is not a valid value for config option `{}` in {}",
|
||||
default_value,
|
||||
config_option.id.0,
|
||||
name
|
||||
);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
for (config_id, default_value, initial_value) in defaults_to_apply {
|
||||
cx.spawn({
|
||||
let default_value_id = acp::SessionConfigValueId::new(default_value.clone());
|
||||
let session_id = response.session_id.clone();
|
||||
let config_id_clone = config_id.clone();
|
||||
let config_opts = config_opts.clone();
|
||||
let conn = conn.clone();
|
||||
async move |_| {
|
||||
let result = conn
|
||||
.set_session_config_option(
|
||||
acp::SetSessionConfigOptionRequest::new(
|
||||
session_id,
|
||||
config_id_clone.clone(),
|
||||
default_value_id,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
|
||||
if result.is_none() {
|
||||
if let Some(initial) = initial_value {
|
||||
let mut opts = config_opts.borrow_mut();
|
||||
if let Some(opt) = opts.iter_mut().find(|o| o.id == config_id_clone) {
|
||||
if let acp::SessionConfigKind::Select(select) =
|
||||
&mut opt.kind
|
||||
{
|
||||
select.current_value = initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let mut opts = config_opts.borrow_mut();
|
||||
if let Some(opt) = opts.iter_mut().find(|o| o.id == config_id) {
|
||||
if let acp::SessionConfigKind::Select(select) = &mut opt.kind {
|
||||
select.current_value = acp::SessionConfigValueId::new(default_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let session_id = response.session_id;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
|
||||
let thread = cx.new(|cx| {
|
||||
@@ -432,6 +551,7 @@ impl AgentConnection for AcpConnection {
|
||||
suppress_abort_err: false,
|
||||
session_modes: modes,
|
||||
models,
|
||||
config_options: config_options.map(|opts| ConfigOptions::new(opts))
|
||||
};
|
||||
sessions.borrow_mut().insert(session_id, session);
|
||||
|
||||
@@ -567,6 +687,25 @@ impl AgentConnection for AcpConnection {
|
||||
}
|
||||
}
|
||||
|
||||
fn session_config_options(
|
||||
&self,
|
||||
session_id: &acp::SessionId,
|
||||
_cx: &App,
|
||||
) -> Option<Rc<dyn acp_thread::AgentSessionConfigOptions>> {
|
||||
let sessions = self.sessions.borrow();
|
||||
let session = sessions.get(session_id)?;
|
||||
|
||||
let config_opts = session.config_options.as_ref()?;
|
||||
|
||||
Some(Rc::new(AcpSessionConfigOptions {
|
||||
session_id: session_id.clone(),
|
||||
connection: self.connection.clone(),
|
||||
state: config_opts.config_options.clone(),
|
||||
watch_tx: config_opts.tx.clone(),
|
||||
watch_rx: config_opts.rx.clone(),
|
||||
}) as _)
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
|
||||
self
|
||||
}
|
||||
@@ -685,6 +824,49 @@ impl acp_thread::AgentModelSelector for AcpModelSelector {
|
||||
}
|
||||
}
|
||||
|
||||
struct AcpSessionConfigOptions {
|
||||
session_id: acp::SessionId,
|
||||
connection: Rc<acp::ClientSideConnection>,
|
||||
state: Rc<RefCell<Vec<acp::SessionConfigOption>>>,
|
||||
watch_tx: Rc<RefCell<watch::Sender<()>>>,
|
||||
watch_rx: watch::Receiver<()>,
|
||||
}
|
||||
|
||||
impl acp_thread::AgentSessionConfigOptions for AcpSessionConfigOptions {
|
||||
fn config_options(&self) -> Vec<acp::SessionConfigOption> {
|
||||
self.state.borrow().clone()
|
||||
}
|
||||
|
||||
fn set_config_option(
|
||||
&self,
|
||||
config_id: acp::SessionConfigId,
|
||||
value: acp::SessionConfigValueId,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Vec<acp::SessionConfigOption>>> {
|
||||
let connection = self.connection.clone();
|
||||
let session_id = self.session_id.clone();
|
||||
let state = self.state.clone();
|
||||
|
||||
let watch_tx = self.watch_tx.clone();
|
||||
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let response = connection
|
||||
.set_session_config_option(acp::SetSessionConfigOptionRequest::new(
|
||||
session_id, config_id, value,
|
||||
))
|
||||
.await?;
|
||||
|
||||
*state.borrow_mut() = response.config_options.clone();
|
||||
watch_tx.borrow_mut().send(()).ok();
|
||||
Ok(response.config_options)
|
||||
})
|
||||
}
|
||||
|
||||
fn watch(&self, _cx: &mut App) -> Option<watch::Receiver<()>> {
|
||||
Some(self.watch_rx.clone())
|
||||
}
|
||||
}
|
||||
|
||||
struct ClientDelegate {
|
||||
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
|
||||
cx: AsyncApp,
|
||||
@@ -778,6 +960,21 @@ impl acp::Client for ClientDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
if let acp::SessionUpdate::ConfigOptionUpdate(acp::ConfigOptionUpdate {
|
||||
config_options,
|
||||
..
|
||||
}) = ¬ification.update
|
||||
{
|
||||
if let Some(opts) = &session.config_options {
|
||||
*opts.config_options.borrow_mut() = config_options.clone();
|
||||
opts.tx.borrow_mut().send(()).ok();
|
||||
} else {
|
||||
log::error!(
|
||||
"Got a `ConfigOptionUpdate` notification, but the agent didn't specify `config_options` during session setup."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Clone so we can inspect meta both before and after handing off to the thread
|
||||
let update_clone = notification.update.clone();
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ pub mod e2e_tests;
|
||||
pub use claude::*;
|
||||
use client::ProxySettings;
|
||||
pub use codex::*;
|
||||
use collections::HashMap;
|
||||
use collections::{HashMap, HashSet};
|
||||
pub use custom::*;
|
||||
use fs::Fs;
|
||||
pub use gemini::*;
|
||||
@@ -56,9 +56,19 @@ impl AgentServerDelegate {
|
||||
pub trait AgentServer: Send {
|
||||
fn logo(&self) -> ui::IconName;
|
||||
fn name(&self) -> SharedString;
|
||||
fn default_mode(&self, _cx: &mut App) -> Option<agent_client_protocol::SessionModeId> {
|
||||
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: &App) -> Option<agent_client_protocol::SessionModeId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_default_mode(
|
||||
&self,
|
||||
_mode_id: Option<agent_client_protocol::SessionModeId>,
|
||||
@@ -67,7 +77,7 @@ pub trait AgentServer: Send {
|
||||
) {
|
||||
}
|
||||
|
||||
fn default_model(&self, _cx: &mut App) -> Option<agent_client_protocol::ModelId> {
|
||||
fn default_model(&self, _cx: &App) -> Option<agent_client_protocol::ModelId> {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -79,14 +89,49 @@ pub trait AgentServer: Send {
|
||||
) {
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: Option<&Path>,
|
||||
delegate: AgentServerDelegate,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>>;
|
||||
fn favorite_model_ids(&self, _cx: &mut App) -> HashSet<agent_client_protocol::ModelId> {
|
||||
HashSet::default()
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
|
||||
fn default_config_option(&self, _config_id: &str, _cx: &App) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_default_config_option(
|
||||
&self,
|
||||
_config_id: &str,
|
||||
_value_id: Option<&str>,
|
||||
_fs: Arc<dyn Fs>,
|
||||
_cx: &mut App,
|
||||
) {
|
||||
}
|
||||
|
||||
fn favorite_config_option_value_ids(
|
||||
&self,
|
||||
_config_id: &agent_client_protocol::SessionConfigId,
|
||||
_cx: &mut App,
|
||||
) -> HashSet<agent_client_protocol::SessionConfigValueId> {
|
||||
HashSet::default()
|
||||
}
|
||||
|
||||
fn toggle_favorite_config_option_value(
|
||||
&self,
|
||||
_config_id: agent_client_protocol::SessionConfigId,
|
||||
_value_id: agent_client_protocol::SessionConfigValueId,
|
||||
_should_be_favorite: bool,
|
||||
_fs: Arc<dyn Fs>,
|
||||
_cx: &App,
|
||||
) {
|
||||
}
|
||||
|
||||
fn toggle_favorite_model(
|
||||
&self,
|
||||
_model_id: agent_client_protocol::ModelId,
|
||||
_should_be_favorite: bool,
|
||||
_fs: Arc<dyn Fs>,
|
||||
_cx: &App,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl dyn AgentServer {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use agent_client_protocol as acp;
|
||||
use collections::HashSet;
|
||||
use fs::Fs;
|
||||
use settings::{SettingsStore, update_settings_file};
|
||||
use std::path::Path;
|
||||
@@ -30,7 +31,7 @@ impl AgentServer for ClaudeCode {
|
||||
ui::IconName::AiClaude
|
||||
}
|
||||
|
||||
fn default_mode(&self, cx: &mut App) -> Option<acp::SessionModeId> {
|
||||
fn default_mode(&self, cx: &App) -> Option<acp::SessionModeId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).claude.clone()
|
||||
});
|
||||
@@ -51,7 +52,7 @@ impl AgentServer for ClaudeCode {
|
||||
});
|
||||
}
|
||||
|
||||
fn default_model(&self, cx: &mut App) -> Option<acp::ModelId> {
|
||||
fn default_model(&self, cx: &App) -> Option<acp::ModelId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).claude.clone()
|
||||
});
|
||||
@@ -72,6 +73,139 @@ 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 default_config_option(&self, config_id: &str, cx: &App) -> Option<String> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).claude.clone()
|
||||
});
|
||||
|
||||
settings
|
||||
.as_ref()
|
||||
.and_then(|s| s.default_config_options.get(config_id).cloned())
|
||||
}
|
||||
|
||||
fn set_default_config_option(
|
||||
&self,
|
||||
config_id: &str,
|
||||
value_id: Option<&str>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let config_id = config_id.to_string();
|
||||
let value_id = value_id.map(|s| s.to_string());
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
let config_options = &mut settings
|
||||
.agent_servers
|
||||
.get_or_insert_default()
|
||||
.claude
|
||||
.get_or_insert_default()
|
||||
.default_config_options;
|
||||
|
||||
if let Some(value) = value_id.clone() {
|
||||
config_options.insert(config_id.clone(), value);
|
||||
} else {
|
||||
config_options.remove(&config_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn favorite_config_option_value_ids(
|
||||
&self,
|
||||
config_id: &acp::SessionConfigId,
|
||||
cx: &mut App,
|
||||
) -> HashSet<acp::SessionConfigValueId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).claude.clone()
|
||||
});
|
||||
|
||||
settings
|
||||
.as_ref()
|
||||
.and_then(|s| s.favorite_config_option_values.get(config_id.0.as_ref()))
|
||||
.map(|values| {
|
||||
values
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(acp::SessionConfigValueId::new)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn toggle_favorite_config_option_value(
|
||||
&self,
|
||||
config_id: acp::SessionConfigId,
|
||||
value_id: acp::SessionConfigValueId,
|
||||
should_be_favorite: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &App,
|
||||
) {
|
||||
let config_id = config_id.to_string();
|
||||
let value_id = value_id.to_string();
|
||||
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
let favorites = &mut settings
|
||||
.agent_servers
|
||||
.get_or_insert_default()
|
||||
.claude
|
||||
.get_or_insert_default()
|
||||
.favorite_config_option_values;
|
||||
|
||||
let entry = favorites.entry(config_id.clone()).or_insert_with(Vec::new);
|
||||
|
||||
if should_be_favorite {
|
||||
if !entry.iter().any(|v| v == &value_id) {
|
||||
entry.push(value_id.clone());
|
||||
}
|
||||
} else {
|
||||
entry.retain(|v| v != &value_id);
|
||||
if entry.is_empty() {
|
||||
favorites.remove(&config_id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: Option<&Path>,
|
||||
@@ -85,6 +219,14 @@ impl AgentServer for ClaudeCode {
|
||||
let extra_env = load_proxy_env(cx);
|
||||
let default_mode = self.default_mode(cx);
|
||||
let default_model = self.default_model(cx);
|
||||
let default_config_options = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
.claude
|
||||
.as_ref()
|
||||
.map(|s| s.default_config_options.clone())
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let (command, root_dir, login) = store
|
||||
@@ -107,6 +249,7 @@ impl AgentServer for ClaudeCode {
|
||||
root_dir.as_ref(),
|
||||
default_mode,
|
||||
default_model,
|
||||
default_config_options,
|
||||
is_remote,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ 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};
|
||||
@@ -31,7 +32,7 @@ impl AgentServer for Codex {
|
||||
ui::IconName::AiOpenAi
|
||||
}
|
||||
|
||||
fn default_mode(&self, cx: &mut App) -> Option<acp::SessionModeId> {
|
||||
fn default_mode(&self, cx: &App) -> Option<acp::SessionModeId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).codex.clone()
|
||||
});
|
||||
@@ -52,7 +53,7 @@ impl AgentServer for Codex {
|
||||
});
|
||||
}
|
||||
|
||||
fn default_model(&self, cx: &mut App) -> Option<acp::ModelId> {
|
||||
fn default_model(&self, cx: &App) -> Option<acp::ModelId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).codex.clone()
|
||||
});
|
||||
@@ -73,6 +74,139 @@ 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 default_config_option(&self, config_id: &str, cx: &App) -> Option<String> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).codex.clone()
|
||||
});
|
||||
|
||||
settings
|
||||
.as_ref()
|
||||
.and_then(|s| s.default_config_options.get(config_id).cloned())
|
||||
}
|
||||
|
||||
fn set_default_config_option(
|
||||
&self,
|
||||
config_id: &str,
|
||||
value_id: Option<&str>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let config_id = config_id.to_string();
|
||||
let value_id = value_id.map(|s| s.to_string());
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
let config_options = &mut settings
|
||||
.agent_servers
|
||||
.get_or_insert_default()
|
||||
.codex
|
||||
.get_or_insert_default()
|
||||
.default_config_options;
|
||||
|
||||
if let Some(value) = value_id.clone() {
|
||||
config_options.insert(config_id.clone(), value);
|
||||
} else {
|
||||
config_options.remove(&config_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn favorite_config_option_value_ids(
|
||||
&self,
|
||||
config_id: &acp::SessionConfigId,
|
||||
cx: &mut App,
|
||||
) -> HashSet<acp::SessionConfigValueId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).codex.clone()
|
||||
});
|
||||
|
||||
settings
|
||||
.as_ref()
|
||||
.and_then(|s| s.favorite_config_option_values.get(config_id.0.as_ref()))
|
||||
.map(|values| {
|
||||
values
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(acp::SessionConfigValueId::new)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn toggle_favorite_config_option_value(
|
||||
&self,
|
||||
config_id: acp::SessionConfigId,
|
||||
value_id: acp::SessionConfigValueId,
|
||||
should_be_favorite: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &App,
|
||||
) {
|
||||
let config_id = config_id.to_string();
|
||||
let value_id = value_id.to_string();
|
||||
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
let favorites = &mut settings
|
||||
.agent_servers
|
||||
.get_or_insert_default()
|
||||
.codex
|
||||
.get_or_insert_default()
|
||||
.favorite_config_option_values;
|
||||
|
||||
let entry = favorites.entry(config_id.clone()).or_insert_with(Vec::new);
|
||||
|
||||
if should_be_favorite {
|
||||
if !entry.iter().any(|v| v == &value_id) {
|
||||
entry.push(value_id.clone());
|
||||
}
|
||||
} else {
|
||||
entry.retain(|v| v != &value_id);
|
||||
if entry.is_empty() {
|
||||
favorites.remove(&config_id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: Option<&Path>,
|
||||
@@ -86,6 +220,14 @@ impl AgentServer for Codex {
|
||||
let extra_env = load_proxy_env(cx);
|
||||
let default_mode = self.default_mode(cx);
|
||||
let default_model = self.default_model(cx);
|
||||
let default_config_options = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
.codex
|
||||
.as_ref()
|
||||
.map(|s| s.default_config_options.clone())
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let (command, root_dir, login) = store
|
||||
@@ -109,6 +251,7 @@ impl AgentServer for Codex {
|
||||
root_dir.as_ref(),
|
||||
default_mode,
|
||||
default_model,
|
||||
default_config_options,
|
||||
is_remote,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ 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};
|
||||
@@ -29,7 +30,7 @@ impl AgentServer for CustomAgentServer {
|
||||
IconName::Terminal
|
||||
}
|
||||
|
||||
fn default_mode(&self, cx: &mut App) -> Option<acp::SessionModeId> {
|
||||
fn default_mode(&self, cx: &App) -> Option<acp::SessionModeId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
@@ -43,6 +44,86 @@ impl AgentServer for CustomAgentServer {
|
||||
.and_then(|s| s.default_mode().map(acp::SessionModeId::new))
|
||||
}
|
||||
|
||||
fn favorite_config_option_value_ids(
|
||||
&self,
|
||||
config_id: &acp::SessionConfigId,
|
||||
cx: &mut App,
|
||||
) -> HashSet<acp::SessionConfigValueId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
.custom
|
||||
.get(&self.name())
|
||||
.cloned()
|
||||
});
|
||||
|
||||
settings
|
||||
.as_ref()
|
||||
.and_then(|s| s.favorite_config_option_values(config_id.0.as_ref()))
|
||||
.map(|values| {
|
||||
values
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(acp::SessionConfigValueId::new)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn toggle_favorite_config_option_value(
|
||||
&self,
|
||||
config_id: acp::SessionConfigId,
|
||||
value_id: acp::SessionConfigValueId,
|
||||
should_be_favorite: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &App,
|
||||
) {
|
||||
let name = self.name();
|
||||
let config_id = config_id.to_string();
|
||||
let value_id = value_id.to_string();
|
||||
|
||||
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(),
|
||||
default_config_options: Default::default(),
|
||||
favorite_config_option_values: Default::default(),
|
||||
});
|
||||
|
||||
match settings {
|
||||
settings::CustomAgentServerSettings::Custom {
|
||||
favorite_config_option_values,
|
||||
..
|
||||
}
|
||||
| settings::CustomAgentServerSettings::Extension {
|
||||
favorite_config_option_values,
|
||||
..
|
||||
} => {
|
||||
let entry = favorite_config_option_values
|
||||
.entry(config_id.clone())
|
||||
.or_insert_with(Vec::new);
|
||||
|
||||
if should_be_favorite {
|
||||
if !entry.iter().any(|v| v == &value_id) {
|
||||
entry.push(value_id.clone());
|
||||
}
|
||||
} else {
|
||||
entry.retain(|v| v != &value_id);
|
||||
if entry.is_empty() {
|
||||
favorite_config_option_values.remove(&config_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
|
||||
let name = self.name();
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
@@ -54,6 +135,9 @@ impl AgentServer for CustomAgentServer {
|
||||
.or_insert_with(|| settings::CustomAgentServerSettings::Extension {
|
||||
default_model: None,
|
||||
default_mode: None,
|
||||
favorite_models: Vec::new(),
|
||||
default_config_options: Default::default(),
|
||||
favorite_config_option_values: Default::default(),
|
||||
});
|
||||
|
||||
match settings {
|
||||
@@ -65,7 +149,7 @@ impl AgentServer for CustomAgentServer {
|
||||
});
|
||||
}
|
||||
|
||||
fn default_model(&self, cx: &mut App) -> Option<acp::ModelId> {
|
||||
fn default_model(&self, cx: &App) -> Option<acp::ModelId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
@@ -90,6 +174,9 @@ impl AgentServer for CustomAgentServer {
|
||||
.or_insert_with(|| settings::CustomAgentServerSettings::Extension {
|
||||
default_model: None,
|
||||
default_mode: None,
|
||||
favorite_models: Vec::new(),
|
||||
default_config_options: Default::default(),
|
||||
favorite_config_option_values: Default::default(),
|
||||
});
|
||||
|
||||
match settings {
|
||||
@@ -101,6 +188,125 @@ 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(),
|
||||
default_config_options: Default::default(),
|
||||
favorite_config_option_values: Default::default(),
|
||||
});
|
||||
|
||||
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 default_config_option(&self, config_id: &str, cx: &App) -> Option<String> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
.custom
|
||||
.get(&self.name())
|
||||
.cloned()
|
||||
});
|
||||
|
||||
settings
|
||||
.as_ref()
|
||||
.and_then(|s| s.default_config_option(config_id).map(|s| s.to_string()))
|
||||
}
|
||||
|
||||
fn set_default_config_option(
|
||||
&self,
|
||||
config_id: &str,
|
||||
value_id: Option<&str>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let name = self.name();
|
||||
let config_id = config_id.to_string();
|
||||
let value_id = value_id.map(|s| s.to_string());
|
||||
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(),
|
||||
default_config_options: Default::default(),
|
||||
favorite_config_option_values: Default::default(),
|
||||
});
|
||||
|
||||
match settings {
|
||||
settings::CustomAgentServerSettings::Custom {
|
||||
default_config_options,
|
||||
..
|
||||
}
|
||||
| settings::CustomAgentServerSettings::Extension {
|
||||
default_config_options,
|
||||
..
|
||||
} => {
|
||||
if let Some(value) = value_id.clone() {
|
||||
default_config_options.insert(config_id.clone(), value);
|
||||
} else {
|
||||
default_config_options.remove(&config_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: Option<&Path>,
|
||||
@@ -112,6 +318,23 @@ impl AgentServer for CustomAgentServer {
|
||||
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
||||
let default_mode = self.default_mode(cx);
|
||||
let default_model = self.default_model(cx);
|
||||
let default_config_options = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
.custom
|
||||
.get(&self.name())
|
||||
.map(|s| match s {
|
||||
project::agent_server_store::CustomAgentServerSettings::Custom {
|
||||
default_config_options,
|
||||
..
|
||||
}
|
||||
| project::agent_server_store::CustomAgentServerSettings::Extension {
|
||||
default_config_options,
|
||||
..
|
||||
} => default_config_options.clone(),
|
||||
})
|
||||
.unwrap_or_default()
|
||||
});
|
||||
let store = delegate.store.downgrade();
|
||||
let extra_env = load_proxy_env(cx);
|
||||
cx.spawn(async move |cx| {
|
||||
@@ -137,6 +360,7 @@ impl AgentServer for CustomAgentServer {
|
||||
root_dir.as_ref(),
|
||||
default_mode,
|
||||
default_model,
|
||||
default_config_options,
|
||||
is_remote,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -455,20 +455,12 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
|
||||
project::agent_server_store::AllAgentServersSettings {
|
||||
claude: Some(BuiltinAgentServerSettings {
|
||||
path: Some("claude-code-acp".into()),
|
||||
args: None,
|
||||
env: None,
|
||||
ignore_system_version: None,
|
||||
default_mode: None,
|
||||
default_model: None,
|
||||
..Default::default()
|
||||
}),
|
||||
gemini: Some(crate::gemini::tests::local_command().into()),
|
||||
codex: Some(BuiltinAgentServerSettings {
|
||||
path: Some("codex-acp".into()),
|
||||
args: None,
|
||||
env: None,
|
||||
ignore_system_version: None,
|
||||
default_mode: None,
|
||||
default_model: None,
|
||||
..Default::default()
|
||||
}),
|
||||
custom: collections::HashMap::default(),
|
||||
},
|
||||
|
||||
@@ -4,9 +4,10 @@ use std::{any::Any, path::Path};
|
||||
use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
|
||||
use acp_thread::AgentConnection;
|
||||
use anyhow::{Context as _, Result};
|
||||
use gpui::{App, SharedString, Task};
|
||||
use gpui::{App, AppContext as _, SharedString, Task};
|
||||
use language_models::provider::google::GoogleLanguageModelProvider;
|
||||
use project::agent_server_store::GEMINI_NAME;
|
||||
use project::agent_server_store::{AllAgentServersSettings, GEMINI_NAME};
|
||||
use settings::SettingsStore;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Gemini;
|
||||
@@ -33,6 +34,14 @@ impl AgentServer for Gemini {
|
||||
let mut extra_env = load_proxy_env(cx);
|
||||
let default_mode = self.default_mode(cx);
|
||||
let default_model = self.default_model(cx);
|
||||
let default_config_options = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
.gemini
|
||||
.as_ref()
|
||||
.map(|s| s.default_config_options.clone())
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
extra_env.insert("SURFACE".to_owned(), "zed".to_owned());
|
||||
@@ -65,6 +74,7 @@ impl AgentServer for Gemini {
|
||||
root_dir.as_ref(),
|
||||
default_mode,
|
||||
default_model,
|
||||
default_config_options,
|
||||
is_remote,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod config_options;
|
||||
mod entry_view_state;
|
||||
mod message_editor;
|
||||
mod mode_selector;
|
||||
|
||||
775
crates/agent_ui/src/acp/config_options.rs
Normal file
775
crates/agent_ui/src/acp/config_options.rs
Normal file
@@ -0,0 +1,775 @@
|
||||
use std::{cmp::Reverse, rc::Rc, sync::Arc};
|
||||
|
||||
use acp_thread::AgentSessionConfigOptions;
|
||||
use agent_client_protocol as acp;
|
||||
use agent_servers::AgentServer;
|
||||
use collections::HashSet;
|
||||
use fs::Fs;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
BackgroundExecutor, Context, DismissEvent, Entity, Subscription, Task, Window, prelude::*,
|
||||
};
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::popover_menu::PickerPopoverMenu;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use settings::SettingsStore;
|
||||
use ui::{
|
||||
ElevationIndex, IconButton, ListItem, ListItemSpacing, PopoverMenuHandle, Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::ui::HoldForDefault;
|
||||
|
||||
const PICKER_THRESHOLD: usize = 5;
|
||||
|
||||
pub struct ConfigOptionsView {
|
||||
config_options: Rc<dyn AgentSessionConfigOptions>,
|
||||
selectors: Vec<Entity<ConfigOptionSelector>>,
|
||||
agent_server: Rc<dyn AgentServer>,
|
||||
fs: Arc<dyn Fs>,
|
||||
config_option_ids: Vec<acp::SessionConfigId>,
|
||||
_refresh_task: Task<()>,
|
||||
}
|
||||
|
||||
impl ConfigOptionsView {
|
||||
pub fn new(
|
||||
config_options: Rc<dyn AgentSessionConfigOptions>,
|
||||
agent_server: Rc<dyn AgentServer>,
|
||||
fs: Arc<dyn Fs>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let selectors = Self::build_selectors(&config_options, &agent_server, &fs, window, cx);
|
||||
let config_option_ids = Self::config_option_ids(&config_options);
|
||||
|
||||
let rx = config_options.watch(cx);
|
||||
let refresh_task = cx.spawn_in(window, async move |this, cx| {
|
||||
if let Some(mut rx) = rx {
|
||||
while let Ok(()) = rx.recv().await {
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.refresh_selectors_if_needed(window, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
config_options,
|
||||
selectors,
|
||||
agent_server,
|
||||
fs,
|
||||
config_option_ids,
|
||||
_refresh_task: refresh_task,
|
||||
}
|
||||
}
|
||||
|
||||
fn config_option_ids(
|
||||
config_options: &Rc<dyn AgentSessionConfigOptions>,
|
||||
) -> Vec<acp::SessionConfigId> {
|
||||
config_options
|
||||
.config_options()
|
||||
.into_iter()
|
||||
.map(|option| option.id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn refresh_selectors_if_needed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let current_ids = Self::config_option_ids(&self.config_options);
|
||||
if current_ids != self.config_option_ids {
|
||||
self.config_option_ids = current_ids;
|
||||
self.rebuild_selectors(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn rebuild_selectors(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.selectors = Self::build_selectors(
|
||||
&self.config_options,
|
||||
&self.agent_server,
|
||||
&self.fs,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn build_selectors(
|
||||
config_options: &Rc<dyn AgentSessionConfigOptions>,
|
||||
agent_server: &Rc<dyn AgentServer>,
|
||||
fs: &Arc<dyn Fs>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Vec<Entity<ConfigOptionSelector>> {
|
||||
config_options
|
||||
.config_options()
|
||||
.into_iter()
|
||||
.map(|option| {
|
||||
let config_options = config_options.clone();
|
||||
let agent_server = agent_server.clone();
|
||||
let fs = fs.clone();
|
||||
cx.new(|cx| {
|
||||
ConfigOptionSelector::new(
|
||||
config_options,
|
||||
option.id.clone(),
|
||||
agent_server,
|
||||
fs,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ConfigOptionsView {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
if self.selectors.is_empty() {
|
||||
return div().into_any_element();
|
||||
}
|
||||
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.children(self.selectors.iter().cloned())
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
struct ConfigOptionSelector {
|
||||
config_options: Rc<dyn AgentSessionConfigOptions>,
|
||||
config_id: acp::SessionConfigId,
|
||||
picker_handle: PopoverMenuHandle<Picker<ConfigOptionPickerDelegate>>,
|
||||
picker: Entity<Picker<ConfigOptionPickerDelegate>>,
|
||||
setting_value: bool,
|
||||
}
|
||||
|
||||
impl ConfigOptionSelector {
|
||||
pub fn new(
|
||||
config_options: Rc<dyn AgentSessionConfigOptions>,
|
||||
config_id: acp::SessionConfigId,
|
||||
agent_server: Rc<dyn AgentServer>,
|
||||
fs: Arc<dyn Fs>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let option_count = config_options
|
||||
.config_options()
|
||||
.iter()
|
||||
.find(|opt| opt.id == config_id)
|
||||
.map(count_config_options)
|
||||
.unwrap_or(0);
|
||||
|
||||
let is_searchable = option_count >= PICKER_THRESHOLD;
|
||||
|
||||
let picker = {
|
||||
let config_options = config_options.clone();
|
||||
let config_id = config_id.clone();
|
||||
let agent_server = agent_server.clone();
|
||||
let fs = fs.clone();
|
||||
cx.new(move |picker_cx| {
|
||||
let delegate = ConfigOptionPickerDelegate::new(
|
||||
config_options,
|
||||
config_id,
|
||||
agent_server,
|
||||
fs,
|
||||
window,
|
||||
picker_cx,
|
||||
);
|
||||
|
||||
if is_searchable {
|
||||
Picker::list(delegate, window, picker_cx)
|
||||
} else {
|
||||
Picker::nonsearchable_list(delegate, window, picker_cx)
|
||||
}
|
||||
.show_scrollbar(true)
|
||||
.width(rems(20.))
|
||||
.max_height(Some(rems(20.).into()))
|
||||
})
|
||||
};
|
||||
|
||||
Self {
|
||||
config_options,
|
||||
config_id,
|
||||
picker_handle: PopoverMenuHandle::default(),
|
||||
picker,
|
||||
setting_value: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn current_option(&self) -> Option<acp::SessionConfigOption> {
|
||||
self.config_options
|
||||
.config_options()
|
||||
.into_iter()
|
||||
.find(|opt| opt.id == self.config_id)
|
||||
}
|
||||
|
||||
fn current_value_name(&self) -> String {
|
||||
let Some(option) = self.current_option() else {
|
||||
return "Unknown".to_string();
|
||||
};
|
||||
|
||||
match &option.kind {
|
||||
acp::SessionConfigKind::Select(select) => {
|
||||
find_option_name(&select.options, &select.current_value)
|
||||
.unwrap_or_else(|| "Unknown".to_string())
|
||||
}
|
||||
_ => "Unknown".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_trigger_button(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Button {
|
||||
let Some(option) = self.current_option() else {
|
||||
return Button::new("config-option-trigger", "Unknown")
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.disabled(true);
|
||||
};
|
||||
|
||||
let icon = if self.picker_handle.is_deployed() {
|
||||
IconName::ChevronUp
|
||||
} else {
|
||||
IconName::ChevronDown
|
||||
};
|
||||
|
||||
Button::new(
|
||||
ElementId::Name(format!("config-option-{}", option.id.0).into()),
|
||||
self.current_value_name(),
|
||||
)
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.icon(icon)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_position(IconPosition::End)
|
||||
.icon_color(Color::Muted)
|
||||
.disabled(self.setting_value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ConfigOptionSelector {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let Some(option) = self.current_option() else {
|
||||
return div().into_any_element();
|
||||
};
|
||||
|
||||
let trigger_button = self.render_trigger_button(window, cx);
|
||||
|
||||
let option_name = option.name.clone();
|
||||
let option_description: Option<SharedString> = option.description.map(Into::into);
|
||||
|
||||
let tooltip = Tooltip::element(move |_window, _cx| {
|
||||
let mut content = v_flex().gap_1().child(Label::new(option_name.clone()));
|
||||
if let Some(desc) = option_description.as_ref() {
|
||||
content = content.child(
|
||||
Label::new(desc.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
);
|
||||
}
|
||||
content.into_any()
|
||||
});
|
||||
|
||||
PickerPopoverMenu::new(
|
||||
self.picker.clone(),
|
||||
trigger_button,
|
||||
tooltip,
|
||||
gpui::Corner::BottomRight,
|
||||
cx,
|
||||
)
|
||||
.with_handle(self.picker_handle.clone())
|
||||
.render(window, cx)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum ConfigOptionPickerEntry {
|
||||
Separator(SharedString),
|
||||
Option(ConfigOptionValue),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ConfigOptionValue {
|
||||
value: acp::SessionConfigValueId,
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
group: Option<String>,
|
||||
}
|
||||
|
||||
struct ConfigOptionPickerDelegate {
|
||||
config_options: Rc<dyn AgentSessionConfigOptions>,
|
||||
config_id: acp::SessionConfigId,
|
||||
agent_server: Rc<dyn AgentServer>,
|
||||
fs: Arc<dyn Fs>,
|
||||
filtered_entries: Vec<ConfigOptionPickerEntry>,
|
||||
all_options: Vec<ConfigOptionValue>,
|
||||
selected_index: usize,
|
||||
selected_description: Option<(usize, SharedString, bool)>,
|
||||
favorites: HashSet<acp::SessionConfigValueId>,
|
||||
_settings_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl ConfigOptionPickerDelegate {
|
||||
fn new(
|
||||
config_options: Rc<dyn AgentSessionConfigOptions>,
|
||||
config_id: acp::SessionConfigId,
|
||||
agent_server: Rc<dyn AgentServer>,
|
||||
fs: Arc<dyn Fs>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Self {
|
||||
let favorites = agent_server.favorite_config_option_value_ids(&config_id, cx);
|
||||
|
||||
let all_options = extract_options(&config_options, &config_id);
|
||||
let filtered_entries = options_to_picker_entries(&all_options, &favorites);
|
||||
|
||||
let current_value = get_current_value(&config_options, &config_id);
|
||||
let selected_index = current_value
|
||||
.and_then(|current| {
|
||||
filtered_entries.iter().position(|entry| {
|
||||
matches!(entry, ConfigOptionPickerEntry::Option(opt) if opt.value == current)
|
||||
})
|
||||
})
|
||||
.unwrap_or(0);
|
||||
|
||||
let agent_server_for_subscription = agent_server.clone();
|
||||
let config_id_for_subscription = config_id.clone();
|
||||
let settings_subscription =
|
||||
cx.observe_global_in::<SettingsStore>(window, move |picker, window, cx| {
|
||||
let new_favorites = agent_server_for_subscription
|
||||
.favorite_config_option_value_ids(&config_id_for_subscription, cx);
|
||||
if new_favorites != picker.delegate.favorites {
|
||||
picker.delegate.favorites = new_favorites;
|
||||
picker.refresh(window, cx);
|
||||
}
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
|
||||
Self {
|
||||
config_options,
|
||||
config_id,
|
||||
agent_server,
|
||||
fs,
|
||||
filtered_entries,
|
||||
all_options,
|
||||
selected_index,
|
||||
selected_description: None,
|
||||
favorites,
|
||||
_settings_subscription: settings_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
fn current_value(&self) -> Option<acp::SessionConfigValueId> {
|
||||
get_current_value(&self.config_options, &self.config_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for ConfigOptionPickerDelegate {
|
||||
type ListItem = AnyElement;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.filtered_entries.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
self.selected_index = ix.min(self.filtered_entries.len().saturating_sub(1));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn can_select(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
) -> bool {
|
||||
match self.filtered_entries.get(ix) {
|
||||
Some(ConfigOptionPickerEntry::Option(_)) => true,
|
||||
Some(ConfigOptionPickerEntry::Separator(_)) | None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||
"Select an option…".into()
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let all_options = self.all_options.clone();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let filtered_options = match this
|
||||
.read_with(cx, |_, cx| {
|
||||
if query.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some((all_options.clone(), query.clone(), cx.background_executor().clone()))
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
Some((options, q, executor)) => fuzzy_search_options(options, &q, executor).await,
|
||||
None => all_options,
|
||||
};
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.delegate.filtered_entries =
|
||||
options_to_picker_entries(&filtered_options, &this.delegate.favorites);
|
||||
|
||||
let current_value = this.delegate.current_value();
|
||||
let new_index = current_value
|
||||
.and_then(|current| {
|
||||
this.delegate.filtered_entries.iter().position(|entry| {
|
||||
matches!(entry, ConfigOptionPickerEntry::Option(opt) if opt.value == current)
|
||||
})
|
||||
})
|
||||
.unwrap_or(0);
|
||||
|
||||
this.set_selected_index(new_index, Some(picker::Direction::Down), true, window, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
if let Some(ConfigOptionPickerEntry::Option(option)) =
|
||||
self.filtered_entries.get(self.selected_index)
|
||||
{
|
||||
if window.modifiers().secondary() {
|
||||
let default_value = self
|
||||
.agent_server
|
||||
.default_config_option(self.config_id.0.as_ref(), cx);
|
||||
let is_default = default_value.as_deref() == Some(&*option.value.0);
|
||||
|
||||
self.agent_server.set_default_config_option(
|
||||
self.config_id.0.as_ref(),
|
||||
if is_default {
|
||||
None
|
||||
} else {
|
||||
Some(option.value.0.as_ref())
|
||||
},
|
||||
self.fs.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
let task = self.config_options.set_config_option(
|
||||
self.config_id.clone(),
|
||||
option.value.clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.spawn(async move |_, _| {
|
||||
if let Err(err) = task.await {
|
||||
log::error!("Failed to set config option: {:?}", err);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
cx.defer_in(window, |picker, window, cx| {
|
||||
picker.set_query("", window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
match self.filtered_entries.get(ix)? {
|
||||
ConfigOptionPickerEntry::Separator(title) => Some(
|
||||
div()
|
||||
.when(ix > 0, |this| this.mt_1())
|
||||
.child(
|
||||
div()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child(title.clone()),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
ConfigOptionPickerEntry::Option(option) => {
|
||||
let current_value = self.current_value();
|
||||
let is_selected = current_value.as_ref() == Some(&option.value);
|
||||
|
||||
let default_value = self
|
||||
.agent_server
|
||||
.default_config_option(self.config_id.0.as_ref(), cx);
|
||||
let is_default = default_value.as_deref() == Some(&*option.value.0);
|
||||
|
||||
let is_favorite = self.favorites.contains(&option.value);
|
||||
|
||||
let option_name = option.name.clone();
|
||||
let description = option.description.clone();
|
||||
|
||||
Some(
|
||||
div()
|
||||
.id(("config-option-picker-item", ix))
|
||||
.when_some(description, |this, desc| {
|
||||
let desc: SharedString = desc.into();
|
||||
this.on_hover(cx.listener(move |menu, hovered, _, cx| {
|
||||
if *hovered {
|
||||
menu.delegate.selected_description =
|
||||
Some((ix, desc.clone(), is_default));
|
||||
} else if matches!(menu.delegate.selected_description, Some((id, _, _)) if id == ix)
|
||||
{
|
||||
menu.delegate.selected_description = None;
|
||||
}
|
||||
cx.notify();
|
||||
}))
|
||||
})
|
||||
.child(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(h_flex().w_full().child(Label::new(option_name).truncate()))
|
||||
.end_slot(div().pr_2().when(is_selected, |this| {
|
||||
this.child(Icon::new(IconName::Check).color(Color::Accent))
|
||||
}))
|
||||
.end_hover_slot(div().pr_1p5().child({
|
||||
let (icon, color, tooltip) = if is_favorite {
|
||||
(IconName::StarFilled, Color::Accent, "Unfavorite")
|
||||
} else {
|
||||
(IconName::Star, Color::Default, "Favorite")
|
||||
};
|
||||
|
||||
let config_id = self.config_id.clone();
|
||||
let value_id = option.value.clone();
|
||||
let agent_server = self.agent_server.clone();
|
||||
let fs = self.fs.clone();
|
||||
|
||||
IconButton::new(("toggle-favorite-config-option", ix), icon)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.icon_color(color)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text(tooltip))
|
||||
.on_click(move |_, _, cx| {
|
||||
agent_server.toggle_favorite_config_option_value(
|
||||
config_id.clone(),
|
||||
value_id.clone(),
|
||||
!is_favorite,
|
||||
fs.clone(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn documentation_aside(
|
||||
&self,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<ui::DocumentationAside> {
|
||||
self.selected_description
|
||||
.as_ref()
|
||||
.map(|(_, description, is_default)| {
|
||||
let description = description.clone();
|
||||
let is_default = *is_default;
|
||||
|
||||
ui::DocumentationAside::new(
|
||||
ui::DocumentationSide::Left,
|
||||
Rc::new(move |_| {
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(Label::new(description.clone()))
|
||||
.child(HoldForDefault::new(is_default))
|
||||
.into_any_element()
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn documentation_aside_index(&self) -> Option<usize> {
|
||||
self.selected_description.as_ref().map(|(ix, _, _)| *ix)
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_options(
|
||||
config_options: &Rc<dyn AgentSessionConfigOptions>,
|
||||
config_id: &acp::SessionConfigId,
|
||||
) -> Vec<ConfigOptionValue> {
|
||||
let Some(option) = config_options
|
||||
.config_options()
|
||||
.into_iter()
|
||||
.find(|opt| &opt.id == config_id)
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
match &option.kind {
|
||||
acp::SessionConfigKind::Select(select) => match &select.options {
|
||||
acp::SessionConfigSelectOptions::Ungrouped(options) => options
|
||||
.iter()
|
||||
.map(|opt| ConfigOptionValue {
|
||||
value: opt.value.clone(),
|
||||
name: opt.name.clone(),
|
||||
description: opt.description.clone(),
|
||||
group: None,
|
||||
})
|
||||
.collect(),
|
||||
acp::SessionConfigSelectOptions::Grouped(groups) => groups
|
||||
.iter()
|
||||
.flat_map(|group| {
|
||||
group.options.iter().map(|opt| ConfigOptionValue {
|
||||
value: opt.value.clone(),
|
||||
name: opt.name.clone(),
|
||||
description: opt.description.clone(),
|
||||
group: Some(group.name.clone()),
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
_ => Vec::new(),
|
||||
},
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_value(
|
||||
config_options: &Rc<dyn AgentSessionConfigOptions>,
|
||||
config_id: &acp::SessionConfigId,
|
||||
) -> Option<acp::SessionConfigValueId> {
|
||||
config_options
|
||||
.config_options()
|
||||
.into_iter()
|
||||
.find(|opt| &opt.id == config_id)
|
||||
.and_then(|opt| match &opt.kind {
|
||||
acp::SessionConfigKind::Select(select) => Some(select.current_value.clone()),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn options_to_picker_entries(
|
||||
options: &[ConfigOptionValue],
|
||||
favorites: &HashSet<acp::SessionConfigValueId>,
|
||||
) -> Vec<ConfigOptionPickerEntry> {
|
||||
let mut entries = Vec::new();
|
||||
|
||||
let mut favorite_options = Vec::new();
|
||||
|
||||
for option in options {
|
||||
if favorites.contains(&option.value) {
|
||||
favorite_options.push(option.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if !favorite_options.is_empty() {
|
||||
entries.push(ConfigOptionPickerEntry::Separator("Favorites".into()));
|
||||
for option in favorite_options {
|
||||
entries.push(ConfigOptionPickerEntry::Option(option));
|
||||
}
|
||||
|
||||
// If the remaining list would start ungrouped (group == None), insert a separator so
|
||||
// Favorites doesn't visually run into the main list.
|
||||
if let Some(option) = options.first()
|
||||
&& option.group.is_none()
|
||||
{
|
||||
entries.push(ConfigOptionPickerEntry::Separator("All Options".into()));
|
||||
}
|
||||
}
|
||||
|
||||
let mut current_group: Option<String> = None;
|
||||
for option in options {
|
||||
if option.group != current_group {
|
||||
if let Some(group_name) = &option.group {
|
||||
entries.push(ConfigOptionPickerEntry::Separator(
|
||||
group_name.clone().into(),
|
||||
));
|
||||
}
|
||||
current_group = option.group.clone();
|
||||
}
|
||||
entries.push(ConfigOptionPickerEntry::Option(option.clone()));
|
||||
}
|
||||
|
||||
entries
|
||||
}
|
||||
|
||||
async fn fuzzy_search_options(
|
||||
options: Vec<ConfigOptionValue>,
|
||||
query: &str,
|
||||
executor: BackgroundExecutor,
|
||||
) -> Vec<ConfigOptionValue> {
|
||||
let candidates = options
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, opt)| StringMatchCandidate::new(ix, &opt.name))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut matches = fuzzy::match_strings(
|
||||
&candidates,
|
||||
query,
|
||||
false,
|
||||
true,
|
||||
100,
|
||||
&Default::default(),
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
|
||||
matches.sort_unstable_by_key(|mat| {
|
||||
let candidate = &candidates[mat.candidate_id];
|
||||
(Reverse(OrderedFloat(mat.score)), candidate.id)
|
||||
});
|
||||
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|mat| options[mat.candidate_id].clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn find_option_name(
|
||||
options: &acp::SessionConfigSelectOptions,
|
||||
value_id: &acp::SessionConfigValueId,
|
||||
) -> Option<String> {
|
||||
match options {
|
||||
acp::SessionConfigSelectOptions::Ungrouped(opts) => opts
|
||||
.iter()
|
||||
.find(|o| &o.value == value_id)
|
||||
.map(|o| o.name.clone()),
|
||||
acp::SessionConfigSelectOptions::Grouped(groups) => groups.iter().find_map(|group| {
|
||||
group
|
||||
.options
|
||||
.iter()
|
||||
.find(|o| &o.value == value_id)
|
||||
.map(|o| o.name.clone())
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn count_config_options(option: &acp::SessionConfigOption) -> usize {
|
||||
match &option.kind {
|
||||
acp::SessionConfigKind::Select(select) => match &select.options {
|
||||
acp::SessionConfigSelectOptions::Ungrouped(options) => options.len(),
|
||||
acp::SessionConfigSelectOptions::Grouped(groups) => {
|
||||
groups.iter().map(|g| g.options.len()).sum()
|
||||
}
|
||||
_ => 0,
|
||||
},
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
@@ -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::prelude::*;
|
||||
use ui::{ContextMenu, prelude::*};
|
||||
use util::{ResultExt, debug_panic};
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_actions::agent::Chat;
|
||||
use zed_actions::agent::{Chat, PasteRaw};
|
||||
|
||||
pub struct MessageEditor {
|
||||
mention_set: Entity<MentionSet>,
|
||||
@@ -132,6 +132,21 @@ 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 =
|
||||
@@ -543,6 +558,9 @@ 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())
|
||||
@@ -553,133 +571,127 @@ impl MessageEditor {
|
||||
_ => None,
|
||||
});
|
||||
|
||||
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 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;
|
||||
// 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()?;
|
||||
|
||||
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)
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if self.prompt_capabilities.borrow().image
|
||||
@@ -690,6 +702,13 @@ 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>,
|
||||
@@ -967,6 +986,7 @@ 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({
|
||||
|
||||
@@ -7,8 +7,8 @@ use gpui::{Context, Entity, FocusHandle, WeakEntity, Window, prelude::*};
|
||||
use settings::Settings as _;
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
use ui::{
|
||||
Button, ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, KeyBinding,
|
||||
PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*,
|
||||
Button, ContextMenu, ContextMenuEntry, DocumentationSide, KeyBinding, PopoverMenu,
|
||||
PopoverMenuHandle, Tooltip, prelude::*,
|
||||
};
|
||||
|
||||
use crate::{CycleModeSelector, ToggleProfileSelector, ui::HoldForDefault};
|
||||
@@ -105,7 +105,7 @@ impl ModeSelector {
|
||||
.toggleable(IconPosition::End, is_selected);
|
||||
|
||||
let entry = if let Some(description) = &mode.description {
|
||||
entry.documentation_aside(side, DocumentationEdge::Bottom, {
|
||||
entry.documentation_aside(side, {
|
||||
let description = description.clone();
|
||||
|
||||
move |_| {
|
||||
@@ -186,6 +186,17 @@ 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()
|
||||
@@ -200,17 +211,6 @@ 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,22 @@
|
||||
use std::{cmp::Reverse, rc::Rc, sync::Arc};
|
||||
|
||||
use acp_thread::{AgentModelInfo, AgentModelList, AgentModelSelector};
|
||||
use acp_thread::{AgentModelIcon, AgentModelInfo, AgentModelList, AgentModelSelector};
|
||||
use agent_client_protocol::ModelId;
|
||||
use agent_servers::AgentServer;
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::Result;
|
||||
use collections::{HashSet, IndexMap};
|
||||
use fs::Fs;
|
||||
use futures::FutureExt;
|
||||
use fuzzy::{StringMatchCandidate, match_strings};
|
||||
use gpui::{
|
||||
Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, FocusHandle, Task, WeakEntity,
|
||||
Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, Task,
|
||||
WeakEntity,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use settings::Settings;
|
||||
use ui::{DocumentationAside, DocumentationEdge, DocumentationSide, IntoElement, prelude::*};
|
||||
use settings::SettingsStore;
|
||||
use ui::{DocumentationAside, DocumentationSide, IntoElement, prelude::*};
|
||||
use util::ResultExt;
|
||||
use zed_actions::agent::OpenSettings;
|
||||
|
||||
@@ -54,7 +54,9 @@ 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,
|
||||
}
|
||||
|
||||
@@ -102,6 +104,19 @@ 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,
|
||||
@@ -111,7 +126,9 @@ impl AcpModelPickerDelegate {
|
||||
selected_model: None,
|
||||
selected_index: 0,
|
||||
selected_description: None,
|
||||
favorites,
|
||||
_refresh_models_task: refresh_models_task,
|
||||
_settings_subscription: settings_subscription,
|
||||
focus_handle,
|
||||
}
|
||||
}
|
||||
@@ -120,40 +137,37 @@ impl AcpModelPickerDelegate {
|
||||
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.selector.supports_favorites() {
|
||||
if self.favorites.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let favorites = AgentSettings::get_global(cx).favorite_model_ids();
|
||||
|
||||
if favorites.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(models) = self.models.clone() else {
|
||||
let Some(models) = &self.models else {
|
||||
return;
|
||||
};
|
||||
|
||||
let all_models: Vec<AgentModelInfo> = match models {
|
||||
AgentModelList::Flat(list) => list,
|
||||
AgentModelList::Grouped(index_map) => index_map
|
||||
.into_values()
|
||||
.flatten()
|
||||
.collect::<Vec<AgentModelInfo>>(),
|
||||
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 = all_models
|
||||
.iter()
|
||||
.filter(|model| favorites.contains(&model.id))
|
||||
let favorite_models: Vec<_> = all_models
|
||||
.into_iter()
|
||||
.filter(|model| self.favorites.contains(&model.id))
|
||||
.unique_by(|model| &model.id)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
.collect();
|
||||
|
||||
let current_id = self.selected_model.as_ref().map(|m| m.id.clone());
|
||||
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
|
||||
.as_ref()
|
||||
.and_then(|id| favorite_models.iter().position(|m| &m.id == id))
|
||||
.unwrap_or(usize::MAX);
|
||||
|
||||
@@ -220,11 +234,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let favorites = if self.selector.supports_favorites() {
|
||||
Arc::new(AgentSettings::get_global(cx).favorite_model_ids())
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let favorites = self.favorites.clone();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let filtered_models = match this
|
||||
@@ -242,7 +252,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, &favorites);
|
||||
// Finds the currently selected model in the list
|
||||
let new_index = this
|
||||
.delegate
|
||||
@@ -317,21 +327,20 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
let default_model = self.agent_server.default_model(cx);
|
||||
let is_default = default_model.as_ref() == Some(&model_info.id);
|
||||
|
||||
let supports_favorites = self.selector.supports_favorites();
|
||||
|
||||
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();
|
||||
|
||||
move |cx: &App| {
|
||||
crate::favorite_models::toggle_model_id_in_settings(
|
||||
cx.listener(move |_, _, _, cx| {
|
||||
agent_server.toggle_favorite_model(
|
||||
model_id.clone(),
|
||||
!is_favorite,
|
||||
fs.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
Some(
|
||||
@@ -350,13 +359,15 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
})
|
||||
.child(
|
||||
ModelSelectorListItem::new(ix, model_info.name.clone())
|
||||
.when_some(model_info.icon, |this, icon| this.icon(icon))
|
||||
.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_selected(is_selected)
|
||||
.is_focused(selected)
|
||||
.when(supports_favorites, |this| {
|
||||
this.is_favorite(is_favorite)
|
||||
.on_toggle_favorite(handle_action_click)
|
||||
}),
|
||||
.is_favorite(is_favorite)
|
||||
.on_toggle_favorite(handle_action_click),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
@@ -377,7 +388,6 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
|
||||
DocumentationAside::new(
|
||||
DocumentationSide::Left,
|
||||
DocumentationEdge::Top,
|
||||
Rc::new(move |_| {
|
||||
v_flex()
|
||||
.gap_1()
|
||||
@@ -389,6 +399,10 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
})
|
||||
}
|
||||
|
||||
fn documentation_aside_index(&self) -> Option<usize> {
|
||||
self.selected_description.as_ref().map(|(ix, _, _)| *ix)
|
||||
}
|
||||
|
||||
fn render_footer(
|
||||
&self,
|
||||
_window: &mut Window,
|
||||
@@ -406,7 +420,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
|
||||
fn info_list_to_picker_entries(
|
||||
model_list: AgentModelList,
|
||||
favorites: Arc<HashSet<ModelId>>,
|
||||
favorites: &HashSet<ModelId>,
|
||||
) -> Vec<AcpModelPickerEntry> {
|
||||
let mut entries = Vec::new();
|
||||
|
||||
@@ -572,13 +586,11 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_favorites(models: Vec<&str>) -> Arc<HashSet<ModelId>> {
|
||||
Arc::new(
|
||||
models
|
||||
.into_iter()
|
||||
.map(|m| ModelId::new(m.to_string()))
|
||||
.collect(),
|
||||
)
|
||||
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> {
|
||||
@@ -601,141 +613,6 @@ mod tests {
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[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]
|
||||
async fn test_fuzzy_match(cx: &mut TestAppContext) {
|
||||
let models = create_model_list(vec![
|
||||
@@ -775,4 +652,185 @@ 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,18 +1,14 @@
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use acp_thread::{AgentModelInfo, AgentModelSelector};
|
||||
use agent_servers::AgentServer;
|
||||
use agent_settings::AgentSettings;
|
||||
use acp_thread::{AgentModelIcon, AgentModelInfo, AgentModelSelector};
|
||||
use fs::Fs;
|
||||
use gpui::{Entity, FocusHandle};
|
||||
use picker::popover_menu::PickerPopoverMenu;
|
||||
use settings::Settings as _;
|
||||
use ui::{ButtonLike, KeyBinding, PopoverMenuHandle, TintColor, Tooltip, prelude::*};
|
||||
use zed_actions::agent::ToggleModelSelector;
|
||||
use ui::{ButtonLike, PopoverMenuHandle, TintColor, Tooltip, prelude::*};
|
||||
|
||||
use crate::CycleFavoriteModels;
|
||||
use crate::acp::{AcpModelSelector, model_selector::acp_model_selector};
|
||||
use crate::ui::ModelSelectorTooltip;
|
||||
|
||||
pub struct AcpModelSelectorPopover {
|
||||
selector: Entity<AcpModelSelector>,
|
||||
@@ -23,7 +19,7 @@ pub struct AcpModelSelectorPopover {
|
||||
impl AcpModelSelectorPopover {
|
||||
pub(crate) fn new(
|
||||
selector: Rc<dyn AgentModelSelector>,
|
||||
agent_server: Rc<dyn AgentServer>,
|
||||
agent_server: Rc<dyn agent_servers::AgentServer>,
|
||||
fs: Arc<dyn Fs>,
|
||||
menu_handle: PopoverMenuHandle<AcpModelSelector>,
|
||||
focus_handle: FocusHandle,
|
||||
@@ -64,13 +60,14 @@ impl AcpModelSelectorPopover {
|
||||
|
||||
impl Render for AcpModelSelectorPopover {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let model = self.selector.read(cx).delegate.active_model();
|
||||
let selector = self.selector.read(cx);
|
||||
let model = selector.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);
|
||||
let model_icon = model.as_ref().and_then(|model| model.icon.clone());
|
||||
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
@@ -80,43 +77,13 @@ impl Render for AcpModelSelectorPopover {
|
||||
(Color::Muted, IconName::ChevronDown)
|
||||
};
|
||||
|
||||
let tooltip = Tooltip::element({
|
||||
move |_, cx| {
|
||||
let focus_handle = focus_handle.clone();
|
||||
let should_show_cycle_row = !AgentSettings::get_global(cx)
|
||||
.favorite_model_ids()
|
||||
.is_empty();
|
||||
let show_cycle_row = selector.delegate.favorites_count() > 1;
|
||||
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(Label::new("Change Model"))
|
||||
.child(KeyBinding::for_action_in(
|
||||
&ToggleModelSelector,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.when(should_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,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
})
|
||||
.into_any()
|
||||
let tooltip = Tooltip::element({
|
||||
move |_, _cx| {
|
||||
ModelSelectorTooltip::new(focus_handle.clone())
|
||||
.show_cycle_row(show_cycle_row)
|
||||
.into_any_element()
|
||||
}
|
||||
});
|
||||
|
||||
@@ -125,7 +92,14 @@ impl Render for AcpModelSelectorPopover {
|
||||
ButtonLike::new("active-model")
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.when_some(model_icon, |this, icon| {
|
||||
this.child(Icon::new(icon).color(color).size(IconSize::XSmall))
|
||||
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),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
Label::new(model_name)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::acp::AcpThreadView;
|
||||
use crate::{AgentPanel, RemoveHistory, RemoveSelectedThread};
|
||||
use agent::{HistoryEntry, HistoryStore};
|
||||
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
|
||||
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta, Utc};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
@@ -402,7 +402,22 @@ impl AcpThreadHistory {
|
||||
let selected = ix == self.selected_index;
|
||||
let hovered = Some(ix) == self.hovered_index;
|
||||
let timestamp = entry.updated_at().timestamp();
|
||||
let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
|
||||
|
||||
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);
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
@@ -423,11 +438,14 @@ impl AcpThreadHistory {
|
||||
.truncate(),
|
||||
)
|
||||
.child(
|
||||
Label::new(thread_timestamp)
|
||||
Label::new(display_text)
|
||||
.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,7 +22,8 @@ use gpui::{
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
|
||||
IconOrSvg, LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry,
|
||||
ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use language_models::AllLanguageModelSettings;
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
@@ -117,7 +118,7 @@ impl AgentConfiguration {
|
||||
}
|
||||
|
||||
fn build_provider_configuration_views(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let providers = LanguageModelRegistry::read_global(cx).providers();
|
||||
let providers = LanguageModelRegistry::read_global(cx).visible_providers();
|
||||
for provider in providers {
|
||||
self.add_provider_configuration_view(&provider, window, cx);
|
||||
}
|
||||
@@ -261,9 +262,12 @@ impl AgentConfiguration {
|
||||
.w_full()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Icon::new(provider.icon())
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
match provider.icon() {
|
||||
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
|
||||
IconOrSvg::Icon(name) => Icon::new(name),
|
||||
}
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -416,7 +420,7 @@ impl AgentConfiguration {
|
||||
&mut self,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let providers = LanguageModelRegistry::read_global(cx).providers();
|
||||
let providers = LanguageModelRegistry::read_global(cx).visible_providers();
|
||||
|
||||
let popover_menu = PopoverMenu::new("add-provider-popover")
|
||||
.trigger(
|
||||
@@ -1366,6 +1370,9 @@ async fn open_new_agent_servers_entry_in_settings_editor(
|
||||
env: Some(HashMap::default()),
|
||||
default_mode: None,
|
||||
default_model: None,
|
||||
favorite_models: vec![],
|
||||
default_config_options: Default::default(),
|
||||
favorite_config_option_values: Default::default(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ use gpui::{
|
||||
Global, SharedString, Subscription, Task, WeakEntity, Window, prelude::*,
|
||||
};
|
||||
|
||||
use language::{Buffer, Capability, DiskState, OffsetRangeExt, Point};
|
||||
use language::{Buffer, Capability, OffsetRangeExt, Point};
|
||||
use multi_buffer::PathKey;
|
||||
use project::{Project, ProjectItem, ProjectPath};
|
||||
use settings::{Settings, SettingsStore};
|
||||
@@ -146,13 +146,13 @@ impl AgentDiffPane {
|
||||
paths_to_delete.remove(&path_key);
|
||||
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let diff = diff_handle.read(cx);
|
||||
|
||||
let diff_hunk_ranges = diff
|
||||
let diff_hunk_ranges = diff_handle
|
||||
.read(cx)
|
||||
.snapshot(cx)
|
||||
.hunks_intersecting_range(
|
||||
language::Anchor::min_max_range_for_buffer(snapshot.remote_id()),
|
||||
&snapshot,
|
||||
cx,
|
||||
)
|
||||
.map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -192,7 +192,7 @@ impl AgentDiffPane {
|
||||
&& buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.is_some_and(|file| file.disk_state() == DiskState::Deleted)
|
||||
.is_some_and(|file| file.disk_state().is_deleted())
|
||||
{
|
||||
editor.fold_buffer(snapshot.text.remote_id(), cx)
|
||||
}
|
||||
@@ -1363,7 +1363,8 @@ impl AgentDiff {
|
||||
| AcpThreadEvent::PromptCapabilitiesUpdated
|
||||
| AcpThreadEvent::AvailableCommandsUpdated(_)
|
||||
| AcpThreadEvent::Retry(_)
|
||||
| AcpThreadEvent::ModeUpdated(_) => {}
|
||||
| AcpThreadEvent::ModeUpdated(_)
|
||||
| AcpThreadEvent::ConfigOptionsUpdated(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
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>,
|
||||
@@ -80,6 +81,12 @@ 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 {
|
||||
@@ -97,13 +104,30 @@ 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(Icon::new(icon).color(color).size(IconSize::XSmall))
|
||||
this.child(
|
||||
match icon {
|
||||
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
|
||||
IconOrSvg::Icon(name) => Icon::new(name),
|
||||
}
|
||||
.color(color)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
})
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.child(
|
||||
@@ -115,11 +139,9 @@ impl Render for AgentModelSelector {
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(color)
|
||||
.size(IconSize::Small),
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
move |_window, cx| {
|
||||
Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
|
||||
},
|
||||
tooltip,
|
||||
gpui::Corner::TopRight,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -2428,7 +2428,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)
|
||||
.providers()
|
||||
.visible_providers()
|
||||
.iter()
|
||||
.any(|provider| {
|
||||
provider.is_authenticated(cx)
|
||||
|
||||
@@ -348,7 +348,8 @@ 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::RemovedProvider(_)
|
||||
| language_model::Event::ProvidersChanged => {
|
||||
update_active_language_model_from_settings(cx);
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -1586,7 +1586,7 @@ pub(crate) fn search_rules(
|
||||
None
|
||||
} else {
|
||||
Some(RulesContextEntry {
|
||||
prompt_id: metadata.id.user_id()?,
|
||||
prompt_id: metadata.id.as_user()?,
|
||||
title: metadata.title?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent_client_protocol::ModelId;
|
||||
use fs::Fs;
|
||||
use language_model::LanguageModel;
|
||||
use settings::{LanguageModelSelection, update_settings_file};
|
||||
@@ -13,20 +12,11 @@ fn language_model_to_selection(model: &Arc<dyn LanguageModel>) -> LanguageModelS
|
||||
}
|
||||
}
|
||||
|
||||
fn model_id_to_selection(model_id: &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(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_in_settings(
|
||||
model: Arc<dyn LanguageModel>,
|
||||
should_be_favorite: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &App,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let selection = language_model_to_selection(&model);
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
@@ -38,20 +28,3 @@ pub fn toggle_in_settings(
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn toggle_model_id_in_settings(
|
||||
model_id: 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1259,28 +1259,26 @@ impl InlineAssistant {
|
||||
let bottom = top + 1.0;
|
||||
(top, bottom)
|
||||
});
|
||||
let mut scroll_target_top = scroll_target_range.0;
|
||||
let mut scroll_target_bottom = scroll_target_range.1;
|
||||
|
||||
scroll_target_top -= editor.vertical_scroll_margin() as ScrollOffset;
|
||||
scroll_target_bottom += editor.vertical_scroll_margin() as ScrollOffset;
|
||||
|
||||
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 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 {
|
||||
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);
|
||||
}
|
||||
editor.set_scroll_position(
|
||||
point(0., scroll_target_bottom - height_in_lines),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -40,7 +40,9 @@ 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::{CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext};
|
||||
use crate::{
|
||||
CycleFavoriteModels, CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext,
|
||||
};
|
||||
|
||||
actions!(inline_assistant, [ThumbsUpResult, ThumbsDownResult]);
|
||||
|
||||
@@ -148,7 +150,7 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
.into_any_element();
|
||||
|
||||
v_flex()
|
||||
.key_context("PromptEditor")
|
||||
.key_context("InlineAssistant")
|
||||
.capture_action(cx.listener(Self::paste))
|
||||
.block_mouse_except_scroll()
|
||||
.size_full()
|
||||
@@ -162,10 +164,6 @@ 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))
|
||||
@@ -174,6 +172,15 @@ 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()
|
||||
@@ -855,7 +862,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
.map(|this| {
|
||||
if rated {
|
||||
this.disabled(true)
|
||||
.icon_color(Color::Ignored)
|
||||
.icon_color(Color::Disabled)
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Good Result",
|
||||
@@ -865,8 +872,15 @@ impl<T: 'static> PromptEditor<T> {
|
||||
)
|
||||
})
|
||||
} else {
|
||||
this.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Good Result"))
|
||||
this.icon_color(Color::Muted).tooltip(
|
||||
move |_, cx| {
|
||||
Tooltip::for_action(
|
||||
"Good Result",
|
||||
&ThumbsUpResult,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
@@ -879,7 +893,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
.map(|this| {
|
||||
if rated {
|
||||
this.disabled(true)
|
||||
.icon_color(Color::Ignored)
|
||||
.icon_color(Color::Disabled)
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Bad Result",
|
||||
@@ -889,8 +903,15 @@ impl<T: 'static> PromptEditor<T> {
|
||||
)
|
||||
})
|
||||
} else {
|
||||
this.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Bad Result"))
|
||||
this.icon_color(Color::Muted).tooltip(
|
||||
move |_, cx| {
|
||||
Tooltip::for_action(
|
||||
"Bad Result",
|
||||
&ThumbsDownResult,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
@@ -1088,7 +1109,6 @@ impl<T: 'static> PromptEditor<T> {
|
||||
let colors = cx.theme().colors();
|
||||
|
||||
div()
|
||||
.key_context("InlineAssistEditor")
|
||||
.size_full()
|
||||
.p_2()
|
||||
.pl_1()
|
||||
|
||||
@@ -7,8 +7,8 @@ use gpui::{
|
||||
Action, AnyElement, App, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, Task,
|
||||
};
|
||||
use language_model::{
|
||||
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelRegistry,
|
||||
AuthenticateError, ConfiguredModel, IconOrSvg, LanguageModel, LanguageModelId,
|
||||
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry,
|
||||
};
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
@@ -20,14 +20,14 @@ 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, &App) + '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, &App) + 'static,
|
||||
on_toggle_favorite: impl Fn(Arc<dyn LanguageModel>, bool, &mut App) + 'static,
|
||||
popover_styles: bool,
|
||||
focus_handle: FocusHandle,
|
||||
window: &mut Window,
|
||||
@@ -55,7 +55,7 @@ pub fn language_model_selector(
|
||||
|
||||
fn all_models(cx: &App) -> GroupedModels {
|
||||
let lm_registry = LanguageModelRegistry::global(cx).read(cx);
|
||||
let providers = lm_registry.providers();
|
||||
let providers = lm_registry.visible_providers();
|
||||
|
||||
let mut favorites_index = FavoritesIndex::default();
|
||||
|
||||
@@ -94,7 +94,7 @@ type FavoritesIndex = HashMap<LanguageModelProviderId, HashSet<LanguageModelId>>
|
||||
#[derive(Clone)]
|
||||
struct ModelInfo {
|
||||
model: Arc<dyn LanguageModel>,
|
||||
icon: IconName,
|
||||
icon: IconOrSvg,
|
||||
is_favorite: bool,
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ 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, &App) + 'static,
|
||||
on_toggle_favorite: impl Fn(Arc<dyn LanguageModel>, bool, &mut App) + 'static,
|
||||
popover_styles: bool,
|
||||
focus_handle: FocusHandle,
|
||||
window: &mut Window,
|
||||
@@ -203,7 +203,7 @@ impl LanguageModelPickerDelegate {
|
||||
fn authenticate_all_providers(cx: &mut App) -> Task<()> {
|
||||
let authenticate_all_providers = LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.providers()
|
||||
.visible_providers()
|
||||
.iter()
|
||||
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -250,6 +250,10 @@ impl LanguageModelPickerDelegate {
|
||||
(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;
|
||||
@@ -474,7 +478,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
|
||||
let configured_providers = language_model_registry
|
||||
.read(cx)
|
||||
.providers()
|
||||
.visible_providers()
|
||||
.into_iter()
|
||||
.filter(|provider| provider.is_authenticated(cx))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -561,12 +565,18 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
let handle_action_click = {
|
||||
let model = model_info.model.clone();
|
||||
let on_toggle_favorite = self.on_toggle_favorite.clone();
|
||||
move |cx: &App| on_toggle_favorite(model.clone(), !is_favorite, cx)
|
||||
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)
|
||||
.icon(model_info.icon)
|
||||
.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_selected(is_selected)
|
||||
.is_focused(selected)
|
||||
.is_favorite(is_favorite)
|
||||
@@ -702,7 +712,7 @@ mod tests {
|
||||
.any(|(fav_provider, fav_name)| *fav_provider == provider && *fav_name == name);
|
||||
ModelInfo {
|
||||
model: Arc::new(TestLanguageModel::new(name, provider)),
|
||||
icon: IconName::Ai,
|
||||
icon: IconOrSvg::Icon(IconName::Ai),
|
||||
is_favorite,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -12,8 +12,8 @@ use editor::{
|
||||
};
|
||||
use futures::{AsyncReadExt as _, FutureExt as _, future::Shared};
|
||||
use gpui::{
|
||||
Animation, AnimationExt as _, AppContext, ClipboardEntry, Context, Empty, Entity, EntityId,
|
||||
Image, ImageFormat, Img, SharedString, Task, WeakEntity, pulsating_between,
|
||||
AppContext, ClipboardEntry, Context, Empty, Entity, EntityId, Image, ImageFormat, Img,
|
||||
SharedString, Task, WeakEntity,
|
||||
};
|
||||
use http_client::{AsyncBody, HttpClientWithUrl};
|
||||
use itertools::Either;
|
||||
@@ -32,13 +32,14 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use text::OffsetRangeExt;
|
||||
use ui::{ButtonLike, Disclosure, TintColor, Toggleable, prelude::*};
|
||||
use ui::{Disclosure, Toggleable, prelude::*};
|
||||
use util::{ResultExt, debug_panic, rel_path::RelPath};
|
||||
use workspace::{Workspace, notifications::NotifyResultExt as _};
|
||||
|
||||
use crate::ui::MentionCrease;
|
||||
|
||||
pub type MentionTask = Shared<Task<Result<Mention, String>>>;
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
@@ -754,25 +755,8 @@ fn render_fold_icon_button(
|
||||
.update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx))
|
||||
.unwrap_or_default();
|
||||
|
||||
ButtonLike::new(fold_id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.toggle_state(is_in_text_selection)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::from_path(icon_path.clone())
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Label::new(label.clone())
|
||||
.size(LabelSize::Small)
|
||||
.buffer_font(cx)
|
||||
.single_line(),
|
||||
),
|
||||
)
|
||||
MentionCrease::new(fold_id, icon_path.clone(), label.clone())
|
||||
.is_toggled(is_in_text_selection)
|
||||
.into_any_element()
|
||||
}
|
||||
})
|
||||
@@ -947,12 +931,14 @@ impl Render for LoadingContext {
|
||||
.editor
|
||||
.update(cx, |editor, cx| editor.is_range_selected(&self.range, cx))
|
||||
.unwrap_or_default();
|
||||
ButtonLike::new(("loading-context", self.id))
|
||||
.style(ButtonStyle::Filled)
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.toggle_state(is_in_text_selection)
|
||||
.when_some(self.image.clone(), |el, image_task| {
|
||||
el.hoverable_tooltip(move |_, cx| {
|
||||
|
||||
let id = ElementId::from(("loading_context", self.id));
|
||||
|
||||
MentionCrease::new(id, self.icon.clone(), self.label.clone())
|
||||
.is_toggled(is_in_text_selection)
|
||||
.is_loading(self.loading.is_some())
|
||||
.when_some(self.image.clone(), |this, image_task| {
|
||||
this.image_preview(move |_, cx| {
|
||||
let image = image_task.peek().cloned().transpose().ok().flatten();
|
||||
let image_task = image_task.clone();
|
||||
cx.new::<ImageHover>(|cx| ImageHover {
|
||||
@@ -971,35 +957,6 @@ impl Render for LoadingContext {
|
||||
.into()
|
||||
})
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::from_path(self.icon.clone())
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Label::new(self.label.clone())
|
||||
.size(LabelSize::Small)
|
||||
.buffer_font(cx)
|
||||
.single_line(),
|
||||
)
|
||||
.map(|el| {
|
||||
if self.loading.is_some() {
|
||||
el.with_animation(
|
||||
"loading-context-crease",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.4, 0.8)),
|
||||
|label, delta| label.opacity(delta),
|
||||
)
|
||||
.into_any()
|
||||
} else {
|
||||
el.into_any()
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ use std::{
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
use ui::{
|
||||
DocumentationAside, DocumentationEdge, DocumentationSide, HighlightedLabel, KeyBinding,
|
||||
LabelSize, ListItem, ListItemSpacing, PopoverMenuHandle, TintColor, Tooltip, prelude::*,
|
||||
DocumentationAside, DocumentationSide, HighlightedLabel, KeyBinding, LabelSize, ListItem,
|
||||
ListItemSpacing, PopoverMenuHandle, TintColor, Tooltip, prelude::*,
|
||||
};
|
||||
|
||||
/// Trait for types that can provide and manage agent profiles
|
||||
@@ -191,6 +191,9 @@ 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()
|
||||
@@ -203,9 +206,6 @@ impl Render for ProfileSelector {
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.child(container().child(Label::new("Toggle Profile Menu")).child(
|
||||
KeyBinding::for_action_in(&ToggleProfileSelector, &focus_handle, cx),
|
||||
))
|
||||
.into_any()
|
||||
}
|
||||
}),
|
||||
@@ -244,6 +244,7 @@ pub(crate) struct ProfilePickerDelegate {
|
||||
string_candidates: Arc<Vec<StringMatchCandidate>>,
|
||||
filtered_entries: Vec<ProfilePickerEntry>,
|
||||
selected_index: usize,
|
||||
hovered_index: Option<usize>,
|
||||
query: String,
|
||||
cancel: Option<Arc<AtomicBool>>,
|
||||
focus_handle: FocusHandle,
|
||||
@@ -270,6 +271,7 @@ impl ProfilePickerDelegate {
|
||||
string_candidates,
|
||||
filtered_entries,
|
||||
selected_index: 0,
|
||||
hovered_index: None,
|
||||
query: String::new(),
|
||||
cancel: None,
|
||||
focus_handle,
|
||||
@@ -578,23 +580,38 @@ impl PickerDelegate for ProfilePickerDelegate {
|
||||
let candidate = self.candidates.get(entry.candidate_index)?;
|
||||
let active_id = self.provider.profile_id(cx);
|
||||
let is_active = active_id == candidate.id;
|
||||
let has_documentation = Self::documentation(candidate).is_some();
|
||||
|
||||
Some(
|
||||
ListItem::new(candidate.id.0.clone())
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(HighlightedLabel::new(
|
||||
candidate.name.clone(),
|
||||
entry.positions.clone(),
|
||||
))
|
||||
.when(is_active, |this| {
|
||||
this.end_slot(
|
||||
div()
|
||||
.pr_2()
|
||||
.child(Icon::new(IconName::Check).color(Color::Accent)),
|
||||
)
|
||||
div()
|
||||
.id(("profile-picker-item", ix))
|
||||
.when(has_documentation, |this| {
|
||||
this.on_hover(cx.listener(move |picker, hovered, _, cx| {
|
||||
if *hovered {
|
||||
picker.delegate.hovered_index = Some(ix);
|
||||
} else if picker.delegate.hovered_index == Some(ix) {
|
||||
picker.delegate.hovered_index = None;
|
||||
}
|
||||
cx.notify();
|
||||
}))
|
||||
})
|
||||
.child(
|
||||
ListItem::new(candidate.id.0.clone())
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(HighlightedLabel::new(
|
||||
candidate.name.clone(),
|
||||
entry.positions.clone(),
|
||||
))
|
||||
.when(is_active, |this| {
|
||||
this.end_slot(
|
||||
div()
|
||||
.pr_2()
|
||||
.child(Icon::new(IconName::Check).color(Color::Accent)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
@@ -608,7 +625,8 @@ impl PickerDelegate for ProfilePickerDelegate {
|
||||
) -> Option<DocumentationAside> {
|
||||
use std::rc::Rc;
|
||||
|
||||
let entry = match self.filtered_entries.get(self.selected_index)? {
|
||||
let hovered_index = self.hovered_index?;
|
||||
let entry = match self.filtered_entries.get(hovered_index)? {
|
||||
ProfilePickerEntry::Profile(entry) => entry,
|
||||
ProfilePickerEntry::Header(_) => return None,
|
||||
};
|
||||
@@ -626,11 +644,14 @@ impl PickerDelegate for ProfilePickerDelegate {
|
||||
|
||||
Some(DocumentationAside {
|
||||
side,
|
||||
edge: DocumentationEdge::Top,
|
||||
render: Rc::new(move |_| Label::new(docs_aside.clone()).into_any_element()),
|
||||
})
|
||||
}
|
||||
|
||||
fn documentation_aside_index(&self) -> Option<usize> {
|
||||
self.hovered_index
|
||||
}
|
||||
|
||||
fn render_footer(
|
||||
&self,
|
||||
_: &mut Window,
|
||||
@@ -718,6 +739,7 @@ mod tests {
|
||||
string_candidates: Arc::new(Vec::new()),
|
||||
filtered_entries: Vec::new(),
|
||||
selected_index: 0,
|
||||
hovered_index: None,
|
||||
query: String::new(),
|
||||
cancel: None,
|
||||
focus_handle,
|
||||
@@ -752,6 +774,7 @@ mod tests {
|
||||
background: cx.background_executor().clone(),
|
||||
candidates,
|
||||
string_candidates: Arc::new(Vec::new()),
|
||||
hovered_index: None,
|
||||
filtered_entries: vec![
|
||||
ProfilePickerEntry::Profile(ProfileMatchEntry {
|
||||
candidate_index: 0,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{
|
||||
language_model_selector::{LanguageModelSelector, language_model_selector},
|
||||
ui::BurnModeTooltip,
|
||||
ui::{BurnModeTooltip, ModelSelectorTooltip},
|
||||
};
|
||||
use agent_settings::{AgentSettings, CompletionMode};
|
||||
use agent_settings::CompletionMode;
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
|
||||
use assistant_slash_commands::{DefaultSlashCommand, FileSlashCommand, selections_creases};
|
||||
@@ -33,7 +33,8 @@ use language::{
|
||||
language_settings::{SoftWrap, all_language_settings},
|
||||
};
|
||||
use language_model::{
|
||||
ConfigurationError, LanguageModelExt, LanguageModelImage, LanguageModelRegistry, Role,
|
||||
ConfigurationError, IconOrSvg, LanguageModelExt, LanguageModelImage, LanguageModelRegistry,
|
||||
Role,
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use picker::{Picker, popover_menu::PickerPopoverMenu};
|
||||
@@ -71,7 +72,7 @@ use workspace::{
|
||||
pane,
|
||||
searchable::{SearchEvent, SearchableItem},
|
||||
};
|
||||
use zed_actions::agent::{AddSelectionToThread, ToggleModelSelector};
|
||||
use zed_actions::agent::{AddSelectionToThread, PasteRaw, ToggleModelSelector};
|
||||
|
||||
use crate::CycleFavoriteModels;
|
||||
|
||||
@@ -1698,6 +1699,9 @@ 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())
|
||||
@@ -1708,84 +1712,101 @@ impl TextThreadEditor {
|
||||
_ => None,
|
||||
});
|
||||
|
||||
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())
|
||||
});
|
||||
// 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()?;
|
||||
|
||||
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();
|
||||
if line_range.start() == line_range.end() {
|
||||
return Some(false);
|
||||
}
|
||||
|
||||
let text = clipboard_text.text();
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let mut current_offset = 0;
|
||||
let weak_editor = cx.entity().downgrade();
|
||||
Some(
|
||||
workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.project_path_for_absolute_path(file_path, cx)
|
||||
.is_some(),
|
||||
)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
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```");
|
||||
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();
|
||||
|
||||
let insert_point = editor
|
||||
.selections
|
||||
.newest::<Point>(&editor.display_snapshot(cx))
|
||||
.head();
|
||||
let start_row = MultiBufferRow(insert_point.row);
|
||||
let text = clipboard_text.text();
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let mut current_offset = 0;
|
||||
let weak_editor = cx.entity().downgrade();
|
||||
|
||||
editor.insert(&formatted_text, window, cx);
|
||||
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 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 insert_point = editor
|
||||
.selections
|
||||
.newest::<Point>(&editor.display_snapshot(cx))
|
||||
.head();
|
||||
let start_row = MultiBufferRow(insert_point.row);
|
||||
|
||||
editor.insert("\n", window, cx);
|
||||
editor.insert(&formatted_text, window, cx);
|
||||
|
||||
let crease_text = acp_thread::selection_name(
|
||||
Some(file_path.as_ref()),
|
||||
&line_range,
|
||||
);
|
||||
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 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);
|
||||
editor.insert("\n", window, cx);
|
||||
|
||||
current_offset += selection.len;
|
||||
if !selection.is_entire_line && current_offset < text.len() {
|
||||
current_offset += 1;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1944,6 +1965,12 @@ 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);
|
||||
@@ -2205,10 +2232,10 @@ impl TextThreadEditor {
|
||||
.default_model()
|
||||
.map(|default| default.provider);
|
||||
|
||||
let provider_icon = match active_provider {
|
||||
Some(provider) => provider.icon(),
|
||||
None => IconName::Ai,
|
||||
};
|
||||
let provider_icon = active_provider
|
||||
.as_ref()
|
||||
.map(|p| p.icon())
|
||||
.unwrap_or(IconOrSvg::Icon(IconName::Ai));
|
||||
|
||||
let focus_handle = self.editor().focus_handle(cx);
|
||||
|
||||
@@ -2218,43 +2245,25 @@ impl TextThreadEditor {
|
||||
(Color::Muted, IconName::ChevronDown)
|
||||
};
|
||||
|
||||
let tooltip = Tooltip::element({
|
||||
move |_, cx| {
|
||||
let focus_handle = focus_handle.clone();
|
||||
let should_show_cycle_row = !AgentSettings::get_global(cx)
|
||||
.favorite_model_ids()
|
||||
.is_empty();
|
||||
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);
|
||||
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(Label::new("Change Model"))
|
||||
.child(KeyBinding::for_action_in(
|
||||
&ToggleModelSelector,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.when(should_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,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
})
|
||||
.into_any()
|
||||
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()
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2265,7 +2274,7 @@ impl TextThreadEditor {
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(Icon::new(provider_icon).color(color).size(IconSize::XSmall))
|
||||
.child(provider_icon_element)
|
||||
.child(
|
||||
Label::new(model_name)
|
||||
.color(color)
|
||||
@@ -2627,6 +2636,7 @@ 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))
|
||||
|
||||
@@ -4,6 +4,7 @@ mod burn_mode_tooltip;
|
||||
mod claude_code_onboarding_modal;
|
||||
mod end_trial_upsell;
|
||||
mod hold_for_default;
|
||||
mod mention_crease;
|
||||
mod model_selector_components;
|
||||
mod onboarding_modal;
|
||||
mod usage_callout;
|
||||
@@ -14,6 +15,7 @@ pub use burn_mode_tooltip::*;
|
||||
pub use claude_code_onboarding_modal::*;
|
||||
pub use end_trial_upsell::*;
|
||||
pub use hold_for_default::*;
|
||||
pub use mention_crease::*;
|
||||
pub use model_selector_components::*;
|
||||
pub use onboarding_modal::*;
|
||||
pub use usage_callout::*;
|
||||
|
||||
@@ -27,7 +27,7 @@ impl RenderOnce for HoldForDefault {
|
||||
PlatformStyle::platform(),
|
||||
None,
|
||||
Some(TextSize::Default.rems(cx).into()),
|
||||
true,
|
||||
false,
|
||||
)))
|
||||
.child(div().map(|this| {
|
||||
if self.is_default {
|
||||
|
||||
100
crates/agent_ui/src/ui/mention_crease.rs
Normal file
100
crates/agent_ui/src/ui/mention_crease.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::{Animation, AnimationExt, AnyView, IntoElement, Window, pulsating_between};
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{ButtonLike, TintColor, prelude::*};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct MentionCrease {
|
||||
id: ElementId,
|
||||
icon: SharedString,
|
||||
label: SharedString,
|
||||
is_toggled: bool,
|
||||
is_loading: bool,
|
||||
image_preview: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
|
||||
}
|
||||
|
||||
impl MentionCrease {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
icon: impl Into<SharedString>,
|
||||
label: impl Into<SharedString>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
icon: icon.into(),
|
||||
label: label.into(),
|
||||
is_toggled: false,
|
||||
is_loading: false,
|
||||
image_preview: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_toggled(mut self, is_toggled: bool) -> Self {
|
||||
self.is_toggled = is_toggled;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_loading(mut self, is_loading: bool) -> Self {
|
||||
self.is_loading = is_loading;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn image_preview(
|
||||
mut self,
|
||||
builder: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
|
||||
) -> Self {
|
||||
self.image_preview = Some(Box::new(builder));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for MentionCrease {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let font_size = settings.agent_buffer_font_size(cx);
|
||||
let buffer_font = settings.buffer_font.clone();
|
||||
|
||||
let button_height = DefiniteLength::Absolute(AbsoluteLength::Pixels(
|
||||
px(window.line_height().into()) - px(1.),
|
||||
));
|
||||
|
||||
ButtonLike::new(self.id)
|
||||
.style(ButtonStyle::Outlined)
|
||||
.size(ButtonSize::Compact)
|
||||
.height(button_height)
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.toggle_state(self.is_toggled)
|
||||
.when_some(self.image_preview, |this, image_preview| {
|
||||
this.hoverable_tooltip(image_preview)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.pb_px()
|
||||
.gap_1()
|
||||
.font(buffer_font)
|
||||
.text_size(font_size)
|
||||
.child(
|
||||
Icon::from_path(self.icon.clone())
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(self.label.clone())
|
||||
.map(|this| {
|
||||
if self.is_loading {
|
||||
this.with_animation(
|
||||
"loading-context-crease",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.4, 0.8)),
|
||||
|label, delta| label.opacity(delta),
|
||||
)
|
||||
.into_any()
|
||||
} else {
|
||||
this.into_any()
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,13 @@
|
||||
use gpui::{Action, FocusHandle, prelude::*};
|
||||
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),
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ModelSelectorHeader {
|
||||
@@ -39,11 +47,11 @@ impl RenderOnce for ModelSelectorHeader {
|
||||
pub struct ModelSelectorListItem {
|
||||
index: usize,
|
||||
title: SharedString,
|
||||
icon: Option<IconName>,
|
||||
icon: Option<ModelIcon>,
|
||||
is_selected: bool,
|
||||
is_focused: bool,
|
||||
is_favorite: bool,
|
||||
on_toggle_favorite: Option<Box<dyn Fn(&App) + 'static>>,
|
||||
on_toggle_favorite: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
||||
}
|
||||
|
||||
impl ModelSelectorListItem {
|
||||
@@ -60,7 +68,12 @@ impl ModelSelectorListItem {
|
||||
}
|
||||
|
||||
pub fn icon(mut self, icon: IconName) -> Self {
|
||||
self.icon = Some(icon);
|
||||
self.icon = Some(ModelIcon::Name(icon));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn icon_path(mut self, path: SharedString) -> Self {
|
||||
self.icon = Some(ModelIcon::Path(path));
|
||||
self
|
||||
}
|
||||
|
||||
@@ -79,7 +92,10 @@ impl ModelSelectorListItem {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_toggle_favorite(mut self, handler: impl Fn(&App) + 'static) -> 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
|
||||
}
|
||||
@@ -105,9 +121,12 @@ impl RenderOnce for ModelSelectorListItem {
|
||||
.gap_1p5()
|
||||
.when_some(self.icon, |this, icon| {
|
||||
this.child(
|
||||
Icon::new(icon)
|
||||
.color(model_icon_color)
|
||||
.size(IconSize::Small),
|
||||
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),
|
||||
)
|
||||
})
|
||||
.child(Label::new(self.title).truncate()),
|
||||
@@ -128,7 +147,7 @@ impl RenderOnce for ModelSelectorListItem {
|
||||
.icon_color(color)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text(tooltip))
|
||||
.on_click(move |_, _, cx| (handle_click)(cx)),
|
||||
.on_click(move |event, window, cx| (handle_click)(event, window, cx)),
|
||||
)
|
||||
}
|
||||
}))
|
||||
@@ -174,3 +193,57 @@ 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,
|
||||
)),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use agent::{HistoryEntry, HistoryStore};
|
||||
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
|
||||
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta, Utc};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
@@ -411,7 +411,22 @@ impl AcpThreadHistory {
|
||||
let selected = ix == self.selected_index;
|
||||
let hovered = Some(ix) == self.hovered_index;
|
||||
let timestamp = entry.updated_at().timestamp();
|
||||
let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
|
||||
|
||||
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);
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
@@ -432,11 +447,14 @@ impl AcpThreadHistory {
|
||||
.truncate(),
|
||||
)
|
||||
.child(
|
||||
Label::new(thread_timestamp)
|
||||
Label::new(display_text)
|
||||
.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::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
|
||||
use language_model::{IconOrSvg, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
|
||||
use ui::{Divider, List, ListBulletItem, prelude::*};
|
||||
|
||||
pub struct ApiKeysWithProviders {
|
||||
configured_providers: Vec<(IconName, SharedString)>,
|
||||
configured_providers: Vec<(IconOrSvg, SharedString)>,
|
||||
}
|
||||
|
||||
impl ApiKeysWithProviders {
|
||||
@@ -13,7 +13,8 @@ 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::RemovedProvider(_)
|
||||
| language_model::Event::ProvidersChanged => {
|
||||
this.configured_providers = Self::compute_configured_providers(cx)
|
||||
}
|
||||
_ => {}
|
||||
@@ -26,9 +27,9 @@ impl ApiKeysWithProviders {
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_configured_providers(cx: &App) -> Vec<(IconName, SharedString)> {
|
||||
fn compute_configured_providers(cx: &App) -> Vec<(IconOrSvg, SharedString)> {
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.providers()
|
||||
.visible_providers()
|
||||
.iter()
|
||||
.filter(|provider| {
|
||||
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
|
||||
@@ -47,7 +48,14 @@ impl Render for ApiKeysWithProviders {
|
||||
.map(|(icon, name)| {
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
|
||||
.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(Label::new(name))
|
||||
});
|
||||
div()
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{AgentPanelOnboardingCard, ApiKeysWithoutProviders, ZedAiOnboarding};
|
||||
pub struct AgentPanelOnboarding {
|
||||
user_store: Entity<UserStore>,
|
||||
client: Arc<Client>,
|
||||
configured_providers: Vec<(IconName, SharedString)>,
|
||||
has_configured_providers: bool,
|
||||
continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||
}
|
||||
|
||||
@@ -27,8 +27,9 @@ 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(_) => {
|
||||
this.configured_providers = Self::compute_available_providers(cx)
|
||||
| language_model::Event::RemovedProvider(_)
|
||||
| language_model::Event::ProvidersChanged => {
|
||||
this.has_configured_providers = Self::has_configured_providers(cx)
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
@@ -38,20 +39,16 @@ impl AgentPanelOnboarding {
|
||||
Self {
|
||||
user_store,
|
||||
client,
|
||||
configured_providers: Self::compute_available_providers(cx),
|
||||
has_configured_providers: Self::has_configured_providers(cx),
|
||||
continue_with_zed_ai: Arc::new(continue_with_zed_ai),
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_available_providers(cx: &App) -> Vec<(IconName, SharedString)> {
|
||||
fn has_configured_providers(cx: &App) -> bool {
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.providers()
|
||||
.visible_providers()
|
||||
.iter()
|
||||
.filter(|provider| {
|
||||
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
|
||||
})
|
||||
.map(|provider| (provider.icon(), provider.name().0))
|
||||
.collect()
|
||||
.any(|provider| provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +78,7 @@ impl Render for AgentPanelOnboarding {
|
||||
}),
|
||||
)
|
||||
.map(|this| {
|
||||
if enrolled_in_trial || is_pro_user || !self.configured_providers.is_empty() {
|
||||
if enrolled_in_trial || is_pro_user || self.has_configured_providers {
|
||||
this
|
||||
} else {
|
||||
this.child(ApiKeysWithoutProviders::new())
|
||||
|
||||
@@ -15,7 +15,6 @@ path = "src/buffer_diff.rs"
|
||||
test-support = ["settings"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
clock.workspace = true
|
||||
futures.workspace = true
|
||||
git2.workspace = true
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 {
|
||||
|
||||
@@ -2647,13 +2647,13 @@ async fn test_git_diff_base_change(
|
||||
local_unstaged_diff_a.read_with(cx_a, |diff, cx| {
|
||||
let buffer = buffer_local_a.read(cx);
|
||||
assert_eq!(
|
||||
diff.base_text_string().as_deref(),
|
||||
diff.base_text_string(cx).as_deref(),
|
||||
Some(staged_text.as_str())
|
||||
);
|
||||
assert_hunks(
|
||||
diff.hunks_in_row_range(0..4, buffer, cx),
|
||||
diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&diff.base_text_string(cx).unwrap(),
|
||||
&[(1..2, "", "two\n", DiffHunkStatus::added_none())],
|
||||
);
|
||||
});
|
||||
@@ -2677,13 +2677,13 @@ async fn test_git_diff_base_change(
|
||||
remote_unstaged_diff_a.read_with(cx_b, |diff, cx| {
|
||||
let buffer = remote_buffer_a.read(cx);
|
||||
assert_eq!(
|
||||
diff.base_text_string().as_deref(),
|
||||
diff.base_text_string(cx).as_deref(),
|
||||
Some(staged_text.as_str())
|
||||
);
|
||||
assert_hunks(
|
||||
diff.hunks_in_row_range(0..4, buffer, cx),
|
||||
diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&diff.base_text_string(cx).unwrap(),
|
||||
&[(1..2, "", "two\n", DiffHunkStatus::added_none())],
|
||||
);
|
||||
});
|
||||
@@ -2699,13 +2699,13 @@ async fn test_git_diff_base_change(
|
||||
remote_uncommitted_diff_a.read_with(cx_b, |diff, cx| {
|
||||
let buffer = remote_buffer_a.read(cx);
|
||||
assert_eq!(
|
||||
diff.base_text_string().as_deref(),
|
||||
diff.base_text_string(cx).as_deref(),
|
||||
Some(committed_text.as_str())
|
||||
);
|
||||
assert_hunks(
|
||||
diff.hunks_in_row_range(0..4, buffer, cx),
|
||||
diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&diff.base_text_string(cx).unwrap(),
|
||||
&[(
|
||||
1..2,
|
||||
"TWO\n",
|
||||
@@ -2731,13 +2731,13 @@ async fn test_git_diff_base_change(
|
||||
local_unstaged_diff_a.read_with(cx_a, |diff, cx| {
|
||||
let buffer = buffer_local_a.read(cx);
|
||||
assert_eq!(
|
||||
diff.base_text_string().as_deref(),
|
||||
diff.base_text_string(cx).as_deref(),
|
||||
Some(new_staged_text.as_str())
|
||||
);
|
||||
assert_hunks(
|
||||
diff.hunks_in_row_range(0..4, buffer, cx),
|
||||
diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&diff.base_text_string(cx).unwrap(),
|
||||
&[(2..3, "", "three\n", DiffHunkStatus::added_none())],
|
||||
);
|
||||
});
|
||||
@@ -2746,13 +2746,13 @@ async fn test_git_diff_base_change(
|
||||
remote_unstaged_diff_a.read_with(cx_b, |diff, cx| {
|
||||
let buffer = remote_buffer_a.read(cx);
|
||||
assert_eq!(
|
||||
diff.base_text_string().as_deref(),
|
||||
diff.base_text_string(cx).as_deref(),
|
||||
Some(new_staged_text.as_str())
|
||||
);
|
||||
assert_hunks(
|
||||
diff.hunks_in_row_range(0..4, buffer, cx),
|
||||
diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&diff.base_text_string(cx).unwrap(),
|
||||
&[(2..3, "", "three\n", DiffHunkStatus::added_none())],
|
||||
);
|
||||
});
|
||||
@@ -2760,13 +2760,13 @@ async fn test_git_diff_base_change(
|
||||
remote_uncommitted_diff_a.read_with(cx_b, |diff, cx| {
|
||||
let buffer = remote_buffer_a.read(cx);
|
||||
assert_eq!(
|
||||
diff.base_text_string().as_deref(),
|
||||
diff.base_text_string(cx).as_deref(),
|
||||
Some(new_committed_text.as_str())
|
||||
);
|
||||
assert_hunks(
|
||||
diff.hunks_in_row_range(0..4, buffer, cx),
|
||||
diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&diff.base_text_string(cx).unwrap(),
|
||||
&[(
|
||||
1..2,
|
||||
"TWO_HUNDRED\n",
|
||||
@@ -2813,13 +2813,13 @@ async fn test_git_diff_base_change(
|
||||
local_unstaged_diff_b.read_with(cx_a, |diff, cx| {
|
||||
let buffer = buffer_local_b.read(cx);
|
||||
assert_eq!(
|
||||
diff.base_text_string().as_deref(),
|
||||
diff.base_text_string(cx).as_deref(),
|
||||
Some(staged_text.as_str())
|
||||
);
|
||||
assert_hunks(
|
||||
diff.hunks_in_row_range(0..4, buffer, cx),
|
||||
diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&diff.base_text_string(cx).unwrap(),
|
||||
&[(1..2, "", "two\n", DiffHunkStatus::added_none())],
|
||||
);
|
||||
});
|
||||
@@ -2842,11 +2842,11 @@ async fn test_git_diff_base_change(
|
||||
remote_unstaged_diff_b.read_with(cx_b, |diff, cx| {
|
||||
let buffer = remote_buffer_b.read(cx);
|
||||
assert_eq!(
|
||||
diff.base_text_string().as_deref(),
|
||||
diff.base_text_string(cx).as_deref(),
|
||||
Some(staged_text.as_str())
|
||||
);
|
||||
assert_hunks(
|
||||
diff.hunks_in_row_range(0..4, buffer, cx),
|
||||
diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&staged_text,
|
||||
&[(1..2, "", "two\n", DiffHunkStatus::added_none())],
|
||||
@@ -2864,11 +2864,11 @@ async fn test_git_diff_base_change(
|
||||
local_unstaged_diff_b.read_with(cx_a, |diff, cx| {
|
||||
let buffer = buffer_local_b.read(cx);
|
||||
assert_eq!(
|
||||
diff.base_text_string().as_deref(),
|
||||
diff.base_text_string(cx).as_deref(),
|
||||
Some(new_staged_text.as_str())
|
||||
);
|
||||
assert_hunks(
|
||||
diff.hunks_in_row_range(0..4, buffer, cx),
|
||||
diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&new_staged_text,
|
||||
&[(2..3, "", "three\n", DiffHunkStatus::added_none())],
|
||||
@@ -2878,11 +2878,11 @@ async fn test_git_diff_base_change(
|
||||
remote_unstaged_diff_b.read_with(cx_b, |diff, cx| {
|
||||
let buffer = remote_buffer_b.read(cx);
|
||||
assert_eq!(
|
||||
diff.base_text_string().as_deref(),
|
||||
diff.base_text_string(cx).as_deref(),
|
||||
Some(new_staged_text.as_str())
|
||||
);
|
||||
assert_hunks(
|
||||
diff.hunks_in_row_range(0..4, buffer, cx),
|
||||
diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&new_staged_text,
|
||||
&[(2..3, "", "three\n", DiffHunkStatus::added_none())],
|
||||
@@ -5195,7 +5195,7 @@ async fn test_project_search(
|
||||
cx,
|
||||
)
|
||||
});
|
||||
while let Ok(result) = search_rx.recv().await {
|
||||
while let Ok(result) = search_rx.rx.recv().await {
|
||||
match result {
|
||||
SearchResult::Buffer { buffer, ranges } => {
|
||||
results.entry(buffer).or_insert(ranges);
|
||||
@@ -6745,8 +6745,13 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
// Split pane to the right
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.split(workspace::SplitDirection::Right, cx);
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
pane.split(
|
||||
workspace::SplitDirection::Right,
|
||||
workspace::SplitMode::default(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let right_pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
@@ -905,7 +905,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
drop(project);
|
||||
let search = cx.executor().spawn(async move {
|
||||
let mut results = HashMap::default();
|
||||
while let Ok(result) = search.recv().await {
|
||||
while let Ok(result) = search.rx.recv().await {
|
||||
if let SearchResult::Buffer { buffer, ranges } = result {
|
||||
results.entry(buffer).or_insert(ranges);
|
||||
}
|
||||
@@ -1377,7 +1377,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
.get_unstaged_diff(host_buffer.read(cx).remote_id(), cx)
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.base_text_string()
|
||||
.base_text_string(cx)
|
||||
});
|
||||
let guest_diff_base = guest_project.read_with(client_cx, |project, cx| {
|
||||
project
|
||||
@@ -1386,7 +1386,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
.get_unstaged_diff(guest_buffer.read(cx).remote_id(), cx)
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.base_text_string()
|
||||
.base_text_string(cx)
|
||||
});
|
||||
assert_eq!(
|
||||
guest_diff_base, host_diff_base,
|
||||
|
||||
@@ -855,8 +855,6 @@ async fn test_slow_adapter_startup_retries(
|
||||
|
||||
#[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);
|
||||
@@ -991,23 +989,19 @@ async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &m
|
||||
});
|
||||
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 worktree_store = project_a.read_with(cx_a, |project, _| project.worktree_store());
|
||||
|
||||
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));
|
||||
let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_ids[0], cx)
|
||||
});
|
||||
let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
|
||||
store.can_trust(&worktree_store, 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)
|
||||
});
|
||||
@@ -1054,8 +1048,8 @@ async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &m
|
||||
|
||||
trusted_worktrees.update(cx_a, |store, cx| {
|
||||
store.trust(
|
||||
&worktree_store,
|
||||
HashSet::from_iter([PathTrust::Worktree(worktree_ids[0])]),
|
||||
remote_host.clone(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
@@ -1080,25 +1074,29 @@ async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &m
|
||||
"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));
|
||||
let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_ids[0], cx)
|
||||
});
|
||||
let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
|
||||
store.can_trust(&worktree_store, 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(
|
||||
&worktree_store,
|
||||
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));
|
||||
let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_ids[0], cx)
|
||||
});
|
||||
let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_ids[1], cx)
|
||||
});
|
||||
assert!(can_trust_a, "project_a should remain trusted");
|
||||
assert!(can_trust_b, "project_b should now be trusted");
|
||||
|
||||
|
||||
@@ -31,9 +31,9 @@ use smallvec::SmallVec;
|
||||
use std::{mem, sync::Arc};
|
||||
use theme::{ActiveTheme, ThemeSettings};
|
||||
use ui::{
|
||||
Avatar, AvatarAvailabilityIndicator, Button, Color, ContextMenu, Facepile, HighlightedLabel,
|
||||
Icon, IconButton, IconName, IconSize, Indicator, Label, ListHeader, ListItem, Tab, Tooltip,
|
||||
prelude::*, tooltip_container,
|
||||
Avatar, AvatarAvailabilityIndicator, Button, Color, ContextMenu, CopyButton, Facepile,
|
||||
HighlightedLabel, Icon, IconButton, IconName, IconSize, Indicator, Label, ListHeader, ListItem,
|
||||
Tab, Tooltip, prelude::*, tooltip_container,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt, maybe};
|
||||
use workspace::{
|
||||
@@ -2527,16 +2527,9 @@ impl CollabPanel {
|
||||
|
||||
let button = match section {
|
||||
Section::ActiveCall => channel_link.map(|channel_link| {
|
||||
let channel_link_copy = channel_link;
|
||||
IconButton::new("channel-link", IconName::Copy)
|
||||
.icon_size(IconSize::Small)
|
||||
.size(ButtonSize::None)
|
||||
CopyButton::new(channel_link)
|
||||
.visible_on_hover("section-header")
|
||||
.on_click(move |_, _, cx| {
|
||||
let item = ClipboardItem::new_string(channel_link_copy.clone());
|
||||
cx.write_to_clipboard(item)
|
||||
})
|
||||
.tooltip(Tooltip::text("Copy channel link"))
|
||||
.tooltip_label("Copy Channel Link")
|
||||
.into_any_element()
|
||||
}),
|
||||
Section::Contacts => Some(
|
||||
|
||||
45
crates/component_preview/Cargo.toml
Normal file
45
crates/component_preview/Cargo.toml
Normal file
@@ -0,0 +1,45 @@
|
||||
[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
crates/component_preview/LICENSE-GPL
Symbolic link
1
crates/component_preview/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
LICENSE-GPL
|
||||
18
crates/component_preview/examples/component_preview.rs
Normal file
18
crates/component_preview/examples/component_preview.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
//! 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,7 +1,4 @@
|
||||
//! # Component Preview
|
||||
//!
|
||||
//! A view for exploring Zed components.
|
||||
|
||||
mod component_preview_example;
|
||||
mod persistence;
|
||||
|
||||
use client::UserStore;
|
||||
@@ -11,18 +8,21 @@ use gpui::{
|
||||
App, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity, Window, list, prelude::*,
|
||||
};
|
||||
use gpui::{ListState, ScrollHandle, ScrollStrategy, UniformListScrollHandle};
|
||||
use languages::LanguageRegistry;
|
||||
use language::LanguageRegistry;
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use persistence::COMPONENT_PREVIEW_DB;
|
||||
use project::Project;
|
||||
use std::{iter::Iterator, ops::Range, sync::Arc};
|
||||
use ui::{ButtonLike, Divider, HighlightedLabel, ListItem, ListSubHeader, Tooltip, prelude::*};
|
||||
use ui_input::InputField;
|
||||
use workspace::AppState;
|
||||
use workspace::{
|
||||
AppState, Item, ItemId, SerializableItem, Workspace, WorkspaceId, delete_unloaded_items,
|
||||
item::ItemEvent,
|
||||
Item, ItemId, SerializableItem, Workspace, WorkspaceId, delete_unloaded_items, item::ItemEvent,
|
||||
};
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub use component_preview_example::*;
|
||||
|
||||
pub fn init(app_state: Arc<AppState>, cx: &mut App) {
|
||||
workspace::register_serializable_item::<ComponentPreview>(cx);
|
||||
|
||||
145
crates/component_preview/src/component_preview_example.rs
Normal file
145
crates/component_preview/src/component_preview_example.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
/// 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);
|
||||
});
|
||||
}
|
||||
@@ -34,7 +34,7 @@ impl StdioTransport {
|
||||
let shell = cx.update(|cx| TerminalSettings::get(None, cx).shell.clone())?;
|
||||
let builder = ShellBuilder::new(&shell, cfg!(windows)).non_interactive();
|
||||
let mut command =
|
||||
builder.build_command(Some(binary.executable.display().to_string()), &binary.args);
|
||||
builder.build_smol_command(Some(binary.executable.display().to_string()), &binary.args);
|
||||
|
||||
command
|
||||
.envs(binary.env.unwrap_or_default())
|
||||
|
||||
@@ -24,7 +24,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
use task::TcpArgumentsTemplate;
|
||||
use util::ConnectionResult;
|
||||
use util::{ConnectionResult, ResultExt, process::Child};
|
||||
|
||||
use crate::{
|
||||
adapters::{DebugAdapterBinary, TcpArguments},
|
||||
@@ -528,7 +528,7 @@ impl TcpTransport {
|
||||
command.args(&binary.arguments);
|
||||
command.envs(&binary.envs);
|
||||
|
||||
let mut p = Child::spawn(command, Stdio::null())
|
||||
let mut p = Child::spawn(command, Stdio::null(), Stdio::piped(), Stdio::piped())
|
||||
.with_context(|| "failed to start debug adapter.")?;
|
||||
|
||||
stdout_task = p.stdout.take().map(|stdout| {
|
||||
@@ -582,7 +582,7 @@ impl Transport for TcpTransport {
|
||||
|
||||
fn kill(&mut self) {
|
||||
if let Some(process) = &mut *self.process.lock() {
|
||||
process.kill();
|
||||
process.kill().log_err();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -647,7 +647,7 @@ impl Transport for TcpTransport {
|
||||
impl Drop for TcpTransport {
|
||||
fn drop(&mut self) {
|
||||
if let Some(mut p) = self.process.lock().take() {
|
||||
p.kill()
|
||||
p.kill().log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -678,7 +678,7 @@ impl StdioTransport {
|
||||
command.args(&binary.arguments);
|
||||
command.envs(&binary.envs);
|
||||
|
||||
let mut process = Child::spawn(command, Stdio::piped())?;
|
||||
let mut process = Child::spawn(command, Stdio::piped(), Stdio::piped(), Stdio::piped())?;
|
||||
|
||||
let _stderr_task = process.stderr.take().map(|stderr| {
|
||||
cx.background_spawn(TransportDelegate::handle_adapter_log(
|
||||
@@ -703,7 +703,7 @@ impl Transport for StdioTransport {
|
||||
}
|
||||
|
||||
fn kill(&mut self) {
|
||||
self.process.lock().kill();
|
||||
self.process.lock().kill().log_err();
|
||||
}
|
||||
|
||||
fn connect(
|
||||
@@ -731,7 +731,7 @@ impl Transport for StdioTransport {
|
||||
|
||||
impl Drop for StdioTransport {
|
||||
fn drop(&mut self) {
|
||||
self.process.lock().kill();
|
||||
self.process.lock().kill().log_err();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1024,68 +1024,3 @@ impl Transport for FakeTransport {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
struct Child {
|
||||
process: smol::process::Child,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for Child {
|
||||
type Target = smol::process::Child;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.process
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for Child {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.process
|
||||
}
|
||||
}
|
||||
|
||||
impl Child {
|
||||
fn into_inner(self) -> smol::process::Child {
|
||||
self.process
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn spawn(mut command: std::process::Command, stdin: Stdio) -> Result<Self> {
|
||||
util::set_pre_exec_to_start_new_session(&mut command);
|
||||
let mut command = smol::process::Command::from(command);
|
||||
let process = command
|
||||
.stdin(stdin)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.with_context(|| format!("failed to spawn command `{command:?}`",))?;
|
||||
Ok(Self { process })
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn spawn(command: std::process::Command, stdin: Stdio) -> Result<Self> {
|
||||
// TODO(windows): create a job object and add the child process handle to it,
|
||||
// see https://learn.microsoft.com/en-us/windows/win32/procthread/job-objects
|
||||
let mut command = smol::process::Command::from(command);
|
||||
let process = command
|
||||
.stdin(stdin)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.with_context(|| format!("failed to spawn command `{command:?}`",))?;
|
||||
Ok(Self { process })
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn kill(&mut self) {
|
||||
let pid = self.process.id();
|
||||
unsafe {
|
||||
libc::killpg(pid as i32, libc::SIGKILL);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn kill(&mut self) {
|
||||
// TODO(windows): terminate the job object in kill
|
||||
let _ = self.process.kill();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1579,8 +1579,10 @@ impl Panel for DebugPanel {
|
||||
Some(proto::PanelId::DebugPanel)
|
||||
}
|
||||
|
||||
fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
|
||||
Some(IconName::Debug)
|
||||
fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
|
||||
DebuggerSettings::get_global(cx)
|
||||
.button
|
||||
.then_some(IconName::Debug)
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
|
||||
|
||||
@@ -103,8 +103,9 @@ impl Model {
|
||||
|
||||
pub fn max_output_tokens(&self) -> Option<u64> {
|
||||
match self {
|
||||
Self::Chat => Some(8_192),
|
||||
Self::Reasoner => Some(64_000),
|
||||
// Their API treats this max against the context window, which means we hit the limit a lot
|
||||
// Using the default value of None in the API instead
|
||||
Self::Chat | Self::Reasoner => None,
|
||||
Self::Custom {
|
||||
max_output_tokens, ..
|
||||
} => *max_output_tokens,
|
||||
|
||||
@@ -7,8 +7,6 @@ license = "GPL-3.0-or-later"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
command_palette.workspace = true
|
||||
gpui.workspace = true
|
||||
# We are specifically pinning this version of mdbook, as later versions introduce issues with double-nested subdirectories.
|
||||
# Ask @maxdeviant about this before bumping.
|
||||
mdbook = "= 0.4.40"
|
||||
@@ -17,7 +15,6 @@ serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
util.workspace = true
|
||||
zed.workspace = true
|
||||
zlog.workspace = true
|
||||
task.workspace = true
|
||||
theme.workspace = true
|
||||
@@ -27,4 +24,4 @@ workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "docs_preprocessor"
|
||||
path = "src/main.rs"
|
||||
path = "src/main.rs"
|
||||
@@ -22,16 +22,13 @@ static KEYMAP_WINDOWS: LazyLock<KeymapFile> = LazyLock::new(|| {
|
||||
load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap")
|
||||
});
|
||||
|
||||
static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(dump_all_gpui_actions);
|
||||
static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(load_all_actions);
|
||||
|
||||
const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->";
|
||||
|
||||
fn main() -> Result<()> {
|
||||
zlog::init();
|
||||
zlog::init_output_stderr();
|
||||
// call a zed:: function so everything in `zed` crate is linked and
|
||||
// all actions in the actual app are registered
|
||||
zed::stdout_is_a_pty();
|
||||
let args = std::env::args().skip(1).collect::<Vec<_>>();
|
||||
|
||||
match args.get(0).map(String::as_str) {
|
||||
@@ -72,8 +69,8 @@ enum PreprocessorError {
|
||||
impl PreprocessorError {
|
||||
fn new_for_not_found_action(action_name: String) -> Self {
|
||||
for action in &*ALL_ACTIONS {
|
||||
for alias in action.deprecated_aliases {
|
||||
if alias == &action_name {
|
||||
for alias in &action.deprecated_aliases {
|
||||
if alias == action_name.as_str() {
|
||||
return PreprocessorError::DeprecatedActionUsed {
|
||||
used: action_name,
|
||||
should_be: action.name.to_string(),
|
||||
@@ -214,7 +211,7 @@ fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<Prepr
|
||||
chapter.content = regex
|
||||
.replace_all(&chapter.content, |caps: ®ex::Captures| {
|
||||
let action = caps[1].trim();
|
||||
if find_action_by_name(action).is_none() {
|
||||
if is_missing_action(action) {
|
||||
errors.insert(PreprocessorError::new_for_not_found_action(
|
||||
action.to_string(),
|
||||
));
|
||||
@@ -244,10 +241,12 @@ fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet<Preproces
|
||||
.replace_all(&chapter.content, |caps: ®ex::Captures| {
|
||||
let name = caps[1].trim();
|
||||
let Some(action) = find_action_by_name(name) else {
|
||||
errors.insert(PreprocessorError::new_for_not_found_action(
|
||||
name.to_string(),
|
||||
));
|
||||
return String::new();
|
||||
if actions_available() {
|
||||
errors.insert(PreprocessorError::new_for_not_found_action(
|
||||
name.to_string(),
|
||||
));
|
||||
}
|
||||
return format!("<code class=\"hljs\">{}</code>", name);
|
||||
};
|
||||
format!("<code class=\"hljs\">{}</code>", &action.human_name)
|
||||
})
|
||||
@@ -257,11 +256,19 @@ fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet<Preproces
|
||||
|
||||
fn find_action_by_name(name: &str) -> Option<&ActionDef> {
|
||||
ALL_ACTIONS
|
||||
.binary_search_by(|action| action.name.cmp(name))
|
||||
.binary_search_by(|action| action.name.as_str().cmp(name))
|
||||
.ok()
|
||||
.map(|index| &ALL_ACTIONS[index])
|
||||
}
|
||||
|
||||
fn actions_available() -> bool {
|
||||
!ALL_ACTIONS.is_empty()
|
||||
}
|
||||
|
||||
fn is_missing_action(name: &str) -> bool {
|
||||
actions_available() && find_action_by_name(name).is_none()
|
||||
}
|
||||
|
||||
fn find_binding(os: &str, action: &str) -> Option<String> {
|
||||
let keymap = match os {
|
||||
"macos" => &KEYMAP_MACOS,
|
||||
@@ -384,18 +391,13 @@ fn template_and_validate_json_snippets(book: &mut Book, errors: &mut HashSet<Pre
|
||||
let keymap = settings::KeymapFile::parse(&snippet_json_fixed)
|
||||
.context("Failed to parse keymap JSON")?;
|
||||
for section in keymap.sections() {
|
||||
for (keystrokes, action) in section.bindings() {
|
||||
keystrokes
|
||||
.split_whitespace()
|
||||
.map(|source| gpui::Keystroke::parse(source))
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.context("Failed to parse keystroke")?;
|
||||
for (_keystrokes, action) in section.bindings() {
|
||||
if let Some((action_name, _)) = settings::KeymapFile::parse_action(action)
|
||||
.map_err(|err| anyhow::format_err!(err))
|
||||
.context("Failed to parse action")?
|
||||
{
|
||||
anyhow::ensure!(
|
||||
find_action_by_name(action_name).is_some(),
|
||||
!is_missing_action(action_name),
|
||||
"Action not found: {}",
|
||||
action_name
|
||||
);
|
||||
@@ -491,27 +493,35 @@ where
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
struct ActionDef {
|
||||
name: &'static str,
|
||||
name: String,
|
||||
human_name: String,
|
||||
deprecated_aliases: &'static [&'static str],
|
||||
docs: Option<&'static str>,
|
||||
deprecated_aliases: Vec<String>,
|
||||
#[serde(rename = "documentation")]
|
||||
docs: Option<String>,
|
||||
}
|
||||
|
||||
fn dump_all_gpui_actions() -> Vec<ActionDef> {
|
||||
let mut actions = gpui::generate_list_of_all_registered_actions()
|
||||
.map(|action| ActionDef {
|
||||
name: action.name,
|
||||
human_name: command_palette::humanize_action_name(action.name),
|
||||
deprecated_aliases: action.deprecated_aliases,
|
||||
docs: action.documentation,
|
||||
})
|
||||
.collect::<Vec<ActionDef>>();
|
||||
|
||||
actions.sort_by_key(|a| a.name);
|
||||
|
||||
actions
|
||||
fn load_all_actions() -> Vec<ActionDef> {
|
||||
let asset_path = concat!(env!("CARGO_MANIFEST_DIR"), "/actions.json");
|
||||
match std::fs::read_to_string(asset_path) {
|
||||
Ok(content) => {
|
||||
let mut actions: Vec<ActionDef> =
|
||||
serde_json::from_str(&content).expect("Failed to parse actions.json");
|
||||
actions.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
actions
|
||||
}
|
||||
Err(err) => {
|
||||
if std::env::var("CI").is_ok() {
|
||||
panic!("actions.json not found at {}: {}", asset_path, err);
|
||||
}
|
||||
eprintln!(
|
||||
"Warning: actions.json not found, action validation will be skipped: {}",
|
||||
err
|
||||
);
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_postprocessing() -> Result<()> {
|
||||
@@ -647,7 +657,7 @@ fn generate_big_table_of_actions() -> String {
|
||||
let mut output = String::new();
|
||||
|
||||
let mut actions_sorted = actions.iter().collect::<Vec<_>>();
|
||||
actions_sorted.sort_by_key(|a| a.name);
|
||||
actions_sorted.sort_by_key(|a| a.name.as_str());
|
||||
|
||||
// Start the definition list with custom styling for better spacing
|
||||
output.push_str("<dl style=\"line-height: 1.8;\">\n");
|
||||
@@ -664,7 +674,7 @@ fn generate_big_table_of_actions() -> String {
|
||||
output.push_str("<dd style=\"margin-left: 2em; margin-bottom: 1em;\">\n");
|
||||
|
||||
// Add the description, escaping HTML if needed
|
||||
if let Some(description) = action.docs {
|
||||
if let Some(description) = action.docs.as_ref() {
|
||||
output.push_str(
|
||||
&description
|
||||
.replace("&", "&")
|
||||
@@ -674,7 +684,7 @@ fn generate_big_table_of_actions() -> String {
|
||||
output.push_str("<br>\n");
|
||||
}
|
||||
output.push_str("Keymap Name: <code>");
|
||||
output.push_str(action.name);
|
||||
output.push_str(&action.name);
|
||||
output.push_str("</code><br>\n");
|
||||
if !action.deprecated_aliases.is_empty() {
|
||||
output.push_str("Deprecated Alias(es): ");
|
||||
|
||||
@@ -19,6 +19,7 @@ ai_onboarding.workspace = true
|
||||
anyhow.workspace = true
|
||||
arrayvec.workspace = true
|
||||
brotli.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
@@ -52,7 +53,9 @@ settings.workspace = true
|
||||
strum.workspace = true
|
||||
telemetry.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
text.workspace = true
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
375
crates/edit_prediction/src/capture_example.rs
Normal file
375
crates/edit_prediction/src/capture_example.rs
Normal file
@@ -0,0 +1,375 @@
|
||||
use crate::{
|
||||
EditPredictionStore, StoredEvent,
|
||||
cursor_excerpt::editable_and_context_ranges_for_cursor_position, example_spec::ExampleSpec,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use buffer_diff::BufferDiffSnapshot;
|
||||
use collections::HashMap;
|
||||
use gpui::{App, Entity, Task};
|
||||
use language::{Buffer, ToPoint as _};
|
||||
use project::Project;
|
||||
use std::{collections::hash_map, fmt::Write as _, path::Path, sync::Arc};
|
||||
use text::{BufferSnapshot as TextBufferSnapshot, ToOffset as _};
|
||||
|
||||
pub fn capture_example(
|
||||
project: Entity<Project>,
|
||||
buffer: Entity<Buffer>,
|
||||
cursor_anchor: language::Anchor,
|
||||
last_event_is_expected_patch: bool,
|
||||
cx: &mut App,
|
||||
) -> Option<Task<Result<ExampleSpec>>> {
|
||||
let ep_store = EditPredictionStore::try_global(cx)?;
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let file = snapshot.file()?;
|
||||
let worktree_id = file.worktree_id(cx);
|
||||
let repository = project.read(cx).active_repository(cx)?;
|
||||
let repository_snapshot = repository.read(cx).snapshot();
|
||||
let worktree = project.read(cx).worktree_for_id(worktree_id, cx)?;
|
||||
let cursor_path = worktree.read(cx).root_name().join(file.path());
|
||||
if worktree.read(cx).abs_path() != repository_snapshot.work_directory_abs_path {
|
||||
return None;
|
||||
}
|
||||
|
||||
let repository_url = repository_snapshot
|
||||
.remote_origin_url
|
||||
.clone()
|
||||
.or_else(|| repository_snapshot.remote_upstream_url.clone())?;
|
||||
let revision = repository_snapshot.head_commit.as_ref()?.sha.to_string();
|
||||
|
||||
let mut events = ep_store.update(cx, |store, cx| {
|
||||
store.edit_history_for_project_with_pause_split_last_event(&project, cx)
|
||||
});
|
||||
|
||||
let git_store = project.read(cx).git_store().clone();
|
||||
|
||||
Some(cx.spawn(async move |mut cx| {
|
||||
let snapshots_by_path = collect_snapshots(&project, &git_store, &events, &mut cx).await?;
|
||||
let cursor_excerpt = cx
|
||||
.background_executor()
|
||||
.spawn(async move { compute_cursor_excerpt(&snapshot, cursor_anchor) })
|
||||
.await;
|
||||
let uncommitted_diff = cx
|
||||
.background_executor()
|
||||
.spawn(async move { compute_uncommitted_diff(snapshots_by_path) })
|
||||
.await;
|
||||
|
||||
let mut edit_history = String::new();
|
||||
let mut expected_patch = String::new();
|
||||
if last_event_is_expected_patch {
|
||||
if let Some(stored_event) = events.pop() {
|
||||
zeta_prompt::write_event(&mut expected_patch, &stored_event.event);
|
||||
}
|
||||
}
|
||||
|
||||
for stored_event in &events {
|
||||
zeta_prompt::write_event(&mut edit_history, &stored_event.event);
|
||||
if !edit_history.ends_with('\n') {
|
||||
edit_history.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
let name = generate_timestamp_name();
|
||||
|
||||
Ok(ExampleSpec {
|
||||
name,
|
||||
repository_url,
|
||||
revision,
|
||||
uncommitted_diff,
|
||||
cursor_path: cursor_path.as_std_path().into(),
|
||||
cursor_position: cursor_excerpt,
|
||||
edit_history,
|
||||
expected_patch,
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn compute_cursor_excerpt(
|
||||
snapshot: &language::BufferSnapshot,
|
||||
cursor_anchor: language::Anchor,
|
||||
) -> String {
|
||||
let cursor_point = cursor_anchor.to_point(snapshot);
|
||||
let (_editable_range, context_range) =
|
||||
editable_and_context_ranges_for_cursor_position(cursor_point, snapshot, 100, 50);
|
||||
|
||||
let context_start_offset = context_range.start.to_offset(snapshot);
|
||||
let cursor_offset = cursor_anchor.to_offset(snapshot);
|
||||
let cursor_offset_in_excerpt = cursor_offset.saturating_sub(context_start_offset);
|
||||
let mut excerpt = snapshot.text_for_range(context_range).collect::<String>();
|
||||
if cursor_offset_in_excerpt <= excerpt.len() {
|
||||
excerpt.insert_str(cursor_offset_in_excerpt, zeta_prompt::CURSOR_MARKER);
|
||||
}
|
||||
excerpt
|
||||
}
|
||||
|
||||
async fn collect_snapshots(
|
||||
project: &Entity<Project>,
|
||||
git_store: &Entity<project::git_store::GitStore>,
|
||||
events: &[StoredEvent],
|
||||
cx: &mut gpui::AsyncApp,
|
||||
) -> Result<HashMap<Arc<Path>, (TextBufferSnapshot, BufferDiffSnapshot)>> {
|
||||
let mut snapshots_by_path = HashMap::default();
|
||||
for stored_event in events {
|
||||
let zeta_prompt::Event::BufferChange { path, .. } = stored_event.event.as_ref();
|
||||
if let Some((project_path, full_path)) = project.read_with(cx, |project, cx| {
|
||||
let project_path = project.find_project_path(path, cx)?;
|
||||
let full_path = project
|
||||
.worktree_for_id(project_path.worktree_id, cx)?
|
||||
.read(cx)
|
||||
.root_name()
|
||||
.join(&project_path.path)
|
||||
.as_std_path()
|
||||
.into();
|
||||
Some((project_path, full_path))
|
||||
})? {
|
||||
if let hash_map::Entry::Vacant(entry) = snapshots_by_path.entry(full_path) {
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer(project_path.clone(), cx)
|
||||
})?
|
||||
.await?;
|
||||
let diff = git_store
|
||||
.update(cx, |git_store, cx| {
|
||||
git_store.open_uncommitted_diff(buffer.clone(), cx)
|
||||
})?
|
||||
.await?;
|
||||
let diff_snapshot = diff.update(cx, |diff, cx| diff.snapshot(cx))?;
|
||||
entry.insert((stored_event.old_snapshot.clone(), diff_snapshot));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(snapshots_by_path)
|
||||
}
|
||||
|
||||
fn compute_uncommitted_diff(
|
||||
snapshots_by_path: HashMap<Arc<Path>, (TextBufferSnapshot, BufferDiffSnapshot)>,
|
||||
) -> String {
|
||||
let mut uncommitted_diff = String::new();
|
||||
for (full_path, (before_text, diff_snapshot)) in snapshots_by_path {
|
||||
if let Some(head_text) = &diff_snapshot.base_text_string() {
|
||||
let file_diff = language::unified_diff(head_text, &before_text.text());
|
||||
if !file_diff.is_empty() {
|
||||
let path_str = full_path.to_string_lossy();
|
||||
writeln!(uncommitted_diff, "--- a/{path_str}").ok();
|
||||
writeln!(uncommitted_diff, "+++ b/{path_str}").ok();
|
||||
uncommitted_diff.push_str(&file_diff);
|
||||
if !uncommitted_diff.ends_with('\n') {
|
||||
uncommitted_diff.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
uncommitted_diff
|
||||
}
|
||||
|
||||
fn generate_timestamp_name() -> String {
|
||||
let format = time::format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]");
|
||||
match format {
|
||||
Ok(format) => {
|
||||
let now = time::OffsetDateTime::now_local()
|
||||
.unwrap_or_else(|_| time::OffsetDateTime::now_utc());
|
||||
now.format(&format)
|
||||
.unwrap_or_else(|_| "unknown-time".to_string())
|
||||
}
|
||||
Err(_) => "unknown-time".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use client::{Client, UserStore};
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::{AppContext as _, TestAppContext, http_client::FakeHttpClient};
|
||||
use indoc::indoc;
|
||||
use language::{Anchor, Point};
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::path::Path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_capture_example(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
|
||||
let committed_contents = indoc! {"
|
||||
fn main() {
|
||||
one();
|
||||
two();
|
||||
three();
|
||||
four();
|
||||
five();
|
||||
six();
|
||||
seven();
|
||||
eight();
|
||||
nine();
|
||||
}
|
||||
"};
|
||||
|
||||
let disk_contents = indoc! {"
|
||||
fn main() {
|
||||
// comment 1
|
||||
one();
|
||||
two();
|
||||
three();
|
||||
four();
|
||||
five();
|
||||
six();
|
||||
seven();
|
||||
eight();
|
||||
// comment 2
|
||||
nine();
|
||||
}
|
||||
"};
|
||||
|
||||
fs.insert_tree(
|
||||
"/project",
|
||||
json!({
|
||||
".git": {},
|
||||
"src": {
|
||||
"main.rs": disk_contents,
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
fs.set_head_for_repo(
|
||||
Path::new("/project/.git"),
|
||||
&[("src/main.rs", committed_contents.to_string())],
|
||||
"abc123def456",
|
||||
);
|
||||
fs.set_remote_for_repo(
|
||||
Path::new("/project/.git"),
|
||||
"origin",
|
||||
"https://github.com/test/repo.git",
|
||||
);
|
||||
|
||||
let project = Project::test(fs.clone(), ["/project".as_ref()], cx).await;
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/project/src/main.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let ep_store = cx.read(|cx| EditPredictionStore::try_global(cx).unwrap());
|
||||
ep_store.update(cx, |ep_store, cx| {
|
||||
ep_store.register_buffer(&buffer, &project, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
let point = Point::new(6, 0);
|
||||
buffer.edit([(point..point, " // comment 3\n")], None, cx);
|
||||
let point = Point::new(4, 0);
|
||||
buffer.edit([(point..point, " // comment 4\n")], None, cx);
|
||||
|
||||
pretty_assertions::assert_eq!(
|
||||
buffer.text(),
|
||||
indoc! {"
|
||||
fn main() {
|
||||
// comment 1
|
||||
one();
|
||||
two();
|
||||
// comment 4
|
||||
three();
|
||||
four();
|
||||
// comment 3
|
||||
five();
|
||||
six();
|
||||
seven();
|
||||
eight();
|
||||
// comment 2
|
||||
nine();
|
||||
}
|
||||
"}
|
||||
);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
let mut example = cx
|
||||
.update(|cx| {
|
||||
capture_example(project.clone(), buffer.clone(), Anchor::MIN, false, cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
example.name = "test".to_string();
|
||||
|
||||
pretty_assertions::assert_eq!(
|
||||
example,
|
||||
ExampleSpec {
|
||||
name: "test".to_string(),
|
||||
repository_url: "https://github.com/test/repo.git".to_string(),
|
||||
revision: "abc123def456".to_string(),
|
||||
uncommitted_diff: indoc! {"
|
||||
--- a/project/src/main.rs
|
||||
+++ b/project/src/main.rs
|
||||
@@ -1,4 +1,5 @@
|
||||
fn main() {
|
||||
+ // comment 1
|
||||
one();
|
||||
two();
|
||||
three();
|
||||
@@ -7,5 +8,6 @@
|
||||
six();
|
||||
seven();
|
||||
eight();
|
||||
+ // comment 2
|
||||
nine();
|
||||
}
|
||||
"}
|
||||
.to_string(),
|
||||
cursor_path: Path::new("project/src/main.rs").into(),
|
||||
cursor_position: indoc! {"
|
||||
<|user_cursor|>fn main() {
|
||||
// comment 1
|
||||
one();
|
||||
two();
|
||||
// comment 4
|
||||
three();
|
||||
four();
|
||||
// comment 3
|
||||
five();
|
||||
six();
|
||||
seven();
|
||||
eight();
|
||||
// comment 2
|
||||
nine();
|
||||
}
|
||||
"}
|
||||
.to_string(),
|
||||
edit_history: indoc! {"
|
||||
--- a/project/src/main.rs
|
||||
+++ b/project/src/main.rs
|
||||
@@ -2,8 +2,10 @@
|
||||
// comment 1
|
||||
one();
|
||||
two();
|
||||
+ // comment 4
|
||||
three();
|
||||
four();
|
||||
+ // comment 3
|
||||
five();
|
||||
six();
|
||||
seven();
|
||||
"}
|
||||
.to_string(),
|
||||
expected_patch: "".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
zlog::init_test();
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(Arc::new(FakeSystemClock::new()), http_client, cx);
|
||||
language_model::init(client.clone(), cx);
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
EditPredictionStore::global(&client, &user_store, cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ use semver::Version;
|
||||
use serde::de::DeserializeOwned;
|
||||
use settings::{EditPredictionProvider, SettingsStore, update_settings_file};
|
||||
use std::collections::{VecDeque, hash_map};
|
||||
use text::Edit;
|
||||
use workspace::Workspace;
|
||||
|
||||
use std::ops::Range;
|
||||
@@ -57,9 +58,9 @@ pub mod open_ai_response;
|
||||
mod prediction;
|
||||
pub mod sweep_ai;
|
||||
|
||||
#[cfg(any(test, feature = "test-support", feature = "cli-support"))]
|
||||
pub mod udiff;
|
||||
|
||||
mod capture_example;
|
||||
mod zed_edit_prediction_delegate;
|
||||
pub mod zeta1;
|
||||
pub mod zeta2;
|
||||
@@ -74,6 +75,7 @@ pub use crate::prediction::EditPrediction;
|
||||
pub use crate::prediction::EditPredictionId;
|
||||
use crate::prediction::EditPredictionResult;
|
||||
pub use crate::sweep_ai::SweepAi;
|
||||
pub use capture_example::capture_example;
|
||||
pub use language_model::ApiKeyState;
|
||||
pub use telemetry_events::EditPredictionRating;
|
||||
pub use zed_edit_prediction_delegate::ZedEditPredictionDelegate;
|
||||
@@ -231,8 +233,15 @@ pub struct EditPredictionFinishedDebugEvent {
|
||||
|
||||
pub type RequestDebugInfo = predict_edits_v3::DebugInfo;
|
||||
|
||||
/// An event with associated metadata for reconstructing buffer state.
|
||||
#[derive(Clone)]
|
||||
pub struct StoredEvent {
|
||||
pub event: Arc<zeta_prompt::Event>,
|
||||
pub old_snapshot: TextBufferSnapshot,
|
||||
}
|
||||
|
||||
struct ProjectState {
|
||||
events: VecDeque<Arc<zeta_prompt::Event>>,
|
||||
events: VecDeque<StoredEvent>,
|
||||
last_event: Option<LastEvent>,
|
||||
recent_paths: VecDeque<ProjectPath>,
|
||||
registered_buffers: HashMap<gpui::EntityId, RegisteredBuffer>,
|
||||
@@ -248,7 +257,7 @@ struct ProjectState {
|
||||
}
|
||||
|
||||
impl ProjectState {
|
||||
pub fn events(&self, cx: &App) -> Vec<Arc<zeta_prompt::Event>> {
|
||||
pub fn events(&self, cx: &App) -> Vec<StoredEvent> {
|
||||
self.events
|
||||
.iter()
|
||||
.cloned()
|
||||
@@ -260,7 +269,7 @@ impl ProjectState {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn events_split_by_pause(&self, cx: &App) -> Vec<Arc<zeta_prompt::Event>> {
|
||||
pub fn events_split_by_pause(&self, cx: &App) -> Vec<StoredEvent> {
|
||||
self.events
|
||||
.iter()
|
||||
.cloned()
|
||||
@@ -415,7 +424,7 @@ impl LastEvent {
|
||||
&self,
|
||||
license_detection_watchers: &HashMap<WorktreeId, Rc<LicenseDetectionWatcher>>,
|
||||
cx: &App,
|
||||
) -> Option<Arc<zeta_prompt::Event>> {
|
||||
) -> Option<StoredEvent> {
|
||||
let path = buffer_path_with_id_fallback(self.new_file.as_ref(), &self.new_snapshot, cx);
|
||||
let old_path = buffer_path_with_id_fallback(self.old_file.as_ref(), &self.old_snapshot, cx);
|
||||
|
||||
@@ -430,19 +439,22 @@ impl LastEvent {
|
||||
})
|
||||
});
|
||||
|
||||
let diff = language::unified_diff(&self.old_snapshot.text(), &self.new_snapshot.text());
|
||||
let diff = compute_diff_between_snapshots(&self.old_snapshot, &self.new_snapshot)?;
|
||||
|
||||
if path == old_path && diff.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Arc::new(zeta_prompt::Event::BufferChange {
|
||||
old_path,
|
||||
path,
|
||||
diff,
|
||||
in_open_source_repo,
|
||||
// TODO: Actually detect if this edit was predicted or not
|
||||
predicted: false,
|
||||
}))
|
||||
Some(StoredEvent {
|
||||
event: Arc::new(zeta_prompt::Event::BufferChange {
|
||||
old_path,
|
||||
path,
|
||||
diff,
|
||||
in_open_source_repo,
|
||||
// TODO: Actually detect if this edit was predicted or not
|
||||
predicted: false,
|
||||
}),
|
||||
old_snapshot: self.old_snapshot.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,6 +487,52 @@ impl LastEvent {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn compute_diff_between_snapshots(
|
||||
old_snapshot: &TextBufferSnapshot,
|
||||
new_snapshot: &TextBufferSnapshot,
|
||||
) -> Option<String> {
|
||||
let edits: Vec<Edit<usize>> = new_snapshot
|
||||
.edits_since::<usize>(&old_snapshot.version)
|
||||
.collect();
|
||||
|
||||
let (first_edit, last_edit) = edits.first().zip(edits.last())?;
|
||||
|
||||
let old_start_point = old_snapshot.offset_to_point(first_edit.old.start);
|
||||
let old_end_point = old_snapshot.offset_to_point(last_edit.old.end);
|
||||
let new_start_point = new_snapshot.offset_to_point(first_edit.new.start);
|
||||
let new_end_point = new_snapshot.offset_to_point(last_edit.new.end);
|
||||
|
||||
const CONTEXT_LINES: u32 = 3;
|
||||
|
||||
let old_context_start_row = old_start_point.row.saturating_sub(CONTEXT_LINES);
|
||||
let new_context_start_row = new_start_point.row.saturating_sub(CONTEXT_LINES);
|
||||
let old_context_end_row =
|
||||
(old_end_point.row + 1 + CONTEXT_LINES).min(old_snapshot.max_point().row);
|
||||
let new_context_end_row =
|
||||
(new_end_point.row + 1 + CONTEXT_LINES).min(new_snapshot.max_point().row);
|
||||
|
||||
let old_start_line_offset = old_snapshot.point_to_offset(Point::new(old_context_start_row, 0));
|
||||
let new_start_line_offset = new_snapshot.point_to_offset(Point::new(new_context_start_row, 0));
|
||||
let old_end_line_offset = old_snapshot
|
||||
.point_to_offset(Point::new(old_context_end_row + 1, 0).min(old_snapshot.max_point()));
|
||||
let new_end_line_offset = new_snapshot
|
||||
.point_to_offset(Point::new(new_context_end_row + 1, 0).min(new_snapshot.max_point()));
|
||||
let old_edit_range = old_start_line_offset..old_end_line_offset;
|
||||
let new_edit_range = new_start_line_offset..new_end_line_offset;
|
||||
|
||||
let old_region_text: String = old_snapshot.text_for_range(old_edit_range).collect();
|
||||
let new_region_text: String = new_snapshot.text_for_range(new_edit_range).collect();
|
||||
|
||||
let diff = language::unified_diff_with_offsets(
|
||||
&old_region_text,
|
||||
&new_region_text,
|
||||
old_context_start_row,
|
||||
new_context_start_row,
|
||||
);
|
||||
|
||||
Some(diff)
|
||||
}
|
||||
|
||||
fn buffer_path_with_id_fallback(
|
||||
file: Option<&Arc<dyn File>>,
|
||||
snapshot: &TextBufferSnapshot,
|
||||
@@ -643,7 +701,7 @@ impl EditPredictionStore {
|
||||
&self,
|
||||
project: &Entity<Project>,
|
||||
cx: &App,
|
||||
) -> Vec<Arc<zeta_prompt::Event>> {
|
||||
) -> Vec<StoredEvent> {
|
||||
self.projects
|
||||
.get(&project.entity_id())
|
||||
.map(|project_state| project_state.events(cx))
|
||||
@@ -654,7 +712,7 @@ impl EditPredictionStore {
|
||||
&self,
|
||||
project: &Entity<Project>,
|
||||
cx: &App,
|
||||
) -> Vec<Arc<zeta_prompt::Event>> {
|
||||
) -> Vec<StoredEvent> {
|
||||
self.projects
|
||||
.get(&project.entity_id())
|
||||
.map(|project_state| project_state.events_split_by_pause(cx))
|
||||
@@ -1536,8 +1594,10 @@ impl EditPredictionStore {
|
||||
|
||||
self.get_or_init_project(&project, cx);
|
||||
let project_state = self.projects.get(&project.entity_id()).unwrap();
|
||||
let events = project_state.events(cx);
|
||||
let has_events = !events.is_empty();
|
||||
let stored_events = project_state.events(cx);
|
||||
let has_events = !stored_events.is_empty();
|
||||
let events: Vec<Arc<zeta_prompt::Event>> =
|
||||
stored_events.into_iter().map(|e| e.event).collect();
|
||||
let debug_tx = project_state.debug_tx.clone();
|
||||
|
||||
let snapshot = active_buffer.read(cx).snapshot();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::*;
|
||||
use crate::{udiff::apply_diff_to_string, zeta1::MAX_EVENT_TOKENS};
|
||||
use crate::{compute_diff_between_snapshots, udiff::apply_diff_to_string, zeta1::MAX_EVENT_TOKENS};
|
||||
use client::{UserStore, test::FakeServer};
|
||||
use clock::{FakeSystemClock, ReplicaId};
|
||||
use cloud_api_types::{CreateLlmTokenResponse, LlmToken};
|
||||
@@ -360,7 +360,7 @@ async fn test_edit_history_getter_pause_splits_last_event(cx: &mut TestAppContex
|
||||
ep_store.edit_history_for_project(&project, cx)
|
||||
});
|
||||
assert_eq!(events.len(), 1);
|
||||
let zeta_prompt::Event::BufferChange { diff, .. } = events[0].as_ref();
|
||||
let zeta_prompt::Event::BufferChange { diff, .. } = events[0].event.as_ref();
|
||||
assert_eq!(
|
||||
diff.as_str(),
|
||||
indoc! {"
|
||||
@@ -377,7 +377,7 @@ async fn test_edit_history_getter_pause_splits_last_event(cx: &mut TestAppContex
|
||||
ep_store.edit_history_for_project_with_pause_split_last_event(&project, cx)
|
||||
});
|
||||
assert_eq!(events.len(), 2);
|
||||
let zeta_prompt::Event::BufferChange { diff, .. } = events[0].as_ref();
|
||||
let zeta_prompt::Event::BufferChange { diff, .. } = events[0].event.as_ref();
|
||||
assert_eq!(
|
||||
diff.as_str(),
|
||||
indoc! {"
|
||||
@@ -389,7 +389,7 @@ async fn test_edit_history_getter_pause_splits_last_event(cx: &mut TestAppContex
|
||||
"}
|
||||
);
|
||||
|
||||
let zeta_prompt::Event::BufferChange { diff, .. } = events[1].as_ref();
|
||||
let zeta_prompt::Event::BufferChange { diff, .. } = events[1].event.as_ref();
|
||||
assert_eq!(
|
||||
diff.as_str(),
|
||||
indoc! {"
|
||||
@@ -2082,6 +2082,74 @@ async fn test_unauthenticated_with_custom_url_allows_prediction_impl(cx: &mut Te
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_compute_diff_between_snapshots(cx: &mut TestAppContext) {
|
||||
let buffer = cx.new(|cx| {
|
||||
Buffer::local(
|
||||
indoc! {"
|
||||
zero
|
||||
one
|
||||
two
|
||||
three
|
||||
four
|
||||
five
|
||||
six
|
||||
seven
|
||||
eight
|
||||
nine
|
||||
ten
|
||||
eleven
|
||||
twelve
|
||||
thirteen
|
||||
fourteen
|
||||
fifteen
|
||||
sixteen
|
||||
seventeen
|
||||
eighteen
|
||||
nineteen
|
||||
twenty
|
||||
twenty-one
|
||||
twenty-two
|
||||
twenty-three
|
||||
twenty-four
|
||||
"},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let old_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
|
||||
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
let point = Point::new(12, 0);
|
||||
buffer.edit([(point..point, "SECOND INSERTION\n")], None, cx);
|
||||
let point = Point::new(8, 0);
|
||||
buffer.edit([(point..point, "FIRST INSERTION\n")], None, cx);
|
||||
});
|
||||
|
||||
let new_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
|
||||
|
||||
let diff = compute_diff_between_snapshots(&old_snapshot, &new_snapshot).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
diff,
|
||||
indoc! {"
|
||||
@@ -6,10 +6,12 @@
|
||||
five
|
||||
six
|
||||
seven
|
||||
+FIRST INSERTION
|
||||
eight
|
||||
nine
|
||||
ten
|
||||
eleven
|
||||
+SECOND INSERTION
|
||||
twelve
|
||||
thirteen
|
||||
fourteen
|
||||
"}
|
||||
);
|
||||
}
|
||||
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
zlog::init_test();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt::Write as _, mem, path::Path, sync::Arc};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ExampleSpec {
|
||||
#[serde(default)]
|
||||
pub name: String,
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
use anyhow::{Context as _, Result};
|
||||
use futures::AsyncReadExt as _;
|
||||
use gpui::{
|
||||
App, AppContext as _, Entity, SharedString, Task,
|
||||
App, AppContext as _, Entity, Global, SharedString, Task,
|
||||
http_client::{self, AsyncBody, Method},
|
||||
};
|
||||
use language::{OffsetRangeExt as _, ToOffset, ToPoint as _};
|
||||
@@ -300,14 +300,19 @@ pub const MERCURY_CREDENTIALS_URL: SharedString =
|
||||
SharedString::new_static("https://api.inceptionlabs.ai/v1/edit/completions");
|
||||
pub const MERCURY_CREDENTIALS_USERNAME: &str = "mercury-api-token";
|
||||
pub static MERCURY_TOKEN_ENV_VAR: std::sync::LazyLock<EnvVar> = env_var!("MERCURY_AI_TOKEN");
|
||||
pub static MERCURY_API_KEY: std::sync::OnceLock<Entity<ApiKeyState>> = std::sync::OnceLock::new();
|
||||
|
||||
struct GlobalMercuryApiKey(Entity<ApiKeyState>);
|
||||
|
||||
impl Global for GlobalMercuryApiKey {}
|
||||
|
||||
pub fn mercury_api_token(cx: &mut App) -> Entity<ApiKeyState> {
|
||||
MERCURY_API_KEY
|
||||
.get_or_init(|| {
|
||||
cx.new(|_| ApiKeyState::new(MERCURY_CREDENTIALS_URL, MERCURY_TOKEN_ENV_VAR.clone()))
|
||||
})
|
||||
.clone()
|
||||
if let Some(global) = cx.try_global::<GlobalMercuryApiKey>() {
|
||||
return global.0.clone();
|
||||
}
|
||||
let entity =
|
||||
cx.new(|_| ApiKeyState::new(MERCURY_CREDENTIALS_URL, MERCURY_TOKEN_ENV_VAR.clone()));
|
||||
cx.set_global(GlobalMercuryApiKey(entity.clone()));
|
||||
entity
|
||||
}
|
||||
|
||||
pub fn load_mercury_api_token(cx: &mut App) -> Task<Result<(), language_model::AuthenticateError>> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use futures::AsyncReadExt as _;
|
||||
use gpui::{
|
||||
App, AppContext as _, Entity, SharedString, Task,
|
||||
App, AppContext as _, Entity, Global, SharedString, Task,
|
||||
http_client::{self, AsyncBody, Method},
|
||||
};
|
||||
use language::{Point, ToOffset as _};
|
||||
@@ -272,14 +272,19 @@ pub const SWEEP_CREDENTIALS_URL: SharedString =
|
||||
SharedString::new_static("https://autocomplete.sweep.dev");
|
||||
pub const SWEEP_CREDENTIALS_USERNAME: &str = "sweep-api-token";
|
||||
pub static SWEEP_AI_TOKEN_ENV_VAR: std::sync::LazyLock<EnvVar> = env_var!("SWEEP_AI_TOKEN");
|
||||
pub static SWEEP_API_KEY: std::sync::OnceLock<Entity<ApiKeyState>> = std::sync::OnceLock::new();
|
||||
|
||||
struct GlobalSweepApiKey(Entity<ApiKeyState>);
|
||||
|
||||
impl Global for GlobalSweepApiKey {}
|
||||
|
||||
pub fn sweep_api_token(cx: &mut App) -> Entity<ApiKeyState> {
|
||||
SWEEP_API_KEY
|
||||
.get_or_init(|| {
|
||||
cx.new(|_| ApiKeyState::new(SWEEP_CREDENTIALS_URL, SWEEP_AI_TOKEN_ENV_VAR.clone()))
|
||||
})
|
||||
.clone()
|
||||
if let Some(global) = cx.try_global::<GlobalSweepApiKey>() {
|
||||
return global.0.clone();
|
||||
}
|
||||
let entity =
|
||||
cx.new(|_| ApiKeyState::new(SWEEP_CREDENTIALS_URL, SWEEP_AI_TOKEN_ENV_VAR.clone()));
|
||||
cx.set_global(GlobalSweepApiKey(entity.clone()));
|
||||
entity
|
||||
}
|
||||
|
||||
pub fn load_sweep_api_token(cx: &mut App) -> Task<Result<(), language_model::AuthenticateError>> {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user