mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-21 04:41:16 -05:00
Merge branch 'RooVetGit:main' into main
This commit is contained in:
18
.changeset/changelog-config.js
Normal file
18
.changeset/changelog-config.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
const getReleaseLine = async (changeset) => {
|
||||||
|
const [firstLine] = changeset.summary
|
||||||
|
.split('\n')
|
||||||
|
.map(l => l.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
return `- ${firstLine}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDependencyReleaseLine = async () => {
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const changelogFunctions = {
|
||||||
|
getReleaseLine,
|
||||||
|
getDependencyReleaseLine,
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = changelogFunctions;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://unpkg.com/@changesets/config@3.0.4/schema.json",
|
"$schema": "https://unpkg.com/@changesets/config@3.0.4/schema.json",
|
||||||
"changelog": "@changesets/cli/changelog",
|
"changelog": "./changelog-config.js",
|
||||||
"commit": false,
|
"commit": false,
|
||||||
"fixed": [],
|
"fixed": [],
|
||||||
"linked": [],
|
"linked": [],
|
||||||
|
|||||||
5
.changeset/eighty-nails-peel.md
Normal file
5
.changeset/eighty-nails-peel.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"roo-cline": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Add volume slider in settings and change sound effects to only trigger when user intervention is required, an error occurs, or a task is completed.
|
||||||
5
.changeset/shaggy-moons-dance.md
Normal file
5
.changeset/shaggy-moons-dance.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"roo-cline": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix lint errors and change npm run lint to also run on webview-ui
|
||||||
15
.github/actions/ai-release-notes/action.yml
vendored
15
.github/actions/ai-release-notes/action.yml
vendored
@@ -20,17 +20,14 @@ inputs:
|
|||||||
default: ''
|
default: ''
|
||||||
type: string
|
type: string
|
||||||
git_ref:
|
git_ref:
|
||||||
required: false
|
required: true
|
||||||
type: string
|
type: string
|
||||||
default: ''
|
|
||||||
head_ref:
|
head_ref:
|
||||||
required: false
|
required: true
|
||||||
type: string
|
type: string
|
||||||
default: main
|
|
||||||
base_ref:
|
base_ref:
|
||||||
required: false
|
required: true
|
||||||
type: string
|
type: string
|
||||||
default: main
|
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
RELEASE_NOTES:
|
RELEASE_NOTES:
|
||||||
@@ -41,9 +38,9 @@ outputs:
|
|||||||
value: ${{ steps.ai_prompt.outputs.OPENAI_PROMPT }}
|
value: ${{ steps.ai_prompt.outputs.OPENAI_PROMPT }}
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GITHUB_REF: ${{ inputs.git_ref == '' && github.event.pull_request.head.ref || inputs.git_ref }}
|
GITHUB_REF: ${{ inputs.git_ref }}
|
||||||
BASE_REF: ${{ inputs.base_ref == '' && github.base_ref || inputs.base_ref }}
|
BASE_REF: ${{ inputs.base_ref }}
|
||||||
HEAD_REF: ${{ inputs.head_ref == '' && github.event.pull_request.head.sha || inputs.head_ref }}
|
HEAD_REF: ${{ inputs.head_ref }}
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
|
|||||||
216
.github/workflows/changeset-ai-releases.yml
vendored
216
.github/workflows/changeset-ai-releases.yml
vendored
@@ -1,216 +0,0 @@
|
|||||||
name: Changeset AI Release
|
|
||||||
run-name: Changeset AI Release ${{ github.actor != 'R00-B0T' && '- Create PR' || '- Approve & Release' }}
|
|
||||||
|
|
||||||
# This workflow automates the release process by:
|
|
||||||
# 1. Creating a version bump PR when changesets are merged to main
|
|
||||||
# 2. Using AI to generate release notes for the version bump PR
|
|
||||||
# 3. Auto-approving and merging the version bump PR
|
|
||||||
# 4. Creating a GitHub release with the AI-generated notes
|
|
||||||
|
|
||||||
on:
|
|
||||||
# pull_request:
|
|
||||||
# types: [closed, opened, synchronize, labeled]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
|
||||||
REPO_PATH: ${{ github.repository }}
|
|
||||||
GIT_REF: ${{ github.event.pull_request.head.sha }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# Job 1: Create version bump PR when changesets are merged to main
|
|
||||||
changeset-pr-version-bump:
|
|
||||||
if: >
|
|
||||||
github.event_name == 'pull_request' &&
|
|
||||||
github.event.pull_request.merged == true &&
|
|
||||||
github.event.pull_request.base.ref == 'main' &&
|
|
||||||
github.actor != 'R00-B0T'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
|
||||||
- name: Git Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
ref: ${{ env.GIT_REF }}
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
cache: 'npm'
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: npm install
|
|
||||||
|
|
||||||
# Check if there are any new changesets to process
|
|
||||||
- name: Check for changesets
|
|
||||||
id: check-changesets
|
|
||||||
run: |
|
|
||||||
NEW_CHANGESETS=$(find .changeset -name "*.md" ! -name "README.md" | wc -l | tr -d ' ')
|
|
||||||
echo "Changesets diff with previous version: $NEW_CHANGESETS"
|
|
||||||
echo "new_changesets=$NEW_CHANGESETS" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# Create version bump PR using changesets/action if there are new changesets
|
|
||||||
- name: Changeset Pull Request
|
|
||||||
if: steps.check-changesets.outputs.new_changesets != '0'
|
|
||||||
id: changesets
|
|
||||||
uses: changesets/action@v1
|
|
||||||
with:
|
|
||||||
commit: "changeset version bump"
|
|
||||||
title: "Changeset version bump"
|
|
||||||
version: npm run version-packages # This performs the changeset version bump
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.CROSS_REPO_ACCESS_TOKEN }}
|
|
||||||
|
|
||||||
# Job 2: Process version bump PR created by R00-B0T
|
|
||||||
changeset-pr-approve-merge:
|
|
||||||
name: Auto approve and merge Bump version PRs
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
if: >
|
|
||||||
github.event_name == 'pull_request' &&
|
|
||||||
github.event.pull_request.base.ref == 'main' &&
|
|
||||||
github.actor == 'R00-B0T' &&
|
|
||||||
contains(github.event.pull_request.title, 'Changeset version bump')
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repo
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CROSS_REPO_ACCESS_TOKEN }}
|
|
||||||
fetch-depth: 0
|
|
||||||
ref: ${{ env.GIT_REF }}
|
|
||||||
|
|
||||||
# Get current and previous versions for changelog processing
|
|
||||||
- name: Get version
|
|
||||||
id: get_version
|
|
||||||
run: |
|
|
||||||
VERSION=$(git show HEAD:package.json | jq -r '.version')
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
PREV_VERSION=$(git show origin/main:package.json | jq -r '.version')
|
|
||||||
echo "prev_version=$PREV_VERSION" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
echo "version=$VERSION"
|
|
||||||
echo "prev_version=$PREV_VERSION"
|
|
||||||
|
|
||||||
# Get previous version refs, GITHUB_OUTPUT: 'BASE_REF' and 'HEAD_REF'
|
|
||||||
- name: Get Previous Version Refs
|
|
||||||
id: version_refs
|
|
||||||
run: python .github/scripts/get_prev_version_refs.py
|
|
||||||
|
|
||||||
# Generate release notes using OpenAI if not already edited, GITHUB_OUTPUT: 'RELEASE_NOTES' and 'OPENAI_PROMPT'
|
|
||||||
- name: AI Release Notes
|
|
||||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'openai-edited') }}
|
|
||||||
uses: ./.github/actions/ai-release-notes
|
|
||||||
id: ai_release_notes
|
|
||||||
with:
|
|
||||||
GHA_PAT: ${{ secrets.CROSS_REPO_ACCESS_TOKEN }}
|
|
||||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
||||||
model_name: gpt-4o-mini
|
|
||||||
repo_path: ${{ env.REPO_PATH }}
|
|
||||||
base_ref: ${{ steps.version_refs.outputs.base_ref }}
|
|
||||||
head_ref: ${{ steps.version_refs.outputs.head_ref }}
|
|
||||||
|
|
||||||
# Update CHANGELOG.md with AI-generated notes
|
|
||||||
- name: Update Changeset Changelog
|
|
||||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'openai-edited') }}
|
|
||||||
env:
|
|
||||||
VERSION: ${{ steps.get_version.outputs.version }}
|
|
||||||
PREV_VERSION: ${{ steps.get_version.outputs.prev_version }}
|
|
||||||
NEW_CONTENT: ${{ steps.ai_release_notes.outputs.RELEASE_NOTES }}
|
|
||||||
run: python .github/scripts/overwrite_changeset_changelog.py
|
|
||||||
|
|
||||||
# Commit and push changelog updates
|
|
||||||
- name: Push Changelog updates
|
|
||||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'openai-edited') }}
|
|
||||||
run: |
|
|
||||||
git config user.name "R00-B0T"
|
|
||||||
git config user.email github-actions@github.com
|
|
||||||
git status
|
|
||||||
|
|
||||||
echo "Updating changelog.md..."
|
|
||||||
git add CHANGELOG.md
|
|
||||||
git commit -m "Updating changeset changelog"
|
|
||||||
|
|
||||||
echo "--------------------------------------------------------------------------------"
|
|
||||||
echo "Pushing to remote..."
|
|
||||||
echo "--------------------------------------------------------------------------------"
|
|
||||||
git push
|
|
||||||
|
|
||||||
# Add label to indicate OpenAI has processed this PR
|
|
||||||
- name: Add openai-edited label
|
|
||||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'openai-edited') }}
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
script: |
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: context.issue.number,
|
|
||||||
labels: ['openai-edited']
|
|
||||||
});
|
|
||||||
|
|
||||||
# Auto-approve PR once OpenAI has processed it
|
|
||||||
- name: Auto approve PR
|
|
||||||
if: contains(github.event.pull_request.labels.*.name, 'openai-edited')
|
|
||||||
uses: hmarr/auto-approve-action@v4
|
|
||||||
with:
|
|
||||||
review-message: "I'm approving since it's a bump version PR"
|
|
||||||
|
|
||||||
# Enable auto-merge for the PR
|
|
||||||
- name: Enable automerge on PR
|
|
||||||
if: contains(github.event.pull_request.labels.*.name, 'openai-edited')
|
|
||||||
run: gh pr merge --squash --auto ${{ github.event.pull_request.number }}
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.CROSS_REPO_ACCESS_TOKEN }}
|
|
||||||
|
|
||||||
# Job 3: Create GitHub release after version bump PR is merged
|
|
||||||
github-release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: >
|
|
||||||
github.event_name == 'pull_request' &&
|
|
||||||
github.event.pull_request.merged == true &&
|
|
||||||
github.event.pull_request.base.ref == 'main' &&
|
|
||||||
github.actor == 'R00-B0T' &&
|
|
||||||
contains(github.event.pull_request.title, 'Changeset version bump')
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repo
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Get version
|
|
||||||
id: get_version
|
|
||||||
run: |
|
|
||||||
VERSION=$(git show HEAD:package.json | jq -r '.version')
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# Extract release notes from CHANGELOG.md, GITHUB_OUTPUT: 'release-notes'
|
|
||||||
- name: Parse CHANGELOG.md
|
|
||||||
id: changelog
|
|
||||||
env:
|
|
||||||
CHANGELOG_PATH: CHANGELOG.md
|
|
||||||
VERSION: ${{ steps.get_version.outputs.version }}
|
|
||||||
run: python .github/scripts/parse_changeset_changelog.py
|
|
||||||
|
|
||||||
# Create GitHub release with extracted notes
|
|
||||||
- name: Create or Update Release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
tag_name: v${{ steps.get_version.outputs.version }}
|
|
||||||
name: Release v${{ steps.get_version.outputs.version }}
|
|
||||||
draft: false
|
|
||||||
prerelease: false
|
|
||||||
append_body: false
|
|
||||||
make_latest: true
|
|
||||||
body: ${{ steps.changelog.outputs.release-notes }}
|
|
||||||
91
.github/workflows/changeset-release.yml
vendored
Normal file
91
.github/workflows/changeset-release.yml
vendored
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
name: Changeset Release
|
||||||
|
run-name: Changeset Release ${{ github.actor != 'R00-B0T' && '- Create PR' || '- Approve & Merge' }}
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [closed, opened, synchronize, labeled]
|
||||||
|
|
||||||
|
env:
|
||||||
|
REPO_PATH: ${{ github.repository }}
|
||||||
|
GIT_REF: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Job 1: Create version bump PR when changesets are merged to main
|
||||||
|
changeset-pr-version-bump:
|
||||||
|
if: >
|
||||||
|
github.event_name == 'pull_request' &&
|
||||||
|
github.event.pull_request.merged == true &&
|
||||||
|
github.event.pull_request.base.ref == 'main' &&
|
||||||
|
github.actor != 'R00-B0T'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Git Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ env.GIT_REF }}
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: npm run install:all
|
||||||
|
|
||||||
|
# Check if there are any new changesets to process
|
||||||
|
- name: Check for changesets
|
||||||
|
id: check-changesets
|
||||||
|
run: |
|
||||||
|
NEW_CHANGESETS=$(find .changeset -name "*.md" ! -name "README.md" | wc -l | tr -d ' ')
|
||||||
|
echo "Changesets diff with previous version: $NEW_CHANGESETS"
|
||||||
|
echo "new_changesets=$NEW_CHANGESETS" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# Create version bump PR using changesets/action if there are new changesets
|
||||||
|
- name: Changeset Pull Request
|
||||||
|
if: steps.check-changesets.outputs.new_changesets != '0'
|
||||||
|
id: changesets
|
||||||
|
uses: changesets/action@v1
|
||||||
|
with:
|
||||||
|
commit: "changeset version bump"
|
||||||
|
title: "Changeset version bump"
|
||||||
|
version: npm run version-packages # This performs the changeset version bump
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.CROSS_REPO_ACCESS_TOKEN }}
|
||||||
|
|
||||||
|
# Job 2: Process version bump PR created by R00-B0T
|
||||||
|
changeset-pr-approve-merge:
|
||||||
|
name: Auto approve and merge Bump version PRs
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
if: >
|
||||||
|
github.event_name == 'pull_request' &&
|
||||||
|
github.event.pull_request.base.ref == 'main' &&
|
||||||
|
github.actor == 'R00-B0T' &&
|
||||||
|
contains(github.event.pull_request.title, 'Changeset version bump')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CROSS_REPO_ACCESS_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ env.GIT_REF }}
|
||||||
|
|
||||||
|
# Auto-approve PR
|
||||||
|
- name: Auto approve PR
|
||||||
|
uses: hmarr/auto-approve-action@v4
|
||||||
|
with:
|
||||||
|
review-message: "I'm approving since it's a bump version PR"
|
||||||
|
|
||||||
|
# Enable auto-merge for the PR
|
||||||
|
- name: Enable automerge on PR
|
||||||
|
run: gh pr merge --squash --auto ${{ github.event.pull_request.number }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.CROSS_REPO_ACCESS_TOKEN }}
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -8,3 +8,6 @@ node_modules
|
|||||||
# Builds
|
# Builds
|
||||||
bin/
|
bin/
|
||||||
roo-cline-*.vsix
|
roo-cline-*.vsix
|
||||||
|
|
||||||
|
# Local prompts and rules
|
||||||
|
/local-prompts
|
||||||
|
|||||||
12
CHANGELOG.md
12
CHANGELOG.md
@@ -1,5 +1,17 @@
|
|||||||
# Roo Cline Changelog
|
# Roo Cline Changelog
|
||||||
|
|
||||||
|
## [2.2.13]
|
||||||
|
|
||||||
|
- Fixes to sound playing and applying diffs
|
||||||
|
|
||||||
|
## [2.2.12]
|
||||||
|
|
||||||
|
- Better support for pure deletion and insertion diffs
|
||||||
|
|
||||||
|
## [2.2.11]
|
||||||
|
|
||||||
|
- Added settings checkbox for verbose diff debugging
|
||||||
|
|
||||||
## [2.2.6 - 2.2.10]
|
## [2.2.6 - 2.2.10]
|
||||||
|
|
||||||
- More fixes to search/replace diffs
|
- More fixes to search/replace diffs
|
||||||
|
|||||||
29
package-lock.json
generated
29
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "roo-cline",
|
"name": "roo-cline",
|
||||||
"version": "2.2.10",
|
"version": "2.2.13",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "roo-cline",
|
"name": "roo-cline",
|
||||||
"version": "2.2.10",
|
"version": "2.2.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/bedrock-sdk": "^0.10.2",
|
"@anthropic-ai/bedrock-sdk": "^0.10.2",
|
||||||
"@anthropic-ai/sdk": "^0.26.0",
|
"@anthropic-ai/sdk": "^0.26.0",
|
||||||
@@ -35,10 +35,10 @@
|
|||||||
"os-name": "^6.0.0",
|
"os-name": "^6.0.0",
|
||||||
"p-wait-for": "^5.0.2",
|
"p-wait-for": "^5.0.2",
|
||||||
"pdf-parse": "^1.1.1",
|
"pdf-parse": "^1.1.1",
|
||||||
"play-sound": "^1.1.6",
|
|
||||||
"puppeteer-chromium-resolver": "^23.0.0",
|
"puppeteer-chromium-resolver": "^23.0.0",
|
||||||
"puppeteer-core": "^23.4.0",
|
"puppeteer-core": "^23.4.0",
|
||||||
"serialize-error": "^11.0.3",
|
"serialize-error": "^11.0.3",
|
||||||
|
"sound-play": "^1.1.0",
|
||||||
"strip-ansi": "^7.1.0",
|
"strip-ansi": "^7.1.0",
|
||||||
"tree-sitter-wasms": "^0.1.11",
|
"tree-sitter-wasms": "^0.1.11",
|
||||||
"turndown": "^7.2.0",
|
"turndown": "^7.2.0",
|
||||||
@@ -47,6 +47,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@changesets/cli": "^2.27.10",
|
"@changesets/cli": "^2.27.10",
|
||||||
|
"@changesets/types": "^6.0.0",
|
||||||
"@types/diff": "^5.2.1",
|
"@types/diff": "^5.2.1",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/mocha": "^10.0.7",
|
"@types/mocha": "^10.0.7",
|
||||||
@@ -8851,14 +8852,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/find-exec": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/find-exec/-/find-exec-1.0.3.tgz",
|
|
||||||
"integrity": "sha512-gnG38zW90mS8hm5smNcrBnakPEt+cGJoiMkJwCU0IYnEb0H2NQk0NIljhNW+48oniCriFek/PH6QXbwsJo/qug==",
|
|
||||||
"dependencies": {
|
|
||||||
"shell-quote": "^1.8.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/find-up": {
|
"node_modules/find-up": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
||||||
@@ -13103,14 +13096,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/play-sound": {
|
|
||||||
"version": "1.1.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/play-sound/-/play-sound-1.1.6.tgz",
|
|
||||||
"integrity": "sha512-09eO4QiXNFXJffJaOW5P6x6F5RLihpLUkXttvUZeWml0fU6x6Zp7AjG9zaeMpgH2ZNvq4GR1ytB22ddYcqJIZA==",
|
|
||||||
"dependencies": {
|
|
||||||
"find-exec": "1.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/possible-typed-array-names": {
|
"node_modules/possible-typed-array-names": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
|
||||||
@@ -13874,6 +13859,7 @@
|
|||||||
"version": "1.8.2",
|
"version": "1.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz",
|
||||||
"integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==",
|
"integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
},
|
},
|
||||||
@@ -14002,6 +13988,11 @@
|
|||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sound-play": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/sound-play/-/sound-play-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-Bd/L0AoCwITFeOnpNLMsfPXrV5GG5NhrC/T6odveahYbhPZkdTnrFXRia9FCC5WBWdUTw1d+yvLBvi4wnD1xOA=="
|
||||||
|
},
|
||||||
"node_modules/source-map": {
|
"node_modules/source-map": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"displayName": "Roo Cline",
|
"displayName": "Roo Cline",
|
||||||
"description": "A fork of Cline, an autonomous coding agent, with some added experimental configuration and automation features.",
|
"description": "A fork of Cline, an autonomous coding agent, with some added experimental configuration and automation features.",
|
||||||
"publisher": "RooVeterinaryInc",
|
"publisher": "RooVeterinaryInc",
|
||||||
"version": "2.2.10",
|
"version": "2.2.13",
|
||||||
"icon": "assets/icons/rocket.png",
|
"icon": "assets/icons/rocket.png",
|
||||||
"galleryBanner": {
|
"galleryBanner": {
|
||||||
"color": "#617A91",
|
"color": "#617A91",
|
||||||
@@ -153,7 +153,7 @@
|
|||||||
"compile": "npm run check-types && npm run lint && node esbuild.js",
|
"compile": "npm run check-types && npm run lint && node esbuild.js",
|
||||||
"compile-tests": "tsc -p . --outDir out",
|
"compile-tests": "tsc -p . --outDir out",
|
||||||
"install:all": "npm install && cd webview-ui && npm install",
|
"install:all": "npm install && cd webview-ui && npm install",
|
||||||
"lint": "eslint src --ext ts",
|
"lint": "eslint src --ext ts && npm run lint --prefix webview-ui",
|
||||||
"package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production",
|
"package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production",
|
||||||
"pretest": "npm run compile-tests && npm run compile && npm run lint",
|
"pretest": "npm run compile-tests && npm run compile && npm run lint",
|
||||||
"start:webview": "cd webview-ui && npm run start",
|
"start:webview": "cd webview-ui && npm run start",
|
||||||
@@ -172,6 +172,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@changesets/cli": "^2.27.10",
|
"@changesets/cli": "^2.27.10",
|
||||||
|
"@changesets/types": "^6.0.0",
|
||||||
"@types/diff": "^5.2.1",
|
"@types/diff": "^5.2.1",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/mocha": "^10.0.7",
|
"@types/mocha": "^10.0.7",
|
||||||
@@ -217,10 +218,10 @@
|
|||||||
"os-name": "^6.0.0",
|
"os-name": "^6.0.0",
|
||||||
"p-wait-for": "^5.0.2",
|
"p-wait-for": "^5.0.2",
|
||||||
"pdf-parse": "^1.1.1",
|
"pdf-parse": "^1.1.1",
|
||||||
"play-sound": "^1.1.6",
|
|
||||||
"puppeteer-chromium-resolver": "^23.0.0",
|
"puppeteer-chromium-resolver": "^23.0.0",
|
||||||
"puppeteer-core": "^23.4.0",
|
"puppeteer-core": "^23.4.0",
|
||||||
"serialize-error": "^11.0.3",
|
"serialize-error": "^11.0.3",
|
||||||
|
"sound-play": "^1.1.0",
|
||||||
"strip-ansi": "^7.1.0",
|
"strip-ansi": "^7.1.0",
|
||||||
"tree-sitter-wasms": "^0.1.11",
|
"tree-sitter-wasms": "^0.1.11",
|
||||||
"turndown": "^7.2.0",
|
"turndown": "^7.2.0",
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ export class Cline {
|
|||||||
apiConfiguration: ApiConfiguration,
|
apiConfiguration: ApiConfiguration,
|
||||||
customInstructions?: string,
|
customInstructions?: string,
|
||||||
diffEnabled?: boolean,
|
diffEnabled?: boolean,
|
||||||
|
debugDiffEnabled?: boolean,
|
||||||
task?: string,
|
task?: string,
|
||||||
images?: string[],
|
images?: string[],
|
||||||
historyItem?: HistoryItem,
|
historyItem?: HistoryItem,
|
||||||
@@ -109,7 +110,7 @@ export class Cline {
|
|||||||
this.diffViewProvider = new DiffViewProvider(cwd)
|
this.diffViewProvider = new DiffViewProvider(cwd)
|
||||||
this.customInstructions = customInstructions
|
this.customInstructions = customInstructions
|
||||||
if (diffEnabled && this.api.getModel().id) {
|
if (diffEnabled && this.api.getModel().id) {
|
||||||
this.diffStrategy = getDiffStrategy(this.api.getModel().id)
|
this.diffStrategy = getDiffStrategy(this.api.getModel().id, debugDiffEnabled)
|
||||||
}
|
}
|
||||||
if (historyItem) {
|
if (historyItem) {
|
||||||
this.taskId = historyItem.id
|
this.taskId = historyItem.id
|
||||||
@@ -1237,7 +1238,12 @@ export class Cline {
|
|||||||
const originalContent = await fs.readFile(absolutePath, "utf-8")
|
const originalContent = await fs.readFile(absolutePath, "utf-8")
|
||||||
|
|
||||||
// Apply the diff to the original content
|
// Apply the diff to the original content
|
||||||
const diffResult = this.diffStrategy?.applyDiff(originalContent, diffContent) ?? {
|
const diffResult = this.diffStrategy?.applyDiff(
|
||||||
|
originalContent,
|
||||||
|
diffContent,
|
||||||
|
parseInt(block.params.start_line ?? ''),
|
||||||
|
parseInt(block.params.end_line ?? '')
|
||||||
|
) ?? {
|
||||||
success: false,
|
success: false,
|
||||||
error: "No diff strategy available"
|
error: "No diff strategy available"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -278,7 +278,8 @@ describe('Cline', () => {
|
|||||||
mockProvider,
|
mockProvider,
|
||||||
mockApiConfig,
|
mockApiConfig,
|
||||||
'custom instructions',
|
'custom instructions',
|
||||||
false,
|
false, // diffEnabled
|
||||||
|
false, // debugDiffEnabled
|
||||||
'test task'
|
'test task'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ export const toolParamNames = [
|
|||||||
"question",
|
"question",
|
||||||
"result",
|
"result",
|
||||||
"diff",
|
"diff",
|
||||||
|
"start_line",
|
||||||
|
"end_line",
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export type ToolParamName = (typeof toolParamNames)[number]
|
export type ToolParamName = (typeof toolParamNames)[number]
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import { SearchReplaceDiffStrategy } from './strategies/search-replace'
|
|||||||
* @param model The name of the model being used (e.g., 'gpt-4', 'claude-3-opus')
|
* @param model The name of the model being used (e.g., 'gpt-4', 'claude-3-opus')
|
||||||
* @returns The appropriate diff strategy for the model
|
* @returns The appropriate diff strategy for the model
|
||||||
*/
|
*/
|
||||||
export function getDiffStrategy(model: string): DiffStrategy {
|
export function getDiffStrategy(model: string, debugEnabled?: boolean): DiffStrategy {
|
||||||
// For now, return SearchReplaceDiffStrategy for all models (with a fuzzy threshold of 0.9)
|
// For now, return SearchReplaceDiffStrategy for all models (with a fuzzy threshold of 0.9)
|
||||||
// This architecture allows for future optimizations based on model capabilities
|
// This architecture allows for future optimizations based on model capabilities
|
||||||
return new SearchReplaceDiffStrategy(0.9)
|
return new SearchReplaceDiffStrategy(0.9, debugEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { DiffStrategy }
|
export type { DiffStrategy }
|
||||||
|
|||||||
@@ -591,6 +591,26 @@ this.init();
|
|||||||
expect(result.content).toBe('function test() {\n return false;\n}\n')
|
expect(result.content).toBe('function test() {\n return false;\n}\n')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should strip line numbers with leading spaces', () => {
|
||||||
|
const originalContent = 'function test() {\n return true;\n}\n'
|
||||||
|
const diffContent = `test.ts
|
||||||
|
<<<<<<< SEARCH
|
||||||
|
1 | function test() {
|
||||||
|
2 | return true;
|
||||||
|
3 | }
|
||||||
|
=======
|
||||||
|
1 | function test() {
|
||||||
|
2 | return false;
|
||||||
|
3 | }
|
||||||
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
|
const result = strategy.applyDiff(originalContent, diffContent)
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.content).toBe('function test() {\n return false;\n}\n')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
it('should not strip when not all lines have numbers in either section', () => {
|
it('should not strip when not all lines have numbers in either section', () => {
|
||||||
const originalContent = 'function test() {\n return true;\n}\n'
|
const originalContent = 'function test() {\n return true;\n}\n'
|
||||||
@@ -711,6 +731,212 @@ this.init();
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('insertion/deletion', () => {
|
||||||
|
let strategy: SearchReplaceDiffStrategy
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
strategy = new SearchReplaceDiffStrategy()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('deletion', () => {
|
||||||
|
it('should delete code when replace block is empty', () => {
|
||||||
|
const originalContent = `function test() {
|
||||||
|
console.log("hello");
|
||||||
|
// Comment to remove
|
||||||
|
console.log("world");
|
||||||
|
}`
|
||||||
|
const diffContent = `test.ts
|
||||||
|
<<<<<<< SEARCH
|
||||||
|
// Comment to remove
|
||||||
|
=======
|
||||||
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
|
const result = strategy.applyDiff(originalContent, diffContent)
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.content).toBe(`function test() {
|
||||||
|
console.log("hello");
|
||||||
|
console.log("world");
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should delete multiple lines when replace block is empty', () => {
|
||||||
|
const originalContent = `class Example {
|
||||||
|
constructor() {
|
||||||
|
// Initialize
|
||||||
|
this.value = 0;
|
||||||
|
// Set defaults
|
||||||
|
this.name = "";
|
||||||
|
// End init
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
const diffContent = `test.ts
|
||||||
|
<<<<<<< SEARCH
|
||||||
|
// Initialize
|
||||||
|
this.value = 0;
|
||||||
|
// Set defaults
|
||||||
|
this.name = "";
|
||||||
|
// End init
|
||||||
|
=======
|
||||||
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
|
const result = strategy.applyDiff(originalContent, diffContent)
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.content).toBe(`class Example {
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should preserve indentation when deleting nested code', () => {
|
||||||
|
const originalContent = `function outer() {
|
||||||
|
if (true) {
|
||||||
|
// Remove this
|
||||||
|
console.log("test");
|
||||||
|
// And this
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}`
|
||||||
|
const diffContent = `test.ts
|
||||||
|
<<<<<<< SEARCH
|
||||||
|
// Remove this
|
||||||
|
console.log("test");
|
||||||
|
// And this
|
||||||
|
=======
|
||||||
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
|
const result = strategy.applyDiff(originalContent, diffContent)
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.content).toBe(`function outer() {
|
||||||
|
if (true) {
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('insertion', () => {
|
||||||
|
it('should insert code at specified line when search block is empty', () => {
|
||||||
|
const originalContent = `function test() {
|
||||||
|
const x = 1;
|
||||||
|
return x;
|
||||||
|
}`
|
||||||
|
const diffContent = `test.ts
|
||||||
|
<<<<<<< SEARCH
|
||||||
|
=======
|
||||||
|
console.log("Adding log");
|
||||||
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
|
const result = strategy.applyDiff(originalContent, diffContent, 2, 2)
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.content).toBe(`function test() {
|
||||||
|
console.log("Adding log");
|
||||||
|
const x = 1;
|
||||||
|
return x;
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should preserve indentation when inserting at nested location', () => {
|
||||||
|
const originalContent = `function test() {
|
||||||
|
if (true) {
|
||||||
|
const x = 1;
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
const diffContent = `test.ts
|
||||||
|
<<<<<<< SEARCH
|
||||||
|
=======
|
||||||
|
console.log("Before");
|
||||||
|
console.log("After");
|
||||||
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
|
const result = strategy.applyDiff(originalContent, diffContent, 3, 3)
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.content).toBe(`function test() {
|
||||||
|
if (true) {
|
||||||
|
console.log("Before");
|
||||||
|
console.log("After");
|
||||||
|
const x = 1;
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle insertion at start of file', () => {
|
||||||
|
const originalContent = `function test() {
|
||||||
|
return true;
|
||||||
|
}`
|
||||||
|
const diffContent = `test.ts
|
||||||
|
<<<<<<< SEARCH
|
||||||
|
=======
|
||||||
|
// Copyright 2024
|
||||||
|
// License: MIT
|
||||||
|
|
||||||
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
|
const result = strategy.applyDiff(originalContent, diffContent, 1, 1)
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.content).toBe(`// Copyright 2024
|
||||||
|
// License: MIT
|
||||||
|
|
||||||
|
function test() {
|
||||||
|
return true;
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle insertion at end of file', () => {
|
||||||
|
const originalContent = `function test() {
|
||||||
|
return true;
|
||||||
|
}`
|
||||||
|
const diffContent = `test.ts
|
||||||
|
<<<<<<< SEARCH
|
||||||
|
=======
|
||||||
|
|
||||||
|
// End of file
|
||||||
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
|
const result = strategy.applyDiff(originalContent, diffContent, 4, 4)
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.content).toBe(`function test() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// End of file`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should insert at the start of the file if no start_line is provided for insertion', () => {
|
||||||
|
const originalContent = `function test() {
|
||||||
|
return true;
|
||||||
|
}`
|
||||||
|
const diffContent = `test.ts
|
||||||
|
<<<<<<< SEARCH
|
||||||
|
=======
|
||||||
|
console.log("test");
|
||||||
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
|
const result = strategy.applyDiff(originalContent, diffContent)
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.content).toBe(`console.log("test");
|
||||||
|
function test() {
|
||||||
|
return true;
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('fuzzy matching', () => {
|
describe('fuzzy matching', () => {
|
||||||
let strategy: SearchReplaceDiffStrategy
|
let strategy: SearchReplaceDiffStrategy
|
||||||
|
|
||||||
@@ -1241,8 +1467,8 @@ function two() {
|
|||||||
|
|
||||||
it('should document start_line and end_line parameters', () => {
|
it('should document start_line and end_line parameters', () => {
|
||||||
const description = strategy.getToolDescription('/test')
|
const description = strategy.getToolDescription('/test')
|
||||||
expect(description).toContain('start_line: (required) The line number where the search block starts.')
|
expect(description).toContain('start_line: (required) The line number where the search block starts (inclusive).')
|
||||||
expect(description).toContain('end_line: (required) The line number where the search block ends.')
|
expect(description).toContain('end_line: (required) The line number where the search block ends (inclusive).')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { DiffStrategy, DiffResult } from "../types"
|
import { DiffStrategy, DiffResult } from "../types"
|
||||||
|
import { addLineNumbers } from "../../../integrations/misc/extract-text"
|
||||||
|
|
||||||
|
const BUFFER_LINES = 5; // Number of extra context lines to show before and after matches
|
||||||
|
|
||||||
function levenshteinDistance(a: string, b: string): number {
|
function levenshteinDistance(a: string, b: string): number {
|
||||||
const matrix: number[][] = [];
|
const matrix: number[][] = [];
|
||||||
@@ -30,6 +33,10 @@ function levenshteinDistance(a: string, b: string): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getSimilarity(original: string, search: string): number {
|
function getSimilarity(original: string, search: string): number {
|
||||||
|
if (original === '' || search === '') {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Normalize strings by removing extra whitespace but preserve case
|
// Normalize strings by removing extra whitespace but preserve case
|
||||||
const normalizeStr = (str: string) => str.replace(/\s+/g, ' ').trim();
|
const normalizeStr = (str: string) => str.replace(/\s+/g, ' ').trim();
|
||||||
|
|
||||||
@@ -48,10 +55,12 @@ function getSimilarity(original: string, search: string): number {
|
|||||||
|
|
||||||
export class SearchReplaceDiffStrategy implements DiffStrategy {
|
export class SearchReplaceDiffStrategy implements DiffStrategy {
|
||||||
private fuzzyThreshold: number;
|
private fuzzyThreshold: number;
|
||||||
|
public debugEnabled: boolean;
|
||||||
|
|
||||||
constructor(fuzzyThreshold?: number) {
|
constructor(fuzzyThreshold?: number, debugEnabled?: boolean) {
|
||||||
// Default to exact matching (1.0) unless fuzzy threshold specified
|
// Default to exact matching (1.0) unless fuzzy threshold specified
|
||||||
this.fuzzyThreshold = fuzzyThreshold ?? 1.0;
|
this.fuzzyThreshold = fuzzyThreshold ?? 1.0;
|
||||||
|
this.debugEnabled = debugEnabled ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
getToolDescription(cwd: string): string {
|
getToolDescription(cwd: string): string {
|
||||||
@@ -66,8 +75,8 @@ If you're not confident in the exact content to search for, use the read_file to
|
|||||||
Parameters:
|
Parameters:
|
||||||
- path: (required) The path of the file to modify (relative to the current working directory ${cwd})
|
- path: (required) The path of the file to modify (relative to the current working directory ${cwd})
|
||||||
- diff: (required) The search/replace block defining the changes.
|
- diff: (required) The search/replace block defining the changes.
|
||||||
- start_line: (required) The line number where the search block starts.
|
- start_line: (required) The line number where the search block starts (inclusive).
|
||||||
- end_line: (required) The line number where the search block ends.
|
- end_line: (required) The line number where the search block ends (inclusive).
|
||||||
|
|
||||||
Diff format:
|
Diff format:
|
||||||
\`\`\`
|
\`\`\`
|
||||||
@@ -89,62 +98,107 @@ Original file:
|
|||||||
5 | return total
|
5 | return total
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
Search/Replace content:
|
1. Search/replace a specific chunk of code:
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
<apply_diff>
|
||||||
|
<path>File path here</path>
|
||||||
|
<diff>
|
||||||
<<<<<<< SEARCH
|
<<<<<<< SEARCH
|
||||||
def calculate_total(items):
|
|
||||||
total = 0
|
total = 0
|
||||||
for item in items:
|
for item in items:
|
||||||
total += item
|
total += item
|
||||||
return total
|
return total
|
||||||
=======
|
=======
|
||||||
def calculate_total(items):
|
|
||||||
"""Calculate total with 10% markup"""
|
"""Calculate total with 10% markup"""
|
||||||
return sum(item * 1.1 for item in items)
|
return sum(item * 1.1 for item in items)
|
||||||
>>>>>>> REPLACE
|
>>>>>>> REPLACE
|
||||||
|
</diff>
|
||||||
|
<start_line>2</start_line>
|
||||||
|
<end_line>5</end_line>
|
||||||
|
</apply_diff>
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
Usage:
|
Result:
|
||||||
|
\`\`\`
|
||||||
|
1 | def calculate_total(items):
|
||||||
|
2 | """Calculate total with 10% markup"""
|
||||||
|
3 | return sum(item * 1.1 for item in items)
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
2. Insert code at a specific line (start_line and end_line must be the same, and the content gets inserted before whatever is currently at that line):
|
||||||
|
\`\`\`
|
||||||
<apply_diff>
|
<apply_diff>
|
||||||
<path>File path here</path>
|
<path>File path here</path>
|
||||||
<diff>
|
<diff>
|
||||||
Your search/replace content here
|
<<<<<<< SEARCH
|
||||||
|
=======
|
||||||
|
"""TODO: Write a test for this"""
|
||||||
|
>>>>>>> REPLACE
|
||||||
</diff>
|
</diff>
|
||||||
<start_line>1</start_line>
|
<start_line>2</start_line>
|
||||||
|
<end_line>2</end_line>
|
||||||
|
</apply_diff>
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Result:
|
||||||
|
\`\`\`
|
||||||
|
1 | def calculate_total(items):
|
||||||
|
2 | """TODO: Write a test for this"""
|
||||||
|
3 | """Calculate total with 10% markup"""
|
||||||
|
4 | return sum(item * 1.1 for item in items)
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
3. Delete code at a specific line range:
|
||||||
|
\`\`\`
|
||||||
|
<apply_diff>
|
||||||
|
<path>File path here</path>
|
||||||
|
<diff>
|
||||||
|
<<<<<<< SEARCH
|
||||||
|
total = 0
|
||||||
|
for item in items:
|
||||||
|
total += item
|
||||||
|
return total
|
||||||
|
=======
|
||||||
|
>>>>>>> REPLACE
|
||||||
|
</diff>
|
||||||
|
<start_line>2</start_line>
|
||||||
<end_line>5</end_line>
|
<end_line>5</end_line>
|
||||||
</apply_diff>`
|
</apply_diff>
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Result:
|
||||||
|
\`\`\`
|
||||||
|
1 | def calculate_total(items):
|
||||||
|
\`\`\`
|
||||||
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
applyDiff(originalContent: string, diffContent: string, startLine?: number, endLine?: number): DiffResult {
|
applyDiff(originalContent: string, diffContent: string, startLine?: number, endLine?: number): DiffResult {
|
||||||
// Extract the search and replace blocks
|
// Extract the search and replace blocks
|
||||||
const match = diffContent.match(/<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> REPLACE/);
|
const match = diffContent.match(/<<<<<<< SEARCH\n([\s\S]*?)\n?=======\n([\s\S]*?)\n?>>>>>>> REPLACE/);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
// Log detailed format information
|
const debugInfo = this.debugEnabled ? `\n\nDebug Info:\n- Expected Format: <<<<<<< SEARCH\\n[search content]\\n=======\\n[replace content]\\n>>>>>>> REPLACE\n- Tip: Make sure to include both SEARCH and REPLACE sections with correct markers` : '';
|
||||||
console.log('Invalid Diff Format Debug:', {
|
|
||||||
expectedFormat: "<<<<<<< SEARCH\\n[search content]\\n=======\\n[replace content]\\n>>>>>>> REPLACE",
|
|
||||||
tip: "Make sure to include both SEARCH and REPLACE sections with correct markers"
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: "Invalid diff format - missing required SEARCH/REPLACE sections"
|
error: `Invalid diff format - missing required SEARCH/REPLACE sections${debugInfo}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let [_, searchContent, replaceContent] = match;
|
let [_, searchContent, replaceContent] = match;
|
||||||
|
|
||||||
// Detect line ending from original content
|
// Detect line ending from original content
|
||||||
const lineEnding = originalContent.includes('\r\n') ? '\r\n' : '\n';
|
const lineEnding = originalContent.includes('\r\n') ? '\r\n' : '\n';
|
||||||
|
|
||||||
// Strip line numbers from search and replace content if every line starts with a line number
|
// Strip line numbers from search and replace content if every line starts with a line number
|
||||||
const hasLineNumbers = (content: string) => {
|
const hasLineNumbers = (content: string) => {
|
||||||
const lines = content.split(/\r?\n/);
|
const lines = content.split(/\r?\n/);
|
||||||
return lines.length > 0 && lines.every(line => /^\d+\s+\|(?!\|)/.test(line));
|
return lines.length > 0 && lines.every(line => /^\s*\d+\s+\|(?!\|)/.test(line));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (hasLineNumbers(searchContent) && hasLineNumbers(replaceContent)) {
|
if (hasLineNumbers(searchContent) && hasLineNumbers(replaceContent)) {
|
||||||
const stripLineNumbers = (content: string) => {
|
const stripLineNumbers = (content: string) => {
|
||||||
return content.replace(/^\d+\s+\|(?!\|)/gm, '')
|
return content.replace(/^\s*\d+\s+\|(?!\|)/gm, '');
|
||||||
};
|
};
|
||||||
|
|
||||||
searchContent = stripLineNumbers(searchContent);
|
searchContent = stripLineNumbers(searchContent);
|
||||||
@@ -152,8 +206,8 @@ Your search/replace content here
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Split content into lines, handling both \n and \r\n
|
// Split content into lines, handling both \n and \r\n
|
||||||
const searchLines = searchContent.split(/\r?\n/);
|
const searchLines = searchContent === '' ? [] : searchContent.split(/\r?\n/);
|
||||||
const replaceLines = replaceContent.split(/\r?\n/);
|
const replaceLines = replaceContent === '' ? [] : replaceContent.split(/\r?\n/);
|
||||||
const originalLines = originalContent.split(/\r?\n/);
|
const originalLines = originalContent.split(/\r?\n/);
|
||||||
|
|
||||||
// First try exact line range if provided
|
// First try exact line range if provided
|
||||||
@@ -161,12 +215,14 @@ Your search/replace content here
|
|||||||
let bestMatchScore = 0;
|
let bestMatchScore = 0;
|
||||||
let bestMatchContent = "";
|
let bestMatchContent = "";
|
||||||
|
|
||||||
if (startLine !== undefined && endLine !== undefined) {
|
if (startLine && endLine) {
|
||||||
// Convert to 0-based index
|
// Convert to 0-based index
|
||||||
const exactStartIndex = startLine - 1;
|
const exactStartIndex = startLine - 1;
|
||||||
const exactEndIndex = endLine - 1;
|
const exactEndIndex = endLine - 1;
|
||||||
|
|
||||||
if (exactStartIndex < 0 || exactEndIndex >= originalLines.length) {
|
if (exactStartIndex < 0 || exactEndIndex > originalLines.length || exactStartIndex > exactEndIndex) {
|
||||||
|
const debugInfo = this.debugEnabled ? `\n\nDebug Info:\n- Requested Range: lines ${startLine}-${endLine}\n- File Bounds: lines 1-${originalLines.length}` : '';
|
||||||
|
|
||||||
// Log detailed debug information
|
// Log detailed debug information
|
||||||
console.log('Invalid Line Range Debug:', {
|
console.log('Invalid Line Range Debug:', {
|
||||||
requestedRange: { start: startLine, end: endLine },
|
requestedRange: { start: startLine, end: endLine },
|
||||||
@@ -175,7 +231,7 @@ Your search/replace content here
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: `Line range ${startLine}-${endLine} is invalid (file has ${originalLines.length} lines)`,
|
error: `Line range ${startLine}-${endLine} is invalid (file has ${originalLines.length} lines)${debugInfo}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,13 +252,13 @@ Your search/replace content here
|
|||||||
let searchStartIndex = 0;
|
let searchStartIndex = 0;
|
||||||
let searchEndIndex = originalLines.length;
|
let searchEndIndex = originalLines.length;
|
||||||
|
|
||||||
if (startLine !== undefined || endLine !== undefined) {
|
if (startLine || endLine) {
|
||||||
// Convert to 0-based index and add buffer
|
// Convert to 0-based index and add buffer
|
||||||
if (startLine !== undefined) {
|
if (startLine) {
|
||||||
searchStartIndex = Math.max(0, startLine - 6);
|
searchStartIndex = Math.max(0, startLine - (BUFFER_LINES + 1));
|
||||||
}
|
}
|
||||||
if (endLine !== undefined) {
|
if (endLine) {
|
||||||
searchEndIndex = Math.min(originalLines.length, endLine + 5);
|
searchEndIndex = Math.min(originalLines.length, endLine + BUFFER_LINES);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,17 +280,27 @@ Your search/replace content here
|
|||||||
// Require similarity to meet threshold
|
// Require similarity to meet threshold
|
||||||
if (matchIndex === -1 || bestMatchScore < this.fuzzyThreshold) {
|
if (matchIndex === -1 || bestMatchScore < this.fuzzyThreshold) {
|
||||||
const searchChunk = searchLines.join('\n');
|
const searchChunk = searchLines.join('\n');
|
||||||
// Log detailed debug information to console
|
const originalContentSection = startLine !== undefined && endLine !== undefined
|
||||||
console.log('Search/Replace Debug Info:', {
|
? `\n\nOriginal Content:\n${addLineNumbers(
|
||||||
similarity: bestMatchScore,
|
originalLines.slice(
|
||||||
threshold: this.fuzzyThreshold,
|
Math.max(0, startLine - 1 - BUFFER_LINES),
|
||||||
searchContent: searchChunk,
|
Math.min(originalLines.length, endLine + BUFFER_LINES)
|
||||||
bestMatch: bestMatchContent || undefined
|
).join('\n'),
|
||||||
});
|
Math.max(1, startLine - BUFFER_LINES)
|
||||||
|
)}`
|
||||||
|
: `\n\nOriginal Content:\n${addLineNumbers(originalLines.join('\n'))}`;
|
||||||
|
|
||||||
|
const bestMatchSection = bestMatchContent
|
||||||
|
? `\n\nBest Match Found:\n${addLineNumbers(bestMatchContent, matchIndex + 1)}`
|
||||||
|
: `\n\nBest Match Found:\n(no match)`;
|
||||||
|
|
||||||
|
const debugInfo = this.debugEnabled ? `\n\nDebug Info:\n- Similarity Score: ${Math.floor(bestMatchScore * 100)}%\n- Required Threshold: ${Math.floor(this.fuzzyThreshold * 100)}%\n- Search Range: ${startLine && endLine ? `lines ${startLine}-${endLine}` : 'start to end'}\n\nSearch Content:\n${searchChunk}${bestMatchSection}${originalContentSection}` : '';
|
||||||
|
|
||||||
|
const lineRange = startLine || endLine ?
|
||||||
|
` at ${startLine ? `start: ${startLine}` : 'start'} to ${endLine ? `end: ${endLine}` : 'end'}` : '';
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: `No sufficiently similar match found${startLine !== undefined ? ` near lines ${startLine}-${endLine}` : ''} (${Math.round(bestMatchScore * 100)}% similar, needs ${Math.round(this.fuzzyThreshold * 100)}%)`
|
error: `No sufficiently similar match found${lineRange} (${Math.floor(bestMatchScore * 100)}% similar, needs ${Math.floor(this.fuzzyThreshold * 100)}%)${debugInfo}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,7 +322,7 @@ Your search/replace content here
|
|||||||
// Apply the replacement while preserving exact indentation
|
// Apply the replacement while preserving exact indentation
|
||||||
const indentedReplaceLines = replaceLines.map((line, i) => {
|
const indentedReplaceLines = replaceLines.map((line, i) => {
|
||||||
// Get the matched line's exact indentation
|
// Get the matched line's exact indentation
|
||||||
const matchedIndent = originalIndents[0];
|
const matchedIndent = originalIndents[0] || '';
|
||||||
|
|
||||||
// Get the current line's indentation relative to the search content
|
// Get the current line's indentation relative to the search content
|
||||||
const currentIndentMatch = line.match(/^[\t ]*/);
|
const currentIndentMatch = line.match(/^[\t ]*/);
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ export type DiffResult =
|
|||||||
}};
|
}};
|
||||||
|
|
||||||
export interface DiffStrategy {
|
export interface DiffStrategy {
|
||||||
|
/**
|
||||||
|
* Whether to enable detailed debug logging
|
||||||
|
*/
|
||||||
|
debugEnabled?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the tool description for this diff strategy
|
* Get the tool description for this diff strategy
|
||||||
* @param cwd The current working directory
|
* @param cwd The current working directory
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { Cline } from "../Cline"
|
|||||||
import { openMention } from "../mentions"
|
import { openMention } from "../mentions"
|
||||||
import { getNonce } from "./getNonce"
|
import { getNonce } from "./getNonce"
|
||||||
import { getUri } from "./getUri"
|
import { getUri } from "./getUri"
|
||||||
import { playSound, setSoundEnabled } from "../../utils/sound"
|
import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||||
@@ -66,7 +66,9 @@ type GlobalStateKey =
|
|||||||
| "openRouterUseMiddleOutTransform"
|
| "openRouterUseMiddleOutTransform"
|
||||||
| "allowedCommands"
|
| "allowedCommands"
|
||||||
| "soundEnabled"
|
| "soundEnabled"
|
||||||
|
| "soundVolume"
|
||||||
| "diffEnabled"
|
| "diffEnabled"
|
||||||
|
| "debugDiffEnabled"
|
||||||
| "alwaysAllowMcp"
|
| "alwaysAllowMcp"
|
||||||
|
|
||||||
export const GlobalFileNames = {
|
export const GlobalFileNames = {
|
||||||
@@ -136,6 +138,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
this.outputChannel.appendLine("Resolving webview view")
|
this.outputChannel.appendLine("Resolving webview view")
|
||||||
this.view = webviewView
|
this.view = webviewView
|
||||||
|
|
||||||
|
// Initialize sound enabled state
|
||||||
|
this.getState().then(({ soundEnabled }) => {
|
||||||
|
setSoundEnabled(soundEnabled ?? false)
|
||||||
|
})
|
||||||
|
|
||||||
webviewView.webview.options = {
|
webviewView.webview.options = {
|
||||||
// Allow scripts in the webview
|
// Allow scripts in the webview
|
||||||
enableScripts: true,
|
enableScripts: true,
|
||||||
@@ -207,28 +214,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
|
|
||||||
async initClineWithTask(task?: string, images?: string[]) {
|
async initClineWithTask(task?: string, images?: string[]) {
|
||||||
await this.clearTask()
|
await this.clearTask()
|
||||||
const {
|
const {
|
||||||
apiConfiguration,
|
apiConfiguration,
|
||||||
customInstructions,
|
customInstructions,
|
||||||
diffEnabled,
|
|
||||||
} = await this.getState()
|
|
||||||
|
|
||||||
this.cline = new Cline(
|
|
||||||
this,
|
|
||||||
apiConfiguration,
|
|
||||||
customInstructions,
|
|
||||||
diffEnabled,
|
|
||||||
task,
|
|
||||||
images
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async initClineWithHistoryItem(historyItem: HistoryItem) {
|
|
||||||
await this.clearTask()
|
|
||||||
const {
|
|
||||||
apiConfiguration,
|
|
||||||
customInstructions,
|
|
||||||
diffEnabled,
|
diffEnabled,
|
||||||
|
debugDiffEnabled,
|
||||||
} = await this.getState()
|
} = await this.getState()
|
||||||
|
|
||||||
this.cline = new Cline(
|
this.cline = new Cline(
|
||||||
@@ -236,6 +226,27 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
apiConfiguration,
|
apiConfiguration,
|
||||||
customInstructions,
|
customInstructions,
|
||||||
diffEnabled,
|
diffEnabled,
|
||||||
|
debugDiffEnabled,
|
||||||
|
task,
|
||||||
|
images
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async initClineWithHistoryItem(historyItem: HistoryItem) {
|
||||||
|
await this.clearTask()
|
||||||
|
const {
|
||||||
|
apiConfiguration,
|
||||||
|
customInstructions,
|
||||||
|
diffEnabled,
|
||||||
|
debugDiffEnabled,
|
||||||
|
} = await this.getState()
|
||||||
|
|
||||||
|
this.cline = new Cline(
|
||||||
|
this,
|
||||||
|
apiConfiguration,
|
||||||
|
customInstructions,
|
||||||
|
diffEnabled,
|
||||||
|
debugDiffEnabled,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
historyItem,
|
historyItem,
|
||||||
@@ -592,11 +603,22 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
setSoundEnabled(soundEnabled) // Add this line to update the sound utility
|
setSoundEnabled(soundEnabled) // Add this line to update the sound utility
|
||||||
await this.postStateToWebview()
|
await this.postStateToWebview()
|
||||||
break
|
break
|
||||||
|
case "soundVolume":
|
||||||
|
const soundVolume = message.value ?? 0.5
|
||||||
|
await this.updateGlobalState("soundVolume", soundVolume)
|
||||||
|
setSoundVolume(soundVolume)
|
||||||
|
await this.postStateToWebview()
|
||||||
|
break
|
||||||
case "diffEnabled":
|
case "diffEnabled":
|
||||||
const diffEnabled = message.bool ?? true
|
const diffEnabled = message.bool ?? true
|
||||||
await this.updateGlobalState("diffEnabled", diffEnabled)
|
await this.updateGlobalState("diffEnabled", diffEnabled)
|
||||||
await this.postStateToWebview()
|
await this.postStateToWebview()
|
||||||
break
|
break
|
||||||
|
case "debugDiffEnabled":
|
||||||
|
const debugDiffEnabled = message.bool ?? false
|
||||||
|
await this.updateGlobalState("debugDiffEnabled", debugDiffEnabled)
|
||||||
|
await this.postStateToWebview()
|
||||||
|
break
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
@@ -923,7 +945,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
alwaysAllowMcp,
|
alwaysAllowMcp,
|
||||||
soundEnabled,
|
soundEnabled,
|
||||||
diffEnabled,
|
diffEnabled,
|
||||||
|
debugDiffEnabled,
|
||||||
taskHistory,
|
taskHistory,
|
||||||
|
soundVolume,
|
||||||
} = await this.getState()
|
} = await this.getState()
|
||||||
|
|
||||||
const allowedCommands = vscode.workspace
|
const allowedCommands = vscode.workspace
|
||||||
@@ -946,8 +970,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
.sort((a, b) => b.ts - a.ts),
|
.sort((a, b) => b.ts - a.ts),
|
||||||
soundEnabled: soundEnabled ?? false,
|
soundEnabled: soundEnabled ?? false,
|
||||||
diffEnabled: diffEnabled ?? false,
|
diffEnabled: diffEnabled ?? false,
|
||||||
|
debugDiffEnabled: debugDiffEnabled ?? false,
|
||||||
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
|
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
|
||||||
allowedCommands,
|
allowedCommands,
|
||||||
|
soundVolume: soundVolume ?? 0.5,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1040,6 +1066,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
allowedCommands,
|
allowedCommands,
|
||||||
soundEnabled,
|
soundEnabled,
|
||||||
diffEnabled,
|
diffEnabled,
|
||||||
|
debugDiffEnabled,
|
||||||
|
soundVolume,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
|
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
|
||||||
this.getGlobalState("apiModelId") as Promise<string | undefined>,
|
this.getGlobalState("apiModelId") as Promise<string | undefined>,
|
||||||
@@ -1077,6 +1105,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
this.getGlobalState("allowedCommands") as Promise<string[] | undefined>,
|
this.getGlobalState("allowedCommands") as Promise<string[] | undefined>,
|
||||||
this.getGlobalState("soundEnabled") as Promise<boolean | undefined>,
|
this.getGlobalState("soundEnabled") as Promise<boolean | undefined>,
|
||||||
this.getGlobalState("diffEnabled") as Promise<boolean | undefined>,
|
this.getGlobalState("diffEnabled") as Promise<boolean | undefined>,
|
||||||
|
this.getGlobalState("debugDiffEnabled") as Promise<boolean | undefined>,
|
||||||
|
this.getGlobalState("soundVolume") as Promise<number | undefined>,
|
||||||
])
|
])
|
||||||
|
|
||||||
let apiProvider: ApiProvider
|
let apiProvider: ApiProvider
|
||||||
@@ -1130,8 +1160,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
alwaysAllowMcp: alwaysAllowMcp ?? false,
|
alwaysAllowMcp: alwaysAllowMcp ?? false,
|
||||||
taskHistory,
|
taskHistory,
|
||||||
allowedCommands,
|
allowedCommands,
|
||||||
soundEnabled,
|
soundEnabled: soundEnabled ?? false,
|
||||||
diffEnabled,
|
diffEnabled: diffEnabled ?? false,
|
||||||
|
debugDiffEnabled: debugDiffEnabled ?? false,
|
||||||
|
soundVolume,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
32
src/integrations/misc/__tests__/extract-text.test.ts
Normal file
32
src/integrations/misc/__tests__/extract-text.test.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { addLineNumbers } from '../extract-text';
|
||||||
|
|
||||||
|
describe('addLineNumbers', () => {
|
||||||
|
it('should add line numbers starting from 1 by default', () => {
|
||||||
|
const input = 'line 1\nline 2\nline 3';
|
||||||
|
const expected = '1 | line 1\n2 | line 2\n3 | line 3';
|
||||||
|
expect(addLineNumbers(input)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add line numbers starting from specified line number', () => {
|
||||||
|
const input = 'line 1\nline 2\nline 3';
|
||||||
|
const expected = '10 | line 1\n11 | line 2\n12 | line 3';
|
||||||
|
expect(addLineNumbers(input, 10)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty content', () => {
|
||||||
|
expect(addLineNumbers('')).toBe('1 | ');
|
||||||
|
expect(addLineNumbers('', 5)).toBe('5 | ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle single line content', () => {
|
||||||
|
expect(addLineNumbers('single line')).toBe('1 | single line');
|
||||||
|
expect(addLineNumbers('single line', 42)).toBe('42 | single line');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pad line numbers based on the highest line number', () => {
|
||||||
|
const input = 'line 1\nline 2';
|
||||||
|
// When starting from 99, highest line will be 100, so needs 3 spaces padding
|
||||||
|
const expected = ' 99 | line 1\n100 | line 2';
|
||||||
|
expect(addLineNumbers(input, 99)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -53,15 +53,12 @@ async function extractTextFromIPYNB(filePath: string): Promise<string> {
|
|||||||
|
|
||||||
return addLineNumbers(extractedText)
|
return addLineNumbers(extractedText)
|
||||||
}
|
}
|
||||||
|
export function addLineNumbers(content: string, startLine: number = 1): string {
|
||||||
export function addLineNumbers(content: string): string {
|
|
||||||
const lines = content.split('\n')
|
const lines = content.split('\n')
|
||||||
const maxLineNumberWidth = String(lines.length).length
|
const maxLineNumberWidth = String(startLine + lines.length - 1).length
|
||||||
return lines
|
return lines
|
||||||
.map((line, index) => {
|
.map((line, index) => {
|
||||||
const lineNumber = String(index + 1).padStart(maxLineNumberWidth, ' ')
|
const lineNumber = String(startLine + index).padStart(maxLineNumberWidth, ' ')
|
||||||
return `${lineNumber} | ${line}`
|
return `${lineNumber} | ${line}`
|
||||||
}).join('\n')
|
}).join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -51,7 +51,9 @@ export interface ExtensionState {
|
|||||||
uriScheme?: string
|
uriScheme?: string
|
||||||
allowedCommands?: string[]
|
allowedCommands?: string[]
|
||||||
soundEnabled?: boolean
|
soundEnabled?: boolean
|
||||||
|
soundVolume?: number
|
||||||
diffEnabled?: boolean
|
diffEnabled?: boolean
|
||||||
|
debugDiffEnabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClineMessage {
|
export interface ClineMessage {
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ export interface WebviewMessage {
|
|||||||
| "alwaysAllowMcp"
|
| "alwaysAllowMcp"
|
||||||
| "playSound"
|
| "playSound"
|
||||||
| "soundEnabled"
|
| "soundEnabled"
|
||||||
|
| "soundVolume"
|
||||||
| "diffEnabled"
|
| "diffEnabled"
|
||||||
|
| "debugDiffEnabled"
|
||||||
| "openMcpSettings"
|
| "openMcpSettings"
|
||||||
| "restartMcpServer"
|
| "restartMcpServer"
|
||||||
| "toggleToolAlwaysAllow"
|
| "toggleToolAlwaysAllow"
|
||||||
@@ -43,6 +45,7 @@ export interface WebviewMessage {
|
|||||||
apiConfiguration?: ApiConfiguration
|
apiConfiguration?: ApiConfiguration
|
||||||
images?: string[]
|
images?: string[]
|
||||||
bool?: boolean
|
bool?: boolean
|
||||||
|
value?: number
|
||||||
commands?: string[]
|
commands?: string[]
|
||||||
audioType?: AudioType
|
audioType?: AudioType
|
||||||
// For toggleToolAutoApprove
|
// For toggleToolAutoApprove
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const isWAV = (filepath: string): boolean => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let isSoundEnabled = false
|
let isSoundEnabled = false
|
||||||
|
let volume = .5
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set sound configuration
|
* Set sound configuration
|
||||||
@@ -30,6 +31,14 @@ export const setSoundEnabled = (enabled: boolean): void => {
|
|||||||
isSoundEnabled = enabled
|
isSoundEnabled = enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set sound volume
|
||||||
|
* @param volume number
|
||||||
|
*/
|
||||||
|
export const setSoundVolume = (newVolume: number): void => {
|
||||||
|
volume = newVolume
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Play a sound file
|
* Play a sound file
|
||||||
* @param filepath string
|
* @param filepath string
|
||||||
@@ -54,11 +63,9 @@ export const playSound = (filepath: string): void => {
|
|||||||
return // Skip playback within minimum interval to prevent continuous playback
|
return // Skip playback within minimum interval to prevent continuous playback
|
||||||
}
|
}
|
||||||
|
|
||||||
const player = require("play-sound")()
|
const sound = require("sound-play")
|
||||||
player.play(filepath, function (err: any) {
|
sound.play(filepath, volume).catch(() => {
|
||||||
if (err) {
|
throw new Error("Failed to play sound effect")
|
||||||
throw new Error("Failed to play sound effect")
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
lastPlayedTime = currentTime
|
lastPlayedTime = currentTime
|
||||||
|
|||||||
@@ -20,6 +20,6 @@
|
|||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"useUnknownInCatchVariables": false
|
"useUnknownInCatchVariables": false
|
||||||
},
|
},
|
||||||
"include": ["src/**/*", "scripts/**/*"],
|
"include": ["src/**/*", "scripts/**/*", ".changeset/**/*"],
|
||||||
"exclude": ["node_modules", ".vscode-test", "webview-ui"]
|
"exclude": ["node_modules", ".vscode-test", "webview-ui"]
|
||||||
}
|
}
|
||||||
|
|||||||
3
webview-ui/package-lock.json
generated
3
webview-ui/package-lock.json
generated
@@ -34,7 +34,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@types/vscode-webview": "^1.57.5"
|
"@types/vscode-webview": "^1.57.5",
|
||||||
|
"eslint": "^8.57.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@adobe/css-tools": {
|
"node_modules/@adobe/css-tools": {
|
||||||
|
|||||||
@@ -31,7 +31,8 @@
|
|||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
"build": "node ./scripts/build-react-no-split.js",
|
"build": "node ./scripts/build-react-no-split.js",
|
||||||
"test": "react-scripts test --watchAll=false",
|
"test": "react-scripts test --watchAll=false",
|
||||||
"eject": "react-scripts eject"
|
"eject": "react-scripts eject",
|
||||||
|
"lint": "eslint src --ext ts,tsx"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
@@ -53,7 +54,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@types/vscode-webview": "^1.57.5"
|
"@types/vscode-webview": "^1.57.5",
|
||||||
|
"eslint": "^8.57.0"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"transformIgnorePatterns": [
|
"transformIgnorePatterns": [
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
const [isAtBottom, setIsAtBottom] = useState(false)
|
const [isAtBottom, setIsAtBottom] = useState(false)
|
||||||
|
|
||||||
const [wasStreaming, setWasStreaming] = useState<boolean>(false)
|
const [wasStreaming, setWasStreaming] = useState<boolean>(false)
|
||||||
const [hasStarted, setHasStarted] = useState(false)
|
|
||||||
|
|
||||||
// UI layout depends on the last 2 messages
|
// UI layout depends on the last 2 messages
|
||||||
// (since it relies on the content of these messages, we are deep comparing. i.e. the button state after hitting button sets enableButtons to false, and this effect otherwise would have to true again even if messages didn't change
|
// (since it relies on the content of these messages, we are deep comparing. i.e. the button state after hitting button sets enableButtons to false, and this effect otherwise would have to true again even if messages didn't change
|
||||||
@@ -75,12 +74,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
vscode.postMessage({ type: "playSound", audioType })
|
vscode.postMessage({ type: "playSound", audioType })
|
||||||
}
|
}
|
||||||
|
|
||||||
function playSoundOnMessage(audioType: AudioType) {
|
|
||||||
if (hasStarted && !isStreaming) {
|
|
||||||
playSound(audioType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useDeepCompareEffect(() => {
|
useDeepCompareEffect(() => {
|
||||||
// if last message is an ask, show user ask UI
|
// if last message is an ask, show user ask UI
|
||||||
// if user finished a task, then start a new task with a new conversation history since in this moment that the extension is waiting for user response, the user could close the extension and the conversation history would be lost.
|
// if user finished a task, then start a new task with a new conversation history since in this moment that the extension is waiting for user response, the user could close the extension and the conversation history would be lost.
|
||||||
@@ -91,7 +84,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
const isPartial = lastMessage.partial === true
|
const isPartial = lastMessage.partial === true
|
||||||
switch (lastMessage.ask) {
|
switch (lastMessage.ask) {
|
||||||
case "api_req_failed":
|
case "api_req_failed":
|
||||||
playSoundOnMessage("progress_loop")
|
playSound("progress_loop")
|
||||||
setTextAreaDisabled(true)
|
setTextAreaDisabled(true)
|
||||||
setClineAsk("api_req_failed")
|
setClineAsk("api_req_failed")
|
||||||
setEnableButtons(true)
|
setEnableButtons(true)
|
||||||
@@ -99,7 +92,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
setSecondaryButtonText("Start New Task")
|
setSecondaryButtonText("Start New Task")
|
||||||
break
|
break
|
||||||
case "mistake_limit_reached":
|
case "mistake_limit_reached":
|
||||||
playSoundOnMessage("progress_loop")
|
playSound("progress_loop")
|
||||||
setTextAreaDisabled(false)
|
setTextAreaDisabled(false)
|
||||||
setClineAsk("mistake_limit_reached")
|
setClineAsk("mistake_limit_reached")
|
||||||
setEnableButtons(true)
|
setEnableButtons(true)
|
||||||
@@ -107,7 +100,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
setSecondaryButtonText("Start New Task")
|
setSecondaryButtonText("Start New Task")
|
||||||
break
|
break
|
||||||
case "followup":
|
case "followup":
|
||||||
playSoundOnMessage("notification")
|
|
||||||
setTextAreaDisabled(isPartial)
|
setTextAreaDisabled(isPartial)
|
||||||
setClineAsk("followup")
|
setClineAsk("followup")
|
||||||
setEnableButtons(isPartial)
|
setEnableButtons(isPartial)
|
||||||
@@ -115,7 +107,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
// setSecondaryButtonText(undefined)
|
// setSecondaryButtonText(undefined)
|
||||||
break
|
break
|
||||||
case "tool":
|
case "tool":
|
||||||
playSoundOnMessage("notification")
|
if (!isAutoApproved(lastMessage)) {
|
||||||
|
playSound("notification")
|
||||||
|
}
|
||||||
setTextAreaDisabled(isPartial)
|
setTextAreaDisabled(isPartial)
|
||||||
setClineAsk("tool")
|
setClineAsk("tool")
|
||||||
setEnableButtons(!isPartial)
|
setEnableButtons(!isPartial)
|
||||||
@@ -134,7 +128,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
case "browser_action_launch":
|
case "browser_action_launch":
|
||||||
playSoundOnMessage("notification")
|
if (!isAutoApproved(lastMessage)) {
|
||||||
|
playSound("notification")
|
||||||
|
}
|
||||||
setTextAreaDisabled(isPartial)
|
setTextAreaDisabled(isPartial)
|
||||||
setClineAsk("browser_action_launch")
|
setClineAsk("browser_action_launch")
|
||||||
setEnableButtons(!isPartial)
|
setEnableButtons(!isPartial)
|
||||||
@@ -142,7 +138,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
setSecondaryButtonText("Reject")
|
setSecondaryButtonText("Reject")
|
||||||
break
|
break
|
||||||
case "command":
|
case "command":
|
||||||
playSoundOnMessage("notification")
|
if (!isAutoApproved(lastMessage)) {
|
||||||
|
playSound("notification")
|
||||||
|
}
|
||||||
setTextAreaDisabled(isPartial)
|
setTextAreaDisabled(isPartial)
|
||||||
setClineAsk("command")
|
setClineAsk("command")
|
||||||
setEnableButtons(!isPartial)
|
setEnableButtons(!isPartial)
|
||||||
@@ -150,7 +148,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
setSecondaryButtonText("Reject")
|
setSecondaryButtonText("Reject")
|
||||||
break
|
break
|
||||||
case "command_output":
|
case "command_output":
|
||||||
playSoundOnMessage("notification")
|
|
||||||
setTextAreaDisabled(false)
|
setTextAreaDisabled(false)
|
||||||
setClineAsk("command_output")
|
setClineAsk("command_output")
|
||||||
setEnableButtons(true)
|
setEnableButtons(true)
|
||||||
@@ -166,7 +163,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
break
|
break
|
||||||
case "completion_result":
|
case "completion_result":
|
||||||
// extension waiting for feedback. but we can just present a new task button
|
// extension waiting for feedback. but we can just present a new task button
|
||||||
playSoundOnMessage("celebration")
|
playSound("celebration")
|
||||||
setTextAreaDisabled(isPartial)
|
setTextAreaDisabled(isPartial)
|
||||||
setClineAsk("completion_result")
|
setClineAsk("completion_result")
|
||||||
setEnableButtons(!isPartial)
|
setEnableButtons(!isPartial)
|
||||||
@@ -174,7 +171,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
setSecondaryButtonText(undefined)
|
setSecondaryButtonText(undefined)
|
||||||
break
|
break
|
||||||
case "resume_task":
|
case "resume_task":
|
||||||
playSoundOnMessage("notification")
|
|
||||||
setTextAreaDisabled(false)
|
setTextAreaDisabled(false)
|
||||||
setClineAsk("resume_task")
|
setClineAsk("resume_task")
|
||||||
setEnableButtons(true)
|
setEnableButtons(true)
|
||||||
@@ -183,7 +179,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
setDidClickCancel(false) // special case where we reset the cancel button state
|
setDidClickCancel(false) // special case where we reset the cancel button state
|
||||||
break
|
break
|
||||||
case "resume_completed_task":
|
case "resume_completed_task":
|
||||||
playSoundOnMessage("celebration")
|
playSound("celebration")
|
||||||
setTextAreaDisabled(false)
|
setTextAreaDisabled(false)
|
||||||
setClineAsk("resume_completed_task")
|
setClineAsk("resume_completed_task")
|
||||||
setEnableButtons(true)
|
setEnableButtons(true)
|
||||||
@@ -482,36 +478,122 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}, [modifiedMessages])
|
}, [modifiedMessages])
|
||||||
useEffect(() => {
|
|
||||||
if (isStreaming) {
|
const isReadOnlyToolAction = useCallback((message: ClineMessage | undefined) => {
|
||||||
// Set to true once any request has started
|
if (message?.type === "ask") {
|
||||||
setHasStarted(true)
|
if (!message.text) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const tool = JSON.parse(message.text)
|
||||||
|
return ["readFile", "listFiles", "listFilesTopLevel", "listFilesRecursive", "listCodeDefinitionNames", "searchFiles"].includes(tool.tool)
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const isWriteToolAction = useCallback((message: ClineMessage | undefined) => {
|
||||||
|
if (message?.type === "ask") {
|
||||||
|
if (!message.text) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const tool = JSON.parse(message.text)
|
||||||
|
return ["editedExistingFile", "appliedDiff", "newFileCreated"].includes(tool.tool)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const isMcpToolAlwaysAllowed = useCallback((message: ClineMessage | undefined) => {
|
||||||
|
if (message?.type === "ask" && message.ask === "use_mcp_server") {
|
||||||
|
if (!message.text) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const mcpServerUse = JSON.parse(message.text) as { type: string; serverName: string; toolName: string }
|
||||||
|
if (mcpServerUse.type === "use_mcp_tool") {
|
||||||
|
const server = mcpServers?.find((s: McpServer) => s.name === mcpServerUse.serverName)
|
||||||
|
const tool = server?.tools?.find((t: McpTool) => t.name === mcpServerUse.toolName)
|
||||||
|
return tool?.alwaysAllow || false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}, [mcpServers])
|
||||||
|
|
||||||
|
const isAllowedCommand = useCallback((message: ClineMessage | undefined) => {
|
||||||
|
if (message?.type === "ask") {
|
||||||
|
const command = message.text
|
||||||
|
if (!command) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split command by chaining operators
|
||||||
|
const commands = command.split(/&&|\|\||;|\||\$\(|`/).map(cmd => cmd.trim())
|
||||||
|
|
||||||
|
// Check if all individual commands are allowed
|
||||||
|
return commands.every((cmd) => {
|
||||||
|
const trimmedCommand = cmd.toLowerCase()
|
||||||
|
return allowedCommands?.some((prefix) => trimmedCommand.startsWith(prefix.toLowerCase()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}, [allowedCommands])
|
||||||
|
|
||||||
|
const isAutoApproved = useCallback(
|
||||||
|
(message: ClineMessage | undefined) => {
|
||||||
|
if (!message || message.type !== "ask") return false
|
||||||
|
|
||||||
|
return (
|
||||||
|
(alwaysAllowBrowser && message.ask === "browser_action_launch") ||
|
||||||
|
(alwaysAllowReadOnly && message.ask === "tool" && isReadOnlyToolAction(message)) ||
|
||||||
|
(alwaysAllowWrite && message.ask === "tool" && isWriteToolAction(message)) ||
|
||||||
|
(alwaysAllowExecute && message.ask === "command" && isAllowedCommand(message)) ||
|
||||||
|
(alwaysAllowMcp && message.ask === "use_mcp_server" && isMcpToolAlwaysAllowed(message))
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[
|
||||||
|
alwaysAllowBrowser,
|
||||||
|
alwaysAllowReadOnly,
|
||||||
|
alwaysAllowWrite,
|
||||||
|
alwaysAllowExecute,
|
||||||
|
alwaysAllowMcp,
|
||||||
|
isReadOnlyToolAction,
|
||||||
|
isWriteToolAction,
|
||||||
|
isAllowedCommand,
|
||||||
|
isMcpToolAlwaysAllowed
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
// Only execute when isStreaming changes from true to false
|
// Only execute when isStreaming changes from true to false
|
||||||
if (wasStreaming && !isStreaming && lastMessage) {
|
if (wasStreaming && !isStreaming && lastMessage) {
|
||||||
// Play appropriate sound based on lastMessage content
|
// Play appropriate sound based on lastMessage content
|
||||||
if (lastMessage.type === "ask") {
|
if (lastMessage.type === "ask") {
|
||||||
switch (lastMessage.ask) {
|
// Don't play sounds for auto-approved actions
|
||||||
case "api_req_failed":
|
if (!isAutoApproved(lastMessage)) {
|
||||||
case "mistake_limit_reached":
|
switch (lastMessage.ask) {
|
||||||
playSound("progress_loop")
|
case "api_req_failed":
|
||||||
break
|
case "mistake_limit_reached":
|
||||||
case "tool":
|
playSound("progress_loop")
|
||||||
case "followup":
|
break
|
||||||
case "browser_action_launch":
|
case "followup":
|
||||||
case "resume_task":
|
if (!lastMessage.partial) {
|
||||||
playSound("notification")
|
playSound("notification")
|
||||||
break
|
}
|
||||||
case "completion_result":
|
break
|
||||||
case "resume_completed_task":
|
case "tool":
|
||||||
playSound("celebration")
|
case "browser_action_launch":
|
||||||
break
|
case "resume_task":
|
||||||
|
case "use_mcp_server":
|
||||||
|
playSound("notification")
|
||||||
|
break
|
||||||
|
case "completion_result":
|
||||||
|
case "resume_completed_task":
|
||||||
|
playSound("celebration")
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Update previous value
|
// Update previous value
|
||||||
setWasStreaming(isStreaming)
|
setWasStreaming(isStreaming)
|
||||||
}, [isStreaming, lastMessage, wasStreaming])
|
}, [isStreaming, lastMessage, wasStreaming, isAutoApproved])
|
||||||
|
|
||||||
const isBrowserSessionMessage = (message: ClineMessage): boolean => {
|
const isBrowserSessionMessage = (message: ClineMessage): boolean => {
|
||||||
// which of visible messages are browser session messages, see above
|
// which of visible messages are browser session messages, see above
|
||||||
@@ -750,64 +832,10 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
// Only proceed if we have an ask and buttons are enabled
|
// Only proceed if we have an ask and buttons are enabled
|
||||||
if (!clineAsk || !enableButtons) return
|
if (!clineAsk || !enableButtons) return
|
||||||
|
|
||||||
const isReadOnlyToolAction = () => {
|
if (isAutoApproved(lastMessage)) {
|
||||||
const lastMessage = messages.at(-1)
|
|
||||||
if (lastMessage?.type === "ask" && lastMessage.text) {
|
|
||||||
const tool = JSON.parse(lastMessage.text)
|
|
||||||
return ["readFile", "listFiles", "listFilesTopLevel", "listFilesRecursive", "listCodeDefinitionNames", "searchFiles"].includes(tool.tool)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const isWriteToolAction = () => {
|
|
||||||
const lastMessage = messages.at(-1)
|
|
||||||
if (lastMessage?.type === "ask" && lastMessage.text) {
|
|
||||||
const tool = JSON.parse(lastMessage.text)
|
|
||||||
return ["editedExistingFile", "appliedDiff", "newFileCreated"].includes(tool.tool)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const isMcpToolAlwaysAllowed = () => {
|
|
||||||
const lastMessage = messages.at(-1)
|
|
||||||
if (lastMessage?.type === "ask" && lastMessage.ask === "use_mcp_server" && lastMessage.text) {
|
|
||||||
const mcpServerUse = JSON.parse(lastMessage.text) as { type: string; serverName: string; toolName: string }
|
|
||||||
if (mcpServerUse.type === "use_mcp_tool") {
|
|
||||||
const server = mcpServers?.find((s: McpServer) => s.name === mcpServerUse.serverName)
|
|
||||||
const tool = server?.tools?.find((t: McpTool) => t.name === mcpServerUse.toolName)
|
|
||||||
return tool?.alwaysAllow || false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAllowedCommand = () => {
|
|
||||||
const lastMessage = messages.at(-1)
|
|
||||||
if (lastMessage?.type === "ask" && lastMessage.text) {
|
|
||||||
const command = lastMessage.text
|
|
||||||
|
|
||||||
// Split command by chaining operators
|
|
||||||
const commands = command.split(/&&|\|\||;|\||\$\(|`/).map(cmd => cmd.trim())
|
|
||||||
|
|
||||||
// Check if all individual commands are allowed
|
|
||||||
return commands.every((cmd) => {
|
|
||||||
const trimmedCommand = cmd.toLowerCase()
|
|
||||||
return allowedCommands?.some((prefix) => trimmedCommand.startsWith(prefix.toLowerCase()))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
(alwaysAllowBrowser && clineAsk === "browser_action_launch") ||
|
|
||||||
(alwaysAllowReadOnly && clineAsk === "tool" && isReadOnlyToolAction()) ||
|
|
||||||
(alwaysAllowWrite && clineAsk === "tool" && isWriteToolAction()) ||
|
|
||||||
(alwaysAllowExecute && clineAsk === "command" && isAllowedCommand()) ||
|
|
||||||
(alwaysAllowMcp && clineAsk === "use_mcp_server" && isMcpToolAlwaysAllowed())
|
|
||||||
) {
|
|
||||||
handlePrimaryButtonClick()
|
handlePrimaryButtonClick()
|
||||||
}
|
}
|
||||||
}, [clineAsk, enableButtons, handlePrimaryButtonClick, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, alwaysAllowMcp, messages, allowedCommands, mcpServers])
|
}, [clineAsk, enableButtons, handlePrimaryButtonClick, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, alwaysAllowMcp, messages, allowedCommands, mcpServers, isAutoApproved, lastMessage])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -547,3 +547,247 @@ describe('ChatView - Auto Approval Tests', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('ChatView - Sound Playing Tests', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not play sound for auto-approved browser actions', async () => {
|
||||||
|
render(
|
||||||
|
<ExtensionStateContextProvider>
|
||||||
|
<ChatView
|
||||||
|
isHidden={false}
|
||||||
|
showAnnouncement={false}
|
||||||
|
hideAnnouncement={() => {}}
|
||||||
|
showHistoryView={() => {}}
|
||||||
|
/>
|
||||||
|
</ExtensionStateContextProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
// First hydrate state with initial task and streaming
|
||||||
|
mockPostMessage({
|
||||||
|
alwaysAllowBrowser: true,
|
||||||
|
clineMessages: [
|
||||||
|
{
|
||||||
|
type: 'say',
|
||||||
|
say: 'task',
|
||||||
|
ts: Date.now() - 2000,
|
||||||
|
text: 'Initial task'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'say',
|
||||||
|
say: 'api_req_started',
|
||||||
|
ts: Date.now() - 1000,
|
||||||
|
text: JSON.stringify({}),
|
||||||
|
partial: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Then send the browser action ask message (streaming finished)
|
||||||
|
mockPostMessage({
|
||||||
|
alwaysAllowBrowser: true,
|
||||||
|
clineMessages: [
|
||||||
|
{
|
||||||
|
type: 'say',
|
||||||
|
say: 'task',
|
||||||
|
ts: Date.now() - 2000,
|
||||||
|
text: 'Initial task'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'ask',
|
||||||
|
ask: 'browser_action_launch',
|
||||||
|
ts: Date.now(),
|
||||||
|
text: JSON.stringify({ action: 'launch', url: 'http://example.com' }),
|
||||||
|
partial: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Verify no sound was played
|
||||||
|
expect(vscode.postMessage).not.toHaveBeenCalledWith({
|
||||||
|
type: 'playSound',
|
||||||
|
audioType: expect.any(String)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('plays notification sound for non-auto-approved browser actions', async () => {
|
||||||
|
render(
|
||||||
|
<ExtensionStateContextProvider>
|
||||||
|
<ChatView
|
||||||
|
isHidden={false}
|
||||||
|
showAnnouncement={false}
|
||||||
|
hideAnnouncement={() => {}}
|
||||||
|
showHistoryView={() => {}}
|
||||||
|
/>
|
||||||
|
</ExtensionStateContextProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
// First hydrate state with initial task and streaming
|
||||||
|
mockPostMessage({
|
||||||
|
alwaysAllowBrowser: false,
|
||||||
|
clineMessages: [
|
||||||
|
{
|
||||||
|
type: 'say',
|
||||||
|
say: 'task',
|
||||||
|
ts: Date.now() - 2000,
|
||||||
|
text: 'Initial task'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'say',
|
||||||
|
say: 'api_req_started',
|
||||||
|
ts: Date.now() - 1000,
|
||||||
|
text: JSON.stringify({}),
|
||||||
|
partial: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Then send the browser action ask message (streaming finished)
|
||||||
|
mockPostMessage({
|
||||||
|
alwaysAllowBrowser: false,
|
||||||
|
clineMessages: [
|
||||||
|
{
|
||||||
|
type: 'say',
|
||||||
|
say: 'task',
|
||||||
|
ts: Date.now() - 2000,
|
||||||
|
text: 'Initial task'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'ask',
|
||||||
|
ask: 'browser_action_launch',
|
||||||
|
ts: Date.now(),
|
||||||
|
text: JSON.stringify({ action: 'launch', url: 'http://example.com' }),
|
||||||
|
partial: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Verify notification sound was played
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||||
|
type: 'playSound',
|
||||||
|
audioType: 'notification'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('plays celebration sound for completion results', async () => {
|
||||||
|
render(
|
||||||
|
<ExtensionStateContextProvider>
|
||||||
|
<ChatView
|
||||||
|
isHidden={false}
|
||||||
|
showAnnouncement={false}
|
||||||
|
hideAnnouncement={() => {}}
|
||||||
|
showHistoryView={() => {}}
|
||||||
|
/>
|
||||||
|
</ExtensionStateContextProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
// First hydrate state with initial task and streaming
|
||||||
|
mockPostMessage({
|
||||||
|
clineMessages: [
|
||||||
|
{
|
||||||
|
type: 'say',
|
||||||
|
say: 'task',
|
||||||
|
ts: Date.now() - 2000,
|
||||||
|
text: 'Initial task'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'say',
|
||||||
|
say: 'api_req_started',
|
||||||
|
ts: Date.now() - 1000,
|
||||||
|
text: JSON.stringify({}),
|
||||||
|
partial: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Then send the completion result message (streaming finished)
|
||||||
|
mockPostMessage({
|
||||||
|
clineMessages: [
|
||||||
|
{
|
||||||
|
type: 'say',
|
||||||
|
say: 'task',
|
||||||
|
ts: Date.now() - 2000,
|
||||||
|
text: 'Initial task'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'ask',
|
||||||
|
ask: 'completion_result',
|
||||||
|
ts: Date.now(),
|
||||||
|
text: 'Task completed successfully',
|
||||||
|
partial: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Verify celebration sound was played
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||||
|
type: 'playSound',
|
||||||
|
audioType: 'celebration'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('plays progress_loop sound for api failures', async () => {
|
||||||
|
render(
|
||||||
|
<ExtensionStateContextProvider>
|
||||||
|
<ChatView
|
||||||
|
isHidden={false}
|
||||||
|
showAnnouncement={false}
|
||||||
|
hideAnnouncement={() => {}}
|
||||||
|
showHistoryView={() => {}}
|
||||||
|
/>
|
||||||
|
</ExtensionStateContextProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
// First hydrate state with initial task and streaming
|
||||||
|
mockPostMessage({
|
||||||
|
clineMessages: [
|
||||||
|
{
|
||||||
|
type: 'say',
|
||||||
|
say: 'task',
|
||||||
|
ts: Date.now() - 2000,
|
||||||
|
text: 'Initial task'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'say',
|
||||||
|
say: 'api_req_started',
|
||||||
|
ts: Date.now() - 1000,
|
||||||
|
text: JSON.stringify({}),
|
||||||
|
partial: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Then send the api failure message (streaming finished)
|
||||||
|
mockPostMessage({
|
||||||
|
clineMessages: [
|
||||||
|
{
|
||||||
|
type: 'say',
|
||||||
|
say: 'task',
|
||||||
|
ts: Date.now() - 2000,
|
||||||
|
text: 'Initial task'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'ask',
|
||||||
|
ask: 'api_req_failed',
|
||||||
|
ts: Date.now(),
|
||||||
|
text: 'API request failed',
|
||||||
|
partial: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Verify progress_loop sound was played
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||||
|
type: 'playSound',
|
||||||
|
audioType: 'progress_loop'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ jest.mock('@vscode/webview-ui-toolkit/react', () => ({
|
|||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
role="checkbox"
|
|
||||||
checked={checked}
|
checked={checked}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -29,8 +29,12 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
setAlwaysAllowMcp,
|
setAlwaysAllowMcp,
|
||||||
soundEnabled,
|
soundEnabled,
|
||||||
setSoundEnabled,
|
setSoundEnabled,
|
||||||
|
soundVolume,
|
||||||
|
setSoundVolume,
|
||||||
diffEnabled,
|
diffEnabled,
|
||||||
setDiffEnabled,
|
setDiffEnabled,
|
||||||
|
debugDiffEnabled,
|
||||||
|
setDebugDiffEnabled,
|
||||||
openRouterModels,
|
openRouterModels,
|
||||||
setAllowedCommands,
|
setAllowedCommands,
|
||||||
allowedCommands,
|
allowedCommands,
|
||||||
@@ -46,7 +50,10 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
setApiErrorMessage(apiValidationResult)
|
setApiErrorMessage(apiValidationResult)
|
||||||
setModelIdErrorMessage(modelIdValidationResult)
|
setModelIdErrorMessage(modelIdValidationResult)
|
||||||
if (!apiValidationResult && !modelIdValidationResult) {
|
if (!apiValidationResult && !modelIdValidationResult) {
|
||||||
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
|
vscode.postMessage({
|
||||||
|
type: "apiConfiguration",
|
||||||
|
apiConfiguration
|
||||||
|
})
|
||||||
vscode.postMessage({ type: "customInstructions", text: customInstructions })
|
vscode.postMessage({ type: "customInstructions", text: customInstructions })
|
||||||
vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly })
|
vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly })
|
||||||
vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite })
|
vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite })
|
||||||
@@ -55,7 +62,9 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
vscode.postMessage({ type: "alwaysAllowMcp", bool: alwaysAllowMcp })
|
vscode.postMessage({ type: "alwaysAllowMcp", bool: alwaysAllowMcp })
|
||||||
vscode.postMessage({ type: "allowedCommands", commands: allowedCommands ?? [] })
|
vscode.postMessage({ type: "allowedCommands", commands: allowedCommands ?? [] })
|
||||||
vscode.postMessage({ type: "soundEnabled", bool: soundEnabled })
|
vscode.postMessage({ type: "soundEnabled", bool: soundEnabled })
|
||||||
|
vscode.postMessage({ type: "soundVolume", value: soundVolume })
|
||||||
vscode.postMessage({ type: "diffEnabled", bool: diffEnabled })
|
vscode.postMessage({ type: "diffEnabled", bool: diffEnabled })
|
||||||
|
vscode.postMessage({ type: "debugDiffEnabled", bool: debugDiffEnabled })
|
||||||
onDone()
|
onDone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,7 +164,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
marginTop: "5px",
|
marginTop: "5px",
|
||||||
color: "var(--vscode-descriptionForeground)",
|
color: "var(--vscode-descriptionForeground)",
|
||||||
}}>
|
}}>
|
||||||
When enabled, Cline will be able to edit files more quickly and will automatically reject truncated full-file writes.
|
When enabled, Cline will be able to edit files more quickly and will automatically reject truncated full-file writes. Works best with the latest Claude 3.5 Sonnet model.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -312,8 +321,47 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
<h4 style={{ fontWeight: 500, marginBottom: 10 }}>Experimental Features</h4>
|
<h4 style={{ fontWeight: 500, marginBottom: 10 }}>Experimental Features</h4>
|
||||||
|
|
||||||
<div style={{ marginBottom: 5 }}>
|
<div style={{ marginBottom: 5 }}>
|
||||||
<VSCodeCheckbox checked={soundEnabled} onChange={(e: any) => setSoundEnabled(e.target.checked)}>
|
<div style={{ marginBottom: 10 }}>
|
||||||
<span style={{ fontWeight: "500" }}>Enable sound effects</span>
|
<VSCodeCheckbox checked={soundEnabled} onChange={(e: any) => setSoundEnabled(e.target.checked)}>
|
||||||
|
<span style={{ fontWeight: "500" }}>Enable sound effects</span>
|
||||||
|
</VSCodeCheckbox>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
fontSize: "12px",
|
||||||
|
marginTop: "5px",
|
||||||
|
color: "var(--vscode-descriptionForeground)",
|
||||||
|
}}>
|
||||||
|
When enabled, Cline will play sound effects for notifications and events.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{soundEnabled && (
|
||||||
|
<div style={{ marginLeft: 0 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||||||
|
<span style={{ fontWeight: "500", minWidth: '50px' }}>Volume</span>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.01"
|
||||||
|
value={soundVolume ?? 0.5}
|
||||||
|
onChange={(e) => setSoundVolume(parseFloat(e.target.value))}
|
||||||
|
style={{
|
||||||
|
flexGrow: 1,
|
||||||
|
accentColor: 'var(--vscode-button-background)',
|
||||||
|
height: '2px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span style={{ minWidth: '35px', textAlign: 'left' }}>
|
||||||
|
{Math.round((soundVolume ?? 0.5) * 100)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: 5 }}>
|
||||||
|
<VSCodeCheckbox checked={debugDiffEnabled} onChange={(e: any) => setDebugDiffEnabled(e.target.checked)}>
|
||||||
|
<span style={{ fontWeight: "500" }}>Debug diff operations</span>
|
||||||
</VSCodeCheckbox>
|
</VSCodeCheckbox>
|
||||||
<p
|
<p
|
||||||
style={{
|
style={{
|
||||||
@@ -321,7 +369,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
marginTop: "5px",
|
marginTop: "5px",
|
||||||
color: "var(--vscode-descriptionForeground)",
|
color: "var(--vscode-descriptionForeground)",
|
||||||
}}>
|
}}>
|
||||||
When enabled, Cline will play sound effects for notifications and events.
|
When enabled, Cline will show detailed debug information when applying diffs fails.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ jest.mock('@vscode/webview-ui-toolkit/react', () => ({
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
VSCodeTextArea: () => <textarea />,
|
VSCodeTextArea: () => <textarea />,
|
||||||
VSCodeLink: () => <a />,
|
VSCodeLink: ({ children, href }: any) => <a href={href || '#'}>{children}</a>,
|
||||||
VSCodeDropdown: ({ children, value, onChange }: any) => (
|
VSCodeDropdown: ({ children, value, onChange }: any) => (
|
||||||
<select value={value} onChange={onChange}>
|
<select value={value} onChange={onChange}>
|
||||||
{children}
|
{children}
|
||||||
@@ -104,6 +104,9 @@ describe('SettingsView - Sound Settings', () => {
|
|||||||
name: /Enable sound effects/i
|
name: /Enable sound effects/i
|
||||||
})
|
})
|
||||||
expect(soundCheckbox).not.toBeChecked()
|
expect(soundCheckbox).not.toBeChecked()
|
||||||
|
|
||||||
|
// Volume slider should not be visible when sound is disabled
|
||||||
|
expect(screen.queryByRole('slider')).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('toggles sound setting and sends message to VSCode', () => {
|
it('toggles sound setting and sends message to VSCode', () => {
|
||||||
@@ -128,6 +131,50 @@ describe('SettingsView - Sound Settings', () => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('shows volume slider when sound is enabled', () => {
|
||||||
|
renderSettingsView()
|
||||||
|
|
||||||
|
// Enable sound
|
||||||
|
const soundCheckbox = screen.getByRole('checkbox', {
|
||||||
|
name: /Enable sound effects/i
|
||||||
|
})
|
||||||
|
fireEvent.click(soundCheckbox)
|
||||||
|
|
||||||
|
// Volume slider should be visible
|
||||||
|
const volumeSlider = screen.getByRole('slider')
|
||||||
|
expect(volumeSlider).toBeInTheDocument()
|
||||||
|
expect(volumeSlider).toHaveValue('0.5') // Default value
|
||||||
|
})
|
||||||
|
|
||||||
|
it('updates volume and sends message to VSCode when slider changes', () => {
|
||||||
|
renderSettingsView()
|
||||||
|
|
||||||
|
// Enable sound
|
||||||
|
const soundCheckbox = screen.getByRole('checkbox', {
|
||||||
|
name: /Enable sound effects/i
|
||||||
|
})
|
||||||
|
fireEvent.click(soundCheckbox)
|
||||||
|
|
||||||
|
// Change volume
|
||||||
|
const volumeSlider = screen.getByRole('slider')
|
||||||
|
fireEvent.change(volumeSlider, { target: { value: '0.75' } })
|
||||||
|
|
||||||
|
// Verify volume display updates
|
||||||
|
expect(screen.getByText('75%')).toBeInTheDocument()
|
||||||
|
|
||||||
|
// Click Done to save settings
|
||||||
|
const doneButton = screen.getByText('Done')
|
||||||
|
fireEvent.click(doneButton)
|
||||||
|
|
||||||
|
// Verify message sent to VSCode
|
||||||
|
expect(vscode.postMessage).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: 'soundVolume',
|
||||||
|
value: 0.75
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('SettingsView - Allowed Commands', () => {
|
describe('SettingsView - Allowed Commands', () => {
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||||||
setShowAnnouncement: (value: boolean) => void
|
setShowAnnouncement: (value: boolean) => void
|
||||||
setAllowedCommands: (value: string[]) => void
|
setAllowedCommands: (value: string[]) => void
|
||||||
setSoundEnabled: (value: boolean) => void
|
setSoundEnabled: (value: boolean) => void
|
||||||
|
setSoundVolume: (value: number) => void
|
||||||
setDiffEnabled: (value: boolean) => void
|
setDiffEnabled: (value: boolean) => void
|
||||||
|
setDebugDiffEnabled: (value: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
|
const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
|
||||||
@@ -42,7 +44,9 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||||||
shouldShowAnnouncement: false,
|
shouldShowAnnouncement: false,
|
||||||
allowedCommands: [],
|
allowedCommands: [],
|
||||||
soundEnabled: false,
|
soundEnabled: false,
|
||||||
|
soundVolume: 0.5,
|
||||||
diffEnabled: false,
|
diffEnabled: false,
|
||||||
|
debugDiffEnabled: false,
|
||||||
})
|
})
|
||||||
const [didHydrateState, setDidHydrateState] = useState(false)
|
const [didHydrateState, setDidHydrateState] = useState(false)
|
||||||
const [showWelcome, setShowWelcome] = useState(false)
|
const [showWelcome, setShowWelcome] = useState(false)
|
||||||
@@ -129,7 +133,11 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||||||
openRouterModels,
|
openRouterModels,
|
||||||
mcpServers,
|
mcpServers,
|
||||||
filePaths,
|
filePaths,
|
||||||
setApiConfiguration: (value) => setState((prevState) => ({ ...prevState, apiConfiguration: value })),
|
soundVolume: state.soundVolume,
|
||||||
|
setApiConfiguration: (value) => setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
apiConfiguration: value
|
||||||
|
})),
|
||||||
setCustomInstructions: (value) => setState((prevState) => ({ ...prevState, customInstructions: value })),
|
setCustomInstructions: (value) => setState((prevState) => ({ ...prevState, customInstructions: value })),
|
||||||
setAlwaysAllowReadOnly: (value) => setState((prevState) => ({ ...prevState, alwaysAllowReadOnly: value })),
|
setAlwaysAllowReadOnly: (value) => setState((prevState) => ({ ...prevState, alwaysAllowReadOnly: value })),
|
||||||
setAlwaysAllowWrite: (value) => setState((prevState) => ({ ...prevState, alwaysAllowWrite: value })),
|
setAlwaysAllowWrite: (value) => setState((prevState) => ({ ...prevState, alwaysAllowWrite: value })),
|
||||||
@@ -139,7 +147,12 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||||||
setShowAnnouncement: (value) => setState((prevState) => ({ ...prevState, shouldShowAnnouncement: value })),
|
setShowAnnouncement: (value) => setState((prevState) => ({ ...prevState, shouldShowAnnouncement: value })),
|
||||||
setAllowedCommands: (value) => setState((prevState) => ({ ...prevState, allowedCommands: value })),
|
setAllowedCommands: (value) => setState((prevState) => ({ ...prevState, allowedCommands: value })),
|
||||||
setSoundEnabled: (value) => setState((prevState) => ({ ...prevState, soundEnabled: value })),
|
setSoundEnabled: (value) => setState((prevState) => ({ ...prevState, soundEnabled: value })),
|
||||||
|
setSoundVolume: (value) => setState((prevState) => ({ ...prevState, soundVolume: value })),
|
||||||
setDiffEnabled: (value) => setState((prevState) => ({ ...prevState, diffEnabled: value })),
|
setDiffEnabled: (value) => setState((prevState) => ({ ...prevState, diffEnabled: value })),
|
||||||
|
setDebugDiffEnabled: (value) => setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
debugDiffEnabled: value
|
||||||
|
})),
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
|
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
|
||||||
|
|||||||
Reference in New Issue
Block a user