mirror of
https://github.com/cline/cline.git
synced 2026-09-04 11:44:01 +08:00
Compare commits
108 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 19606382ed | |||
| 3389aa7d14 | |||
| 23547207f2 | |||
| 540aa1d810 | |||
| 60d3048aa0 | |||
| aed3ac6597 | |||
| 86e2a3e7ce | |||
| 47856c70d2 | |||
| f1a84ddbde | |||
| ebcc927cc7 | |||
| 1e108d87e0 | |||
| 7b62d7786e | |||
| 042f5c9823 | |||
| 2f8a4525a4 | |||
| 20774a4187 | |||
| be5bda2740 | |||
| 261fd9036f | |||
| 2334d4d531 | |||
| 6390d854f7 | |||
| 7d5c56a55a | |||
| 0dc760ac03 | |||
| 8c1241c8cd | |||
| 17686ae3d9 | |||
| 97460d2952 | |||
| e629ed0ef6 | |||
| 96788e9127 | |||
| d7716a514d | |||
| 7aaa5966d6 | |||
| bb1d068139 | |||
| 450945ae0e | |||
| 191e9635bd | |||
| c13a7a80b3 | |||
| d4a4adfa5f | |||
| 557e20224e | |||
| 0e9a326a6a | |||
| f5ecb6db0c | |||
| 8f1405b881 | |||
| 8a9e03c8ff | |||
| edba02b45e | |||
| cc36c67fc9 | |||
| d77032bc8a | |||
| 608dde94b3 | |||
| 47ff7c1620 | |||
| 1b7f971c34 | |||
| 12eadd3378 | |||
| b3e0ef9ed7 | |||
| fb94d8d3d4 | |||
| d11bd15d60 | |||
| 31c48898a6 | |||
| 5c9901d68a | |||
| 45b79dc3d7 | |||
| 26b6c7bdb6 | |||
| d0678a2ad1 | |||
| 09276ebf43 | |||
| af8b51b189 | |||
| 3e6b3f252b | |||
| f019c365a6 | |||
| f6fe843cfb | |||
| 01f26c21ae | |||
| 031c2f5b05 | |||
| c999e269db | |||
| 032c1bf792 | |||
| fa73d60b7a | |||
| 841bb7f1d7 | |||
| f01428884d | |||
| 2ce5548250 | |||
| 9f32cd247f | |||
| ace48198cc | |||
| 9f0240dfd7 | |||
| 5530cfe375 | |||
| af4d99e0bc | |||
| cd011a0e4a | |||
| d44184ab03 | |||
| 5d96704e92 | |||
| 49812eb332 | |||
| 57b72519ae | |||
| 1ed4d00a16 | |||
| c090f5b1a7 | |||
| 3c917ec99d | |||
| b315be397d | |||
| dc9e7916de | |||
| cb9d1e81b8 | |||
| eb5a452c9c | |||
| 6a90294a2a | |||
| 2e1334c10a | |||
| 18b77ee5e5 | |||
| 4bf8feecac | |||
| 2f3450f667 | |||
| 3e89c28727 | |||
| d065ac7b37 | |||
| 5a0a25601a | |||
| 48d822a030 | |||
| 417104505c | |||
| e21d3ff1bb | |||
| da5477f891 | |||
| 4903dfcb6e | |||
| 37b2f7fbc9 | |||
| 11fbe4b21d | |||
| e0844ac6e2 | |||
| f28d760675 | |||
| 8363d090e0 | |||
| 3409fa7442 | |||
| a20685d289 | |||
| 926c5e189e | |||
| 2a5ca9d312 | |||
| d06717342b | |||
| c904cfe376 | |||
| 924ca1278c |
@@ -1,194 +0,0 @@
|
||||
# Hotfix Release
|
||||
|
||||
Create a hotfix release by cherry-picking specific commits from main onto the latest release tag.
|
||||
|
||||
## Overview
|
||||
|
||||
This workflow helps you:
|
||||
1. Select specific commits from main to include in a hotfix
|
||||
2. Create a release notes commit on main (changelog + version bump)
|
||||
3. Cherry-pick everything onto the latest release tag
|
||||
4. Tag and push the new release
|
||||
|
||||
## Step 1: Setup and Gather Information
|
||||
|
||||
First, ensure we're on main and up to date:
|
||||
|
||||
```bash
|
||||
git checkout main && git pull origin main
|
||||
```
|
||||
|
||||
Get the latest release tag:
|
||||
|
||||
```bash
|
||||
git tag --sort=-v:refname | head -1
|
||||
```
|
||||
|
||||
## Step 2: Present Commits Since Last Release
|
||||
|
||||
Show all commits on main since the last release tag:
|
||||
|
||||
```bash
|
||||
LAST_TAG=$(git tag --sort=-v:refname | head -1)
|
||||
git log ${LAST_TAG}..HEAD --oneline --format="%h %s (%an)"
|
||||
```
|
||||
|
||||
Also get the commit messages already on the tag (to identify previously cherry-picked commits). Note: Run these as separate commands to avoid shell parsing issues with parentheses in author names:
|
||||
|
||||
```bash
|
||||
LAST_TAG=$(git tag --sort=-v:refname | head -1)
|
||||
PREV_TAG=$(git tag --sort=-v:refname | head -2 | tail -1)
|
||||
```
|
||||
|
||||
```bash
|
||||
git log $PREV_TAG..$LAST_TAG --oneline --format="%s"
|
||||
```
|
||||
|
||||
**Present the list** to the user in a numbered format with commit hash, subject, and author. For any commits whose subject line already appears in the tag's history (previously cherry-picked in an earlier hotfix) or are "Release Notes" commits, add `(already in previous hotfix)` or `(release notes - skip)` after them so the user knows to skip those.
|
||||
|
||||
Ask which commits to include in the hotfix.
|
||||
|
||||
Use the ask_followup_question tool to let the user specify which commits they want (by number or hash).
|
||||
|
||||
## Step 3: Analyze Selected Commits
|
||||
|
||||
For each selected commit:
|
||||
1. Get the full commit message: `git show --no-patch --format="%B" <hash>`
|
||||
2. Get the diff to understand the change: `git show <hash> --stat`
|
||||
3. Find the associated PR if any: `gh pr list --search "<hash>" --state merged --json number,title --jq '.[0]'`
|
||||
|
||||
Build a mental model of what these changes do for the changelog.
|
||||
|
||||
## Step 4: Determine New Version Number
|
||||
|
||||
Parse the current version from package.json and the last tag:
|
||||
|
||||
```bash
|
||||
LAST_TAG=$(git tag --sort=-v:refname | head -1)
|
||||
echo "Last release: $LAST_TAG"
|
||||
cat package.json | grep '"version"'
|
||||
```
|
||||
|
||||
Hotfixes always increment the patch version (e.g., 3.40.0 -> 3.40.1, or 3.40.1 -> 3.40.2).
|
||||
|
||||
**Ask the user to confirm the new version number.**
|
||||
|
||||
## Step 5: Create Release Notes Commit on Main
|
||||
|
||||
On the main branch, create a commit that updates:
|
||||
|
||||
1. **CHANGELOG.md** - Add a new section for the hotfix version at the top:
|
||||
```markdown
|
||||
## [3.40.1]
|
||||
|
||||
- Description of fix 1
|
||||
- Description of fix 2
|
||||
```
|
||||
|
||||
Write clear, user-friendly descriptions based on your analysis of the commits.
|
||||
|
||||
2. **package.json** - Update the version field to the new version
|
||||
|
||||
3. **Delete changesets** for the commits being included in the hotfix. This prevents the changeset bot from including duplicate entries in the next regular release.
|
||||
|
||||
Find and delete the changeset files associated with the selected commits:
|
||||
```bash
|
||||
ls .changeset/
|
||||
```
|
||||
|
||||
Each changeset file in `.changeset/` corresponds to a PR. Read them to identify which ones belong to the commits you're hotfixing, then delete those files.
|
||||
|
||||
**Skip running `npm run install:all`** - the automation handles outdated lockfiles.
|
||||
|
||||
Commit with message format: `v{VERSION} Release Notes (hotfix)`
|
||||
|
||||
In the commit body, mention:
|
||||
- This is for a hotfix release
|
||||
- List the cherry-picked commits that will be included
|
||||
|
||||
```bash
|
||||
git add CHANGELOG.md package.json .changeset/
|
||||
git commit -m "v3.40.1 Release Notes (hotfix)
|
||||
|
||||
Hotfix release including:
|
||||
- <commit1-hash>: <description>
|
||||
- <commit2-hash>: <description>
|
||||
"
|
||||
```
|
||||
|
||||
Push to main:
|
||||
|
||||
```bash
|
||||
git push origin main
|
||||
```
|
||||
|
||||
## Step 6: Build the Hotfix on the Tag
|
||||
|
||||
Checkout the last release tag (detached HEAD):
|
||||
|
||||
```bash
|
||||
LAST_TAG=$(git tag --sort=-v:refname | head -1)
|
||||
git checkout $LAST_TAG
|
||||
```
|
||||
|
||||
Cherry-pick the selected commits in order:
|
||||
|
||||
```bash
|
||||
git cherry-pick <commit1-hash>
|
||||
git cherry-pick <commit2-hash>
|
||||
# ... etc
|
||||
```
|
||||
|
||||
Finally, cherry-pick the release notes commit you just pushed to main:
|
||||
|
||||
```bash
|
||||
# Get the hash of the release notes commit (should be HEAD of main)
|
||||
RELEASE_NOTES_COMMIT=$(git rev-parse main)
|
||||
git cherry-pick $RELEASE_NOTES_COMMIT
|
||||
```
|
||||
|
||||
## Step 7: Tag and Push
|
||||
|
||||
After all cherry-picks are applied successfully:
|
||||
|
||||
```bash
|
||||
# Tag the new release
|
||||
git tag v{VERSION}
|
||||
|
||||
# Push the tag to remote
|
||||
git push origin v{VERSION}
|
||||
```
|
||||
|
||||
## Step 8: Return to Main and Summary
|
||||
|
||||
Return to main branch:
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
```
|
||||
|
||||
**Copy a Slack announcement message to clipboard** with the version and PR links for each included fix:
|
||||
|
||||
```
|
||||
VS Code Hotfix v{VERSION} Published
|
||||
|
||||
- Description of fix 1 https://github.com/cline/cline/pull/{PR_NUMBER}
|
||||
- Description of fix 2 https://github.com/cline/cline/pull/{PR_NUMBER}
|
||||
```
|
||||
|
||||
Present a final summary:
|
||||
- New version: v{VERSION}
|
||||
- Tag pushed: yes
|
||||
- Commits included: (list them)
|
||||
- Slack message copied to clipboard: yes
|
||||
|
||||
Remind the user to:
|
||||
1. Manually trigger the publish release GitHub Action at: https://github.com/cline/cline/actions/workflows/publish.yml (paste `v{VERSION}` as the tag)
|
||||
2. Post the Slack message to announce the hotfix
|
||||
|
||||
## Important Notes
|
||||
|
||||
- This workflow does NOT create a release branch - only tags
|
||||
- The release notes commit goes to main first, then gets cherry-picked to the tag
|
||||
- This keeps main's history accurate while allowing hotfix releases from tags
|
||||
- If cherry-pick conflicts occur, resolve them before continuing
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../.clinerules/workflows/hotfix-release.md
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../.clinerules/workflows/release.md
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Only run in Claude Code remote environments
|
||||
if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cd "$CLAUDE_PROJECT_DIR"
|
||||
|
||||
echo "=== Claude Code for Web Setup ==="
|
||||
echo ""
|
||||
|
||||
# Install latest gh CLI tool
|
||||
echo "Installing GitHub CLI..."
|
||||
GH_VERSION=$(curl -s https://api.github.com/repos/cli/cli/releases/latest | grep '"tag_name"' | cut -d'"' -f4 | sed 's/^v//')
|
||||
curl -sL "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_amd64.tar.gz" -o /tmp/gh.tar.gz
|
||||
tar -xzf /tmp/gh.tar.gz -C /tmp
|
||||
sudo mv "/tmp/gh_${GH_VERSION}_linux_amd64/bin/gh" /usr/local/bin/gh
|
||||
rm -rf /tmp/gh.tar.gz /tmp/gh_${GH_VERSION}_linux_amd64
|
||||
echo "Installed gh version: $(gh --version | head -1)"
|
||||
echo ""
|
||||
|
||||
# Check if GITHUB_TOKEN is set and configure gh
|
||||
if [ -n "${GITHUB_TOKEN:-}" ]; then
|
||||
echo "GITHUB_TOKEN is configured - gh CLI is ready to use"
|
||||
echo ""
|
||||
echo "You can use gh commands directly, for example:"
|
||||
echo " gh issue list --repo cline/cline --limit 5"
|
||||
echo " gh pr list --repo cline/cline --state open"
|
||||
echo " gh issue view 123 --repo cline/cline"
|
||||
echo ""
|
||||
else
|
||||
echo "GITHUB_TOKEN is not set - gh CLI will have limited functionality"
|
||||
echo ""
|
||||
echo "To enable full GitHub API access:"
|
||||
echo "1. Create a Fine-grained Personal Access Token at https://github.com/settings/tokens?type=beta"
|
||||
echo "2. Add it as GITHUB_TOKEN in your Claude Code environment settings"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Install project dependencies
|
||||
echo "Installing dependencies..."
|
||||
npm run install:all
|
||||
|
||||
# Generate gRPC/protobuf types (required for TypeScript)
|
||||
echo "Generating proto types..."
|
||||
npm run protos
|
||||
|
||||
echo ""
|
||||
echo "Session setup complete!"
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/claude-code-for-web-setup.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
../../.claude/commands/hotfix-release.md
|
||||
@@ -0,0 +1,194 @@
|
||||
# Hotfix Release
|
||||
|
||||
Create a hotfix release by cherry-picking specific commits from main onto the latest release tag.
|
||||
|
||||
## Overview
|
||||
|
||||
This workflow helps you:
|
||||
1. Select specific commits from main to include in a hotfix
|
||||
2. Create a release notes commit on main (changelog + version bump)
|
||||
3. Cherry-pick everything onto the latest release tag
|
||||
4. Tag and push the new release
|
||||
|
||||
## Step 1: Setup and Gather Information
|
||||
|
||||
First, ensure we're on main and up to date:
|
||||
|
||||
```bash
|
||||
git checkout main && git pull origin main
|
||||
```
|
||||
|
||||
Get the latest release tag:
|
||||
|
||||
```bash
|
||||
git tag --sort=-v:refname | head -1
|
||||
```
|
||||
|
||||
## Step 2: Present Commits Since Last Release
|
||||
|
||||
Show all commits on main since the last release tag:
|
||||
|
||||
```bash
|
||||
LAST_TAG=$(git tag --sort=-v:refname | head -1)
|
||||
git log ${LAST_TAG}..HEAD --oneline --format="%h %s (%an)"
|
||||
```
|
||||
|
||||
Also get the commit messages already on the tag (to identify previously cherry-picked commits). Note: Run these as separate commands to avoid shell parsing issues with parentheses in author names:
|
||||
|
||||
```bash
|
||||
LAST_TAG=$(git tag --sort=-v:refname | head -1)
|
||||
PREV_TAG=$(git tag --sort=-v:refname | head -2 | tail -1)
|
||||
```
|
||||
|
||||
```bash
|
||||
git log $PREV_TAG..$LAST_TAG --oneline --format="%s"
|
||||
```
|
||||
|
||||
**Present the list** to the user in a numbered format with commit hash, subject, and author. For any commits whose subject line already appears in the tag's history (previously cherry-picked in an earlier hotfix) or are "Release Notes" commits, add `(already in previous hotfix)` or `(release notes - skip)` after them so the user knows to skip those.
|
||||
|
||||
Ask which commits to include in the hotfix.
|
||||
|
||||
Use the ask_followup_question tool to let the user specify which commits they want (by number or hash).
|
||||
|
||||
## Step 3: Analyze Selected Commits
|
||||
|
||||
For each selected commit:
|
||||
1. Get the full commit message: `git show --no-patch --format="%B" <hash>`
|
||||
2. Get the diff to understand the change: `git show <hash> --stat`
|
||||
3. Find the associated PR if any: `gh pr list --search "<hash>" --state merged --json number,title --jq '.[0]'`
|
||||
|
||||
Build a mental model of what these changes do for the changelog.
|
||||
|
||||
## Step 4: Determine New Version Number
|
||||
|
||||
Parse the current version from package.json and the last tag:
|
||||
|
||||
```bash
|
||||
LAST_TAG=$(git tag --sort=-v:refname | head -1)
|
||||
echo "Last release: $LAST_TAG"
|
||||
cat package.json | grep '"version"'
|
||||
```
|
||||
|
||||
Hotfixes always increment the patch version (e.g., 3.40.0 -> 3.40.1, or 3.40.1 -> 3.40.2).
|
||||
|
||||
**Ask the user to confirm the new version number.**
|
||||
|
||||
## Step 5: Create Release Notes Commit on Main
|
||||
|
||||
On the main branch, create a commit that updates:
|
||||
|
||||
1. **CHANGELOG.md** - Add a new section for the hotfix version at the top:
|
||||
```markdown
|
||||
## [3.40.1]
|
||||
|
||||
- Description of fix 1
|
||||
- Description of fix 2
|
||||
```
|
||||
|
||||
Write clear, user-friendly descriptions based on your analysis of the commits.
|
||||
|
||||
2. **package.json** - Update the version field to the new version
|
||||
|
||||
3. **Delete changesets** for the commits being included in the hotfix. This prevents the changeset bot from including duplicate entries in the next regular release.
|
||||
|
||||
Find and delete the changeset files associated with the selected commits:
|
||||
```bash
|
||||
ls .changeset/
|
||||
```
|
||||
|
||||
Each changeset file in `.changeset/` corresponds to a PR. Read them to identify which ones belong to the commits you're hotfixing, then delete those files.
|
||||
|
||||
**Skip running `npm run install:all`** - the automation handles outdated lockfiles.
|
||||
|
||||
Commit with message format: `v{VERSION} Release Notes (hotfix)`
|
||||
|
||||
In the commit body, mention:
|
||||
- This is for a hotfix release
|
||||
- List the cherry-picked commits that will be included
|
||||
|
||||
```bash
|
||||
git add CHANGELOG.md package.json .changeset/
|
||||
git commit -m "v3.40.1 Release Notes (hotfix)
|
||||
|
||||
Hotfix release including:
|
||||
- <commit1-hash>: <description>
|
||||
- <commit2-hash>: <description>
|
||||
"
|
||||
```
|
||||
|
||||
Push to main:
|
||||
|
||||
```bash
|
||||
git push origin main
|
||||
```
|
||||
|
||||
## Step 6: Build the Hotfix on the Tag
|
||||
|
||||
Checkout the last release tag (detached HEAD):
|
||||
|
||||
```bash
|
||||
LAST_TAG=$(git tag --sort=-v:refname | head -1)
|
||||
git checkout $LAST_TAG
|
||||
```
|
||||
|
||||
Cherry-pick the selected commits in order:
|
||||
|
||||
```bash
|
||||
git cherry-pick <commit1-hash>
|
||||
git cherry-pick <commit2-hash>
|
||||
# ... etc
|
||||
```
|
||||
|
||||
Finally, cherry-pick the release notes commit you just pushed to main:
|
||||
|
||||
```bash
|
||||
# Get the hash of the release notes commit (should be HEAD of main)
|
||||
RELEASE_NOTES_COMMIT=$(git rev-parse main)
|
||||
git cherry-pick $RELEASE_NOTES_COMMIT
|
||||
```
|
||||
|
||||
## Step 7: Tag and Push
|
||||
|
||||
After all cherry-picks are applied successfully:
|
||||
|
||||
```bash
|
||||
# Tag the new release
|
||||
git tag v{VERSION}
|
||||
|
||||
# Push the tag to remote
|
||||
git push origin v{VERSION}
|
||||
```
|
||||
|
||||
## Step 8: Return to Main and Summary
|
||||
|
||||
Return to main branch:
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
```
|
||||
|
||||
**Copy a Slack announcement message to clipboard** with the version and PR links for each included fix:
|
||||
|
||||
```
|
||||
VS Code Hotfix v{VERSION} Published
|
||||
|
||||
- Description of fix 1 https://github.com/cline/cline/pull/{PR_NUMBER}
|
||||
- Description of fix 2 https://github.com/cline/cline/pull/{PR_NUMBER}
|
||||
```
|
||||
|
||||
Present a final summary:
|
||||
- New version: v{VERSION}
|
||||
- Tag pushed: yes
|
||||
- Commits included: (list them)
|
||||
- Slack message copied to clipboard: yes
|
||||
|
||||
Remind the user to:
|
||||
1. Manually trigger the publish release GitHub Action at: https://github.com/cline/cline/actions/workflows/publish.yml (paste `v{VERSION}` as the tag)
|
||||
2. Post the Slack message to announce the hotfix
|
||||
|
||||
## Important Notes
|
||||
|
||||
- This workflow does NOT create a release branch - only tags
|
||||
- The release notes commit goes to main first, then gets cherry-picked to the tag
|
||||
- This keeps main's history accurate while allowing hotfix releases from tags
|
||||
- If cherry-pick conflicts occur, resolve them before continuing
|
||||
@@ -0,0 +1,232 @@
|
||||
# Release
|
||||
|
||||
Prepare and publish a release from the open changeset PR.
|
||||
|
||||
## Overview
|
||||
|
||||
This workflow helps you:
|
||||
1. Find and checkout the open changeset PR
|
||||
2. Clean up the changelog (fix version format, wordsmith entries)
|
||||
3. Push changes back to the PR branch
|
||||
4. Merge with proper commit message format
|
||||
5. Tag and push the release (after verifying the commit)
|
||||
6. Trigger the publish workflow
|
||||
7. Update GitHub release notes
|
||||
8. Provide final summary with Slack announcement
|
||||
|
||||
## Step 1: Find the Changeset PR
|
||||
|
||||
Look for the open changeset PR:
|
||||
|
||||
```bash
|
||||
gh pr list --search "Changeset version bump" --state open --json number,title,headRefName,url
|
||||
```
|
||||
|
||||
If no PR is found, inform the user there's no changeset PR ready. They may need to:
|
||||
- Merge PRs with changesets to main first
|
||||
- Manually trigger the Changeset Converter workflow at: https://github.com/cline/cline/actions/workflows/changeset-converter.yml
|
||||
|
||||
## Step 2: Gather PR Information
|
||||
|
||||
Get the PR details:
|
||||
|
||||
```bash
|
||||
PR_NUMBER=<number from step 1>
|
||||
gh pr view $PR_NUMBER --json body,files,headRefName
|
||||
```
|
||||
|
||||
Checkout the PR branch:
|
||||
|
||||
```bash
|
||||
git fetch origin changeset-release/main
|
||||
git checkout changeset-release/main
|
||||
```
|
||||
|
||||
If the branch has diverged from remote, reset to the remote version:
|
||||
|
||||
```bash
|
||||
git reset --hard origin/changeset-release/main
|
||||
```
|
||||
|
||||
## Step 3: Analyze the Changes
|
||||
|
||||
Read the current CHANGELOG.md to see what the automation generated:
|
||||
|
||||
```bash
|
||||
head -50 CHANGELOG.md
|
||||
```
|
||||
|
||||
Get the version from package.json:
|
||||
|
||||
```bash
|
||||
cat package.json | grep '"version"'
|
||||
```
|
||||
|
||||
**Present to the user:**
|
||||
- The version number that will be released
|
||||
- The raw changelog entries from the changeset PR
|
||||
- Whether this is a patch, minor, or major release
|
||||
|
||||
## Step 4: Clean Up the Changelog
|
||||
|
||||
The changelog needs these fixes:
|
||||
|
||||
1. **Add brackets to version number**: Change `## 3.44.1` to `## [3.44.1]`
|
||||
|
||||
2. **No category headers**: Don't use `### Added`, `### Fixed`, etc. Just a flat list of bullet points.
|
||||
|
||||
3. **Order entries from most important to least important**:
|
||||
- Lead with major new features or significant fixes users care about
|
||||
- End with minor fixes or internal changes
|
||||
|
||||
4. **Write user-friendly descriptions**:
|
||||
- This is for end users, not developers—explain what changed in plain language
|
||||
- Remove commit hashes from the beginning of lines (the automation adds these)
|
||||
- Look at the actual commit diffs (`git show <hash>`) and PRs to understand what changed
|
||||
- Write colorful descriptions that explain the value and impact, not just technical details
|
||||
- Consolidate related changes into single entries when appropriate
|
||||
|
||||
**Ask the user** to review the proposed changelog changes before applying them. Show them:
|
||||
- Current (raw) changelog section
|
||||
- Proposed (cleaned) changelog section
|
||||
|
||||
Once approved, apply the changes to CHANGELOG.md.
|
||||
|
||||
## Step 5: Commit and Push Changes
|
||||
|
||||
After making changelog edits:
|
||||
|
||||
```bash
|
||||
git add CHANGELOG.md
|
||||
git commit -m "Clean up changelog formatting"
|
||||
git push origin changeset-release/main
|
||||
```
|
||||
|
||||
## Step 6: Merge the PR
|
||||
|
||||
**Ask the user to confirm** they're ready to merge.
|
||||
|
||||
Merge the PR with the proper commit message format:
|
||||
|
||||
```bash
|
||||
VERSION=<version from package.json>
|
||||
gh pr merge $PR_NUMBER --squash --subject "v${VERSION} Release Notes" --body ""
|
||||
```
|
||||
|
||||
**If merge is blocked by branch protection:**
|
||||
- Users with admin privileges can add the `--admin` flag to bypass
|
||||
- Users without admin privileges need to get the PR approved through normal review first before merging
|
||||
|
||||
## Step 7: Tag the Release
|
||||
|
||||
After the merge completes, checkout main and pull:
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git pull origin main
|
||||
```
|
||||
|
||||
**IMPORTANT: Verify the latest commit is the release commit before tagging:**
|
||||
|
||||
```bash
|
||||
git log -1 --oneline
|
||||
```
|
||||
|
||||
Confirm the commit message matches `v{VERSION} Release Notes` (e.g., `v3.44.1 Release Notes`). Do NOT blindly tag HEAD without verification.
|
||||
|
||||
Once verified, tag and push:
|
||||
|
||||
```bash
|
||||
VERSION=<version>
|
||||
git tag v${VERSION}
|
||||
git push origin v${VERSION}
|
||||
```
|
||||
|
||||
## Step 8: Trigger Publish Workflow
|
||||
|
||||
**Copy the tag to clipboard** so the user can easily paste it into the GitHub Actions workflow:
|
||||
|
||||
```bash
|
||||
echo -n "v{VERSION}" | pbcopy
|
||||
```
|
||||
|
||||
**Tell the user to trigger the publish workflow:**
|
||||
1. Go to: https://github.com/cline/cline/actions/workflows/publish.yml
|
||||
2. Select **"release"** for release-type
|
||||
3. Paste **`v{VERSION}`** as the tag (already in clipboard)
|
||||
|
||||
**Wait for the user** to confirm the publish workflow has completed before proceeding.
|
||||
|
||||
## Step 9: Update GitHub Release Notes
|
||||
|
||||
Once the user confirms the publish workflow is done, fetch the auto-generated release content:
|
||||
|
||||
```bash
|
||||
VERSION=<version>
|
||||
gh release view v${VERSION} --json body --jq '.body'
|
||||
```
|
||||
|
||||
The auto-generated release has:
|
||||
- `## What's Changed` - PR list (we'll replace this with our changelog)
|
||||
- `## New Contributors` - First-time contributors (keep this if present)
|
||||
- `**Full Changelog**` - Comparison link (keep this)
|
||||
|
||||
Build the new release body:
|
||||
1. Start with `## What's Changed` header
|
||||
2. Add our changelog content (from CHANGELOG.md for this version)
|
||||
3. Keep the `## New Contributors` section if it exists
|
||||
4. Keep the `**Full Changelog**` link
|
||||
|
||||
Update the release:
|
||||
|
||||
```bash
|
||||
gh release edit v${VERSION} --notes "<new body content>"
|
||||
```
|
||||
|
||||
Verify the release was updated:
|
||||
|
||||
```bash
|
||||
gh release view v${VERSION}
|
||||
```
|
||||
|
||||
## Step 10: Final Summary
|
||||
|
||||
**Copy a Slack announcement message to clipboard** (include the full changelog, not just highlights):
|
||||
|
||||
```bash
|
||||
echo "VS Code v{VERSION} Released
|
||||
|
||||
- Changelog entry 1
|
||||
- Changelog entry 2
|
||||
- Changelog entry 3" | pbcopy
|
||||
```
|
||||
|
||||
**Present a final summary:**
|
||||
- Version released: v{VERSION}
|
||||
- PR merged: #{PR_NUMBER}
|
||||
- Tag pushed: v{VERSION}
|
||||
- Release: https://github.com/cline/cline/releases/tag/v{VERSION}
|
||||
- Slack message copied to clipboard
|
||||
|
||||
**Final reminder:**
|
||||
Post the Slack message to announce the release
|
||||
|
||||
## Handling Edge Cases
|
||||
|
||||
### No changesets found
|
||||
If the changeset PR body shows no changes, inform the user they need to merge PRs with changesets first.
|
||||
|
||||
### Merge conflicts
|
||||
If there are conflicts on the changeset branch, help the user resolve them:
|
||||
```bash
|
||||
git fetch origin main
|
||||
git rebase origin/main
|
||||
# resolve conflicts
|
||||
git push origin changeset-release/main --force-with-lease
|
||||
```
|
||||
|
||||
### User wants to add more changes
|
||||
If the user wants to include additional PRs before releasing:
|
||||
1. Ask them to merge those PRs to main first
|
||||
2. The changeset automation will update the PR automatically
|
||||
3. Re-run this workflow after the PR is updated
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/docs/
|
||||
/.github/ @saoudrizwan @garoth @sjf
|
||||
/README.md @saoudrizwan @nickbaumann98
|
||||
/src/core/storage/ @celestial-vault
|
||||
/src/core/storage/ @celestial-vault @abeatrix
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
name: Claude Issue Triage
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
# Manual trigger for backfilling existing issues. Run from terminal:
|
||||
# gh workflow run claude-issue-triage.yml -f issue_number=1234
|
||||
# Or batch process:
|
||||
# gh issue list --state open --limit 10 --json number --jq '.[].number' | while read num; do
|
||||
# gh workflow run claude-issue-triage.yml -f issue_number=$num
|
||||
# sleep 60
|
||||
# done
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue_number:
|
||||
description: 'Issue number to triage'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
claude-issue-triage:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
# SECURITY: These permissions are intentionally restrictive.
|
||||
# - contents: read -> Claude can read the codebase but CANNOT write/push any code
|
||||
# - issues: write -> Claude can comment and add labels (the only write access needed)
|
||||
# - pull-requests: read -> Claude can view PR context but CANNOT create PRs
|
||||
# This ensures that even if a malicious user attempts prompt injection via issue content,
|
||||
# Claude cannot modify repository code, create branches, or open PRs.
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Run Issue Response & Triage
|
||||
id: triage
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
allowed_non_write_users: "*"
|
||||
# Allow all tools - security is enforced by GitHub permissions above (contents: read, issues: write)
|
||||
claude_args: --model claude-opus-4-5-20251101 --allowedTools "Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch"
|
||||
prompt: |
|
||||
You're a GitHub issue first responder for the open source Cline repository.
|
||||
|
||||
**Issue:** #${{ github.event.issue.number || inputs.issue_number }}
|
||||
**Title:** ${{ github.event.issue.title || 'See issue details below' }}
|
||||
**Author:** @${{ github.event.issue.user.login || 'See issue details below' }}
|
||||
|
||||
## Your job
|
||||
|
||||
Investigate this issue thoroughly, then post a single helpful comment that helps the user and gives maintainers the context they need.
|
||||
|
||||
## Investigation
|
||||
|
||||
Start by reading the full issue:
|
||||
gh issue view ${{ github.event.issue.number || inputs.issue_number }}
|
||||
|
||||
### Search for duplicates and related issues
|
||||
|
||||
Search thoroughly for existing issues that match this one:
|
||||
gh issue list --search "<keywords from the issue>" --state all --limit 30
|
||||
gh issue list --search "<error messages>" --state all --limit 20
|
||||
gh issue list --search "<affected feature/component>" --state all --limit 20
|
||||
|
||||
For each relevant issue you find, read it including its comments:
|
||||
gh issue view <number> --comments
|
||||
|
||||
You're looking for:
|
||||
- **Duplicates**: Issues describing the same problem. Link to them and explain why you think they're duplicates. If closed, check how they were resolved - the solution might apply here.
|
||||
- **Related issues**: Similar problems or context that could help. Pull useful information from their comments (workarounds others found, debugging steps that helped, maintainer explanations). Link to them and explain the connection.
|
||||
|
||||
If there are closed issues with solutions, surface those solutions prominently - this might immediately solve the user's problem.
|
||||
|
||||
### Analyze recent changes (ALWAYS DO THIS)
|
||||
|
||||
Many issues are regressions from recent releases. **Always** check what changed recently:
|
||||
gh release list --limit 10
|
||||
gh pr list --state merged --limit 50 --json number,title,mergedAt,author,body
|
||||
|
||||
Look for PRs merged in the last few weeks that might correlate with the issue. If you find a likely connection:
|
||||
gh pr view <number>
|
||||
gh pr diff <number>
|
||||
git log --since="1 month ago" --oneline -- <relevant paths>
|
||||
git show <commit>
|
||||
|
||||
**Always include your findings in your comment:**
|
||||
- If you find a regression, call it out explicitly: which PR/commit likely caused it, who authored it, what changed, and suggest a fix direction if you can see one.
|
||||
- If you don't find anything related, still mention it: "I analyzed recent PRs and releases but didn't find any changes that seem related to this issue."
|
||||
|
||||
### Search the codebase
|
||||
|
||||
Find the relevant code:
|
||||
- Use grep/find to locate code related to the issue
|
||||
- Key areas: `src/api/` (providers/models), `src/core/prompts/` (tools/prompts), platform-specific code for VS Code vs JetBrains
|
||||
|
||||
### Find documentation
|
||||
|
||||
Cline docs are at **https://docs.cline.bot/** and built with Mintlify from the `docs/` directory.
|
||||
|
||||
The URL structure maps directly to the file structure:
|
||||
- `docs/getting-started/selecting-your-model.mdx` → https://docs.cline.bot/getting-started/selecting-your-model
|
||||
- `docs/troubleshooting.mdx` → https://docs.cline.bot/troubleshooting
|
||||
- Headings become anchors: `## Which Model` → `#which-model`
|
||||
|
||||
Search the `docs/` directory to find relevant documentation, then construct URLs to link users to:
|
||||
```bash
|
||||
ls docs/
|
||||
grep -r "keyword" docs/ --include="*.mdx" -l
|
||||
```
|
||||
|
||||
### Identify subject matter experts
|
||||
|
||||
For issues that clearly need engineering attention:
|
||||
git log --since="6 months ago" --format="%an" -- <relevant paths> | sort | uniq -c | sort -rn | head -5
|
||||
|
||||
Cross-reference with GitHub usernames. Include in your response (@mention, do NOT assign):
|
||||
|
||||
| SME | Reason |
|
||||
|-----|--------|
|
||||
| @username1 | Authored PR #X which modified this area |
|
||||
| @username2 | Primary contributor to affected file |
|
||||
|
||||
## Weak model detection
|
||||
|
||||
Many issues are caused by users running small or non-frontier models that don't tool-call reliably. Signs include:
|
||||
- Model failing to use tools correctly
|
||||
- Nonsensical or malformed responses
|
||||
- User is running a small/local model or older model version
|
||||
|
||||
If this looks like a weak model issue, kindly suggest they try reproducing with Claude Sonnet and report back if it persists. Link to https://docs.cline.bot/getting-started/selecting-your-model if helpful. Still label and triage normally.
|
||||
|
||||
## Your comment
|
||||
|
||||
Write a single comment as a helpful community member. Be conversational, not robotic. Include what's relevant:
|
||||
|
||||
- **Helpful response** - Answer their question, suggest a fix, provide a workaround. If you found solutions in related closed issues, surface those prominently.
|
||||
- **Duplicates and related issues** - Link to any you found and explain why they're duplicates/related. Summarize useful context from their comments.
|
||||
- **Regression analysis** - If this looks like a regression, explain what change likely caused it, link to the PR/commit, and tag the author.
|
||||
- **Clarifying questions** - If you need more info, ask specific questions. Don't ask for things already provided.
|
||||
- **SME table** - Include the table above if this needs engineering attention. Don't tag people for questions with obvious answers or weak-model issues.
|
||||
- **Context for maintainers** - Relevant code paths, what you found. Keep it concise.
|
||||
- **Docs links** - If there's relevant documentation, link to it naturally in your response as a recommendation (e.g., "For more details, check out [the Ollama setup guide](url)"). Do NOT add a "Sources" section at the end - integrate doc links into your response where they're helpful.
|
||||
- **Possible Duplicates section** - ALWAYS include a "Possible Duplicates" section at the end of your comment listing issues that might be duplicates so maintainers can quickly close if appropriate. If none found, say "No obvious duplicates found."
|
||||
|
||||
## Labels
|
||||
gh label list --json name,description --limit 100
|
||||
gh issue edit ${{ github.event.issue.number || inputs.issue_number }} --add-label "label1,label2" # Only use available labels, don't create new ones
|
||||
|
||||
IMPORTANT: After posting your comment, add the "Bot Responded" label to indicate this issue has received an automated response:
|
||||
gh issue edit ${{ github.event.issue.number || inputs.issue_number }} --add-label "Bot Responded"
|
||||
|
||||
## Remember
|
||||
|
||||
- **This is a one-time automated response** - you will NOT see their reply or respond again. Never say things like "I can help you", "let me know", "once I have that info", or "I can give you more targeted help" - you won't be there to follow up. If you ask clarifying questions, frame them for the maintainers who will follow up, e.g., "If you can share X, that would help the maintainers diagnose this."
|
||||
- Don't be formulaic. Respond to what the issue actually needs.
|
||||
- Surface solutions from past issues - often the fastest path to helping.
|
||||
- Connecting regressions to specific changes is extremely valuable.
|
||||
- Link issues with #number so they're clickable.
|
||||
@@ -36,6 +36,8 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.tag }}
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
@@ -116,22 +118,31 @@ jobs:
|
||||
echo "Successfully published release version ${{ steps.get_version.outputs.version }} to VS Code Marketplace and Open VSX Registry"
|
||||
fi
|
||||
|
||||
# - name: Get Changelog Entry
|
||||
# id: changelog
|
||||
# uses: mindsers/changelog-reader-action@v2
|
||||
# with:
|
||||
# # This expects a standard Keep a Changelog format
|
||||
# # "latest" means it will read whichever is the most recent version
|
||||
# # set in "## [1.2.3] - 2025-01-28" style
|
||||
# version: latest
|
||||
- name: Get Previous Tag
|
||||
id: prev_tag
|
||||
run: |
|
||||
CURRENT_TAG="${{ steps.validate_tag.outputs.tag }}"
|
||||
PREV_TAG=$(git describe --tags --abbrev=0 "$CURRENT_TAG^" 2>/dev/null || echo "")
|
||||
echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get Changelog Entry
|
||||
id: changelog
|
||||
run: |
|
||||
# Get content between first ## [ and second ## [
|
||||
CONTENT=$(awk '/^## \[/{if(found) exit; found=1; next} found{print}' CHANGELOG.md)
|
||||
echo "content<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$CONTENT" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ steps.validate_tag.outputs.tag }}
|
||||
files: "*.vsix"
|
||||
# body: ${{ steps.changelog.outputs.content }}
|
||||
generate_release_notes: true
|
||||
body: |
|
||||
${{ steps.changelog.outputs.content }}
|
||||
|
||||
**Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ steps.prev_tag.outputs.prev_tag }}...${{ steps.validate_tag.outputs.tag }}
|
||||
prerelease: ${{ github.event.inputs.release-type == 'pre-release' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -14,6 +14,7 @@ pnpm-lock.yaml
|
||||
.clineignore
|
||||
.venv
|
||||
.actrc
|
||||
CLAUDE.local.md
|
||||
|
||||
webview-ui/src/**/*.js
|
||||
webview-ui/src/**/*.js.map
|
||||
|
||||
Vendored
+16
-4
@@ -12,7 +12,10 @@
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
"--disable-workspace-trust",
|
||||
"--disable-extensions", // Avoid conflicts with installed extensions
|
||||
"--disable-extension",
|
||||
"saoudrizwan.claude-dev", // Avoid conflicts with installed Cline
|
||||
"--disable-extension",
|
||||
"saoudrizwan.cline-nightly", // Avoid conflicts with installed Cline Nightly
|
||||
"${workspaceFolder}"
|
||||
],
|
||||
"outFiles": [
|
||||
@@ -33,7 +36,10 @@
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
"--disable-workspace-trust",
|
||||
"--disable-extensions", // Avoid conflicts with installed extensions
|
||||
"--disable-extension",
|
||||
"saoudrizwan.claude-dev", // Avoid conflicts with installed Cline
|
||||
"--disable-extension",
|
||||
"saoudrizwan.cline-nightly", // Avoid conflicts with installed Cline Nightly
|
||||
"${workspaceFolder}"
|
||||
],
|
||||
"outFiles": [
|
||||
@@ -54,7 +60,10 @@
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
"--disable-workspace-trust",
|
||||
"--disable-extensions", // Avoid conflicts with installed extensions
|
||||
"--disable-extension",
|
||||
"saoudrizwan.claude-dev", // Avoid conflicts with installed Cline
|
||||
"--disable-extension",
|
||||
"saoudrizwan.cline-nightly", // Avoid conflicts with installed Cline Nightly
|
||||
"${workspaceFolder}"
|
||||
],
|
||||
"outFiles": [
|
||||
@@ -77,7 +86,10 @@
|
||||
"--user-data-dir=${workspaceFolder}/dist/tmp/user",
|
||||
"--profile-temp",
|
||||
"--sync=off",
|
||||
"--disable-extensions", // Avoid conflicts with installed extensions
|
||||
"--disable-extension",
|
||||
"saoudrizwan.claude-dev", // Avoid conflicts with installed Cline
|
||||
"--disable-extension",
|
||||
"saoudrizwan.cline-nightly", // Avoid conflicts with installed Cline Nightly
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
"${workspaceFolder}"
|
||||
],
|
||||
|
||||
Vendored
+3
-1
@@ -27,5 +27,7 @@
|
||||
"source.removeUnused.biome": "always",
|
||||
"source.removeUnusedImports": "always",
|
||||
"source.organizeImports.biome": "always"
|
||||
}
|
||||
},
|
||||
// Disable auto-forwarding ports to prevent Simple Browser from opening the Vite dev server
|
||||
"remote.autoForwardPorts": false
|
||||
}
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
.gitignore
|
||||
@@ -1,5 +1,80 @@
|
||||
# Changelog
|
||||
|
||||
## [3.46.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Added GLM 4.7 model
|
||||
- Enhanced background terminal execution with command tracking, log file output, zombie process prevention (10-minute timeout), and clickable log paths in UI
|
||||
- Apply Patch tool for GPT-5+ models (replacing current diff edit tools)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Duplicate error messages during streaming for Diff Edit tool when Parallel Tool Calling is not enabled
|
||||
- Banner carousel styling and dismiss functionality
|
||||
- Typos in Gemini system prompt overrides
|
||||
- Model picker favorites ordering, star toggle, and keyboard navigation for OpenRouter and Vercel AI Gateway providers
|
||||
- Fetch remote config values from the cache
|
||||
|
||||
### Refactored
|
||||
|
||||
- Anthropic handler to use metadata for reasoning support
|
||||
- Bedrock provider to use metadata for reasoning support
|
||||
|
||||
## [3.45.1]
|
||||
|
||||
- Fixed MCP settings race condition where toggling auto-approve or changing timeout settings would cause the UI to flash and revert
|
||||
|
||||
## [3.45.0]
|
||||
|
||||
- Added Gemini 3 Flash Preview model
|
||||
|
||||
## [3.44.2]
|
||||
|
||||
- Polished the model picker UI with checkmarks for selected models, tooltips on Plan/Act tabs, and consistent arrow pointers across all popup modals
|
||||
- Improved WhatsNew modal responsiveness and cleaned up redundant UI elements
|
||||
- Fixed GLM models outputting garbled text in thinking tags—reasoning is now properly disabled for these models
|
||||
|
||||
## [3.44.1]
|
||||
|
||||
- Fixed a critical bug where local MCP servers stopped connecting after v3.42.0—all user-configured stdio-based MCP servers should now work again
|
||||
- Fixed remotely configured API keys not being extracted correctly for enterprise users
|
||||
- Added support for dynamic tool instructions that adapt based on runtime context, laying groundwork for future context-aware features
|
||||
|
||||
## [3.44.0]
|
||||
|
||||
## Added
|
||||
|
||||
- Updating minor version to show a proper banner for the release
|
||||
|
||||
## [3.43.1]
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Fix GLM-4.6 Model reference id
|
||||
|
||||
## [3.43.0]
|
||||
|
||||
### Added
|
||||
|
||||
- GLM-4.6
|
||||
- kat-coder-pro
|
||||
- Add parsing of env variable patterns to the mcpconfig.json
|
||||
|
||||
### Fixed
|
||||
|
||||
- TLS Proxy support issues for VSCode
|
||||
- Add supportsReasoning flag to OpenAI reasoning models
|
||||
- Fix thinking not available for some models in the OpenAI provider
|
||||
- Fix invalid signature field issues when switching between Gemini and Anthropic providers
|
||||
- Extract OpenRouter model filtering into reusable utility and use it in different model pickers
|
||||
- Fix a11y for auto approve checkbox
|
||||
- Improve ModelPickerModal provider list layout
|
||||
|
||||
### Refactored
|
||||
|
||||
- Migrate WhatsNewModal to new shared dialogue component
|
||||
|
||||
## [3.42.0]
|
||||
|
||||
### Added
|
||||
|
||||
@@ -70,3 +70,4 @@ Apache-2.0 - see [LICENSE](https://github.com/cline/cline/blob/main/LICENSE) for
|
||||
- Report issues: [GitHub Issues](https://github.com/cline/cline/issues)
|
||||
- Community: [GitHub Discussions](https://github.com/cline/cline/discussions)
|
||||
- Documentation: [docs.cline.bot](https://docs.cline.bot)
|
||||
- Cline CLI Architecture: [architecture.md](./architecture.md)
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
# Cline CLI Architecture
|
||||
|
||||
The CLI is a **standalone terminal interface** for the Cline AI coding assistant, written in Go. It provides the same autonomous coding capabilities as the VS Code extension but runs entirely in the terminal.
|
||||
|
||||
## High-Level Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ User Terminal │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ cline (Go binary) │
|
||||
│ cmd/cline/main.go │
|
||||
│ • Cobra CLI commands (task, auth, config, instance, etc.) │
|
||||
│ • Interactive input via Bubble Tea │
|
||||
│ • Streaming output with markdown rendering │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
│ gRPC (50052) │ starts subprocess
|
||||
▼ ▼
|
||||
┌─────────────────────────┐ ┌─────────────────────────┐
|
||||
│ cline-core │◄────────────────►│ cline-host │
|
||||
│ (Node.js) │ gRPC (51052) │ (Go binary) │
|
||||
│ │ │ cmd/cline-host/main.go│
|
||||
│ • AI/LLM orchestration │ │ │
|
||||
│ • Tool execution │ │ • Workspace paths │
|
||||
│ • Task state mgmt │ │ • File diff editing │
|
||||
│ • Message handling │ │ • Clipboard access │
|
||||
└─────────────────────────┘ │ • Environment info │
|
||||
│ └─────────────────────────┘
|
||||
│ SQLite (self-registration)
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ ~/.cline/data/locks/locks.db │
|
||||
│ (Instance registry - core self-registers on startup) │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Entry Points (`cmd/`)
|
||||
|
||||
### `cmd/cline/main.go` - Main CLI
|
||||
|
||||
Cobra-based CLI with commands:
|
||||
|
||||
- **Root**: `cline [prompt]` - Start a task directly
|
||||
- **task**: Create, send, view, list, pause, restore tasks
|
||||
- **auth**: Authentication setup and provider configuration
|
||||
- **config**: Read/write settings
|
||||
- **instance**: Manage running Cline instances
|
||||
- **logs**: View and clean log files
|
||||
- **doctor**: System health check
|
||||
|
||||
### `cmd/cline-host/main.go` - Host Bridge Service
|
||||
|
||||
Separate gRPC server providing host environment operations to cline-core:
|
||||
|
||||
- Workspace paths
|
||||
- File diff editing
|
||||
- Clipboard access
|
||||
- Shutdown coordination
|
||||
|
||||
---
|
||||
|
||||
## `pkg/cli/` Subsystems
|
||||
|
||||
### 1. `auth/` - Authentication System
|
||||
|
||||
Handles authentication with Cline service and BYO (Bring Your Own) API providers.
|
||||
|
||||
| File | Purpose |
|
||||
| ------------------------- | ------------------------------------------------------------------------ |
|
||||
| `auth_cline_provider.go` | OAuth login flow - opens browser, subscribes to auth callback stream |
|
||||
| `auth_menu.go` | Interactive menu showing auth options based on current state |
|
||||
| `auth_subscription.go` | gRPC stream subscription for auth status updates |
|
||||
| `wizard_byo.go` | Interactive wizard for configuring BYO providers |
|
||||
| `wizard_byo_bedrock.go` | AWS Bedrock-specific credential setup |
|
||||
| `wizard_byo_oca.go` | Oracle Code Assist setup |
|
||||
| `providers_list.go` | Retrieves configured providers from core state |
|
||||
| `providers_byo.go` | Provider selection UI and field configuration |
|
||||
| `models_*.go` | Model listing (static lists + dynamic fetch from OpenRouter/OpenAI/Ollama) |
|
||||
|
||||
**Flow**: User runs `cline auth` → Menu shows options → For BYO: wizard guides through provider/key/model selection → Config saved via gRPC to core.
|
||||
|
||||
---
|
||||
|
||||
### 2. `clerror/` - Error Handling
|
||||
|
||||
Parses and classifies API errors from the Cline service.
|
||||
|
||||
**Error Types:**
|
||||
|
||||
- `ErrorTypeAuth` - 401, bad API key
|
||||
- `ErrorTypeBalance` - Insufficient credits
|
||||
- `ErrorTypeRateLimit` - 429, quota exceeded
|
||||
- `ErrorTypeNetwork` - Connection issues
|
||||
- `ErrorTypeUnknown` - Catch-all
|
||||
|
||||
Extracts billing details (balance, spent, buy credits URL) from error responses.
|
||||
|
||||
---
|
||||
|
||||
### 3. `config/` - Configuration Management
|
||||
|
||||
| File | Purpose |
|
||||
| --------------------- | -------------------------------------------------------------------------- |
|
||||
| `manager.go` | gRPC interface for reading/writing settings via `UpdateSettingsCli` RPC |
|
||||
| `settings_renderer.go`| Pretty-prints config values, censors sensitive fields (keys, secrets) |
|
||||
|
||||
Supports dot-notation paths: `cline config get auto-approval-settings.actions.read-files`
|
||||
|
||||
---
|
||||
|
||||
### 4. `display/` - Terminal Display System
|
||||
|
||||
The most complex subsystem - handles all visual output.
|
||||
|
||||
| File | Purpose |
|
||||
| ----------------------- | -------------------------------------------------------------------- |
|
||||
| `renderer.go` | Central coordinator with lipgloss styles, color methods, markdown delegation |
|
||||
| `streaming.go` | Real-time streaming display with deduplication |
|
||||
| `segment_streamer.go` | Streaming segments (header + body) with context-aware headers |
|
||||
| `typewriter.go` | Character-by-character animation with variable delays |
|
||||
| `markdown_renderer.go` | Glamour wrapper for terminal markdown rendering |
|
||||
| `tool_renderer.go` | Tool operation formatting ("Cline is editing `file.ts`") |
|
||||
| `tool_result_parser.go` | Parses structured tool results (file lists, search results) |
|
||||
| `banner.go` | Session startup banner with version/model/workspace |
|
||||
| `deduplicator.go` | MD5-based deduplication with 2-second window |
|
||||
| `system_renderer.go` | Rich error/warning boxes for balance errors, auth failures |
|
||||
| `ansi.go` | TTY detection, line clearing with escape codes |
|
||||
|
||||
---
|
||||
|
||||
### 5. `global/` - Global State Management
|
||||
|
||||
| File | Purpose |
|
||||
| ------------------ | -------------------------------------------------------------------------- |
|
||||
| `global.go` | Global config (paths, verbosity, output format), initialization |
|
||||
| `registry.go` | Instance discovery via SQLite, health checking, default instance management|
|
||||
| `cline-clients.go` | Starts cline-core + cline-host processes, port allocation, cleanup |
|
||||
|
||||
**Instance lifecycle:**
|
||||
|
||||
1. Find available port pair
|
||||
2. Start `cline-host` on port+1000
|
||||
3. Start `cline-core` on port
|
||||
4. Wait for core to self-register in SQLite
|
||||
5. Set as default if first instance
|
||||
|
||||
---
|
||||
|
||||
### 6. `handlers/` - Message Handlers
|
||||
|
||||
Routes incoming messages from cline-core to appropriate renderers.
|
||||
|
||||
| File | Purpose |
|
||||
| ------------------ | --------------------------------------------------------------------- |
|
||||
| `handler.go` | Handler registry with priority-based routing |
|
||||
| `ask_handlers.go` | Approval requests: tool, command, followup, api_req_failed, etc. |
|
||||
| `say_handlers.go` | Status messages: text, reasoning, command_output, tool, checkpoint, etc. |
|
||||
|
||||
Uses `DisplayContext` providing renderer access, state, and context flags (isLast, isPartial, isStreamingMode).
|
||||
|
||||
---
|
||||
|
||||
### 7. `output/` - Output Coordination
|
||||
|
||||
| File | Purpose |
|
||||
| --------------------- | ----------------------------------------------------------------------- |
|
||||
| `coordinator.go` | Coordinates streaming output with interactive input (saves/restores input state) |
|
||||
| `input_model.go` | Bubble Tea model for rich input (message, approval, feedback types) |
|
||||
| `slash_completion.go` | Autocomplete dropdown for slash commands |
|
||||
|
||||
**Key pattern:** When output needs to print while input is visible, the coordinator saves input state, clears the form, prints, then restores input.
|
||||
|
||||
---
|
||||
|
||||
### 8. `slash/` - Slash Command Registry
|
||||
|
||||
Central registry for commands like `/plan`, `/act`, `/cancel`:
|
||||
|
||||
- **CLI-local commands**: Handled directly by CLI
|
||||
- **Backend commands**: Fetched from core via gRPC, filtered by `CliCompatible` flag
|
||||
|
||||
---
|
||||
|
||||
### 9. `sqlite/` - Instance Locking
|
||||
|
||||
Manages the distributed locking system:
|
||||
|
||||
- **Instance locks**: Track running Cline instances by address
|
||||
- **File locks**: Coordinate file access across instances
|
||||
- SQLite database created by cline-core, CLI reads/writes for discovery
|
||||
|
||||
---
|
||||
|
||||
### 10. `task/` - Task Management
|
||||
|
||||
| File | Purpose |
|
||||
| ----------------------- | -------------------------------------------------------------------- |
|
||||
| `manager.go` | Core orchestrator: create, cancel, resume, restore tasks; stream handling |
|
||||
| `stream_coordinator.go` | Deduplication and turn management for dual streams |
|
||||
| `input_handler.go` | Interactive input during follow mode (polling, approval detection) |
|
||||
| `history_handler.go` | Direct disk access to `taskHistory.json` |
|
||||
| `settings_parser.go` | Parse settings from CLI flags |
|
||||
| `follow_options.go` | Configuration for follow behavior |
|
||||
|
||||
**Streaming:** Task manager subscribes to two gRPC streams:
|
||||
|
||||
1. `SubscribeToState` - Full state updates
|
||||
2. `SubscribeToPartialMessage` - Streaming AI responses
|
||||
|
||||
---
|
||||
|
||||
### 11. `terminal/` - Terminal Handling
|
||||
|
||||
Enhanced keyboard protocol support and terminal configuration:
|
||||
|
||||
- Enables modifyOtherKeys and Kitty keyboard protocol
|
||||
- Detects terminal type (VS Code, iTerm, Ghostty, Kitty, etc.)
|
||||
- Auto-configures shift+enter keybindings for various terminals
|
||||
|
||||
---
|
||||
|
||||
### 12. `types/` - Type Definitions
|
||||
|
||||
| File | Purpose |
|
||||
| -------------- | ----------------------------------------------------------------- |
|
||||
| `messages.go` | `ClineMessage`, `AskType`, `SayType`, `ToolType` enums, proto conversion |
|
||||
| `state.go` | `ConversationState` with thread-safe message access |
|
||||
| `history.go` | `HistoryItem` matching taskHistory.json format |
|
||||
|
||||
---
|
||||
|
||||
### 13. `updater/` - Auto-Update
|
||||
|
||||
Background auto-update checking:
|
||||
|
||||
- 24-hour check interval (cached)
|
||||
- Queries npm registry for newer versions
|
||||
- Supports `latest` and `nightly` channels
|
||||
- Runs `npm install -g cline` to update
|
||||
|
||||
---
|
||||
|
||||
## `pkg/common/` - Shared Types
|
||||
|
||||
| File | Purpose |
|
||||
| --------------- | ------------------------------------------------------------ |
|
||||
| `constants.go` | `SETTINGS_SUBFOLDER`, `DEFAULT_CLINE_CORE_PORT` |
|
||||
| `schema.go` | SQL queries for instance/file locks |
|
||||
| `types.go` | `CoreInstanceInfo`, `LockRow`, `DefaultCoreInstance` |
|
||||
| `utils.go` | Port checking, health checks, address normalization, retry logic |
|
||||
|
||||
---
|
||||
|
||||
## `pkg/generated/` - Auto-Generated
|
||||
|
||||
| File | Purpose |
|
||||
| --------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
||||
| `providers.go` | Provider definitions (Anthropic, OpenAI, Bedrock, etc.) with field metadata and model specs - generated from TypeScript sources |
|
||||
| `field_overrides.go` | Manual overrides for field filtering |
|
||||
|
||||
---
|
||||
|
||||
## `pkg/hostbridge/` - CLI-to-Core Bridge
|
||||
|
||||
This is the **reverse bridge** allowing cline-core to request host environment operations:
|
||||
|
||||
| File | Purpose |
|
||||
| ----------------------- | ---------------------------------------------------- |
|
||||
| `grpc_server.go` | Main server registering all services |
|
||||
| `simple_workspace.go` | Workspace service: returns CWD as workspace path |
|
||||
| `diff.go` | In-memory file diff editing with line-based operations |
|
||||
| `env.go` | Clipboard access, version info, shutdown coordination |
|
||||
| `window.go` | UI stubs (no-ops or console output) |
|
||||
|
||||
**Why this exists:** The same cline-core logic runs in VS Code and CLI. In VS Code, the "host" is the extension with editor APIs. In CLI, hostbridge emulates these capabilities with terminal-appropriate implementations.
|
||||
|
||||
---
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
1. **Two-process model:** `cline` CLI manages instances; `cline-core` is the actual AI engine (Node.js). This allows reusing the same core as the VS Code extension.
|
||||
|
||||
2. **Self-registration via SQLite:** `cline-core` registers itself in a SQLite database on startup. The CLI discovers instances by reading this database, enabling multi-instance support.
|
||||
|
||||
3. **Host bridge abstraction:** The `cline-host` process provides platform-specific operations (clipboard, workspace paths) via gRPC, allowing `cline-core` to remain host-agnostic.
|
||||
|
||||
4. **Streaming-first UI:** The CLI uses gRPC streaming to display AI responses in real-time with typewriter-style rendering.
|
||||
|
||||
5. **Dual stream handling:** Task manager subscribes to both state updates and partial messages, using deduplication to prevent duplicate rendering.
|
||||
@@ -14,8 +14,9 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
port int
|
||||
verbose bool
|
||||
port int
|
||||
verbose bool
|
||||
workspaces []string
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -28,6 +29,7 @@ func main() {
|
||||
|
||||
rootCmd.Flags().IntVarP(&port, "port", "p", 51052, "port to listen on")
|
||||
rootCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "verbose logging")
|
||||
rootCmd.Flags().StringSliceVar(&workspaces, "workspace", nil, "workspace paths")
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
@@ -39,7 +41,7 @@ func runServer(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
// Create gRPC hostbridge service
|
||||
service := hostbridge.NewGrpcServer(port, verbose)
|
||||
service := hostbridge.NewGrpcServer(port, verbose, workspaces)
|
||||
|
||||
// Handle graceful shutdown
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
+69
-24
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
@@ -25,12 +26,13 @@ var (
|
||||
outputFormat string
|
||||
|
||||
// Task creation flags (for root command)
|
||||
images []string
|
||||
files []string
|
||||
mode string
|
||||
settings []string
|
||||
yolo bool
|
||||
oneshot bool
|
||||
images []string
|
||||
files []string
|
||||
mode string
|
||||
settings []string
|
||||
yolo bool
|
||||
oneshot bool
|
||||
workspaces []string
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -70,12 +72,23 @@ see the manual page: man cline`,
|
||||
|
||||
var instanceAddress string
|
||||
|
||||
// Validate workspace paths exist
|
||||
if err := common.ValidateDirsExist(workspaces); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Build the full workspace list: cwd first, then additional workspaces
|
||||
allWorkspaces, err := buildWorkspaceList(workspaces)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build workspace list: %w", err)
|
||||
}
|
||||
|
||||
// If --address flag not provided, start instance BEFORE getting prompt
|
||||
if !cmd.Flags().Changed("address") {
|
||||
if global.Config.Verbose {
|
||||
fmt.Println("Starting new Cline instance...")
|
||||
}
|
||||
instance, err := global.Clients.StartNewInstance(ctx)
|
||||
instance, err := global.Clients.StartNewInstance(ctx, allWorkspaces...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start new instance: %w", err)
|
||||
}
|
||||
@@ -131,8 +144,8 @@ see the manual page: man cline`,
|
||||
|
||||
// If no prompt from args or stdin, show interactive input
|
||||
if prompt == "" {
|
||||
// Pass the mode flag to banner so it shows correct mode
|
||||
prompt, err = promptForInitialTask(ctx, instanceAddress, mode)
|
||||
// Pass the mode flag and workspaces to banner so it shows correct info
|
||||
prompt, err = promptForInitialTask(ctx, instanceAddress, mode, allWorkspaces)
|
||||
if err != nil {
|
||||
// Check if user cancelled - exit cleanly without error
|
||||
if err == huh.ErrUserAborted {
|
||||
@@ -152,13 +165,14 @@ see the manual page: man cline`,
|
||||
}
|
||||
|
||||
return cli.CreateAndFollowTask(ctx, prompt, cli.TaskOptions{
|
||||
Images: images,
|
||||
Files: files,
|
||||
Mode: mode,
|
||||
Settings: settings,
|
||||
Yolo: yolo,
|
||||
Address: instanceAddress,
|
||||
Verbose: verbose,
|
||||
Images: images,
|
||||
Files: files,
|
||||
Mode: mode,
|
||||
Settings: settings,
|
||||
Yolo: yolo,
|
||||
Address: instanceAddress,
|
||||
Verbose: verbose,
|
||||
Workspaces: allWorkspaces,
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -175,6 +189,7 @@ see the manual page: man cline`,
|
||||
rootCmd.Flags().BoolVarP(&yolo, "yolo", "y", false, "enable yolo mode (non-interactive)")
|
||||
rootCmd.Flags().BoolVar(&yolo, "no-interactive", false, "enable yolo mode (non-interactive)")
|
||||
rootCmd.Flags().BoolVarP(&oneshot, "oneshot", "o", false, "full autonomous mode")
|
||||
rootCmd.Flags().StringSliceVarP(&workspaces, "workspace", "w", nil, "additional workspace paths (can be specified multiple times)")
|
||||
|
||||
rootCmd.AddCommand(cli.NewTaskCommand())
|
||||
rootCmd.AddCommand(cli.NewInstanceCommand())
|
||||
@@ -189,9 +204,9 @@ see the manual page: man cline`,
|
||||
}
|
||||
}
|
||||
|
||||
func promptForInitialTask(ctx context.Context, instanceAddress, modeFlag string) (string, error) {
|
||||
func promptForInitialTask(ctx context.Context, instanceAddress, modeFlag string, workspaces []string) (string, error) {
|
||||
// Show session banner before the initial input
|
||||
showSessionBanner(ctx, instanceAddress, modeFlag)
|
||||
showSessionBanner(ctx, instanceAddress, modeFlag, workspaces)
|
||||
|
||||
var prompt string
|
||||
|
||||
@@ -233,7 +248,7 @@ func promptForInitialTask(ctx context.Context, instanceAddress, modeFlag string)
|
||||
}
|
||||
|
||||
// showSessionBanner displays session info before initial prompt
|
||||
func showSessionBanner(ctx context.Context, instanceAddress, modeFlag string) {
|
||||
func showSessionBanner(ctx context.Context, instanceAddress, modeFlag string, workspaces []string) {
|
||||
bannerInfo := display.BannerInfo{
|
||||
Version: global.CliVersion,
|
||||
Mode: modeFlag, // Use the mode from command flag, not state
|
||||
@@ -244,10 +259,7 @@ func showSessionBanner(ctx context.Context, instanceAddress, modeFlag string) {
|
||||
bannerInfo.Mode = "plan"
|
||||
}
|
||||
|
||||
// Get current working directory (this is what Cline will use)
|
||||
if cwd, err := os.Getwd(); err == nil {
|
||||
bannerInfo.Workdir = cwd
|
||||
}
|
||||
bannerInfo.Workdirs = workspaces
|
||||
|
||||
// Get provider/model using auth functions (same logic as auth menu)
|
||||
manager, err := cli.NewTaskManagerForAddress(ctx, instanceAddress)
|
||||
@@ -345,4 +357,37 @@ func getContentFromStdinAndArgs(args []string) (string, error) {
|
||||
}
|
||||
|
||||
return content.String(), nil
|
||||
}
|
||||
}
|
||||
|
||||
// buildWorkspaceList builds the full workspace list with cwd as the first entry
|
||||
func buildWorkspaceList(additionalWorkspaces []string) ([]string, error) {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current working directory: %w", err)
|
||||
}
|
||||
|
||||
// Start with cwd
|
||||
workspaces := []string{cwd}
|
||||
|
||||
// Add additional workspaces, avoiding duplicates
|
||||
for _, ws := range additionalWorkspaces {
|
||||
// Normalize the path
|
||||
absPath, err := common.AbsPath(ws)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve workspace path %s: %w", ws, err)
|
||||
}
|
||||
|
||||
// Skip if it's the same as cwd
|
||||
if absPath == cwd {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for duplicates
|
||||
isDuplicate := slices.Contains(workspaces, absPath)
|
||||
if !isDuplicate {
|
||||
workspaces = append(workspaces, absPath)
|
||||
}
|
||||
}
|
||||
|
||||
return workspaces, nil
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
module github.com/cline/cli
|
||||
|
||||
go 1.23.0
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4
|
||||
|
||||
@@ -70,6 +70,10 @@ When using the instant task syntax **cline "prompt"** the following options are
|
||||
|
||||
: Starting mode. Options: **act** (default), **plan**
|
||||
|
||||
**-w**, **\--workspace** *path*
|
||||
|
||||
: Additional workspace paths. Can be specified multiple times to include multiple directories. The current working directory is always included as the first workspace. Example: cline -w /path/to/other/project "refactor shared code"
|
||||
|
||||
# GLOBAL OPTIONS
|
||||
|
||||
These options apply to all subcommands:
|
||||
@@ -78,6 +82,28 @@ These options apply to all subcommands:
|
||||
|
||||
: Output format. Options: **rich** (default), **json**, **plain**
|
||||
|
||||
When you use **-F json**, the CLI prints each client message as JSON.
|
||||
|
||||
Each message is a **ClineMessage** object.
|
||||
|
||||
Required fields:
|
||||
|
||||
- **type**: "ask" or "say"
|
||||
- **text**: message text
|
||||
- **ts**: Unix epoch timestamp in milliseconds
|
||||
|
||||
Optional fields (omitted when empty):
|
||||
|
||||
- **reasoning**: reasoning text
|
||||
- **say**: say subtype (present when type is "say")
|
||||
- **ask**: ask subtype (present when type is "ask")
|
||||
- **partial**: streaming flag
|
||||
- **images**: list of image URIs
|
||||
- **files**: list of file paths
|
||||
- **lastCheckpointHash**: git checkpoint hash
|
||||
- **isCheckpointCheckedOut**: checkpoint checkout flag
|
||||
- **isOperationOutsideWorkspace**: workspace safety flag
|
||||
|
||||
**-h**, **\--help**
|
||||
|
||||
: Display help information for the command.
|
||||
|
||||
@@ -47,7 +47,7 @@ func GetProviderConfigurations(ctx context.Context, manager *task.Manager) (*Pro
|
||||
}
|
||||
|
||||
// Parse state_json as map[string]interface{}
|
||||
var stateData map[string]interface{}
|
||||
var stateData map[string]any
|
||||
if err := json.Unmarshal([]byte(stateJSON), &stateData); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse state JSON: %w", err)
|
||||
}
|
||||
@@ -57,7 +57,7 @@ func GetProviderConfigurations(ctx context.Context, manager *task.Manager) (*Pro
|
||||
}
|
||||
|
||||
// Extract apiConfiguration object from state
|
||||
apiConfig, ok := stateData["apiConfiguration"].(map[string]interface{})
|
||||
apiConfig, ok := stateData["apiConfiguration"].(map[string]any)
|
||||
if !ok {
|
||||
if global.Config.Verbose {
|
||||
fmt.Println("[DEBUG] No apiConfiguration found in state")
|
||||
@@ -128,11 +128,11 @@ func (r *ProviderListResult) GetAllReadyProviders() []*ProviderDisplay {
|
||||
modelID := getProviderSpecificModelID(r.apiConfig, "plan", provider)
|
||||
|
||||
// Determine if credentials exist
|
||||
hasCreds := checkAPIKeyExists(r.apiConfig, provider)
|
||||
hasCreds := checkCredentialsExists(r.apiConfig, provider)
|
||||
|
||||
// Determine readiness: OCA uses auth state presence; others need creds and model
|
||||
if provider == cline.ApiProvider_OCA {
|
||||
state, _ := GetLatestOCAState(context.Background(), 2 *time.Second)
|
||||
state, _ := GetLatestOCAState(context.Background(), 2*time.Second)
|
||||
if state == nil || state.User == nil {
|
||||
continue
|
||||
}
|
||||
@@ -156,7 +156,7 @@ func (r *ProviderListResult) GetAllReadyProviders() []*ProviderDisplay {
|
||||
Mode: "Ready",
|
||||
Provider: provider,
|
||||
ModelID: modelID,
|
||||
HasAPIKey: checkAPIKeyExists(r.apiConfig, provider),
|
||||
HasAPIKey: checkCredentialsExists(r.apiConfig, provider),
|
||||
BaseURL: baseURL,
|
||||
})
|
||||
seenProviders[provider] = true
|
||||
@@ -192,7 +192,7 @@ func extractProviderFromState(stateData map[string]interface{}, mode string) *Pr
|
||||
modelID := getProviderSpecificModelID(stateData, mode, provider)
|
||||
|
||||
// Check if API key exists
|
||||
hasAPIKey := checkAPIKeyExists(stateData, provider)
|
||||
hasCredentials := checkCredentialsExists(stateData, provider)
|
||||
|
||||
// Get base URL for Ollama (can be shown publicly)
|
||||
baseURL := ""
|
||||
@@ -206,7 +206,7 @@ func extractProviderFromState(stateData map[string]interface{}, mode string) *Pr
|
||||
Mode: capitalizeMode(mode),
|
||||
Provider: provider,
|
||||
ModelID: modelID,
|
||||
HasAPIKey: hasAPIKey,
|
||||
HasAPIKey: hasCredentials,
|
||||
BaseURL: baseURL,
|
||||
}
|
||||
}
|
||||
@@ -215,7 +215,7 @@ func extractProviderFromState(stateData map[string]interface{}, mode string) *Pr
|
||||
// Returns (provider, ok) where ok is false if the provider is unknown
|
||||
func mapProviderStringToEnum(providerStr string) (cline.ApiProvider, bool) {
|
||||
normalizedStr := strings.ToLower(providerStr)
|
||||
|
||||
|
||||
// Map string values to enum values
|
||||
switch normalizedStr {
|
||||
case "anthropic":
|
||||
@@ -303,23 +303,27 @@ func getProviderSpecificModelID(stateData map[string]interface{}, mode string, p
|
||||
return modelID
|
||||
}
|
||||
|
||||
// checkAPIKeyExists checks if API key field exists in state (never retrieve actual key)
|
||||
func checkAPIKeyExists(stateData map[string]interface{}, provider cline.ApiProvider) bool {
|
||||
// checkCredentialsExists checks if API key field exists in state (never retrieve actual key)
|
||||
func checkCredentialsExists(stateData map[string]interface{}, provider cline.ApiProvider) bool {
|
||||
// Get field mapping from centralized function
|
||||
fields, err := GetProviderFields(provider)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
keyField := fields.APIKeyField
|
||||
|
||||
// Check if the key exists and is not empty
|
||||
if value, ok := stateData[keyField]; ok {
|
||||
if value, ok := stateData[fields.APIKeyField]; ok {
|
||||
if str, ok := value.(string); ok && str != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if value, ok := stateData[fields.UseProfileField]; ok {
|
||||
if hasProfileField, ok := value.(bool); ok && hasProfileField {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -438,13 +442,13 @@ func DetectAllConfiguredProviders(ctx context.Context, manager *task.Manager) ([
|
||||
stateJSON := state.StateJson
|
||||
|
||||
// Parse state_json as map[string]interface{}
|
||||
var stateData map[string]interface{}
|
||||
var stateData map[string]any
|
||||
if err := json.Unmarshal([]byte(stateJSON), &stateData); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse state JSON: %w", err)
|
||||
}
|
||||
|
||||
// Extract apiConfiguration object from state
|
||||
apiConfig, ok := stateData["apiConfiguration"].(map[string]interface{})
|
||||
apiConfig, ok := stateData["apiConfiguration"].(map[string]any)
|
||||
if !ok {
|
||||
verboseLog("[DEBUG] No apiConfiguration found in state")
|
||||
verboseLog("[DEBUG] Available keys in stateData: %v", getMapKeys(stateData))
|
||||
@@ -469,36 +473,38 @@ func DetectAllConfiguredProviders(ctx context.Context, manager *task.Manager) ([
|
||||
|
||||
// Check each BYO provider for API key presence
|
||||
providersToCheck := []struct {
|
||||
provider cline.ApiProvider
|
||||
keyField string
|
||||
provider cline.ApiProvider
|
||||
keyFields []string
|
||||
}{
|
||||
{cline.ApiProvider_ANTHROPIC, "apiKey"},
|
||||
{cline.ApiProvider_OPENAI, "openAiApiKey"},
|
||||
{cline.ApiProvider_OPENAI_NATIVE, "openAiNativeApiKey"},
|
||||
{cline.ApiProvider_OPENROUTER, "openRouterApiKey"},
|
||||
{cline.ApiProvider_XAI, "xaiApiKey"},
|
||||
{cline.ApiProvider_BEDROCK, "awsAccessKey"},
|
||||
{cline.ApiProvider_GEMINI, "geminiApiKey"},
|
||||
{cline.ApiProvider_OLLAMA, "ollamaBaseUrl"}, // Ollama uses baseUrl instead of API key
|
||||
{cline.ApiProvider_CEREBRAS, "cerebrasApiKey"},
|
||||
{cline.ApiProvider_HICAP, "hicapApiKey"},
|
||||
{cline.ApiProvider_NOUSRESEARCH, "nousResearchApiKey"},
|
||||
{cline.ApiProvider_ANTHROPIC, []string{"apiKey"}},
|
||||
{cline.ApiProvider_OPENAI, []string{"openAiApiKey"}},
|
||||
{cline.ApiProvider_OPENAI_NATIVE, []string{"openAiNativeApiKey"}},
|
||||
{cline.ApiProvider_OPENROUTER, []string{"openRouterApiKey"}},
|
||||
{cline.ApiProvider_XAI, []string{"xaiApiKey"}},
|
||||
{cline.ApiProvider_BEDROCK, []string{"awsAccessKey", "awsUseProfile"}},
|
||||
{cline.ApiProvider_GEMINI, []string{"geminiApiKey"}},
|
||||
{cline.ApiProvider_OLLAMA, []string{"ollamaBaseUrl"}}, // Ollama uses baseUrl instead of API key
|
||||
{cline.ApiProvider_CEREBRAS, []string{"cerebrasApiKey"}},
|
||||
{cline.ApiProvider_HICAP, []string{"hicapApiKey"}},
|
||||
{cline.ApiProvider_NOUSRESEARCH, []string{"nousResearchApiKey"}},
|
||||
}
|
||||
|
||||
for _, providerCheck := range providersToCheck {
|
||||
verboseLog("[DEBUG] Checking for %s key: %s", GetProviderDisplayName(providerCheck.provider), providerCheck.keyField)
|
||||
if value, ok := apiConfig[providerCheck.keyField]; ok {
|
||||
verboseLog("[DEBUG] Found key, value type: %T, is empty: %v", value, value == "")
|
||||
if str, ok := value.(string); ok && str != "" {
|
||||
configuredProviders = append(configuredProviders, providerCheck.provider)
|
||||
verboseLog("[DEBUG] ✓ Provider %s is configured", GetProviderDisplayName(providerCheck.provider))
|
||||
verboseLog("[DEBUG] Checking for %s key: %s", GetProviderDisplayName(providerCheck.provider), providerCheck.keyFields)
|
||||
for _, keyField := range providerCheck.keyFields {
|
||||
if value, ok := apiConfig[keyField]; ok {
|
||||
verboseLog("[DEBUG] Found key, value type: %T, is empty: %v", value, value == "")
|
||||
if str, ok := value.(string); ok && str != "" {
|
||||
configuredProviders = append(configuredProviders, providerCheck.provider)
|
||||
verboseLog("[DEBUG] ✓ Provider %s is configured", GetProviderDisplayName(providerCheck.provider))
|
||||
break
|
||||
}
|
||||
} else {
|
||||
verboseLog("[DEBUG] Key %s not found", keyField)
|
||||
}
|
||||
} else {
|
||||
verboseLog("[DEBUG] Key %s not found", providerCheck.keyField)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
verboseLog("[DEBUG] Total configured providers: %d", len(configuredProviders))
|
||||
for _, p := range configuredProviders {
|
||||
verboseLog("[DEBUG] - %s", GetProviderDisplayName(p))
|
||||
|
||||
@@ -54,6 +54,7 @@ type ProviderFields struct {
|
||||
// Provider-specific additional model ID fields
|
||||
PlanModeProviderSpecificModelIDField string // e.g., "planModeOpenRouterModelId"
|
||||
ActModeProviderSpecificModelIDField string // e.g., "actModeOpenRouterModelId"
|
||||
UseProfileField string // e.g., "awsUseProfile" (for bedrock) (optional, empty if not applicable)
|
||||
}
|
||||
|
||||
// GetProviderFields returns the field mapping for a given provider
|
||||
@@ -96,6 +97,7 @@ func GetProviderFields(provider cline.ApiProvider) (ProviderFields, error) {
|
||||
|
||||
case cline.ApiProvider_BEDROCK:
|
||||
return ProviderFields{
|
||||
UseProfileField: "awsUseProfile",
|
||||
APIKeyField: "awsAccessKey",
|
||||
PlanModeModelIDField: "planModeApiModelId",
|
||||
ActModeModelIDField: "actModeApiModelId",
|
||||
|
||||
@@ -15,23 +15,23 @@ import (
|
||||
// BedrockConfig holds all AWS Bedrock-specific configuration fields
|
||||
type BedrockConfig struct {
|
||||
// Profile authentication fields
|
||||
UseProfile bool // Always true for successful config
|
||||
Profile string // Optional: AWS profile name (empty = default)
|
||||
Region string // Required: AWS region
|
||||
Endpoint string // Optional: Custom VPC endpoint URL
|
||||
|
||||
UseProfile bool // Always true for successful config
|
||||
Profile string // Optional: AWS profile name (empty = default)
|
||||
Region string // Required: AWS region
|
||||
Endpoint string // Optional: Custom VPC endpoint URL
|
||||
|
||||
// Optional features
|
||||
UseCrossRegionInference bool // Optional: Enable cross-region inference
|
||||
UseGlobalInference bool // Optional: Use global inference endpoint
|
||||
UsePromptCache bool // Optional: Enable prompt caching
|
||||
|
||||
UseCrossRegionInference bool // Optional: Enable cross-region inference
|
||||
UseGlobalInference bool // Optional: Use global inference endpoint
|
||||
UsePromptCache bool // Optional: Enable prompt caching
|
||||
|
||||
// Authentication method (always "profile")
|
||||
Authentication string // Always set to "profile"
|
||||
|
||||
Authentication string // Always set to "profile"
|
||||
|
||||
// Legacy fields (no longer used in profile-only flow)
|
||||
AccessKey string // No longer used
|
||||
SecretKey string // No longer used
|
||||
SessionToken string // No longer used
|
||||
AccessKey string // No longer used
|
||||
SecretKey string // No longer used
|
||||
SessionToken string // No longer used
|
||||
}
|
||||
|
||||
// PromptForBedrockConfig displays a profile-first authentication form for Bedrock configuration
|
||||
@@ -130,7 +130,12 @@ func ApplyBedrockConfig(ctx context.Context, manager *task.Manager, config *Bedr
|
||||
// Build the API configuration with all Bedrock fields
|
||||
apiConfig := &cline.ModelsApiConfiguration{}
|
||||
|
||||
// Set model ID fields
|
||||
// Set provider for both Plan and Act modes
|
||||
bedrockProvider := cline.ApiProvider_BEDROCK
|
||||
apiConfig.PlanModeApiProvider = &bedrockProvider
|
||||
apiConfig.ActModeApiProvider = &bedrockProvider
|
||||
|
||||
// Set model ID field - this is the primary model ID used by Cline Core
|
||||
apiConfig.PlanModeApiModelId = proto.String(modelID)
|
||||
apiConfig.ActModeApiModelId = proto.String(modelID)
|
||||
apiConfig.PlanModeAwsBedrockCustomModelBaseId = proto.String(modelID)
|
||||
@@ -166,6 +171,8 @@ func ApplyBedrockConfig(ctx context.Context, manager *task.Manager, config *Bedr
|
||||
|
||||
// Build field mask including all fields we're setting (excluding access keys)
|
||||
fieldPaths := []string{
|
||||
"planModeApiProvider",
|
||||
"actModeApiProvider",
|
||||
"planModeApiModelId",
|
||||
"actModeApiModelId",
|
||||
"planModeAwsBedrockCustomModelBaseId",
|
||||
|
||||
+18
-130
@@ -1,22 +1,19 @@
|
||||
package display
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/cline/cli/pkg/common"
|
||||
)
|
||||
|
||||
// BannerInfo contains information to display in the session banner
|
||||
type BannerInfo struct {
|
||||
Version string
|
||||
Provider string
|
||||
ModelID string
|
||||
Workdir string
|
||||
Mode string
|
||||
Version string
|
||||
Provider string
|
||||
ModelID string
|
||||
Workdirs []string // workspace directories
|
||||
Mode string
|
||||
}
|
||||
|
||||
// RenderSessionBanner renders a nice banner showing version, model, and workspace info
|
||||
@@ -81,131 +78,22 @@ func RenderSessionBanner(info BannerInfo) string {
|
||||
|
||||
// Model line - dim gray
|
||||
if info.Provider != "" && info.ModelID != "" {
|
||||
lines = append(lines, dimStyle.Render(info.Provider+"/"+shortenPath(info.ModelID, 30)))
|
||||
lines = append(lines, dimStyle.Render(info.Provider+"/"+common.ShortenPath(info.ModelID, 30)))
|
||||
}
|
||||
|
||||
// Workspace line - dim gray
|
||||
if info.Workdir != "" {
|
||||
lines = append(lines, dimStyle.Render(shortenPath(info.Workdir, 45)))
|
||||
for _, wd := range info.Workdirs {
|
||||
lines = append(lines, dimStyle.Render(common.ShortenPath(wd, 45)))
|
||||
}
|
||||
|
||||
// Checkpoint warning for multi-root workspaces
|
||||
if len(info.Workdirs) > 1 {
|
||||
warningStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("3")). // Yellow warning color
|
||||
Italic(true)
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, warningStyle.Render("⚠ Checkpoints disabled for multi-root workspaces"))
|
||||
}
|
||||
|
||||
content := lipgloss.JoinVertical(lipgloss.Left, lines...)
|
||||
return boxStyle.Render(content)
|
||||
}
|
||||
|
||||
// shortenPath shortens a filesystem path to fit within maxLen
|
||||
func shortenPath(path string, maxLen int) string {
|
||||
// Try to replace home directory with ~ (cross-platform)
|
||||
if homeDir, err := os.UserHomeDir(); err == nil {
|
||||
if strings.HasPrefix(path, homeDir) {
|
||||
shortened := "~" + path[len(homeDir):]
|
||||
// Always use ~ version if we can
|
||||
path = shortened
|
||||
}
|
||||
}
|
||||
|
||||
if len(path) <= maxLen {
|
||||
return path
|
||||
}
|
||||
|
||||
// If still too long, show last few path components
|
||||
if len(path) > maxLen {
|
||||
parts := strings.Split(path, string(filepath.Separator))
|
||||
if len(parts) > 2 {
|
||||
// Show last 2-3 components
|
||||
lastParts := parts[len(parts)-2:]
|
||||
shortened := "..." + string(filepath.Separator) + strings.Join(lastParts, string(filepath.Separator))
|
||||
if len(shortened) <= maxLen {
|
||||
return shortened
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort: truncate with ellipsis
|
||||
if len(path) > maxLen {
|
||||
return "..." + path[len(path)-maxLen+3:]
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
// ExtractBannerInfoFromState extracts banner info from state JSON
|
||||
func ExtractBannerInfoFromState(stateJSON, version string) (BannerInfo, error) {
|
||||
var state map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(stateJSON), &state); err != nil {
|
||||
return BannerInfo{}, fmt.Errorf("failed to parse state JSON: %w", err)
|
||||
}
|
||||
|
||||
info := BannerInfo{
|
||||
Version: version,
|
||||
}
|
||||
|
||||
// Extract mode
|
||||
if mode, ok := state["mode"].(string); ok {
|
||||
info.Mode = mode
|
||||
}
|
||||
|
||||
// Extract workspace roots
|
||||
if workspaceRoots, ok := state["workspaceRoots"].([]interface{}); ok && len(workspaceRoots) > 0 {
|
||||
if root, ok := workspaceRoots[0].(map[string]interface{}); ok {
|
||||
if path, ok := root["path"].(string); ok {
|
||||
info.Workdir = path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract API configuration to get provider/model
|
||||
if apiConfig, ok := state["apiConfiguration"].(map[string]interface{}); ok {
|
||||
// Try common keys for provider and model (both camelCase and lowercase variants)
|
||||
providerKeys := []string{"apiProvider", "api_provider"}
|
||||
modelKeys := []string{"apiModelId", "api_model_id"}
|
||||
|
||||
// Try to extract provider
|
||||
for _, key := range providerKeys {
|
||||
if provider, ok := apiConfig[key].(string); ok && provider != "" {
|
||||
info.Provider = provider
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Try to extract model ID
|
||||
for _, key := range modelKeys {
|
||||
if modelID, ok := apiConfig[key].(string); ok && modelID != "" {
|
||||
info.ModelID = shortenModelID(modelID)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// shortenModelID shortens long model IDs for display
|
||||
func shortenModelID(modelID string) string {
|
||||
// Remove date suffixes only if they're at the end (e.g., -20241022)
|
||||
// Check if the model ID ends with -YYYYMMDD pattern
|
||||
if len(modelID) > 9 {
|
||||
suffix := modelID[len(modelID)-9:] // Last 9 chars: -20241022
|
||||
if suffix[0] == '-' &&
|
||||
(strings.HasPrefix(suffix[1:], "202") || strings.HasPrefix(suffix[1:], "201")) {
|
||||
// Verify all remaining chars are digits
|
||||
allDigits := true
|
||||
for _, c := range suffix[1:] {
|
||||
if c < '0' || c > '9' {
|
||||
allDigits = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allDigits {
|
||||
return modelID[:len(modelID)-9]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If still too long, show first 40 chars
|
||||
if len(modelID) > 40 {
|
||||
return modelID[:37] + "..."
|
||||
}
|
||||
|
||||
return modelID
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func (c *ClineClients) Initialize(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// StartNewInstance starts a new Cline instance and waits for cline-core to self-register
|
||||
func (c *ClineClients) StartNewInstance(ctx context.Context) (*common.CoreInstanceInfo, error) {
|
||||
func (c *ClineClients) StartNewInstance(ctx context.Context, workspaces ...string) (*common.CoreInstanceInfo, error) {
|
||||
// Find available ports
|
||||
corePort, hostPort, err := common.FindAvailablePortPair()
|
||||
if err != nil {
|
||||
@@ -48,7 +48,7 @@ func (c *ClineClients) StartNewInstance(ctx context.Context) (*common.CoreInstan
|
||||
}
|
||||
|
||||
// Start cline-host first
|
||||
hostCmd, err := startClineHost(hostPort, corePort)
|
||||
hostCmd, err := startClineHost(hostPort, workspaces)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to start cline-host: %w", err)
|
||||
}
|
||||
@@ -120,7 +120,7 @@ func (c *ClineClients) StartNewInstance(ctx context.Context) (*common.CoreInstan
|
||||
}
|
||||
|
||||
// StartNewInstanceAtPort starts a new Cline instance at the specified port and waits for self-registration
|
||||
func (c *ClineClients) StartNewInstanceAtPort(ctx context.Context, corePort int) (*common.CoreInstanceInfo, error) {
|
||||
func (c *ClineClients) StartNewInstanceAtPort(ctx context.Context, corePort int, workspaces ...string) (*common.CoreInstanceInfo, error) {
|
||||
// Find available host port (core port + 1000)
|
||||
hostPort := corePort + 1000
|
||||
coreAddress := fmt.Sprintf("localhost:%d", corePort)
|
||||
@@ -135,7 +135,7 @@ func (c *ClineClients) StartNewInstanceAtPort(ctx context.Context, corePort int)
|
||||
}
|
||||
|
||||
// Start cline-host first
|
||||
hostCmd, err := startClineHost(hostPort, corePort)
|
||||
hostCmd, err := startClineHost(hostPort, workspaces)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to start cline-host: %w", err)
|
||||
}
|
||||
@@ -242,7 +242,7 @@ func (c *ClineClients) EnsureInstanceAtAddress(ctx context.Context, address stri
|
||||
return fmt.Errorf("cannot start remote instance at %s", normalized)
|
||||
}
|
||||
|
||||
func startClineHost(hostPort, corePort int) (*exec.Cmd, error) {
|
||||
func startClineHost(hostPort int, workspaces []string) (*exec.Cmd, error) {
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Starting cline-host on port %d\n", hostPort)
|
||||
}
|
||||
@@ -255,10 +255,18 @@ func startClineHost(hostPort, corePort int) (*exec.Cmd, error) {
|
||||
binDir := path.Dir(execPath)
|
||||
clineHostPath := path.Join(binDir, "cline-host")
|
||||
|
||||
// Start the cline-host process
|
||||
cmd := exec.Command(clineHostPath,
|
||||
// Build command arguments
|
||||
args := []string{
|
||||
"--verbose",
|
||||
"--port", fmt.Sprintf("%d", hostPort))
|
||||
"--port", fmt.Sprintf("%d", hostPort),
|
||||
}
|
||||
|
||||
for _, ws := range workspaces {
|
||||
args = append(args, "--workspace", ws)
|
||||
}
|
||||
|
||||
// Start the cline-host process
|
||||
cmd := exec.Command(clineHostPath, args...)
|
||||
|
||||
// Create logs directory in ~/.cline/logs
|
||||
logsDir := path.Join(Config.ConfigPath, "logs")
|
||||
@@ -333,7 +341,7 @@ func KillInstanceByAddress(ctx context.Context, registry *ClientRegistry, addres
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Waiting for instance to clean up registry entry...\n")
|
||||
}
|
||||
for i := 0; i < 5; i++ {
|
||||
for range 5 {
|
||||
time.Sleep(1 * time.Second)
|
||||
if !registry.HasInstanceAtAddress(address) {
|
||||
if Config.Verbose {
|
||||
@@ -408,15 +416,15 @@ func startClineCore(corePort, hostPort int) (*exec.Cmd, error) {
|
||||
// This handles the case where we're running from cli/bin/cline
|
||||
devClineCorePath := path.Join(binDir, "..", "..", "dist-standalone", "cline-core.js")
|
||||
devInstallDir := path.Join(binDir, "..", "..", "dist-standalone")
|
||||
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Primary location not found, trying development path: %s\n", devClineCorePath)
|
||||
}
|
||||
|
||||
|
||||
if _, err := os.Stat(devClineCorePath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("cline-core.js not found at '%s' or '%s'. Please ensure you're running from the correct location or reinstall with 'npm install -g cline'", clineCorePath, devClineCorePath)
|
||||
}
|
||||
|
||||
|
||||
finalClineCorePath = devClineCorePath
|
||||
finalInstallDir = devInstallDir
|
||||
if Config.Verbose {
|
||||
@@ -475,7 +483,7 @@ func startClineCore(corePort, hostPort int) (*exec.Cmd, error) {
|
||||
realNodeModules := path.Join(finalInstallDir, "node_modules")
|
||||
fakeNodeModules := path.Join(finalInstallDir, "fake_node_modules")
|
||||
nodePath := fmt.Sprintf("%s%c%s", realNodeModules, os.PathListSeparator, fakeNodeModules)
|
||||
|
||||
|
||||
env = append(env,
|
||||
fmt.Sprintf("NODE_PATH=%s", nodePath),
|
||||
// These control gRPC debug logging
|
||||
@@ -484,7 +492,7 @@ func startClineCore(corePort, hostPort int) (*exec.Cmd, error) {
|
||||
"NODE_ENV=development",
|
||||
)
|
||||
cmd.Env = env
|
||||
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("NODE_PATH set to: %s\n", nodePath)
|
||||
}
|
||||
|
||||
@@ -9,12 +9,13 @@ import (
|
||||
"github.com/charmbracelet/bubbles/textarea"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/cline/cli/pkg/cli/slash"
|
||||
)
|
||||
|
||||
// InputType represents the type of input being collected
|
||||
type InputType int
|
||||
|
||||
const INPUT_WIDTH = 46
|
||||
const INPUT_WIDTH = 46
|
||||
|
||||
const (
|
||||
InputTypeMessage InputType = iota
|
||||
@@ -24,11 +25,11 @@ const (
|
||||
|
||||
// InputSubmitMsg is sent when the user submits input
|
||||
type InputSubmitMsg struct {
|
||||
Value string
|
||||
InputType InputType
|
||||
Approved bool // For approval type
|
||||
NeedsFeedback bool // For approval type
|
||||
NoAskAgain bool // For approval type - indicates "don't ask again" was selected
|
||||
Value string
|
||||
InputType InputType
|
||||
Approved bool // For approval type
|
||||
NeedsFeedback bool // For approval type
|
||||
NoAskAgain bool // For approval type - indicates "don't ask again" was selected
|
||||
}
|
||||
|
||||
// InputCancelMsg is sent when the user cancels input (Ctrl+C)
|
||||
@@ -36,8 +37,8 @@ type InputCancelMsg struct{}
|
||||
|
||||
// ChangeInputTypeMsg changes the current input type
|
||||
type ChangeInputTypeMsg struct {
|
||||
InputType InputType
|
||||
Title string
|
||||
InputType InputType
|
||||
Title string
|
||||
Placeholder string
|
||||
}
|
||||
|
||||
@@ -57,7 +58,7 @@ type InputModel struct {
|
||||
placeholder string
|
||||
currentMode string // "plan" or "act"
|
||||
width int
|
||||
lastHeight int // Track height for cleanup on submit
|
||||
lastHeight int // Track height for cleanup on submit
|
||||
|
||||
// For approval type
|
||||
approvalOptions []string
|
||||
@@ -66,6 +67,9 @@ type InputModel struct {
|
||||
|
||||
// Styles (huh-inspired theme)
|
||||
styles fieldStyles
|
||||
|
||||
// Slash command autocomplete dropdown
|
||||
completion CompletionModel
|
||||
}
|
||||
|
||||
// fieldStyles holds the styling for the input field
|
||||
@@ -115,12 +119,17 @@ func newFieldStyles() fieldStyles {
|
||||
|
||||
// NewInputModel creates a new input model
|
||||
func NewInputModel(inputType InputType, title, placeholder, currentMode string) InputModel {
|
||||
return NewInputModelWithRegistry(inputType, title, placeholder, currentMode, nil)
|
||||
}
|
||||
|
||||
// NewInputModelWithRegistry creates a new input model with slash command autocomplete support
|
||||
func NewInputModelWithRegistry(inputType InputType, title, placeholder, currentMode string, registry *slash.Registry) InputModel {
|
||||
ta := textarea.New()
|
||||
ta.Placeholder = placeholder
|
||||
ta.Focus()
|
||||
ta.CharLimit = 0
|
||||
ta.ShowLineNumbers = false
|
||||
ta.Prompt = "" // Remove prompt prefix (this is what adds the inner border!)
|
||||
ta.Prompt = "" // Remove prompt prefix (this is what adds the inner border!)
|
||||
ta.SetHeight(5)
|
||||
// Don't set width here - let WindowSizeMsg handle it
|
||||
ta.SetWidth(INPUT_WIDTH)
|
||||
@@ -138,11 +147,11 @@ func NewInputModel(inputType InputType, title, placeholder, currentMode string)
|
||||
cursorColor = lipgloss.Color("39") // Blue for act
|
||||
}
|
||||
|
||||
ta.FocusedStyle.CursorLine = lipgloss.NewStyle() // No cursor line highlighting
|
||||
ta.FocusedStyle.EndOfBuffer = lipgloss.NewStyle() // No end-of-buffer styling
|
||||
ta.FocusedStyle.CursorLine = lipgloss.NewStyle() // No cursor line highlighting
|
||||
ta.FocusedStyle.EndOfBuffer = lipgloss.NewStyle() // No end-of-buffer styling
|
||||
ta.FocusedStyle.Placeholder = styles.placeholder
|
||||
ta.FocusedStyle.Text = styles.textArea
|
||||
ta.FocusedStyle.Prompt = lipgloss.NewStyle() // No prompt styling
|
||||
ta.FocusedStyle.Prompt = lipgloss.NewStyle() // No prompt styling
|
||||
ta.Cursor.Style = lipgloss.NewStyle().Foreground(cursorColor)
|
||||
ta.Cursor.TextStyle = styles.textArea
|
||||
|
||||
@@ -154,6 +163,7 @@ func NewInputModel(inputType InputType, title, placeholder, currentMode string)
|
||||
currentMode: currentMode,
|
||||
width: 0, // Will be set by first WindowSizeMsg
|
||||
styles: styles,
|
||||
completion: NewCompletionModel(registry),
|
||||
}
|
||||
|
||||
// For approval type, set up options
|
||||
@@ -217,13 +227,6 @@ func (m *InputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
return m, nil
|
||||
|
||||
default:
|
||||
// Forward all other messages to textarea (including blink ticks)
|
||||
if !m.suspended && (m.inputType == InputTypeMessage || m.inputType == InputTypeFeedback) {
|
||||
m.textarea, cmd = m.textarea.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
case tea.KeyMsg:
|
||||
if m.suspended {
|
||||
return m, nil
|
||||
@@ -231,6 +234,31 @@ func (m *InputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
// Handle keys for text input types (Message/Feedback)
|
||||
if m.inputType == InputTypeMessage || m.inputType == InputTypeFeedback {
|
||||
// When completion menu is visible, let it handle navigation keys first
|
||||
if m.completion.Visible() {
|
||||
// ctrl+c always cancels, even with dropdown open
|
||||
if msg.String() == "ctrl+c" {
|
||||
return m, func() tea.Msg { return InputCancelMsg{} }
|
||||
}
|
||||
|
||||
var handled bool
|
||||
m.completion, cmd, handled = m.completion.Update(msg)
|
||||
if handled {
|
||||
// Check if a completion was selected
|
||||
if applied := m.completion.Apply(); applied != "" {
|
||||
m.textarea.SetValue(applied)
|
||||
m.textarea.CursorEnd()
|
||||
}
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
// Key not handled by completion - pass to textarea and update completion
|
||||
m.textarea, cmd = m.textarea.Update(msg)
|
||||
m.completion.CheckInput(m.textarea.Value())
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
// Normal key handling when completion menu is NOT visible
|
||||
switch msg.String() {
|
||||
case "ctrl+c":
|
||||
return m, func() tea.Msg { return InputCancelMsg{} }
|
||||
@@ -239,6 +267,11 @@ func (m *InputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
// Open external editor (like huh does)
|
||||
return m, m.openEditor()
|
||||
|
||||
case "tab":
|
||||
// Tab without dropdown visible - do nothing special
|
||||
m.textarea, cmd = m.textarea.Update(msg)
|
||||
return m, cmd
|
||||
|
||||
case "enter":
|
||||
// Intercept enter for submit (textarea handles alt+enter and ctrl+j for newlines)
|
||||
return m.handleSubmit()
|
||||
@@ -249,8 +282,9 @@ func (m *InputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
// Pass all other keys to textarea (including alt+enter, ctrl+j for newlines)
|
||||
// Pass all other keys to textarea, then check for slash completion
|
||||
m.textarea, cmd = m.textarea.Update(msg)
|
||||
m.completion.CheckInput(m.textarea.Value())
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
@@ -276,6 +310,13 @@ func (m *InputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
// Forward all other messages to textarea (including blink ticks)
|
||||
if !m.suspended && (m.inputType == InputTypeMessage || m.inputType == InputTypeFeedback) {
|
||||
m.textarea, cmd = m.textarea.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
@@ -365,6 +406,11 @@ func (m *InputModel) View() string {
|
||||
case InputTypeMessage, InputTypeFeedback:
|
||||
parts = append(parts, m.textarea.View())
|
||||
|
||||
// Render completion dropdown if visible
|
||||
if m.completion.Visible() {
|
||||
parts = append(parts, m.completion.View())
|
||||
}
|
||||
|
||||
case InputTypeApproval:
|
||||
var options []string
|
||||
for i, option := range m.approvalOptions {
|
||||
@@ -411,7 +457,7 @@ func (m *InputModel) Clone() *InputModel {
|
||||
ta.ShowLineNumbers = false
|
||||
ta.Prompt = ""
|
||||
ta.SetHeight(5)
|
||||
ta.SetWidth(INPUT_WIDTH)
|
||||
ta.SetWidth(INPUT_WIDTH)
|
||||
ta.Focus()
|
||||
|
||||
// Configure keybindings
|
||||
@@ -446,6 +492,7 @@ func (m *InputModel) Clone() *InputModel {
|
||||
selectedOption: m.selectedOption,
|
||||
pendingApproval: m.pendingApproval, // Preserve approval decision
|
||||
styles: m.styles,
|
||||
completion: NewCompletionModel(m.completion.registry), // Preserve registry, start fresh state
|
||||
}
|
||||
|
||||
return clone
|
||||
@@ -495,3 +542,8 @@ func (m *InputModel) openEditor() tea.Cmd {
|
||||
return editorFinishedMsg{content: content, err: err}
|
||||
})
|
||||
}
|
||||
|
||||
// SetSlashRegistry sets the slash command registry for autocomplete
|
||||
func (m *InputModel) SetSlashRegistry(registry *slash.Registry) {
|
||||
m.completion.SetRegistry(registry)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/cline/cli/pkg/cli/slash"
|
||||
)
|
||||
|
||||
const maxVisibleCompletions = 7
|
||||
|
||||
// completionStyles holds the styling for the completion dropdown
|
||||
type completionStyles struct {
|
||||
menu lipgloss.Style
|
||||
selected lipgloss.Style
|
||||
normalName lipgloss.Style
|
||||
description lipgloss.Style
|
||||
scrollIndicator lipgloss.Style
|
||||
}
|
||||
|
||||
// newCompletionStyles creates the default styles for the completion dropdown
|
||||
func newCompletionStyles() completionStyles {
|
||||
return completionStyles{
|
||||
menu: lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(lipgloss.Color("238")).
|
||||
Padding(0, 1),
|
||||
selected: lipgloss.NewStyle().
|
||||
Background(lipgloss.Color("62")).
|
||||
Foreground(lipgloss.Color("230")),
|
||||
normalName: lipgloss.NewStyle().
|
||||
Foreground(lipgloss.AdaptiveColor{Light: "235", Dark: "252"}),
|
||||
description: lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("243")),
|
||||
scrollIndicator: lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("243")),
|
||||
}
|
||||
}
|
||||
|
||||
// CompletionModel is a Bubbletea model for slash command autocomplete dropdown
|
||||
type CompletionModel struct {
|
||||
registry *slash.Registry
|
||||
visible bool
|
||||
matches []slash.Command
|
||||
index int // selected item (0-based)
|
||||
scroll int // scroll offset for long lists
|
||||
styles completionStyles
|
||||
|
||||
// pendingApply holds the command to apply after selection
|
||||
pendingApply string
|
||||
}
|
||||
|
||||
// NewCompletionModel creates a new completion model with the given registry
|
||||
func NewCompletionModel(registry *slash.Registry) CompletionModel {
|
||||
return CompletionModel{
|
||||
registry: registry,
|
||||
styles: newCompletionStyles(),
|
||||
}
|
||||
}
|
||||
|
||||
// SetRegistry sets the slash command registry
|
||||
func (m *CompletionModel) SetRegistry(registry *slash.Registry) {
|
||||
m.registry = registry
|
||||
}
|
||||
|
||||
// Visible returns whether the completion dropdown is currently visible
|
||||
func (m CompletionModel) Visible() bool {
|
||||
return m.visible
|
||||
}
|
||||
|
||||
// Update handles key messages for the completion dropdown.
|
||||
// Returns the updated model, any commands, and whether the key was handled.
|
||||
// If handled is true, the parent should NOT pass the key to the textarea.
|
||||
func (m CompletionModel) Update(msg tea.Msg) (CompletionModel, tea.Cmd, bool) {
|
||||
if !m.visible {
|
||||
return m, nil, false
|
||||
}
|
||||
|
||||
keyMsg, ok := msg.(tea.KeyMsg)
|
||||
if !ok {
|
||||
return m, nil, false
|
||||
}
|
||||
|
||||
switch keyMsg.String() {
|
||||
case "up":
|
||||
m.navigateUp()
|
||||
return m, nil, true
|
||||
|
||||
case "down":
|
||||
m.navigateDown()
|
||||
return m, nil, true
|
||||
|
||||
case "tab", "enter":
|
||||
// Select the current completion
|
||||
if len(m.matches) > 0 {
|
||||
selected := m.matches[m.index]
|
||||
m.pendingApply = "/" + selected.Name + " "
|
||||
}
|
||||
m.Hide()
|
||||
return m, nil, true
|
||||
|
||||
case "esc":
|
||||
m.Hide()
|
||||
return m, nil, true
|
||||
}
|
||||
|
||||
// Key not handled by completion - let parent process it
|
||||
return m, nil, false
|
||||
}
|
||||
|
||||
// CheckInput updates the completion state based on the current input value.
|
||||
// Call this after each input change to show/hide/update the dropdown.
|
||||
func (m *CompletionModel) CheckInput(value string) {
|
||||
if m.registry == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Only activate if input starts with "/" (first character requirement)
|
||||
if !strings.HasPrefix(value, "/") {
|
||||
m.Hide()
|
||||
return
|
||||
}
|
||||
|
||||
// Extract the command being typed (everything after "/" until space/newline)
|
||||
rest := value[1:] // Everything after the "/"
|
||||
|
||||
// If there's whitespace, the command is complete - hide dropdown
|
||||
if idx := strings.IndexAny(rest, " \n\t"); idx != -1 {
|
||||
m.Hide()
|
||||
return
|
||||
}
|
||||
|
||||
// Update matches based on prefix
|
||||
m.updateMatches(rest)
|
||||
m.visible = len(m.matches) > 0
|
||||
}
|
||||
|
||||
// Apply returns the command string to insert (if any) and clears the pending state.
|
||||
// The parent should call this after Update returns handled=true for tab/enter.
|
||||
func (m *CompletionModel) Apply() string {
|
||||
result := m.pendingApply
|
||||
m.pendingApply = ""
|
||||
return result
|
||||
}
|
||||
|
||||
// Hide hides the completion dropdown and resets state
|
||||
func (m *CompletionModel) Hide() {
|
||||
m.visible = false
|
||||
m.matches = nil
|
||||
m.index = 0
|
||||
m.scroll = 0
|
||||
}
|
||||
|
||||
// View renders the completion dropdown
|
||||
func (m CompletionModel) View() string {
|
||||
if !m.visible || len(m.matches) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var lines []string
|
||||
|
||||
// Calculate visible range
|
||||
endIdx := min(m.scroll+maxVisibleCompletions, len(m.matches))
|
||||
|
||||
// Show scroll indicator if there are items above
|
||||
if m.scroll > 0 {
|
||||
lines = append(lines, m.styles.scrollIndicator.Render(" ↑ more"))
|
||||
}
|
||||
|
||||
// Find the longest command name for alignment
|
||||
maxNameLen := 0
|
||||
for _, cmd := range m.matches {
|
||||
nameLen := len(cmd.Name) + 1 // +1 for the "/"
|
||||
if nameLen > maxNameLen {
|
||||
maxNameLen = nameLen
|
||||
}
|
||||
}
|
||||
// Cap at reasonable width
|
||||
if maxNameLen > 15 {
|
||||
maxNameLen = 15
|
||||
}
|
||||
|
||||
// Render visible items
|
||||
for i := m.scroll; i < endIdx; i++ {
|
||||
cmd := m.matches[i]
|
||||
name := "/" + cmd.Name
|
||||
desc := cmd.Description
|
||||
|
||||
// Truncate description if too long
|
||||
maxDescLen := 35
|
||||
if len(desc) > maxDescLen {
|
||||
desc = desc[:maxDescLen-3] + "..."
|
||||
}
|
||||
|
||||
// Pad name for alignment
|
||||
paddedName := fmt.Sprintf("%-*s", maxNameLen, name)
|
||||
|
||||
if i == m.index {
|
||||
// Selected item - highlight the entire line
|
||||
line := fmt.Sprintf("> %s %s", paddedName, desc)
|
||||
lines = append(lines, m.styles.selected.Render(line))
|
||||
} else {
|
||||
// Normal item
|
||||
line := fmt.Sprintf(" %s %s", m.styles.normalName.Render(paddedName), m.styles.description.Render(desc))
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
|
||||
// Show scroll indicator if there are items below
|
||||
if endIdx < len(m.matches) {
|
||||
lines = append(lines, m.styles.scrollIndicator.Render(" ↓ more"))
|
||||
}
|
||||
|
||||
return m.styles.menu.Render(strings.Join(lines, "\n"))
|
||||
}
|
||||
|
||||
// updateMatches filters commands by prefix and updates the matches list
|
||||
func (m *CompletionModel) updateMatches(prefix string) {
|
||||
if m.registry == nil {
|
||||
m.matches = nil
|
||||
return
|
||||
}
|
||||
m.matches = m.registry.GetMatching(prefix)
|
||||
// Reset selection if out of bounds
|
||||
if m.index >= len(m.matches) {
|
||||
m.index = 0
|
||||
m.scroll = 0
|
||||
}
|
||||
m.adjustScroll()
|
||||
}
|
||||
|
||||
// navigateUp moves selection up in the dropdown
|
||||
func (m *CompletionModel) navigateUp() {
|
||||
if len(m.matches) == 0 {
|
||||
return
|
||||
}
|
||||
m.index--
|
||||
if m.index < 0 {
|
||||
m.index = len(m.matches) - 1
|
||||
}
|
||||
m.adjustScroll()
|
||||
}
|
||||
|
||||
// navigateDown moves selection down in the dropdown
|
||||
func (m *CompletionModel) navigateDown() {
|
||||
if len(m.matches) == 0 {
|
||||
return
|
||||
}
|
||||
m.index++
|
||||
if m.index >= len(m.matches) {
|
||||
m.index = 0
|
||||
}
|
||||
m.adjustScroll()
|
||||
}
|
||||
|
||||
// adjustScroll ensures the selected item is visible in the dropdown
|
||||
func (m *CompletionModel) adjustScroll() {
|
||||
if m.index < m.scroll {
|
||||
m.scroll = m.index
|
||||
} else if m.index >= m.scroll+maxVisibleCompletions {
|
||||
m.scroll = m.index - maxVisibleCompletions + 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package slash
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/cline/grpc-go/client"
|
||||
"github.com/cline/grpc-go/cline"
|
||||
)
|
||||
|
||||
// Command represents a slash command available for autocomplete
|
||||
type Command struct {
|
||||
Name string
|
||||
Description string
|
||||
Section string // "default", "custom", or "cli"
|
||||
CLICompatible bool
|
||||
}
|
||||
|
||||
// Registry holds available slash commands for autocomplete
|
||||
type Registry struct {
|
||||
mu sync.RWMutex
|
||||
commands []Command
|
||||
}
|
||||
|
||||
// CLI-local commands (handled by CLI, not sent to backend)
|
||||
var cliLocalCommands = []Command{
|
||||
{Name: "plan", Description: "Switch to plan mode", Section: "cli", CLICompatible: true},
|
||||
{Name: "act", Description: "Switch to act mode", Section: "cli", CLICompatible: true},
|
||||
{Name: "cancel", Description: "Cancel the current task", Section: "cli", CLICompatible: true},
|
||||
{Name: "exit", Description: "Exit follow mode", Section: "cli", CLICompatible: true},
|
||||
}
|
||||
|
||||
// NewRegistry creates a new slash command registry
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
commands: make([]Command, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// FetchFromBackend fetches available commands from cline-core backend
|
||||
func (r *Registry) FetchFromBackend(ctx context.Context, c *client.ClineClient) error {
|
||||
resp, err := c.Slash.GetAvailableSlashCommands(ctx, &cline.EmptyRequest{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
// Start with CLI-local commands
|
||||
r.commands = append([]Command{}, cliLocalCommands...)
|
||||
|
||||
// Add backend commands (only CLI-compatible ones)
|
||||
for _, cmd := range resp.Commands {
|
||||
if cmd.CliCompatible {
|
||||
r.commands = append(r.commands, Command{
|
||||
Name: cmd.Name,
|
||||
Description: cmd.Description,
|
||||
Section: cmd.Section,
|
||||
CLICompatible: cmd.CliCompatible,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCommands returns all available commands
|
||||
func (r *Registry) GetCommands() []Command {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
// Return a copy to avoid race conditions
|
||||
result := make([]Command, len(r.commands))
|
||||
copy(result, r.commands)
|
||||
return result
|
||||
}
|
||||
|
||||
// GetMatching returns commands that start with the given prefix (case-insensitive)
|
||||
func (r *Registry) GetMatching(prefix string) []Command {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
prefix = strings.ToLower(prefix)
|
||||
var matches []Command
|
||||
for _, cmd := range r.commands {
|
||||
if strings.HasPrefix(strings.ToLower(cmd.Name), prefix) {
|
||||
matches = append(matches, cmd)
|
||||
}
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
// IsValid checks if a command name is valid
|
||||
func (r *Registry) IsValid(name string) bool {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
name = strings.ToLower(name)
|
||||
for _, cmd := range r.commands {
|
||||
if strings.ToLower(cmd.Name) == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsCLILocal checks if a command is handled locally by CLI (not sent to backend)
|
||||
func (r *Registry) IsCLILocal(name string) bool {
|
||||
name = strings.ToLower(name)
|
||||
for _, cmd := range cliLocalCommands {
|
||||
if strings.ToLower(cmd.Name) == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasCommands returns true if the registry has any commands loaded
|
||||
func (r *Registry) HasCommands() bool {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return len(r.commands) > 0
|
||||
}
|
||||
+8
-7
@@ -20,13 +20,14 @@ import (
|
||||
|
||||
// TaskOptions contains options for creating a task
|
||||
type TaskOptions struct {
|
||||
Images []string
|
||||
Files []string
|
||||
Mode string
|
||||
Settings []string
|
||||
Yolo bool
|
||||
Address string
|
||||
Verbose bool
|
||||
Images []string
|
||||
Files []string
|
||||
Mode string
|
||||
Settings []string
|
||||
Yolo bool
|
||||
Address string
|
||||
Verbose bool
|
||||
Workspaces []string
|
||||
}
|
||||
|
||||
func NewTaskCommand() *cobra.Command {
|
||||
|
||||
@@ -38,13 +38,13 @@ type InputHandler struct {
|
||||
// NewInputHandler creates a new input handler
|
||||
func NewInputHandler(manager *Manager, coordinator *StreamCoordinator, cancelFunc context.CancelFunc) *InputHandler {
|
||||
return &InputHandler{
|
||||
manager: manager,
|
||||
coordinator: coordinator,
|
||||
cancelFunc: cancelFunc,
|
||||
isRunning: false,
|
||||
pollTicker: time.NewTicker(500 * time.Millisecond),
|
||||
resultChan: make(chan output.InputSubmitMsg, 1),
|
||||
cancelChan: make(chan struct{}, 1),
|
||||
manager: manager,
|
||||
coordinator: coordinator,
|
||||
cancelFunc: cancelFunc,
|
||||
isRunning: false,
|
||||
pollTicker: time.NewTicker(500 * time.Millisecond),
|
||||
resultChan: make(chan output.InputSubmitMsg, 1),
|
||||
cancelChan: make(chan struct{}, 1),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,11 +281,12 @@ func determineAutoApprovalAction(msg *types.ClineMessage) (string, error) {
|
||||
func (ih *InputHandler) promptForInput(ctx context.Context) (string, bool, error) {
|
||||
currentMode := ih.manager.GetCurrentMode()
|
||||
|
||||
model := output.NewInputModel(
|
||||
model := output.NewInputModelWithRegistry(
|
||||
output.InputTypeMessage,
|
||||
"Cline is ready for your message...",
|
||||
"/plan or /act to switch modes\nctrl+e to open editor",
|
||||
"/plan or /act to switch modes\nctrl+e to open editor\ntab to autocomplete commands",
|
||||
currentMode,
|
||||
ih.manager.GetSlashRegistry(),
|
||||
)
|
||||
|
||||
return ih.runInputProgram(ctx, model)
|
||||
@@ -295,12 +296,13 @@ func (ih *InputHandler) promptForInput(ctx context.Context) (string, bool, error
|
||||
func (ih *InputHandler) promptForApproval(ctx context.Context, msg *types.ClineMessage) (bool, string, error) {
|
||||
// Store the approval message for later use in determining auto-approval action
|
||||
ih.approvalMessage = msg
|
||||
|
||||
model := output.NewInputModel(
|
||||
|
||||
model := output.NewInputModelWithRegistry(
|
||||
output.InputTypeApproval,
|
||||
"Let Cline use this tool?",
|
||||
"",
|
||||
ih.manager.GetCurrentMode(),
|
||||
ih.manager.GetSlashRegistry(), // Pass registry for feedback input after approval
|
||||
)
|
||||
|
||||
message, shouldSend, err := ih.runInputProgram(ctx, model)
|
||||
@@ -395,7 +397,7 @@ func (ih *InputHandler) runInputProgram(ctx context.Context, model output.InputM
|
||||
// Need to collect feedback - will be handled by model state change
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
|
||||
// Check if NoAskAgain was selected
|
||||
if result.NoAskAgain && result.Approved && ih.approvalMessage != nil {
|
||||
// Determine which auto-approval action to enable
|
||||
@@ -411,7 +413,7 @@ func (ih *InputHandler) runInputProgram(ctx context.Context, model output.InputM
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Store approval state for when feedback comes back
|
||||
ih.feedbackApproval = false
|
||||
ih.feedbackApproved = result.Approved
|
||||
@@ -441,6 +443,7 @@ func (w *inputProgramWrapper) Init() tea.Cmd {
|
||||
}
|
||||
|
||||
func (w *inputProgramWrapper) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case output.InputSubmitMsg:
|
||||
// Handle input submission - clear the screen before quitting
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/cline/cli/pkg/cli/display"
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
"github.com/cline/cli/pkg/cli/handlers"
|
||||
"github.com/cline/cli/pkg/cli/slash"
|
||||
"github.com/cline/cli/pkg/cli/types"
|
||||
"github.com/cline/grpc-go/client"
|
||||
"github.com/cline/grpc-go/cline"
|
||||
@@ -36,6 +37,7 @@ type Manager struct {
|
||||
systemRenderer *display.SystemMessageRenderer
|
||||
streamingDisplay *display.StreamingDisplay
|
||||
handlerRegistry *handlers.HandlerRegistry
|
||||
slashRegistry *slash.Registry
|
||||
isStreamingMode bool
|
||||
isInteractive bool
|
||||
currentMode string // "plan" or "act"
|
||||
@@ -63,6 +65,7 @@ func NewManager(client *client.ClineClient) *Manager {
|
||||
systemRenderer: systemRenderer,
|
||||
streamingDisplay: streamingDisplay,
|
||||
handlerRegistry: registry,
|
||||
slashRegistry: slash.NewRegistry(),
|
||||
currentMode: "plan", // Default mode
|
||||
}
|
||||
}
|
||||
@@ -76,6 +79,10 @@ func NewManagerForAddress(ctx context.Context, address string) (*Manager, error)
|
||||
|
||||
manager := NewManager(client)
|
||||
manager.clientAddress = address
|
||||
|
||||
// Fetch slash commands from backend (non-blocking, errors are logged)
|
||||
manager.fetchSlashCommands(ctx)
|
||||
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
@@ -93,9 +100,25 @@ func NewManagerForDefault(ctx context.Context) (*Manager, error) {
|
||||
manager.clientAddress = global.Clients.GetRegistry().GetDefaultInstance()
|
||||
}
|
||||
|
||||
// Fetch slash commands from backend (non-blocking, errors are logged)
|
||||
manager.fetchSlashCommands(ctx)
|
||||
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
// fetchSlashCommands fetches available slash commands from the backend
|
||||
// This is non-blocking and errors are logged but don't prevent manager creation
|
||||
func (m *Manager) fetchSlashCommands(ctx context.Context) {
|
||||
if err := m.slashRegistry.FetchFromBackend(ctx, m.client); err != nil {
|
||||
if global.Config.Verbose {
|
||||
m.renderer.RenderDebug("Failed to fetch slash commands: %v", err)
|
||||
}
|
||||
// Non-fatal: CLI-local commands are still available
|
||||
} else if global.Config.Verbose {
|
||||
m.renderer.RenderDebug("Loaded %d slash commands", len(m.slashRegistry.GetCommands()))
|
||||
}
|
||||
}
|
||||
|
||||
// SwitchToInstance switches the manager to use a different Cline instance
|
||||
func (m *Manager) SwitchToInstance(ctx context.Context, address string) error {
|
||||
m.mu.Lock()
|
||||
@@ -1263,6 +1286,11 @@ func (m *Manager) GetCurrentMode() string {
|
||||
return m.currentMode
|
||||
}
|
||||
|
||||
// GetSlashRegistry returns the slash command registry
|
||||
func (m *Manager) GetSlashRegistry() *slash.Registry {
|
||||
return m.slashRegistry
|
||||
}
|
||||
|
||||
// extractModeFromState extracts the current mode from state JSON
|
||||
func (m *Manager) extractModeFromState(stateJson string) string {
|
||||
var rawState map[string]interface{}
|
||||
|
||||
@@ -3,15 +3,16 @@ package types
|
||||
// HistoryItem represents a task history item from taskHistory.json
|
||||
// This struct matches the JSON format stored on disk
|
||||
type HistoryItem struct {
|
||||
Id string `json:"id"`
|
||||
Ulid string `json:"ulid,omitempty"`
|
||||
Ts int64 `json:"ts"`
|
||||
Task string `json:"task"`
|
||||
TokensIn int32 `json:"tokensIn"`
|
||||
TokensOut int32 `json:"tokensOut"`
|
||||
CacheWrites int32 `json:"cacheWrites,omitempty"`
|
||||
CacheReads int32 `json:"cacheReads,omitempty"`
|
||||
TotalCost float64 `json:"totalCost"`
|
||||
Size int64 `json:"size,omitempty"`
|
||||
IsFavorited bool `json:"isFavorited,omitempty"`
|
||||
Id string `json:"id"`
|
||||
Ulid string `json:"ulid,omitempty"`
|
||||
Ts int64 `json:"ts"`
|
||||
Task string `json:"task"`
|
||||
TokensIn int32 `json:"tokensIn"`
|
||||
TokensOut int32 `json:"tokensOut"`
|
||||
CacheWrites int32 `json:"cacheWrites,omitempty"`
|
||||
CacheReads int32 `json:"cacheReads,omitempty"`
|
||||
TotalCost float64 `json:"totalCost"`
|
||||
Size int64 `json:"size,omitempty"`
|
||||
IsFavorited bool `json:"isFavorited,omitempty"`
|
||||
WorkspacePaths []string `json:"workspacePaths,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -183,3 +185,72 @@ DEBUGGING STEPS:
|
||||
For additional help, visit: https://github.com/cline/cline/issues
|
||||
`, maxRetries, lastErr, GetNodeVersion())
|
||||
}
|
||||
|
||||
// validateDirsExist validates that all workspace paths exist on the filesystem
|
||||
func ValidateDirsExist(paths []string) error {
|
||||
for _, p := range paths {
|
||||
info, err := os.Stat(p)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("path does not exist: %s", p)
|
||||
}
|
||||
return fmt.Errorf("failed to access path %s: %w", p, err)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return fmt.Errorf("path is not a directory: %s", p)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// absPath returns the absolute path, resolving symlinks
|
||||
func AbsPath(path string) (string, error) {
|
||||
// First get absolute path
|
||||
abs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Then resolve any symlinks
|
||||
resolved, err := filepath.EvalSymlinks(abs)
|
||||
if err != nil {
|
||||
// If symlink resolution fails, return the absolute path
|
||||
return abs, nil
|
||||
}
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
// shortenPath shortens a filesystem path to fit within maxLen
|
||||
func ShortenPath(path string, maxLen int) string {
|
||||
// Try to replace home directory with ~ (cross-platform)
|
||||
if homeDir, err := os.UserHomeDir(); err == nil {
|
||||
if strings.HasPrefix(path, homeDir) {
|
||||
shortened := "~" + path[len(homeDir):]
|
||||
// Always use ~ version if we can
|
||||
path = shortened
|
||||
}
|
||||
}
|
||||
|
||||
if len(path) <= maxLen {
|
||||
return path
|
||||
}
|
||||
|
||||
// If still too long, show last few path components
|
||||
if len(path) > maxLen {
|
||||
parts := strings.Split(path, string(filepath.Separator))
|
||||
if len(parts) > 2 {
|
||||
// Show last 2-3 components
|
||||
lastParts := parts[len(parts)-2:]
|
||||
shortened := "..." + string(filepath.Separator) + strings.Join(lastParts, string(filepath.Separator))
|
||||
if len(shortened) <= maxLen {
|
||||
return shortened
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort: truncate with ellipsis
|
||||
if len(path) > maxLen {
|
||||
return "..." + path[len(path)-maxLen+3:]
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
@@ -16,15 +16,17 @@ import (
|
||||
type GrpcServer struct {
|
||||
port int
|
||||
verbose bool
|
||||
workspaces []string
|
||||
server *grpc.Server
|
||||
shutdownCh chan struct{}
|
||||
}
|
||||
|
||||
// NewGrpcServer creates a new GrpcServer
|
||||
func NewGrpcServer(port int, verbose bool) *GrpcServer {
|
||||
func NewGrpcServer(port int, verbose bool, workspaces []string) *GrpcServer {
|
||||
return &GrpcServer{
|
||||
port: port,
|
||||
verbose: verbose,
|
||||
workspaces: workspaces,
|
||||
shutdownCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
@@ -50,7 +52,7 @@ func (s *GrpcServer) Start(ctx context.Context) error {
|
||||
grpc_health_v1.RegisterHealthServer(s.server, healthServer)
|
||||
|
||||
// Register services
|
||||
workspaceService := NewSimpleWorkspaceService(s.verbose)
|
||||
workspaceService := NewSimpleWorkspaceService(s.verbose, s.workspaces)
|
||||
host.RegisterWorkspaceServiceServer(s.server, workspaceService)
|
||||
|
||||
windowService := NewWindowService(s.verbose)
|
||||
|
||||
@@ -12,13 +12,15 @@ import (
|
||||
// SimpleWorkspaceService implements a basic workspace service without complex dependencies
|
||||
type SimpleWorkspaceService struct {
|
||||
host.UnimplementedWorkspaceServiceServer
|
||||
verbose bool
|
||||
verbose bool
|
||||
workspaces []string
|
||||
}
|
||||
|
||||
// NewSimpleWorkspaceService creates a new SimpleWorkspaceService
|
||||
func NewSimpleWorkspaceService(verbose bool) *SimpleWorkspaceService {
|
||||
func NewSimpleWorkspaceService(verbose bool, workspaces []string) *SimpleWorkspaceService {
|
||||
return &SimpleWorkspaceService{
|
||||
verbose: verbose,
|
||||
verbose: verbose,
|
||||
workspaces: workspaces,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,14 +30,24 @@ func (s *SimpleWorkspaceService) GetWorkspacePaths(ctx context.Context, req *hos
|
||||
log.Printf("GetWorkspacePaths called")
|
||||
}
|
||||
|
||||
// Get current working directory as the workspace
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
paths := []string{}
|
||||
|
||||
if len(s.workspaces) == 0 {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paths = append(paths, cwd)
|
||||
} else {
|
||||
paths = s.workspaces
|
||||
}
|
||||
|
||||
if s.verbose {
|
||||
log.Printf("Returning configured workspaces: %v", paths)
|
||||
}
|
||||
|
||||
return &host.GetWorkspacePathsResponse{
|
||||
Paths: []string{cwd},
|
||||
Paths: paths,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,12 @@ INSTANT TASK OPTIONS
|
||||
-m, --mode mode
|
||||
Starting mode. Options: act (default), plan
|
||||
|
||||
-w, --workspace path
|
||||
Additional workspace paths. Can be specified multiple times to
|
||||
include multiple directories. The current working directory is
|
||||
always included as the first workspace. Example: cline -w
|
||||
/path/to/other/project "refactor shared code"
|
||||
|
||||
GLOBAL OPTIONS
|
||||
These options apply to all subcommands:
|
||||
|
||||
@@ -278,6 +284,29 @@ TASK SETTINGS
|
||||
|
||||
mode Starting mode (act/plan)
|
||||
|
||||
hooks_enabled
|
||||
Enable or disable hooks for the task (true/false)
|
||||
|
||||
HOOKS INTEGRATION
|
||||
Hooks let you inject custom logic into Cline's workflow at key moments.
|
||||
They can validate operations before they execute, monitor tool usage,
|
||||
and shape AI decisions. This allows you to integrate hooks into
|
||||
automated workflows, CI/CD pipelines, and headless task execution.
|
||||
|
||||
Enable hooks for a task:
|
||||
|
||||
cline "prompt" -s hooks_enabled=true
|
||||
|
||||
Configure hooks globally:
|
||||
|
||||
cline config set hooks-enabled=true
|
||||
cline config get hooks-enabled
|
||||
|
||||
Note: Hooks in the CLI are only supported on macOS and Linux.
|
||||
|
||||
For complete hooks documentation, see:
|
||||
<https://docs.cline.bot/features/hooks/index>
|
||||
|
||||
NOTES & EXAMPLES
|
||||
The cline task send and cline task new commands support reading from
|
||||
stdin, enabling powerful pipeline compositions:
|
||||
@@ -348,6 +377,43 @@ COPYRIGHT
|
||||
Copyright © 2025 Cline Bot Inc. Licensed under the Apache License 2.0.
|
||||
```
|
||||
|
||||
## JSON output (-F json)
|
||||
|
||||
When you run a command with `-F json` (or `--output-format json`), Cline prints each client message as JSON.
|
||||
|
||||
### ClineMessage schema
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|-------|------|----------|-------|
|
||||
| `type` | `"ask" or "say"` | Yes | Top-level message category. |
|
||||
| `text` | `string` | Yes | Human-readable message content. |
|
||||
| `ts` | `number` | Yes | Unix epoch timestamp in milliseconds. |
|
||||
| `reasoning` | `string` | No | Omitted when empty. |
|
||||
| `say` | `string` | No | Omitted when empty. Present when `type` is `"say"`. |
|
||||
| `ask` | `string` | No | Omitted when empty. Present when `type` is `"ask"`. |
|
||||
| `partial` | `boolean` | No | Omitted when false. `true` for streaming updates. |
|
||||
| `images` | `string[]` | No | Omitted when empty. Image URIs when included with a message. |
|
||||
| `files` | `string[]` | No | Omitted when empty. File paths when attached to a message. |
|
||||
| `lastCheckpointHash` | `string` | No | Omitted when empty. Git checkpoint hash when available. |
|
||||
| `isCheckpointCheckedOut` | `boolean` | No | Omitted when false. `true` if Cline checked out a checkpoint. |
|
||||
| `isOperationOutsideWorkspace` | `boolean` | No | Omitted when false. `true` if an operation happened outside the workspace. |
|
||||
|
||||
<Note>
|
||||
Most fields are optional and omitted when empty. If you parse this output, treat missing fields as “not present”, not as empty strings.
|
||||
</Note>
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "say",
|
||||
"text": "Cline is about to run a command.",
|
||||
"ts": 1760501486669,
|
||||
"say": "command",
|
||||
"partial": false
|
||||
}
|
||||
```
|
||||
|
||||
### Shell Completion
|
||||
|
||||
Generate autocompletion scripts for various shells:
|
||||
|
||||
@@ -57,6 +57,20 @@ During installation, you'll authenticate and configure your preferred provider u
|
||||
- Create GitLab pipelines that generate migration scripts from schema changes
|
||||
- Build Jenkins jobs that use Cline to analyze test failures and suggest fixes
|
||||
|
||||
## Hooks integration
|
||||
|
||||
[Hooks](/features/hooks/index) let you inject custom logic into Cline's workflow to validate operations and enforce policies. You can enable hooks when running tasks from the command line:
|
||||
|
||||
```bash
|
||||
# Enable hooks for a task
|
||||
cline "What does this repo do?" -s hooks_enabled=true
|
||||
|
||||
# Configure hooks globally via CLI
|
||||
cline config set hooks-enabled=true
|
||||
```
|
||||
|
||||
This allows you to integrate hooks into automated workflows, CI/CD pipelines, and headless task execution for consistent enforcement across all environments.
|
||||
|
||||
## Learn more
|
||||
|
||||
<Columns cols={2}>
|
||||
|
||||
@@ -13,46 +13,6 @@ Cline is your AI assistant that can:
|
||||
- Automate repetitive tasks
|
||||
- Integrate with external tools
|
||||
|
||||
## First Steps
|
||||
|
||||
1. **Start a Task**
|
||||
|
||||
- Type your request in the chat
|
||||
- Example: "Create a new React component called Header"
|
||||
|
||||
2. **Provide Context**
|
||||
|
||||
- Use @ mentions to add files, folders, or URLs
|
||||
- Example: "@file:src/components/App.tsx"
|
||||
|
||||
3. **Review Changes**
|
||||
- Cline will show diffs before making changes
|
||||
- You can edit or reject changes
|
||||
|
||||
## Key Features
|
||||
|
||||
1. **File Editing**
|
||||
|
||||
- Create new files
|
||||
- Modify existing code
|
||||
- Search and replace across files
|
||||
|
||||
2. **Terminal Commands**
|
||||
|
||||
- Run npm commands
|
||||
- Start development servers
|
||||
- Install dependencies
|
||||
|
||||
3. **Code Analysis**
|
||||
|
||||
- Find and fix errors
|
||||
- Refactor code
|
||||
- Add documentation
|
||||
|
||||
4. **Browser Integration**
|
||||
- Test web pages
|
||||
- Capture screenshots
|
||||
- Inspect console logs
|
||||
|
||||
## Available Tools
|
||||
|
||||
@@ -84,6 +44,7 @@ Cline has access to the following tools for various tasks:
|
||||
- `ask_followup_question`: Ask user for clarification
|
||||
- `attempt_completion`: Present final results
|
||||
|
||||
|
||||
Each tool has specific parameters and usage patterns. Here are some examples:
|
||||
|
||||
- Create a new file (write_to_file):
|
||||
|
||||
@@ -1,59 +1,104 @@
|
||||
The Auto Approve menu lets you set fine-grained permissions on what you allow Cline to do in an automated way.
|
||||
---
|
||||
title: "Auto Approve"
|
||||
sidebarTitle: "Auto Approve"
|
||||
description: "Let Cline take specific actions without asking for approval every time."
|
||||
---
|
||||
|
||||
Auto Approve lets you decide which actions Cline can take without prompting you each time. It keeps you out of approval popups during routine work, while still letting you keep tight control over high-risk actions.
|
||||
|
||||
If you find yourself repeatedly clicking approve for the same safe operations, Auto Approve is the setting that fixes that. The goal is fewer interruptions without losing the ability to review changes when it matters.
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/auto-approve.png" alt="Auto Approve" />
|
||||
<video
|
||||
style={{ width: "100%" }}
|
||||
src="https://storage.googleapis.com/cline_public_images/autoapprove.mp4"
|
||||
autoPlay
|
||||
controls
|
||||
playsInline
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
## How it works
|
||||
|
||||
By default, Cline will ask for your permission before calling any tool, including reading or writing files.
|
||||
Auto Approve is evaluated per tool call. When Cline is about to read a file, edit a file, run a command, or use the browser, Cline checks your Auto Approve settings for that category.
|
||||
|
||||
If you want to allow Cline to do something without asking, you can set the Auto Approve permission for that tool.
|
||||
A few details matter in practice:
|
||||
|
||||
## Permission Options
|
||||
- **Workspace vs outside your workspace**: “Read all files” and “Edit all files” only extend the base toggle. If the base toggle is off, the “all files” option does nothing.
|
||||
|
||||
- **Read project files**
|
||||
- **Terminal commands**: Cline treats terminal commands as either safe or requiring approval. “Execute safe commands” covers the first category. “Execute all commands” extends this to commands flagged as requiring approval.
|
||||
|
||||
- Allows Cline to read files within your current workspace without asking
|
||||
- **Read all files**
|
||||
- Extends read permission to files outside your workspace (system files, config files, etc.)
|
||||
- **Notifications**: If enabled, Cline sends OS-level notifications when approval is required, and when an auto-approved terminal command has been running for 30 seconds and may need attention.
|
||||
|
||||
- **Edit project files**
|
||||
<Note>
|
||||
[YOLO mode](/features/yolo-mode) bypasses these granular approvals.
|
||||
</Note>
|
||||
|
||||
- Allows Cline to modify files within your current workspace without confirmation
|
||||
- **Edit all files**
|
||||
- Extends modification permission to files outside your workspace
|
||||
## Permissions
|
||||
|
||||
- **Execute safe commands**
|
||||
These labels match what you see in the Auto Approve menu.
|
||||
|
||||
- Allows execution of terminal commands that the model deems non-destructive
|
||||
- **Execute all commands**
|
||||
- Permits execution of any terminal command without asking
|
||||
| Setting | What it allows | Notes |
|
||||
|--------|-----------------|------|
|
||||
| Read project files | Read files, list files, search in your workspace | Good default for most tasks |
|
||||
| Read all files | Read files outside your workspace | Requires “Read project files” |
|
||||
| Edit project files | Create and edit files in your workspace | Consider using checkpoints |
|
||||
| Edit all files | Edit files outside your workspace | Requires “Edit project files” |
|
||||
| Execute safe commands | Run terminal commands marked safe | Can still run long |
|
||||
| Execute all commands | Run commands marked as requiring approval | Requires “Execute safe commands” |
|
||||
| Use the browser | Allows use of the browser tool for web fetching and searching | Proxy issues can apply |
|
||||
| Use MCP servers | Use MCP tools and access MCP resources | Some servers also have per-tool auto-approve |
|
||||
| Enable notifications | Notifies you about long-running auto-approved commands | Helpful for terminal work |
|
||||
|
||||
- **Use the browser**
|
||||
<Warning>
|
||||
“Read all files” and “Edit all files” only matter if their base toggle is enabled. They extend access outside your workspace.
|
||||
</Warning>
|
||||
|
||||
- Allows Cline to use the browser tool to fetch web content
|
||||
<Card title="Networking & proxies" icon="globe" href="/troubleshooting/networking-and-proxies">
|
||||
If browser-based tools fail in corporate networks, this page covers the common fixes.
|
||||
</Card>
|
||||
|
||||
- **Use MCP servers**
|
||||
## Safe vs approval-required command examples
|
||||
|
||||
- Permits connection to and usage of MCP servers for extended functionality
|
||||
Cline does not use a fixed allowlist of safe or unsafe commands. The model marks each command with a `requires_approval` flag based on the command and its arguments, and Auto Approve uses that flag.
|
||||
|
||||
- **Maximum requests**
|
||||
- Sets the number of consecutive automated actions Cline can take before requiring your input
|
||||
These are examples, not guarantees.
|
||||
|
||||
## Best Practices
|
||||
### Commonly treated as safe
|
||||
|
||||
Personally, I like to keep auto-editing disabled because it gives me a chance to review changes every step of the way.
|
||||
| Example | Why it is usually safe |
|
||||
|--------|-------------------------|
|
||||
| `npm run build` | Build output, no direct file deletions |
|
||||
| `npm test` | Runs tests |
|
||||
| `git status` | Read-only |
|
||||
| `ls -la` | Read-only |
|
||||
| `cat package.json` | Read-only |
|
||||
|
||||
For most serious development workflows, I recommend starting with:
|
||||
### Commonly requires approval
|
||||
|
||||
- Auto-approving read access to project files
|
||||
- Setting a reasonable maximum request limit (10-20)
|
||||
| Example | Why it often needs approval |
|
||||
|--------|------------------------------|
|
||||
| `npm install <pkg>` | Modifies dependencies and lockfiles |
|
||||
| `rm -rf <path>` | Deletes files |
|
||||
| `mv <a> <b>` | Moves files (can overwrite) |
|
||||
| `sed -i ...` | In-place file edits |
|
||||
| `curl https://...` | Downloads and executes remote code |
|
||||
|
||||
This gives Cline enough freedom to explore your codebase without constant interruptions, while still requiring permission for edits or potentially destructive actions.
|
||||
<Note>
|
||||
Whether a command is treated as safe depends on the exact command, flags, and the current task. When in doubt, keep command auto-approval off and approve commands manually.
|
||||
</Note>
|
||||
|
||||
As you build more trust in Cline's capabilities with your specific projects, you can gradually increase the permissions to match your comfort level.
|
||||
## Enable notifications
|
||||
|
||||
Remember that you can always adjust these settings as your needs change - tighten permissions for critical production work, or loosen them when prototyping and exploring.
|
||||
Auto-approved actions can run for a while, especially long terminal commands. If you enable notifications, Cline can notify you when an auto-approved command has been running for a while and may need attention.
|
||||
|
||||
You can even use the quick "star" actions to quickly toggle your auto-approved selections on and off as you go.
|
||||
## Recommendations
|
||||
|
||||
A good default setup is:
|
||||
|
||||
- Enable **Read project files**
|
||||
- Leave **Edit project files**, **Execute safe commands**, **Use the browser**, and **Use MCP servers** off until you have a specific reason to enable them
|
||||
|
||||
If you enable edits, use [Checkpoints](/features/checkpoints) so you can roll back quickly.
|
||||
|
||||
If you’re working in a sensitive environment (production credentials, personal files, corporate devices), keep external file access and command execution locked down and approve actions manually as you go.
|
||||
|
||||
@@ -3,94 +3,192 @@ title: "Keyboard Shortcuts"
|
||||
sidebarTitle: "Keyboard Shortcuts"
|
||||
---
|
||||
|
||||
Cline's keyboard shortcuts let you access AI assistance without taking your hands off the keyboard. Speed up your workflow by using hotkeys for common Cline actions.
|
||||
Speed up your workflow by accessing Cline's AI assistance without taking your hands off the keyboard.
|
||||
|
||||
## Default Keyboard Shortcuts
|
||||
<Tip>
|
||||
**The One Shortcut You Need:** `Ctrl+'` (Windows/Linux) or `Cmd+'` (macOS)
|
||||
|
||||
Cline comes with the following built-in keyboard shortcuts to streamline your workflow:
|
||||
This context-aware shortcut handles your most common needs:
|
||||
- **With text selected:** Adds code to Cline chat
|
||||
- **Without selection:** Focuses the chat input
|
||||
|
||||
| Action | Windows/Linux | macOS | Condition | Description |
|
||||
| ----------------------- | ------------- | ------- | ---------------------------- | ----------------------------------------- |
|
||||
| Add to Cline | `Ctrl+'` | `Cmd+'` | When text is selected | Adds selected code to Cline chat |
|
||||
| Focus Chat Input | `Ctrl+'` | `Cmd+'` | When no text is selected | Focuses the Cline chat input field |
|
||||
| Generate Commit Message | (unset) | (unset) | When Git is the SCM provider | Available through the Source Control view |
|
||||
Master this one shortcut, and you're 90% there.
|
||||
</Tip>
|
||||
|
||||
## Available Commands for Custom Shortcuts
|
||||
## Default Shortcuts
|
||||
|
||||
While Cline has only a few default keyboard shortcuts, you can assign your own shortcuts to any of these commands:
|
||||
Cline has minimal default shortcuts by design, so they won't conflict with your existing VSCode setup:
|
||||
|
||||
| Command ID | Description |
|
||||
| ---------------------------------------------------------------------------------------- | --------------------------------------------- |
|
||||
| [`cline.addToChat`](/features/commands-and-shortcuts/code-commands) | Adds selected code to Cline chat |
|
||||
| [`cline.addTerminalOutputToChat`](/features/commands-and-shortcuts/terminal-integration) | Adds terminal output to Cline |
|
||||
| `cline.focusChatInput` | Focuses the Cline chat input field |
|
||||
| [`cline.generateGitCommitMessage`](/features/commands-and-shortcuts/git-integration) | Generates a commit message for staged changes |
|
||||
| [`cline.explainCode`](/features/commands-and-shortcuts/code-commands) | Explains selected code |
|
||||
| [`cline.improveCode`](/features/commands-and-shortcuts/code-commands) | Suggests improvements for selected code |
|
||||
| [`cline.fixWithCline`](/features/commands-and-shortcuts/code-commands) | Fixes code with errors |
|
||||
| `claude-dev.SidebarProvider.focus` | Opens and focuses the Cline sidebar |
|
||||
| Shortcut | Windows/Linux | macOS | What It Does |
|
||||
| -------- | ------------- | ----- | ------------ |
|
||||
| **Add to Chat / Focus Input** | `Ctrl+'` | `Cmd+'` | Context-aware: adds selected code or focuses chat |
|
||||
|
||||
## Customizing Keyboard Shortcuts
|
||||
That's it! Everything else is available for you to customize.
|
||||
|
||||
You can customize Cline's keyboard shortcuts to match your preferences:
|
||||
## Quick Workflow Examples
|
||||
|
||||
1. Open the Keyboard Shortcuts editor in VSCode:
|
||||
Here's how keyboard shortcuts fit into real coding workflows:
|
||||
|
||||
- Press `Ctrl+K Ctrl+S` (Windows/Linux) or `Cmd+K Cmd+S` (macOS)
|
||||
- Or go to File > Preferences > Keyboard Shortcuts
|
||||
### Debug & Fix Workflow
|
||||
|
||||
2. Search for "Cline" to see all available commands
|
||||
1. **Find error in code** → VSCode highlights it
|
||||
2. **Select the problematic code** → `Shift+Arrow` or `Ctrl+L` / `Cmd+L`
|
||||
3. **Send to Cline** → `Ctrl+'` / `Cmd+'`
|
||||
4. **Ask for help** → Type your question, hit `Enter`
|
||||
|
||||
3. Click on the pencil icon next to any command to change its shortcut
|
||||
### Code Review Workflow
|
||||
|
||||
4. Press the keys you want to assign to that command
|
||||
1. **Review a function** → Select it with `Ctrl+L` / `Cmd+L`
|
||||
2. **Get AI review** → `Ctrl+'` / `Cmd+'` then ask "Review this"
|
||||
3. **Iterate** → Apply suggestions and repeat
|
||||
|
||||
5. Press Enter to save the new shortcut
|
||||
### Terminal Integration Workflow
|
||||
|
||||
## Suggested Custom Shortcuts
|
||||
1. **Open terminal** → Press `` Ctrl+` `` / `` Cmd+` ``
|
||||
2. **Run your command** → Execute in terminal
|
||||
3. **Capture output** → Press `Alt+T` (after assigning shortcut)
|
||||
4. **Get help** → Ask Cline to interpret errors or output
|
||||
|
||||
Here are some suggested shortcuts you might find useful:
|
||||
<Info>
|
||||
**Pro Tip:** Assign `Alt+T` to the `cline.addTerminalOutputToChat` command for quick terminal output capture. Without a shortcut, you can still right-click in the terminal and select "Add to Cline" - but the keyboard approach is much faster for frequent debugging workflows.
|
||||
</Info>
|
||||
|
||||
| Action | Suggested Shortcut | Command ID | Description |
|
||||
| --------------------- | ------------------------------ | ----------------------------------------- | ----------------------------- |
|
||||
| Open Cline Sidebar | `Ctrl+Shift+C` / `Cmd+Shift+C` | `claude-dev.SidebarProvider.focus` | Opens the Cline sidebar panel |
|
||||
| New Task | `Alt+N` | `cline.plusButtonClicked` | Starts a new Cline task |
|
||||
| Add Terminal to Cline | `Alt+T` | `cline.addTerminalOutputToChat` | Adds terminal output to Cline |
|
||||
| Clear Current Task | `Alt+C` | (Requires custom keybinding to UI action) | Clears the current task |
|
||||
## Customizing Shortcuts
|
||||
|
||||
## Keyboard-Only Workflow
|
||||
Want to assign shortcuts to more Cline commands? Here's how:
|
||||
|
||||
With the right shortcuts, you can use Cline without ever touching the mouse:
|
||||
**Step 1:** Open VSCode's Keyboard Shortcuts editor
|
||||
- Press `Ctrl+K Ctrl+S` (Windows/Linux) or `Cmd+K Cmd+S` (macOS)
|
||||
- Or: **File → Preferences → Keyboard Shortcuts**
|
||||
|
||||
1. Select code with keyboard navigation (`Shift+Arrow` keys)
|
||||
2. Send to Cline with `Ctrl+'` / `Cmd+'`
|
||||
3. Type your question and press Enter
|
||||
4. Review the response and apply suggestions
|
||||
**Step 2:** Search for "Cline"
|
||||
|
||||
## Editor Integration Shortcuts
|
||||
**Step 3:** Click the ✏️ icon next to any command
|
||||
|
||||
Cline's keyboard shortcuts integrate seamlessly with VSCode's built-in shortcuts:
|
||||
**Step 4:** Press your desired key combo, then `Enter`
|
||||
|
||||
- Use VSCode's selection shortcuts (`Ctrl+L` / `Cmd+L` to select line, etc.) before sending code to Cline
|
||||
- Combine with VSCode's split editor shortcuts to view code and Cline side by side
|
||||
- Use VSCode's terminal focus shortcut (`` Ctrl+` `` / `` Cmd+` ``) before capturing terminal output
|
||||
<Warning>
|
||||
**Avoid Conflicts:** Check that your shortcut doesn't override important VSCode commands. The shortcuts editor will warn you about conflicts.
|
||||
</Warning>
|
||||
|
||||
## Tips for Effective Use
|
||||
## Available Commands Reference
|
||||
|
||||
- **Learn the default shortcut first**: The `Ctrl+'` / `Cmd+'` shortcut is versatile - it adds selected code to chat when text is selected, or focuses the chat input when nothing is selected
|
||||
- **Create muscle memory**: Use keyboard shortcuts consistently to build habits
|
||||
- **Customize for your workflow**: Assign shortcuts to commands you use frequently
|
||||
- **Consider ergonomics**: Choose shortcuts that are comfortable for your keyboard layout
|
||||
<Accordion title="Task Management Commands">
|
||||
|
||||
Keyboard shortcuts may seem like a small optimization, but they can significantly speed up your workflow when using Cline regularly. By keeping your hands on the keyboard, you maintain your coding flow while still getting AI assistance exactly when you need it.
|
||||
These commands help you navigate and manage Cline tasks:
|
||||
|
||||
## How to Find All Available Commands
|
||||
| Command ID | Description | Suggested Shortcut |
|
||||
| ---------- | ----------- | ------------------ |
|
||||
| `cline.plusButtonClicked` | Start a new task | `Ctrl+Shift+N` / `Cmd+Shift+N` |
|
||||
| `cline.historyButtonClicked` | Open task history | `Ctrl+Shift+H` / `Cmd+Shift+H` |
|
||||
| `claude-dev.SidebarProvider.focus` | Open Cline sidebar | `Ctrl+Shift+L` / `Cmd+Shift+L` |
|
||||
|
||||
To see all Cline commands that can be assigned shortcuts:
|
||||
**Note:** `claude-dev` prefix is for historical reasons - it works with Cline.
|
||||
|
||||
1. Open the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`)
|
||||
2. Type "Cline" to filter the list
|
||||
3. Browse the available commands
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Code Interaction Commands">
|
||||
|
||||
Work directly with your code:
|
||||
|
||||
| Command ID | Description | Suggested Shortcut |
|
||||
| ---------- | ----------- | ------------------ |
|
||||
| `cline.addToChat` | Add selected code to chat | `Ctrl+'` / `Cmd+'` ⭐ (default) |
|
||||
| `cline.focusChatInput` | Focus chat input | `Ctrl+'` / `Cmd+'` ⭐ (default) |
|
||||
| `cline.explainCode` | Explain selected code | `Ctrl+Shift+E` / `Cmd+Shift+E` |
|
||||
| `cline.improveCode` | Suggest code improvements | `Ctrl+Shift+I` / `Cmd+Shift+I` |
|
||||
|
||||
⭐ These share the same shortcut - it's context-aware!
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Terminal Integration Commands">
|
||||
|
||||
Connect Cline with your terminal:
|
||||
|
||||
| Command ID | Description | Suggested Shortcut |
|
||||
| ---------- | ----------- | ------------------ |
|
||||
| `cline.addTerminalOutputToChat` | Add terminal output to Cline | `Alt+T` |
|
||||
|
||||
**Tip:** Use this after running commands to get help interpreting output or fixing errors.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Git Integration Commands">
|
||||
|
||||
Generate commit messages with AI:
|
||||
|
||||
| Command ID | Description | Suggested Shortcut |
|
||||
| ---------- | ----------- | ------------------ |
|
||||
| `cline.generateGitCommitMessage` | Generate commit message | `Ctrl+Shift+G` / `Cmd+Shift+G` |
|
||||
| `cline.abortGitCommitMessage` | Stop generation | `Ctrl+Shift+Esc` / `Cmd+Shift+Esc` |
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Settings & Configuration Commands (Advanced)">
|
||||
|
||||
These commands open Cline's configuration panels. Most users access these via the sidebar buttons, but keyboard shortcuts can be useful for:
|
||||
|
||||
- **Frequent MCP server developers** who constantly adjust server configurations
|
||||
- **Demo/presentation scenarios** where you need quick, keyboard-only navigation
|
||||
- **Accessibility workflows** where mouse usage is minimized
|
||||
|
||||
| Command ID | Description | Suggested Shortcut |
|
||||
| ---------- | ----------- | ------------------ |
|
||||
| `cline.settingsButtonClicked` | Open Cline settings | `Ctrl+Alt+,` / `Cmd+Opt+,` |
|
||||
| `cline.mcpButtonClicked` | Open MCP servers config | `Ctrl+Alt+M` / `Cmd+Opt+M` |
|
||||
| `cline.accountButtonClicked` | Open account settings | `Ctrl+Alt+A` / `Cmd+Opt+A` |
|
||||
| `cline.openWalkthrough` | Open walkthrough guide | (not recommended) |
|
||||
|
||||
**Our take:** Unless you're constantly tweaking settings or building MCP servers, the sidebar buttons are more convenient. But if you find yourself opening these panels frequently, shortcuts can save time.
|
||||
|
||||
</Accordion>
|
||||
|
||||
## What About "Fix with Cline"?
|
||||
|
||||
<Warning>
|
||||
**You CAN'T assign a keyboard shortcut to "Fix with Cline"**
|
||||
|
||||
This command only appears in the **lightbulb menu** (💡) when VSCode detects errors in your code. It needs the error context to work, so it's not available as a standalone command.
|
||||
|
||||
**Workarounds:**
|
||||
- Click the 💡 lightbulb icon that appears next to errors
|
||||
- Or select code with errors and use `Ctrl+'` / `Cmd+'` to ask Cline to fix them
|
||||
- Or right-click and select "Add to Cline"
|
||||
</Warning>
|
||||
|
||||
Learn more about code actions in our [Code Commands documentation](/features/commands-and-shortcuts/code-commands).
|
||||
|
||||
## Best Practices
|
||||
|
||||
<Tip>
|
||||
**Start Simple**
|
||||
|
||||
Don't try to memorize 20 shortcuts on day one. Start with:
|
||||
1. `Ctrl+'` / `Cmd+'` (the essential one)
|
||||
2. Add 1-2 more based on your actual usage patterns
|
||||
3. Build muscle memory over time
|
||||
</Tip>
|
||||
|
||||
**Choose Shortcuts Wisely:**
|
||||
- **Be ergonomic:** Use comfortable key combinations
|
||||
- **Create patterns:** Group related commands (e.g., all Cline shortcuts use `Ctrl+Shift+...`)
|
||||
- **Avoid conflicts:** Don't override VSCode essentials like `Ctrl+C` or `Ctrl+S`
|
||||
- **Use modifiers:** Combine `Ctrl`/`Cmd` + `Shift` + `Alt` to reduce conflicts
|
||||
|
||||
**Build the Habit:**
|
||||
- Use shortcuts consistently for a week to build muscle memory
|
||||
- Keep a note of your custom shortcuts until they're automatic
|
||||
- Review monthly to see if your workflow has changed
|
||||
|
||||
## Discovering Commands
|
||||
|
||||
Not sure what commands are available? Use VSCode's Command Palette:
|
||||
|
||||
1. Press `Ctrl+Shift+P` / `Cmd+Shift+P`
|
||||
2. Type "Cline" to filter
|
||||
3. Browse all available commands
|
||||
4. Assign shortcuts to your favorites
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
@@ -99,4 +197,8 @@ To see all Cline commands that can be assigned shortcuts:
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
This helps you discover features you might not have known about and assign shortcuts to the ones you use most frequently.
|
||||
---
|
||||
|
||||
<Info>
|
||||
**Remember:** The goal isn't to memorize every possible shortcut. Master `Ctrl+'` / `Cmd+'` first, then gradually add shortcuts for commands you use frequently. Quality over quantity!
|
||||
</Info>
|
||||
|
||||
@@ -3,11 +3,13 @@ title: "Explain Changes"
|
||||
sidebarTitle: "Explain Changes"
|
||||
---
|
||||
|
||||
<Note>
|
||||
This feature is only available in **VS Code**. The diff view with inline comments requires VS Code's native diff capabilities.
|
||||
</Note>
|
||||
|
||||
|
||||
Explain Changes is an AI-powered code review feature that adds inline explanations to your code changes. When Cline makes modifications to your codebase, you can click a button to get streaming, contextual explanations that appear directly in VS Code's diff view.
|
||||
|
||||
<Note>
|
||||
Explain Changes requires **[Checkpoints](/features/checkpoints)** to be enabled. Make sure to enable checkpoints in your Cline settings before using this feature.
|
||||
</Note>
|
||||
|
||||
<Frame>
|
||||
<video
|
||||
@@ -21,6 +23,9 @@ Explain Changes requires **[Checkpoints](/features/checkpoints)** to be enabled.
|
||||
|
||||
|
||||
## How It Works
|
||||
<Note>
|
||||
Explain Changes requires **[Checkpoints](/features/checkpoints)** to be enabled. Make sure to enable checkpoints in your Cline settings before using this feature.
|
||||
</Note>
|
||||
|
||||
After Cline completes a task that involves file changes, you'll see an "Explain Changes" button alongside the "View Changes" button in the completion message. Clicking this button:
|
||||
|
||||
|
||||
@@ -137,10 +137,30 @@ The key is combining hooks with external tools. A hook can be the glue between C
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Related Features
|
||||
## CLI support
|
||||
|
||||
Hooks are also available in the [Cline CLI](/cline-cli/overview). You can enable or disable hooks when running tasks from the command line:
|
||||
|
||||
```bash
|
||||
# Enable hooks for a task
|
||||
cline "What does this repo do?" -s hooks_enabled=true
|
||||
|
||||
# Configure hooks globally via CLI
|
||||
cline config set hooks-enabled=true
|
||||
cline config get hooks-enabled
|
||||
```
|
||||
|
||||
This allows you to integrate hooks into automated workflows, CI/CD pipelines, and headless task execution.
|
||||
|
||||
<Note>
|
||||
Hooks in the CLI are only supported on macOS and Linux. Windows support is not yet available.
|
||||
</Note>
|
||||
|
||||
## Related features
|
||||
|
||||
Hooks complement other Cline features:
|
||||
|
||||
- [Cline Rules](/features/cline-rules) define high-level guidance that hooks can enforce
|
||||
- [Checkpoints](/features/checkpoints) let you roll back changes if a hook didn't catch an issue
|
||||
- [Auto-Approve](/features/auto-approve) works well with hooks as safety nets for automated operations
|
||||
- [Cline CLI](/cline-cli/overview) enables hooks in terminal-based and automated workflows
|
||||
|
||||
@@ -1,164 +1,260 @@
|
||||
---
|
||||
title: "Multiroot Workspace Support"
|
||||
sidebarTitle: "Multiroot Workspace"
|
||||
title: "Multi-Root Workspaces"
|
||||
sidebarTitle: "Multi-Root Workspaces"
|
||||
---
|
||||
|
||||
Cline's Multiroot feature works seamlessly with VSCode's multi-root workspaces, letting you manage multiple project folders in a single workspace.
|
||||
Cline works with VSCode's multi-root workspaces, letting you manage multiple project folders or repositories in a single window. Whether you're working with a monorepo or separate Git repositories, Cline can read files, write code, and run commands across all of them.
|
||||
|
||||
<Frame>
|
||||
<video
|
||||
src="https://storage.googleapis.com/cline_public_images/multiworkspace.mp4"
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
controls
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
<Warning>
|
||||
Multi-root workspaces have two limitations:
|
||||
- **Cline rules** only work in the primary workspace folder
|
||||
- **Checkpoints** are disabled (restored when you return to a single folder)
|
||||
|
||||
See [Current Limitations](#current-limitations) for details.
|
||||
</Warning>
|
||||
|
||||
## Understanding Multi-Root Workspaces
|
||||
|
||||
Before diving in, it helps to understand the two common patterns for organizing related projects.
|
||||
|
||||
### Why Use Multi-Root Workspaces?
|
||||
|
||||
Cline can complete tasks that span multiple projects or repositories:
|
||||
|
||||
- **Refactoring**: Update an API contract and fix all consumers across repos
|
||||
- **Feature development**: Implement a feature that touches frontend, backend, and shared code
|
||||
- **Dependency updates**: Coordinate version bumps across related projects
|
||||
- **Documentation**: Generate docs that reference code from multiple repositories
|
||||
|
||||
**Example prompt:**
|
||||
```
|
||||
Update the User type in the contracts repo, then update both the frontend
|
||||
and backend to use the new fields. Make sure the API validates the new
|
||||
required field.
|
||||
```
|
||||
## Setting Up a Multi-Root Workspace
|
||||
|
||||
### Monorepos vs Multiple Repositories
|
||||
|
||||
**Monorepo**: One Git repository containing multiple projects or packages. All code shares the same version history.
|
||||
|
||||
```
|
||||
my-company/ # Single Git repo
|
||||
├── .git/
|
||||
├── packages/
|
||||
│ ├── web/ # React frontend
|
||||
│ ├── api/ # Node.js backend
|
||||
│ └── shared/ # Common utilities
|
||||
└── package.json
|
||||
```
|
||||
|
||||
**Multiple Repositories**: Separate Git repositories, each with their own history, opened together in one VSCode workspace.
|
||||
|
||||
```
|
||||
~/projects/
|
||||
├── fullstack.code-workspace # Workspace config file
|
||||
├── frontend/ # git@github.com:acme/frontend.git
|
||||
│ └── .git/
|
||||
├── backend/ # git@github.com:acme/backend.git
|
||||
│ └── .git/
|
||||
└── contracts/ # git@github.com:acme/api-contracts.git
|
||||
└── .git/
|
||||
```
|
||||
|
||||
Cline supports both patterns, as well as hybrid setups where some folders are Git repositories and others are not. The key difference: with multiple repositories, each folder has its own `.git` directory and Cline tracks them independently.
|
||||
|
||||
### Adding Folders to Your Workspace
|
||||
|
||||
You can add folders to your workspace in several ways:
|
||||
|
||||
- **File menu**: Use `File > Add Folder to Workspace` in VSCode
|
||||
- **Drag and drop**: Drag folders directly into VSCode's file explorer
|
||||
- **Workspace file**: Create a `.code-workspace` file (recommended for teams)
|
||||
- **Command palette**: Run `Workspaces: Add Folder to Workspace`
|
||||
|
||||
For detailed instructions, see [Microsoft's multi-root workspace guide](https://code.visualstudio.com/docs/editor/multi-root-workspaces).
|
||||
|
||||
## Working with Multiple Repositories
|
||||
|
||||
When you open separate Git repositories in one workspace, Cline treats each as an independent project with its own version control.
|
||||
|
||||
### What Cline Tracks Per Repository
|
||||
|
||||
For each workspace folder, Cline detects:
|
||||
|
||||
| Property | Description |
|
||||
|----------|-------------|
|
||||
| **Path** | Absolute path to the folder |
|
||||
| **Name** | Derived from folder name or workspace file |
|
||||
| **VCS Type** | Git, Mercurial, or None |
|
||||
| **Commit Hash** | Current HEAD commit (for Git/Mercurial repos) |
|
||||
|
||||
This means Cline understands that your frontend and backend might be at different commits, on different branches, or even use different version control systems.
|
||||
|
||||
<Note>
|
||||
**Important:** Multi-root workspaces are currently an experimental feature and have the following limitations:
|
||||
- **Cline rules** only work in the first workspace folder
|
||||
- **Checkpoints** are automatically disabled with a warning message
|
||||
- Both features are restored when you return to a single-folder workspace
|
||||
While Cline detects VCS information for all workspace folders, certain features only use the **primary workspace** (the first folder): [Cline rules](/features/cline-rules), [workflows](/features/slash-commands/workflows/index), and [Git-related features](/features/at-mentions/git-mentions) like `@git` mentions.
|
||||
</Note>
|
||||
|
||||
## What is multiroot workspace support?
|
||||
## Referencing Files Across Workspaces
|
||||
|
||||
Instead of being limited to one project folder, Cline can read files, write code, and run commands across all folders in your VSCode workspace. This is helpful when working with monorepos, microservices, or when you're working on related projects simultaneously.
|
||||
### Natural Language References
|
||||
|
||||
### How it works
|
||||
|
||||
When you open multiple workspace folders in VSCode, Cline automatically:
|
||||
- Designates one folder as the **primary workspace** (typically the first folder added)
|
||||
- Tracks all workspace folders and their paths
|
||||
- Resolves file paths intelligently across workspaces
|
||||
- Displays workspace information in the environment details for each API request
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Setting Up Multi-Root Workspaces
|
||||
|
||||
1. **Add folders to your workspace:**
|
||||
- Use `File > Add Folder to Workspace` in VSCode
|
||||
- Or create a `.code-workspace` file with multiple folder paths
|
||||
- Drag and drop folders to the File Explorer
|
||||
- Select multiple folders when opening a new workspace
|
||||
|
||||
2. **Start using Cline** - Cline will automatically detect all your workspace folders and interact with them as needed.
|
||||
|
||||
For detailed instructions on setting up multi-root workspaces in VS Code, see [Microsoft's official guide](https://code.visualstudio.com/docs/editing/workspaces/multi-root-workspaces).
|
||||
|
||||
### Technical behavior
|
||||
|
||||
**Workspace detection**
|
||||
- Cline detects all workspace folders when a task starts
|
||||
- The first workspace folder becomes the primary workspace by default
|
||||
- Each workspace can have its own VCS (Git, SVN, etc.)
|
||||
|
||||
**Path resolution**
|
||||
- Relative paths are resolved relative to the primary workspace
|
||||
- You can use workspace hints to target specific workspaces: `@workspaceName:path/to/file`
|
||||
- Cline attempts to intelligently determine which workspace a file belongs to
|
||||
|
||||
**Command execution**
|
||||
- Commands execute in the appropriate workspace context
|
||||
- The working directory is set based on where files are being accessed
|
||||
|
||||
## Working across workspaces
|
||||
|
||||
### Referencing specific workspaces
|
||||
|
||||
You can reference different workspaces naturally in your prompts:
|
||||
Cline understands natural references to your workspaces:
|
||||
|
||||
```
|
||||
"Read the package.json in my frontend folder and compare it with the backend dependencies"
|
||||
"Read the package.json in the frontend folder"
|
||||
```
|
||||
|
||||
```
|
||||
"Create a shared utility function and update both the client and server to use it"
|
||||
"Compare the user model in backend with the TypeScript types in contracts"
|
||||
```
|
||||
|
||||
```
|
||||
"Search for TODO comments across all my workspace folders"
|
||||
"Search for TODO comments across all workspaces"
|
||||
```
|
||||
|
||||
### Workspace hints
|
||||
### Workspace Hints Syntax
|
||||
|
||||
Use workspace hints to explicitly reference files in specific workspaces:
|
||||
For explicit references, use the `@workspace:path` syntax:
|
||||
|
||||
```
|
||||
@frontend:src/App.tsx
|
||||
@backend:server.ts
|
||||
```
|
||||
| Syntax | Description |
|
||||
|--------|-------------|
|
||||
| `@frontend:src/App.tsx` | File in the "frontend" workspace |
|
||||
| `@backend:server.ts` | File in the "backend" workspace |
|
||||
| `@contracts:types/` | Folder in the "contracts" workspace |
|
||||
|
||||
This syntax helps Cline resolve ambiguity when multiple workspaces contain similarly named files.
|
||||
This syntax is especially useful when:
|
||||
- Multiple workspaces have files with the same name
|
||||
- You want to be explicit about which project you mean
|
||||
- Cline needs to resolve ambiguity
|
||||
|
||||
### How Workspace Names Work
|
||||
|
||||
## Common use cases
|
||||
Workspace names are derived from:
|
||||
1. The `name` field in your `.code-workspace` file (if specified)
|
||||
2. The folder name (default)
|
||||
|
||||
If two folders have the same name, append numbers or use the workspace file to give them unique names.
|
||||
|
||||
## Common Configurations
|
||||
|
||||
### Monorepo Development
|
||||
|
||||
Perfect for when you have related projects in one repository:
|
||||
|
||||
```
|
||||
my-app.code-workspace
|
||||
~/projects/my-app/
|
||||
├── my-app.code-workspace # Workspace config file
|
||||
├── web/ (React frontend)
|
||||
├── api/ (Node.js backend)
|
||||
├── api/ (Node.js backend)
|
||||
├── mobile/ (React Native)
|
||||
└── shared/ (Common utilities)
|
||||
```
|
||||
|
||||
Ask Cline: *"Update the API endpoint in both web and mobile apps to match the new backend route"*
|
||||
All folders share one Git history. Changes across packages are atomic.
|
||||
|
||||
### Microservices Architecture
|
||||
**Example prompt:** *"Update the API endpoint in both web and mobile apps to match the new backend route"*
|
||||
|
||||
Manage multiple services from one workspace:
|
||||
### Microservices with Separate Repos
|
||||
|
||||
```
|
||||
services.code-workspace
|
||||
├── user-service/
|
||||
├── payment-service/
|
||||
├── notifications/
|
||||
└── infrastructure/
|
||||
~/projects/services/
|
||||
├── services.code-workspace # Workspace config file
|
||||
├── user-service/ (git: github.com/acme/user-service)
|
||||
├── payment-service/ (git: github.com/acme/payment-service)
|
||||
├── gateway/ (git: github.com/acme/api-gateway)
|
||||
└── proto/ (git: github.com/acme/service-protos)
|
||||
```
|
||||
|
||||
### Full-Stack Development
|
||||
Each service has its own repository. Cline can update the proto definitions and regenerate clients across all services.
|
||||
|
||||
Keep everything together while maintaining separation:
|
||||
**Example prompt:** *"Add a new field to the UserProfile message in proto, then update user-service and gateway to handle it"*
|
||||
|
||||
### Full-Stack with Shared Contracts
|
||||
|
||||
```
|
||||
fullstack.code-workspace
|
||||
├── client/ (Frontend)
|
||||
├── server/ (Backend API)
|
||||
├── docs/ (Documentation)
|
||||
└── deploy/ (Scripts & config)
|
||||
~/projects/fullstack/
|
||||
├── fullstack.code-workspace # Workspace config file
|
||||
├── client/ (git: github.com/acme/web-client)
|
||||
├── server/ (git: github.com/acme/api-server)
|
||||
└── types/ (git: github.com/acme/shared-types)
|
||||
```
|
||||
|
||||
The types repository defines interfaces used by both client and server. When you update a type, Cline can fix both consumers.
|
||||
|
||||
### Auto-Approve Integration
|
||||
### Hybrid Setup
|
||||
|
||||
Multiroot workspaces work with [Auto Approve](/features/auto-approve):
|
||||
```
|
||||
~/projects/project/
|
||||
├── project.code-workspace # Workspace config file
|
||||
├── main-app/ (git: github.com/acme/main-app)
|
||||
├── vendor/ (no VCS - vendored dependencies)
|
||||
└── scripts/ (no VCS - local automation)
|
||||
```
|
||||
|
||||
- Enable permissions for operations within workspace folders
|
||||
- Restrict auto-approve for files outside your workspace(s)
|
||||
- Configure different levels for different workspace folders
|
||||
Mix of repositories and plain folders. Cline adapts to each folder's configuration.
|
||||
|
||||
### Cross-Workspace Operations
|
||||
## Current Limitations
|
||||
|
||||
Cline can complete tasks spanning multiple workspaces:
|
||||
Two features have limitations in multi-root workspace mode:
|
||||
|
||||
- **Refactoring**: Update imports and references across projects
|
||||
- **Feature development**: Implement features requiring changes in multiple services
|
||||
- **Documentation**: Generate docs referencing code from multiple folders
|
||||
- **Testing**: Build & run tests across all workspaces and analyze results
|
||||
### Cline Rules
|
||||
|
||||
When working with large multiroot workspaces, start in [Plan mode](/features/plan-and-act) to let Cline understand your project structure before making changes.
|
||||
[Cline rules](/features/cline-rules) (`.clinerules/` directory) only work in the **primary workspace** (the first folder in your workspace). Rules in other workspace folders are ignored.
|
||||
|
||||
**Workaround:** Place shared rules in the primary workspace, or use global rules (`~/Documents/Cline/Rules/`) which apply everywhere.
|
||||
|
||||
### Checkpoints
|
||||
|
||||
[Checkpoints](/features/checkpoints) are disabled in multi-root workspace mode. Cline displays a warning when this happens.
|
||||
|
||||
**Why:** Checkpoints use a shadow Git repository to track changes. With multiple repositories, coordinating checkpoints across independent Git histories adds complexity that isn't yet supported.
|
||||
|
||||
**Workaround:** Use your normal Git workflow. Commit frequently, or create branches for experimental work.
|
||||
|
||||
Both limitations are restored when you return to a single-folder workspace.
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Organizing Your Workspaces
|
||||
|
||||
1. **Group related projects** that often need coordinated changes
|
||||
2. **Use consistent folder structures** across workspaces when possible
|
||||
3. **Name folders clearly** so Cline can understand your project structure
|
||||
2. **Use a workspace file** for reproducible setups across your team
|
||||
3. **Name folders clearly** so workspace hints are intuitive
|
||||
4. **Consider the primary workspace** for Cline rules placement
|
||||
|
||||
### Effective Prompting & Tips
|
||||
### Effective Prompting
|
||||
|
||||
When working with multiroot workspaces, these approaches work best:
|
||||
- **Be specific** when it matters: *"Update the user model in the backend workspace"*
|
||||
- **Reference relationships**: *"The frontend uses types from the contracts workspace"*
|
||||
- **Describe cross-workspace changes**: *"This needs to update both web and mobile"*
|
||||
- **Scope searches** for large codebases: *"Search for 'TODO' only in the frontend workspace"*
|
||||
|
||||
- **Be specific** about which workspace when it matters: *"Update the user model in the backend workspace"*
|
||||
- **Reference relationships**: *"The frontend uses the API types from the shared workspace"*
|
||||
- **Describe cross-workspace operations**: *"This change needs to be reflected in both the web and mobile apps"*
|
||||
- **Scope your searches** when dealing with large codebases: *"Search for 'TODO' in just the frontend workspace"*
|
||||
- **Break down large tasks** into workspace-specific operations when possible
|
||||
- **Consider excluding large folders** like `node_modules` from your workspace search Scope
|
||||
### Working with Large Workspaces
|
||||
|
||||
- Break large tasks into workspace-specific operations when possible
|
||||
- Use [Plan mode](/features/plan-and-act) to let Cline understand structure first
|
||||
- Add a `.clineignore` file to reduce noise, speed up scanning, and keep Cline focused on source code:
|
||||
|
||||
```text
|
||||
# Dependencies
|
||||
**/node_modules/
|
||||
|
||||
# Build outputs
|
||||
**/dist/
|
||||
**/build/
|
||||
|
||||
# VCS metadata
|
||||
**/.git/
|
||||
```
|
||||
|
||||
For more patterns and gotchas, see the [.clineignore File Guide](/prompting/prompt-engineering-guide#clineignore-file-guide).
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
title: "Explain Changes Command"
|
||||
sidebarTitle: "/explain-changes"
|
||||
---
|
||||
<Note>
|
||||
This command is only available in **VS Code**. The diff view with inline comments requires VS Code's native diff capabilities.
|
||||
</Note>
|
||||
|
||||
`/explain-changes` is a slash command that generates AI-powered explanations for any git diff. Unlike the [Explain Changes button](/features/explain-changes) which explains changes from a completed task, this command lets you explain changes between any two git references - commits, branches, tags, PRs, staged changes, or your working directory.
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ Cline is an open source AI coding agent that brings frontier AI models directly
|
||||
Master Cline's powerful features and optimize your workflow
|
||||
</Card>
|
||||
|
||||
<Card title="Enterprise" icon="building" href="/enterprise-solutions/security-concerns">
|
||||
<Card title="Enterprise" icon="building" href="/enterprise-solutions/overview">
|
||||
Deploy Cline in your organization with confidence
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
Generated
+2310
-1422
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -13,7 +13,7 @@
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"mintlify": "^4.2.23"
|
||||
"mintlify": "^4.2.249"
|
||||
},
|
||||
"overrides": {
|
||||
"tar-fs": "^3.1.1",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
streamlit>=1.28.0
|
||||
streamlit==1.43.2
|
||||
plotly>=5.17.0
|
||||
pandas>=2.0.0
|
||||
numpy>=1.24.0
|
||||
|
||||
Generated
+1021
-44
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -2,7 +2,7 @@
|
||||
"name": "claude-dev",
|
||||
"displayName": "Cline",
|
||||
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
|
||||
"version": "3.42.0",
|
||||
"version": "3.46.0",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -378,7 +378,7 @@
|
||||
"pretest": "npm run compile && npm run compile-tests && npm run compile-standalone && npm run lint",
|
||||
"test": "npm-run-all test:unit test:integration",
|
||||
"test:integration": "vscode-test",
|
||||
"test:unit": "cross-env TS_NODE_PROJECT=./tsconfig.unit-test.json mocha # Use `UPDATE_SNAPSHOTS=true npm run test:unit` to rebuild prompt snapshots",
|
||||
"test:unit": "cross-env TS_NODE_PROJECT=./tsconfig.unit-test.json mocha",
|
||||
"test:coverage": "vscode-test --coverage",
|
||||
"test:sca-server": "npx tsx watch scripts/test-standalone-core-api-server.ts",
|
||||
"test:tp-orchestrator": "npx tsx scripts/testing-platform-orchestrator.ts",
|
||||
@@ -463,7 +463,7 @@
|
||||
"@grpc/grpc-js": "^1.9.15",
|
||||
"@grpc/reflection": "^1.0.4",
|
||||
"@mistralai/mistralai": "^1.5.0",
|
||||
"@modelcontextprotocol/sdk": "^1.11.1",
|
||||
"@modelcontextprotocol/sdk": "^1.25.1",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/core": "^2.1.0",
|
||||
"@opentelemetry/exporter-logs-otlp-grpc": "^0.56.0",
|
||||
|
||||
@@ -369,6 +369,7 @@ message UpdateSettingsRequest {
|
||||
optional OnboardingModelGroup onboarding_models = 33;
|
||||
optional bool cline_web_tools_enabled = 34;
|
||||
optional bool enable_parallel_tool_calling = 35;
|
||||
optional bool background_edit_enabled = 36;
|
||||
}
|
||||
|
||||
message UpdateTerminalConnectionTimeoutRequest {
|
||||
|
||||
@@ -55,8 +55,10 @@ enum Setting {
|
||||
}
|
||||
message GetTelemetrySettingsResponse {
|
||||
Setting is_enabled = 1;
|
||||
optional string error_level = 2;
|
||||
}
|
||||
|
||||
message TelemetrySettingsEvent {
|
||||
Setting is_enabled = 1;
|
||||
optional string error_level = 2;
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ function createMockService<T extends grpc.UntypedServiceImplementation>(serviceN
|
||||
case "getTelemetrySettings":
|
||||
callback(null, {
|
||||
isEnabled: 2, // Setting.DISABLED
|
||||
errorLevel: "all",
|
||||
})
|
||||
return
|
||||
|
||||
|
||||
@@ -180,6 +180,8 @@ function createHandlerForProvider(
|
||||
openAiNativeApiKey: options.openAiNativeApiKey,
|
||||
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
})
|
||||
case "deepseek":
|
||||
return new DeepSeekHandler({
|
||||
|
||||
@@ -56,87 +56,61 @@ export class AnthropicHandler implements ApiHandler {
|
||||
|
||||
// Tools are available only when native tools are enabled.
|
||||
const nativeToolsOn = tools?.length && tools?.length > 0
|
||||
const reasoningOn = !!(
|
||||
(modelId.includes("3-7") || modelId.includes("4-") || modelId.includes("4-5")) &&
|
||||
budget_tokens !== 0
|
||||
)
|
||||
const reasoningOn = (model.info.supportsReasoning ?? false) && budget_tokens !== 0
|
||||
|
||||
switch (modelId) {
|
||||
// 'latest' alias does not support cache_control
|
||||
case "claude-haiku-4-5@20251001":
|
||||
case "claude-sonnet-4-5@20250929":
|
||||
case "claude-sonnet-4@20250514":
|
||||
case "claude-opus-4-5@20251101":
|
||||
case "claude-opus-4-1@20250805":
|
||||
case "claude-opus-4@20250514":
|
||||
case "claude-haiku-4-5-20251001":
|
||||
case "claude-sonnet-4-5-20250929:1m":
|
||||
case "claude-sonnet-4-5-20250929":
|
||||
case "claude-sonnet-4-20250514":
|
||||
case "claude-3-7-sonnet-20250219":
|
||||
case "claude-3-5-sonnet-20241022":
|
||||
case "claude-3-5-haiku-20241022":
|
||||
case "claude-opus-4-5-20251101":
|
||||
case "claude-opus-4-20250514":
|
||||
case "claude-opus-4-1-20250805":
|
||||
case "claude-3-opus-20240229":
|
||||
case "claude-3-haiku-20240307": {
|
||||
const anthropicMessages = sanitizeAnthropicMessages(messages, true)
|
||||
if (model.info.supportsPromptCache) {
|
||||
const anthropicMessages = sanitizeAnthropicMessages(messages, true)
|
||||
|
||||
stream = await client.messages.create(
|
||||
{
|
||||
model: modelId,
|
||||
thinking: reasoningOn ? { type: "enabled", budget_tokens: budget_tokens } : undefined,
|
||||
max_tokens: model.info.maxTokens || 8192,
|
||||
// "Thinking isn’t compatible with temperature, top_p, or top_k modifications as well as forced tool use."
|
||||
// (https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#important-considerations-when-using-extended-thinking)
|
||||
temperature: reasoningOn ? undefined : 0,
|
||||
system: [
|
||||
{
|
||||
text: systemPrompt,
|
||||
type: "text",
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
], // setting cache breakpoint for system prompt so new tasks can reuse it
|
||||
messages: anthropicMessages,
|
||||
// tools, // cache breakpoints go from tools > system > messages, and since tools dont change, we can just set the breakpoint at the end of system (this avoids having to set a breakpoint at the end of tools which by itself does not meet min requirements for haiku caching)
|
||||
stream: true,
|
||||
tools: nativeToolsOn ? tools : undefined,
|
||||
// tool_choice options:
|
||||
// - none: disables tool use, even if tools are provided. Claude will not call any tools.
|
||||
// - auto: allows Claude to decide whether to call any provided tools or not. This is the default value when tools are provided.
|
||||
// - any: tells Claude that it must use one of the provided tools, but doesn’t force a particular tool.
|
||||
// NOTE: Forcing tool use when tools are provided will result in error when thinking is also enabled.
|
||||
tool_choice: nativeToolsOn && !reasoningOn ? { type: "any" } : undefined,
|
||||
},
|
||||
(() => {
|
||||
// 1m context window beta header
|
||||
if (enable1mContextWindow) {
|
||||
return {
|
||||
headers: {
|
||||
"anthropic-beta": "context-1m-2025-08-07",
|
||||
},
|
||||
}
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
})(),
|
||||
)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
stream = await client.messages.create({
|
||||
stream = await client.messages.create(
|
||||
{
|
||||
model: modelId,
|
||||
thinking: reasoningOn ? { type: "enabled", budget_tokens: budget_tokens } : undefined,
|
||||
max_tokens: model.info.maxTokens || 8192,
|
||||
temperature: 0,
|
||||
system: [{ text: systemPrompt, type: "text" }],
|
||||
messages: sanitizeAnthropicMessages(messages, false),
|
||||
tools: nativeToolsOn ? tools : undefined,
|
||||
tool_choice: { type: "auto" },
|
||||
// "Thinking isn’t compatible with temperature, top_p, or top_k modifications as well as forced tool use."
|
||||
// (https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#important-considerations-when-using-extended-thinking)
|
||||
temperature: reasoningOn ? undefined : 0,
|
||||
system: [
|
||||
{
|
||||
text: systemPrompt,
|
||||
type: "text",
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
], // setting cache breakpoint for system prompt so new tasks can reuse it
|
||||
messages: anthropicMessages,
|
||||
// tools, // cache breakpoints go from tools > system > messages, and since tools dont change, we can just set the breakpoint at the end of system (this avoids having to set a breakpoint at the end of tools which by itself does not meet min requirements for haiku caching)
|
||||
stream: true,
|
||||
})
|
||||
break
|
||||
}
|
||||
tools: nativeToolsOn ? tools : undefined,
|
||||
// tool_choice options:
|
||||
// - none: disables tool use, even if tools are provided. Claude will not call any tools.
|
||||
// - auto: allows Claude to decide whether to call any provided tools or not. This is the default value when tools are provided.
|
||||
// - any: tells Claude that it must use one of the provided tools, but doesn’t force a particular tool.
|
||||
// NOTE: Forcing tool use when tools are provided will result in error when thinking is also enabled.
|
||||
tool_choice: nativeToolsOn && !reasoningOn ? { type: "any" } : undefined,
|
||||
},
|
||||
(() => {
|
||||
// 1m context window beta header
|
||||
if (enable1mContextWindow) {
|
||||
return {
|
||||
headers: {
|
||||
"anthropic-beta": "context-1m-2025-08-07",
|
||||
},
|
||||
}
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
})(),
|
||||
)
|
||||
} else {
|
||||
stream = await client.messages.create({
|
||||
model: modelId,
|
||||
max_tokens: model.info.maxTokens || 8192,
|
||||
temperature: 0,
|
||||
system: [{ text: systemPrompt, type: "text" }],
|
||||
messages: sanitizeAnthropicMessages(messages, false),
|
||||
tools: nativeToolsOn ? tools : undefined,
|
||||
tool_choice: { type: "auto" },
|
||||
stream: true,
|
||||
})
|
||||
}
|
||||
|
||||
const lastStartedToolCall = { id: "", name: "", arguments: "" }
|
||||
|
||||
@@ -755,10 +755,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
// For Anthropic models with thinking enabled, temperature must be 1
|
||||
if (modelType === "anthropic") {
|
||||
const budget_tokens = this.options.thinkingBudgetTokens || 0
|
||||
const baseModelId =
|
||||
(this.options.awsBedrockCustomSelected ? this.options.awsBedrockCustomModelBaseId : this.getModel().id) ||
|
||||
this.getModel().id
|
||||
const reasoningOn = this.shouldEnableReasoning(baseModelId, budget_tokens)
|
||||
const reasoningOn = modelInfo.supportsReasoning && budget_tokens > 0
|
||||
|
||||
return {
|
||||
maxTokens: modelInfo.maxTokens || 8192,
|
||||
@@ -772,20 +769,6 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if reasoning should be enabled for Claude models
|
||||
*/
|
||||
private shouldEnableReasoning(baseModelId: string, budgetTokens: number): boolean {
|
||||
return (
|
||||
(baseModelId.includes("3-7") ||
|
||||
baseModelId.includes("sonnet-4") ||
|
||||
baseModelId.includes("opus-4") ||
|
||||
baseModelId.includes("haiku-4-5") ||
|
||||
baseModelId.includes("sonnet-4-5")) &&
|
||||
budgetTokens !== 0
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a message using Anthropic Claude models through AWS Bedrock Converse API
|
||||
* Implements support for Anthropic Claude models using the unified Converse API
|
||||
@@ -815,10 +798,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
|
||||
// Get thinking configuration
|
||||
const budget_tokens = this.options.thinkingBudgetTokens || 0
|
||||
const baseModelId =
|
||||
(this.options.awsBedrockCustomSelected ? this.options.awsBedrockCustomModelBaseId : this.getModel().id) ||
|
||||
this.getModel().id
|
||||
const reasoningOn = this.shouldEnableReasoning(baseModelId, budget_tokens)
|
||||
const reasoningOn = model.info.supportsReasoning && budget_tokens > 0
|
||||
|
||||
// Prepare request for Anthropic model using Converse API
|
||||
const command = new ConverseStreamCommand({
|
||||
|
||||
@@ -16,9 +16,6 @@ import { RetriableError, withRetry } from "../retry"
|
||||
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
// Define a default TTL for the cache (e.g., 15 minutes in seconds)
|
||||
const _DEFAULT_CACHE_TTL_SECONDS = 900
|
||||
|
||||
const rateLimitPatterns = [/got status: 429/i, /429 Too Many Requests/i, /rate limit exceeded/i, /too many requests/i]
|
||||
|
||||
interface GeminiHandlerOptions extends CommonApiHandlerOptions {
|
||||
|
||||
@@ -23,6 +23,7 @@ import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-p
|
||||
interface OpenAiNativeHandlerOptions extends CommonApiHandlerOptions {
|
||||
openAiNativeApiKey?: string
|
||||
reasoningEffort?: string
|
||||
thinkingBudgetTokens?: number
|
||||
apiModelId?: string
|
||||
}
|
||||
|
||||
@@ -105,19 +106,18 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
const systemRole = model.info.systemRole ?? "system"
|
||||
const includeReasoning = model.info.supportsReasoningEffort ?? false
|
||||
const includeReasoning = this.options.thinkingBudgetTokens && model.info.supportsReasoningEffort
|
||||
const includeTools = model.info.supportsTools ?? true
|
||||
const reasoningEffort = includeReasoning
|
||||
? (this.options.reasoningEffort as ChatCompletionReasoningEffort) || "medium"
|
||||
: undefined
|
||||
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
messages: [{ role: systemRole, content: systemPrompt }, ...convertToOpenAiMessages(messages)],
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
...(includeReasoning
|
||||
? {
|
||||
reasoning_effort: (this.options.reasoningEffort as ChatCompletionReasoningEffort) || "medium",
|
||||
}
|
||||
: {}),
|
||||
reasoning_effort: reasoningEffort,
|
||||
...(model.info.temperature !== undefined ? { temperature: model.info.temperature } : {}),
|
||||
...(includeTools ? getOpenAIToolParams(tools, isGPT5ModelFamily(model.id)) : {}),
|
||||
})
|
||||
|
||||
@@ -878,12 +878,6 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
stream: any,
|
||||
_model: { id: SapAiCoreModelId; info: ModelInfo },
|
||||
): AsyncGenerator<any, void, unknown> {
|
||||
function toStrictJson(str: string): string {
|
||||
// Wrap it in parentheses so JS will treat it as an expression
|
||||
const obj = new Function("return " + str)()
|
||||
return JSON.stringify(obj)
|
||||
}
|
||||
|
||||
const _usage = { input_tokens: 0, output_tokens: 0 }
|
||||
|
||||
try {
|
||||
@@ -898,7 +892,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
|
||||
try {
|
||||
// Parse the incoming JSON data from the stream
|
||||
const data = JSON.parse(toStrictJson(jsonData))
|
||||
const data = JSON.parse(jsonData)
|
||||
|
||||
// Handle metadata (token usage)
|
||||
if (data.metadata?.usage) {
|
||||
|
||||
@@ -2,6 +2,14 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { Content, GenerateContentResponse, Part } from "@google/genai"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
|
||||
// Source: https://ai.google.dev/gemini-api/docs/thought-signatures#faqs
|
||||
// While injecting custom function call blocks into the request is strongly discouraged,
|
||||
// in cases where it can't be avoided, e.g. providing information to the model on function
|
||||
// calls and responses that were executed deterministically by the client, or transferring a
|
||||
// trace from a different model that does not include thought signatures, you can set the following dummy signatures of either
|
||||
// "context_engineering_is_the_way_to_go" or "skip_thought_signature_validator" in the thought signature field to skip validation.
|
||||
const GEMINI_DUMMY_THOUGHT_SIGNATURE = "skip_thought_signature_validator"
|
||||
|
||||
export function convertAnthropicContentToGemini(content: string | ClineStorageMessage["content"]): Part[] {
|
||||
if (typeof content === "string") {
|
||||
return [{ text: content }]
|
||||
@@ -27,7 +35,8 @@ export function convertAnthropicContentToGemini(content: string | ClineStorageMe
|
||||
name: block.name,
|
||||
args: block.input as Record<string, unknown>,
|
||||
},
|
||||
thoughtSignature: block.signature,
|
||||
// Thought signature is required, so provide a dummy one if not present
|
||||
thoughtSignature: block.signature || GEMINI_DUMMY_THOUGHT_SIGNATURE,
|
||||
}
|
||||
case "tool_result":
|
||||
return {
|
||||
|
||||
@@ -35,7 +35,6 @@ import { getDistinctId } from "@/services/logging/distinctId"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { getAxiosSettings } from "@/shared/net"
|
||||
import { ShowMessageType } from "@/shared/proto/host/window"
|
||||
import type { AuthState } from "@/shared/proto/index.cline"
|
||||
import { getLatestAnnouncementId } from "@/utils/announcements"
|
||||
import { getCwd, getDesktopDir } from "@/utils/path"
|
||||
import { BannerService } from "../../services/banner/BannerService"
|
||||
@@ -50,7 +49,6 @@ import {
|
||||
import { fetchRemoteConfig } from "../storage/remote-config/fetch"
|
||||
import { type PersistenceErrorEvent, StateManager } from "../storage/StateManager"
|
||||
import { Task } from "../task"
|
||||
import type { StreamingResponseHandler } from "./grpc-handler"
|
||||
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
|
||||
import { getClineOnboardingModels } from "./models/getClineOnboardingModels"
|
||||
import { appendClineStealthModels } from "./models/refreshOpenRouterModels"
|
||||
@@ -145,13 +143,6 @@ export class Controller {
|
||||
this.ocaAuthService = OcaAuthService.initialize(this)
|
||||
this.accountService = ClineAccountService.getInstance()
|
||||
|
||||
const authStatusHandler: StreamingResponseHandler<AuthState> = async (response, _isLast, _seqNumber): Promise<void> => {
|
||||
if (response.user) {
|
||||
fetchRemoteConfig(this)
|
||||
}
|
||||
}
|
||||
this.authService.subscribeToAuthStatusUpdate(this, {}, authStatusHandler, undefined)
|
||||
|
||||
this.authService.restoreRefreshTokenAndRetrieveAuthInfo().then(() => {
|
||||
this.startRemoteConfigTimer()
|
||||
})
|
||||
@@ -963,6 +954,7 @@ export class Controller {
|
||||
subagentsEnabled,
|
||||
nativeToolCallSetting: this.stateManager.getGlobalStateKey("nativeToolCallEnabled"),
|
||||
enableParallelToolCalling: this.stateManager.getGlobalSettingsKey("enableParallelToolCalling"),
|
||||
backgroundEditEnabled: this.stateManager.getGlobalSettingsKey("backgroundEditEnabled"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -320,6 +320,10 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
}
|
||||
}
|
||||
|
||||
if (request.backgroundEditEnabled !== undefined) {
|
||||
controller.stateManager.setGlobalState("backgroundEditEnabled", !!request.backgroundEditEnabled)
|
||||
}
|
||||
|
||||
if (request.autoCondenseThreshold !== undefined) {
|
||||
const threshold = Math.min(1, Math.max(0, request.autoCondenseThreshold)) // Clamp to 0-1 range
|
||||
controller.stateManager.setGlobalState("autoCondenseThreshold", threshold)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { telemetryService } from "../../services/telemetry"
|
||||
import { getAllHooksDirs } from "../storage/disk"
|
||||
import { HookFactory, Hooks } from "./hook-factory"
|
||||
|
||||
@@ -56,8 +57,8 @@ export class HookDiscoveryCache {
|
||||
// Directories we've tried to watch (even if watcher creation failed)
|
||||
private watchedDirs = new Set<string>()
|
||||
|
||||
// Currently scanning (to prevent concurrent scans)
|
||||
private scanning = new Set<HookName>()
|
||||
// Currently scanning promises (to prevent concurrent scans)
|
||||
private scanningPromises = new Map<HookName, Promise<string[]>>()
|
||||
|
||||
// For disposal
|
||||
private context: ExtensionContext | null = null
|
||||
@@ -105,60 +106,95 @@ export class HookDiscoveryCache {
|
||||
this.log(`Getting hooks for ${hookName}`)
|
||||
|
||||
const cached = this.cache.get(hookName)
|
||||
if (cached) {
|
||||
const cacheHit = cached !== undefined
|
||||
|
||||
let scripts: string[]
|
||||
let initiatedScan = false // Track if this caller initiated the scan
|
||||
|
||||
if (cacheHit) {
|
||||
this.log(`Cache hit for ${hookName}: ${cached.scriptPaths.length} scripts`)
|
||||
return cached.scriptPaths
|
||||
scripts = cached.scriptPaths
|
||||
} else {
|
||||
this.log(`Cache miss for ${hookName}, scanning...`)
|
||||
|
||||
// Check if scan is already in progress
|
||||
const existingPromise = this.scanningPromises.get(hookName)
|
||||
if (existingPromise) {
|
||||
// Another caller is already scanning, reuse their promise
|
||||
this.log(`Reusing existing scan for ${hookName}`)
|
||||
scripts = await existingPromise
|
||||
} else {
|
||||
// This caller initiates the scan
|
||||
initiatedScan = true
|
||||
scripts = await this.scan(hookName)
|
||||
}
|
||||
}
|
||||
|
||||
this.log(`Cache miss for ${hookName}, scanning...`)
|
||||
return this.scan(hookName)
|
||||
// Only report telemetry if:
|
||||
// 1. It was a cache hit, OR
|
||||
// 2. This caller initiated the scan (not reusing another caller's promise)
|
||||
if (cacheHit || initiatedScan) {
|
||||
telemetryService.safeCapture(
|
||||
() => telemetryService.captureHookCacheAccess(hookName, cacheHit),
|
||||
"HookDiscoveryCache.get",
|
||||
)
|
||||
}
|
||||
|
||||
return scripts
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan for hook scripts and cache the result
|
||||
*/
|
||||
private async scan(hookName: HookName): Promise<string[]> {
|
||||
// Prevent concurrent scans of the same hook
|
||||
if (this.scanning.has(hookName)) {
|
||||
this.log(`Already scanning ${hookName}, waiting...`)
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
return this.get(hookName)
|
||||
// Check if a scan is already in progress for this hook
|
||||
const existingPromise = this.scanningPromises.get(hookName)
|
||||
if (existingPromise) {
|
||||
this.log(`Already scanning ${hookName}, waiting for existing scan...`)
|
||||
return existingPromise
|
||||
}
|
||||
|
||||
this.scanning.add(hookName)
|
||||
// Create a new scan promise
|
||||
const scanPromise = (async () => {
|
||||
try {
|
||||
// Get all current hooks directories
|
||||
const hooksDirs = await getAllHooksDirs()
|
||||
this.log(`Scanning ${hooksDirs.length} directories for ${hookName}`)
|
||||
|
||||
try {
|
||||
// Get all current hooks directories
|
||||
const hooksDirs = await getAllHooksDirs()
|
||||
this.log(`Scanning ${hooksDirs.length} directories for ${hookName}`)
|
||||
// Ensure watchers are set up for each directory (lazy initialization)
|
||||
for (const dir of hooksDirs) {
|
||||
this.ensureWatcher(dir)
|
||||
}
|
||||
|
||||
// Ensure watchers are set up for each directory (lazy initialization)
|
||||
for (const dir of hooksDirs) {
|
||||
this.ensureWatcher(dir)
|
||||
// Scan each directory for this hook
|
||||
const scriptPromises = hooksDirs.map((dir) => HookFactory.findHookInHooksDir(hookName, dir))
|
||||
|
||||
const results = await Promise.all(scriptPromises)
|
||||
const scripts = results.filter((path): path is string => path !== undefined)
|
||||
|
||||
this.log(`Found ${scripts.length} scripts for ${hookName}`)
|
||||
|
||||
// Cache the result
|
||||
this.cache.set(hookName, {
|
||||
scriptPaths: scripts,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
|
||||
return scripts
|
||||
} catch (error) {
|
||||
console.error(`Error scanning for ${hookName} hooks:`, error)
|
||||
// Return empty array on error - don't break the whole system
|
||||
return []
|
||||
} finally {
|
||||
// Remove from scanning promises map
|
||||
this.scanningPromises.delete(hookName)
|
||||
}
|
||||
})()
|
||||
|
||||
// Scan each directory for this hook
|
||||
const scriptPromises = hooksDirs.map((dir) => HookFactory.findHookInHooksDir(hookName, dir))
|
||||
// Store the promise so concurrent calls can await it
|
||||
this.scanningPromises.set(hookName, scanPromise)
|
||||
|
||||
const results = await Promise.all(scriptPromises)
|
||||
const scripts = results.filter((path): path is string => path !== undefined)
|
||||
|
||||
this.log(`Found ${scripts.length} scripts for ${hookName}`)
|
||||
|
||||
// Cache the result
|
||||
this.cache.set(hookName, {
|
||||
scriptPaths: scripts,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
|
||||
return scripts
|
||||
} catch (error) {
|
||||
console.error(`Error scanning for ${hookName} hooks:`, error)
|
||||
// Return empty array on error - don't break the whole system
|
||||
return []
|
||||
} finally {
|
||||
this.scanning.delete(hookName)
|
||||
}
|
||||
return scanPromise
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -105,6 +105,8 @@ export async function executeHook<Name extends keyof Hooks>(options: HookExecuti
|
||||
hookName,
|
||||
streamCallback,
|
||||
isCancellable ? abortController.signal : undefined,
|
||||
taskId,
|
||||
options.toolName,
|
||||
)
|
||||
|
||||
const result = await hook.run({
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { version as clineVersion } from "../../../package.json"
|
||||
import { getDistinctId } from "../../services/logging/distinctId"
|
||||
import { telemetryService } from "../../services/telemetry"
|
||||
import {
|
||||
HookInput,
|
||||
HookOutput,
|
||||
@@ -25,6 +26,9 @@ const HOOK_EXECUTION_TIMEOUT_MS = 30000
|
||||
// Maximum size for context modification (to prevent prompt overflow)
|
||||
const MAX_CONTEXT_MODIFICATION_SIZE = 50000 // ~50KB
|
||||
|
||||
// Exit code indicating cancellation/interruption (Unix SIGINT convention: 128 + signal 2)
|
||||
const EXIT_CODE_SIGINT = 130
|
||||
|
||||
/**
|
||||
* Validates hook output JSON structure.
|
||||
* Ensures required fields are present and have correct types.
|
||||
@@ -233,6 +237,7 @@ export type HookStreamCallback = (line: string, stream: "stdout" | "stderr") =>
|
||||
* - Parses JSON output from stdout, attempting to extract it even if mixed with debug output
|
||||
* - Truncates context modifications that exceed 50KB to prevent prompt overflow
|
||||
* - Handles both successful and failed executions gracefully
|
||||
* - Emits per-hook telemetry with source attribution (global or workspace)
|
||||
*
|
||||
* Error handling:
|
||||
* - Treats hooks as "fail-open": only shouldContinue:false blocks tool execution
|
||||
@@ -245,13 +250,31 @@ class StdioHookRunner<Name extends HookName> extends HookRunner<Name> {
|
||||
constructor(
|
||||
hookName: Name,
|
||||
public readonly scriptPath: string,
|
||||
private readonly source: "global" | "workspace",
|
||||
private readonly streamCallback?: HookStreamCallback,
|
||||
private readonly abortSignal?: AbortSignal,
|
||||
private readonly taskId?: string,
|
||||
private readonly toolName?: string,
|
||||
) {
|
||||
super(hookName)
|
||||
}
|
||||
|
||||
override async [exec](input: HookInput): Promise<HookOutput> {
|
||||
const startTime = performance.now()
|
||||
const taskId = this.taskId // Local const for type narrowing in closures
|
||||
|
||||
// Capture telemetry at the start of individual hook execution
|
||||
if (taskId) {
|
||||
telemetryService.safeCapture(
|
||||
() =>
|
||||
telemetryService.captureHookExecution(taskId, this.hookName, "started", {
|
||||
source: this.source,
|
||||
toolName: this.toolName,
|
||||
}),
|
||||
"HookFactory.exec.started",
|
||||
)
|
||||
}
|
||||
|
||||
// Check if already aborted before starting
|
||||
if (this.abortSignal?.aborted) {
|
||||
throw HookExecutionError.cancellation(this.scriptPath)
|
||||
@@ -398,6 +421,8 @@ class StdioHookRunner<Name extends HookName> extends HookRunner<Name> {
|
||||
|
||||
// If we have valid JSON, honor it regardless of exit code
|
||||
if (parsedOutput) {
|
||||
const durationMs = performance.now() - startTime
|
||||
|
||||
// Log warning if non-zero exit but valid JSON (for developers)
|
||||
if (exitCode !== 0) {
|
||||
console.warn(`[Hook ${this.hookName}] Exited with code ${exitCode} but provided valid JSON response`)
|
||||
@@ -406,6 +431,39 @@ class StdioHookRunner<Name extends HookName> extends HookRunner<Name> {
|
||||
}
|
||||
}
|
||||
|
||||
// Capture success/cancellation telemetry
|
||||
if (taskId) {
|
||||
if (parsedOutput.cancel) {
|
||||
telemetryService.safeCapture(
|
||||
() =>
|
||||
telemetryService.captureHookExecution(taskId, this.hookName, "completed", {
|
||||
source: this.source,
|
||||
toolName: this.toolName,
|
||||
durationMs,
|
||||
exitCode: exitCode ?? EXIT_CODE_SIGINT,
|
||||
cancelRequested: true,
|
||||
contextModified: !!parsedOutput.contextModification,
|
||||
contextSize: parsedOutput.contextModification?.length,
|
||||
}),
|
||||
"HookFactory.exec.completed.cancel",
|
||||
)
|
||||
} else {
|
||||
telemetryService.safeCapture(
|
||||
() =>
|
||||
telemetryService.captureHookExecution(taskId, this.hookName, "completed", {
|
||||
source: this.source,
|
||||
toolName: this.toolName,
|
||||
durationMs,
|
||||
exitCode: exitCode ?? 0,
|
||||
cancelRequested: false,
|
||||
contextModified: !!parsedOutput.contextModification,
|
||||
contextSize: parsedOutput.contextModification?.length,
|
||||
}),
|
||||
"HookFactory.exec.completed.success",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return parsedOutput
|
||||
}
|
||||
|
||||
@@ -413,6 +471,24 @@ class StdioHookRunner<Name extends HookName> extends HookRunner<Name> {
|
||||
if (exitCode === 0) {
|
||||
// Hook succeeded but didn't provide JSON - allow execution (no cancellation)
|
||||
console.warn(`[Hook ${this.hookName}] Completed successfully but no JSON response found`)
|
||||
const durationMs = performance.now() - startTime
|
||||
|
||||
// Capture success telemetry even without JSON
|
||||
if (taskId) {
|
||||
telemetryService.safeCapture(
|
||||
() =>
|
||||
telemetryService.captureHookExecution(taskId, this.hookName, "completed", {
|
||||
source: this.source,
|
||||
toolName: this.toolName,
|
||||
durationMs,
|
||||
exitCode: 0,
|
||||
cancelRequested: false,
|
||||
contextModified: false,
|
||||
}),
|
||||
"HookFactory.exec.completed.noJson",
|
||||
)
|
||||
}
|
||||
|
||||
return HookOutput.create({
|
||||
cancel: false,
|
||||
})
|
||||
@@ -421,8 +497,48 @@ class StdioHookRunner<Name extends HookName> extends HookRunner<Name> {
|
||||
throw HookExecutionError.execution(this.scriptPath, exitCode ?? 1, stderr, this.hookName)
|
||||
}
|
||||
} catch (error) {
|
||||
const durationMs = performance.now() - startTime
|
||||
|
||||
// If it's already a HookExecutionError, re-throw it
|
||||
if (HookExecutionError.isHookError(error)) {
|
||||
// Capture failure telemetry based on error type
|
||||
if (taskId) {
|
||||
if (error.errorInfo.type === "cancellation") {
|
||||
telemetryService.safeCapture(
|
||||
() =>
|
||||
telemetryService.captureHookExecution(taskId, this.hookName, "cancelled", {
|
||||
source: this.source,
|
||||
toolName: this.toolName,
|
||||
}),
|
||||
"HookFactory.exec.error.cancellation",
|
||||
)
|
||||
} else if (error.errorInfo.type === "timeout") {
|
||||
telemetryService.safeCapture(
|
||||
() =>
|
||||
telemetryService.captureHookExecution(taskId, this.hookName, "failed", {
|
||||
source: this.source,
|
||||
toolName: this.toolName,
|
||||
durationMs,
|
||||
errorType: "timeout",
|
||||
errorMessage: error.message,
|
||||
}),
|
||||
"HookFactory.exec.error.timeout",
|
||||
)
|
||||
} else {
|
||||
telemetryService.safeCapture(
|
||||
() =>
|
||||
telemetryService.captureHookExecution(taskId, this.hookName, "failed", {
|
||||
source: this.source,
|
||||
toolName: this.toolName,
|
||||
durationMs,
|
||||
exitCode: error.errorInfo.exitCode ?? 1,
|
||||
errorType: error.errorInfo.type as "execution" | "timeout" | "validation",
|
||||
errorMessage: error.message,
|
||||
}),
|
||||
"HookFactory.exec.error.failed",
|
||||
)
|
||||
}
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
@@ -432,15 +548,52 @@ class StdioHookRunner<Name extends HookName> extends HookRunner<Name> {
|
||||
|
||||
// Check for timeout
|
||||
if (error instanceof Error && error.message.includes("timed out")) {
|
||||
if (taskId) {
|
||||
telemetryService.safeCapture(
|
||||
() =>
|
||||
telemetryService.captureHookExecution(taskId, this.hookName, "failed", {
|
||||
source: this.source,
|
||||
toolName: this.toolName,
|
||||
durationMs,
|
||||
errorType: "timeout",
|
||||
errorMessage: error.message,
|
||||
}),
|
||||
"HookFactory.exec.catch.timeout",
|
||||
)
|
||||
}
|
||||
throw HookExecutionError.timeout(this.scriptPath, HOOK_EXECUTION_TIMEOUT_MS, stderr, this.hookName)
|
||||
}
|
||||
|
||||
// Check for cancellation
|
||||
if (error instanceof Error && error.message.includes("cancelled")) {
|
||||
if (taskId) {
|
||||
telemetryService.safeCapture(
|
||||
() =>
|
||||
telemetryService.captureHookExecution(taskId, this.hookName, "cancelled", {
|
||||
source: this.source,
|
||||
toolName: this.toolName,
|
||||
}),
|
||||
"HookFactory.exec.catch.cancelled",
|
||||
)
|
||||
}
|
||||
throw HookExecutionError.cancellation(this.scriptPath, this.hookName)
|
||||
}
|
||||
|
||||
// Generic execution error - include hook name
|
||||
if (taskId) {
|
||||
telemetryService.safeCapture(
|
||||
() =>
|
||||
telemetryService.captureHookExecution(taskId, this.hookName, "failed", {
|
||||
source: this.source,
|
||||
toolName: this.toolName,
|
||||
durationMs,
|
||||
exitCode: exitCode ?? 1,
|
||||
errorType: "execution",
|
||||
errorMessage: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
"HookFactory.exec.catch.execution",
|
||||
)
|
||||
}
|
||||
throw HookExecutionError.execution(this.scriptPath, exitCode ?? 1, stderr, this.hookName)
|
||||
}
|
||||
}
|
||||
@@ -546,8 +699,8 @@ export class HookFactory {
|
||||
/**
|
||||
* Create a hook runner without streaming support (backwards compatible)
|
||||
*/
|
||||
async create<Name extends HookName>(hookName: Name): Promise<HookRunner<Name>> {
|
||||
return this.createWithStreaming(hookName)
|
||||
async create<Name extends HookName>(hookName: Name, taskId?: string, toolName?: string): Promise<HookRunner<Name>> {
|
||||
return this.createWithStreaming(hookName, undefined, undefined, taskId, toolName)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -566,24 +719,94 @@ export class HookFactory {
|
||||
* @param hookName The type of hook to create (e.g., "PreToolUse", "PostToolUse")
|
||||
* @param streamCallback Optional callback for real-time output streaming
|
||||
* @param abortSignal Optional signal to cancel hook execution
|
||||
* @param taskId Optional task ID for telemetry context
|
||||
* @param toolName Optional tool name for telemetry context
|
||||
* @returns A HookRunner that executes the hook(s), or NoOpRunner if none found
|
||||
*/
|
||||
async createWithStreaming<Name extends HookName>(
|
||||
hookName: Name,
|
||||
streamCallback?: HookStreamCallback,
|
||||
abortSignal?: AbortSignal,
|
||||
taskId?: string,
|
||||
toolName?: string,
|
||||
): Promise<HookRunner<Name>> {
|
||||
// Use cache for hook discovery instead of direct file system scan
|
||||
const { HookDiscoveryCache } = await import("./HookDiscoveryCache")
|
||||
const scripts = await HookDiscoveryCache.getInstance().get(hookName)
|
||||
|
||||
const runners = scripts.map((script) => new StdioHookRunner(hookName, script, streamCallback, abortSignal))
|
||||
// Fetch hooks dirs once for source determination and telemetry
|
||||
const hooksDirs = await getAllHooksDirs()
|
||||
|
||||
// Capture hook discovery telemetry
|
||||
// Categorize scripts by location (global vs workspace)
|
||||
const { globalCount, workspaceCount } = this.categorizeHookScripts(scripts, hooksDirs)
|
||||
if (scripts.length > 0) {
|
||||
telemetryService.safeCapture(
|
||||
() => telemetryService.captureHookDiscovery(hookName, globalCount, workspaceCount),
|
||||
"HookFactory.createWithStreaming.discovery",
|
||||
)
|
||||
}
|
||||
|
||||
// Create runners with source determination for each script
|
||||
const runners = scripts.map((script) => {
|
||||
const source = this.determineScriptSource(script, hooksDirs)
|
||||
return new StdioHookRunner(hookName, script, source, streamCallback, abortSignal, taskId, toolName)
|
||||
})
|
||||
|
||||
if (runners.length === 0) {
|
||||
return new NoOpRunner(hookName)
|
||||
}
|
||||
return runners.length === 1 ? runners[0] : new CombinedHookRunner(hookName, runners)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a hooks directory is a global hooks directory.
|
||||
* Global hooks are located in paths containing "Cline/Hooks" or "cline/hooks".
|
||||
*/
|
||||
private static isGlobalHooksDir(dir: string): boolean {
|
||||
return /[/\\][Cc]line[/\\][Hh]ooks/i.test(dir)
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a single script is from global or workspace location
|
||||
*/
|
||||
private determineScriptSource(scriptPath: string, hooksDirs: string[]): "global" | "workspace" {
|
||||
const containingDir = hooksDirs.find((dir) => scriptPath.startsWith(dir))
|
||||
if (containingDir && HookFactory.isGlobalHooksDir(containingDir)) {
|
||||
return "global"
|
||||
}
|
||||
return "workspace" // Default to workspace if uncertain
|
||||
}
|
||||
|
||||
/**
|
||||
* Categorizes hook scripts by their location (global vs workspace).
|
||||
* Global hooks are located in ~/Documents/Cline/Hooks/
|
||||
* Workspace hooks are located in workspace .clinerules/hooks/ directories
|
||||
*
|
||||
* @param scripts Array of hook script paths
|
||||
* @param hooksDirs Array of hooks directories (passed to avoid redundant fetches)
|
||||
* @returns Object with globalCount and workspaceCount
|
||||
*/
|
||||
private categorizeHookScripts(scripts: string[], hooksDirs: string[]): { globalCount: number; workspaceCount: number } {
|
||||
if (scripts.length === 0) {
|
||||
return { globalCount: 0, workspaceCount: 0 }
|
||||
}
|
||||
|
||||
let globalCount = 0
|
||||
let workspaceCount = 0
|
||||
|
||||
for (const script of scripts) {
|
||||
const containingDir = hooksDirs.find((dir) => script.startsWith(dir))
|
||||
if (containingDir && HookFactory.isGlobalHooksDir(containingDir)) {
|
||||
globalCount++
|
||||
} else {
|
||||
workspaceCount++
|
||||
}
|
||||
}
|
||||
|
||||
return { globalCount, workspaceCount }
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns A list of paths to scripts for the given hook name.
|
||||
* Includes both global hooks (from ~/Documents/Cline/Hooks/) and workspace hooks
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
* @returns true if hooks are enabled and supported on this platform, false otherwise
|
||||
*/
|
||||
export function getHooksEnabledSafe(userSetting: boolean | undefined): boolean {
|
||||
// Handle legacy object format: {user: boolean, featureFlag: boolean}, which
|
||||
// can occur if the migration hasn't run yet or if reading from an old state.
|
||||
const booleanValue = Boolean((userSetting as any)?.user ?? userSetting)
|
||||
|
||||
// Force hooks to false on Windows (not yet supported)
|
||||
return process.platform === "win32" ? false : (userSetting ?? false)
|
||||
return process.platform === "win32" ? false : booleanValue
|
||||
}
|
||||
|
||||
+483
@@ -0,0 +1,483 @@
|
||||
[
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "ask_followup_question",
|
||||
"description": "Ask user a question for clarifying or gathering information needed to complete the task. For example, ask the user clarifying questions about a key implementation decision. You should only ask one question.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"question": {
|
||||
"type": "string",
|
||||
"description": "The single question to ask the user. E.g. \"How can I help you?\""
|
||||
},
|
||||
"options": {
|
||||
"type": "string",
|
||||
"description": "An array of 2-5 options (e.x: \"[\"Option 1\", \"Option 2\", \"Option 3\"]\") for the user to choose from. Each option should be a string describing a possible answer to the single question. You may not always need to provide options, but it may be helpful in many cases where it can save the user from having to type out a response manually. IMPORTANT: NEVER include an option to toggle to Act mode, as this would be something you need to direct the user to do manually themselves if needed."
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress after this tool use is completed. The task_progress parameter must be included as a separate parameter inside of the parent tool call, it must be separate from other parameters such as content, arguments, etc. (See 'UPDATING TASK PROGRESS' section for more details)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"question",
|
||||
"options"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "execute_command",
|
||||
"description": "Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "The CLI command to execute. This should be valid for the current operating system. Do not use the ~ character or $HOME to refer to the home directory. Always use absolute paths. The command will be executed from the current workspace, you do not need to cd to the workspace."
|
||||
},
|
||||
"requires_approval": {
|
||||
"type": "boolean",
|
||||
"description": "To indicate whether this command requires explicit user approval or interaction before it should be executed. For system/file altering operations like installing/uninstalling packages, removing/overwriting files, system configuration changes, network operations, or any commands that are considered potentially dangerous must be set to true. False for safe operations like running development servers, building projects, and other non-destructive operations."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"requires_approval"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"description": "Request to read the contents of a file at the specified path. Use this when you need to examine the contents of an existing file you do not know the contents of, for example to analyze code, review text files, or extract information from configuration files. Automatically extracts raw text from PDF and DOCX files. May not be suitable for other types of binary files, as it returns the raw content as a string. Do NOT use this tool to list the contents of a directory. Only use this tool on files.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "The path of the file to read (relative to the current working directory {{CWD}}){{MULTI_ROOT_HINT}}"
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress after this tool use is completed. The task_progress parameter must be included as a separate parameter inside of the parent tool call, it must be separate from other parameters such as content, arguments, etc. (See 'UPDATING TASK PROGRESS' section for more details)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "write_to_file",
|
||||
"description": "[IMPORTANT: Always output the absolutePath first] Request to write content to a file at the specified path. If the file exists, it will be overwritten with the provided content. If the file doesn't exist, it will be created. This tool will automatically create any directories needed to write the file.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"absolutePath": {
|
||||
"type": "string",
|
||||
"description": "The absolute path to the file to write to."
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "After providing the path so a file can be created, then use this to provide the content to write to the file."
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress after this tool use is completed. The task_progress parameter must be included as a separate parameter inside of the parent tool call, it must be separate from other parameters such as content, arguments, etc. (See 'UPDATING TASK PROGRESS' section for more details)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"absolutePath",
|
||||
"content"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "replace_in_file",
|
||||
"description": "[IMPORTANT: Always output the absolutePath first] Request to replace sections of content in an existing file using SEARCH/REPLACE blocks that define exact changes to specific parts of the file. This tool should be used when you need to make targeted changes to specific parts of a file.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"absolutePath": {
|
||||
"type": "string",
|
||||
"description": "The absolute path to the file to write to."
|
||||
},
|
||||
"diff": {
|
||||
"type": "string",
|
||||
"description": "One or more SEARCH/REPLACE blocks following this exact format:\n ```\n ------- SEARCH\n [exact content to find]\n =======\n [new content to replace with]\n +++++++ REPLACE\n ```\n Critical rules:\n 1. SEARCH content must match the associated file section to find EXACTLY:\n\t * Match character-for-character including whitespace, indentation, line endings\n\t * Include all comments, docstrings, etc.\n 2. SEARCH/REPLACE blocks will ONLY replace the first match occurrence.\n\t * Including multiple unique SEARCH/REPLACE blocks if you need to make multiple changes.\n\t * Include *just* enough lines in each SEARCH section to uniquely match each set of lines that need to change.\n\t * When using multiple SEARCH/REPLACE blocks, list them in the order they appear in the file.\n 3. Keep SEARCH/REPLACE blocks concise:\n\t * Break large SEARCH/REPLACE blocks into a series of smaller blocks that each change a small portion of the file.\n\t * Include just the changing lines, and a few surrounding lines if needed for uniqueness.\n\t * Do not include long runs of unchanging lines in SEARCH/REPLACE blocks.\n\t * Each line must be complete. Never truncate lines mid-way through as this can cause matching failures.\n 4. Special operations:\n\t * To move code: Use two SEARCH/REPLACE blocks (one to delete from original + one to insert at new location)\n\t * To delete code: Use empty REPLACE section"
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress after this tool use is completed. The task_progress parameter must be included as a separate parameter inside of the parent tool call, it must be separate from other parameters such as content, arguments, etc. (See 'UPDATING TASK PROGRESS' section for more details)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"absolutePath",
|
||||
"diff"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "search_files",
|
||||
"description": "Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "The path of the directory to search in (relative to the current working directory {{CWD}}){{MULTI_ROOT_HINT}}. This directory will be recursively searched."
|
||||
},
|
||||
"regex": {
|
||||
"type": "string",
|
||||
"description": "The regular expression pattern to search for. Uses Rust regex syntax."
|
||||
},
|
||||
"file_pattern": {
|
||||
"type": "string",
|
||||
"description": "Glob pattern to filter files (e.g., '*.ts' for TypeScript files). If not provided, it will search all files (*)."
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress after this tool use is completed. The task_progress parameter must be included as a separate parameter inside of the parent tool call, it must be separate from other parameters such as content, arguments, etc. (See 'UPDATING TASK PROGRESS' section for more details)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"regex"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_files",
|
||||
"description": "Request to list files and directories within the specified directory. If recursive is true, it will list all files and directories recursively. If recursive is false or not provided, it will only list the top-level contents. Do not use this tool to confirm the existence of files you may have created, as the user will let you know if the files were created successfully or not.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "The path of the directory to list contents for."
|
||||
},
|
||||
"recursive": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to list files recursively. Use true for recursive listing, false or omit for top-level only."
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress after this tool use is completed. The task_progress parameter must be included as a separate parameter inside of the parent tool call, it must be separate from other parameters such as content, arguments, etc. (See 'UPDATING TASK PROGRESS' section for more details)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_code_definition_names",
|
||||
"description": "Request to list definition names (classes, functions, methods, etc.) used in source code files at the top level of the specified directory. This tool provides insights into the codebase structure and important constructs, encapsulating high-level concepts and relationships that are crucial for understanding the overall architecture.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "The path of the directory (relative to the current working directory {{CWD}}){{MULTI_ROOT_HINT}} to list top level source code definitions for."
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress after this tool use is completed. The task_progress parameter must be included as a separate parameter inside of the parent tool call, it must be separate from other parameters such as content, arguments, etc. (See 'UPDATING TASK PROGRESS' section for more details)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "browser_action",
|
||||
"description": "Request to interact with a Puppeteer-controlled browser. Every action, except `close`, will be responded to with a screenshot of the browser's current state, along with any new console logs. You may only perform one browser action per message, and wait for the user's response including a screenshot and logs to determine the next action.\n- The sequence of actions **must always start with** launching the browser at a URL, and **must always end with** closing the browser. If you need to visit a new URL that is not possible to navigate to from the current webpage, you must first close the browser, then launch again at the new URL.\n- While the browser is active, only the `browser_action` tool can be used. No other tools should be called during this time. You may proceed to use other tools only after closing the browser. For example if you run into an error and need to fix a file, you must close the browser, then use other tools to make the necessary changes, then re-launch the browser to verify the result.\n- The browser window has a resolution of **1280x720** pixels. When performing any click actions, ensure the coordinates are within this resolution range.\n- Before clicking on any elements such as icons, links, or buttons, you must consult the provided screenshot of the page to determine the coordinates of the element. The click should be targeted at the **center of the element**, not on its edges.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"description": "The action to perform. The available actions are: \n\t* launch: Launch a new Puppeteer-controlled browser instance at the specified URL. This **must always be the first action**. \n\t\t- Use with the `url` parameter to provide the URL. \n\t\t- Ensure the URL is valid and includes the appropriate protocol (e.g. http://localhost:3000/page, file:///path/to/file.html, etc.) \n\t* click: Click at a specific x,y coordinate. \n\t\t- Use with the `coordinate` parameter to specify the location. \n\t\t- Always click in the center of an element (icon, button, link, etc.) based on coordinates derived from a screenshot. \n\t* type: Type a string of text on the keyboard. You might use this after clicking on a text field to input text. \n\t\t- Use with the `text` parameter to provide the string to type. \n\t* scroll_down: Scroll down the page by one page height. \n\t* scroll_up: Scroll up the page by one page height. \n\t* close: Close the Puppeteer-controlled browser instance. This **must always be the final browser action**. \n\t - Example: 'scroll_up'"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "Use this for providing the URL for the `launch` action."
|
||||
},
|
||||
"coordinate": {
|
||||
"type": "string",
|
||||
"description": "x,y coordinates - The X and Y coordinates for the `click` action. Coordinates should be within the **1280x720** resolution. Example: '450,300'"
|
||||
},
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "Use this for providing the text for the `type` action. Example: 'Hello, world!'"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"action"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "web_fetch",
|
||||
"description": "Fetches and analyzes content from a specified URL.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "The URL to fetch content from"
|
||||
},
|
||||
"prompt": {
|
||||
"type": "string",
|
||||
"description": "Prompt for analyzing the webpage content"
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress after this tool use is completed. The task_progress parameter must be included as a separate parameter inside of the parent tool call, it must be separate from other parameters such as content, arguments, etc. (See 'UPDATING TASK PROGRESS' section for more details)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"url",
|
||||
"prompt"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "web_search",
|
||||
"description": "Performs a web search and returns relevant results with titles and URLs.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "The search query to use"
|
||||
},
|
||||
"allowed_domains": {
|
||||
"type": "string",
|
||||
"description": "JSON array of domains to restrict results to"
|
||||
},
|
||||
"blocked_domains": {
|
||||
"type": "string",
|
||||
"description": "JSON array of domains to exclude from results"
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress after this tool use is completed. The task_progress parameter must be included as a separate parameter inside of the parent tool call, it must be separate from other parameters such as content, arguments, etc. (See 'UPDATING TASK PROGRESS' section for more details)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"query"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "access_mcp_resource",
|
||||
"description": "Request to access a resource provided by a connected MCP server. Resources represent data sources that can be used as context, such as files, API responses, or system information. You must only use this tool if you have been informed of the MCP server and the resource you are trying to access.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"server_name": {
|
||||
"type": "string",
|
||||
"description": "The name of the MCP server providing the resource"
|
||||
},
|
||||
"uri": {
|
||||
"type": "string",
|
||||
"description": "The URI identifying the specific resource to access"
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress after this tool use is completed. The task_progress parameter must be included as a separate parameter inside of the parent tool call, it must be separate from other parameters such as content, arguments, etc. (See 'UPDATING TASK PROGRESS' section for more details)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"server_name",
|
||||
"uri"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "attempt_completion",
|
||||
"description": "Once you've completed the user's task, use this tool to present the final result to the user, including a brief and very short (1-2 paragraph) summary of the task and what was done to resolve it. Provide the basics, hitting the highlights, but do delve into the specifics. You should only call this tool when you have completed all tasks in the task_progress list, and completed all changes that are necessary to satisfy the user's request. You should not provide the contents of the task_progress list in the result parameter, it must be included in the task_progress parameter.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"result": {
|
||||
"type": "string",
|
||||
"description": "A clear, brief and very short (1-2 paragraph) summary of the final result of the task."
|
||||
},
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "An actionable terminal command that is non-verbose that allows user to review the result of your work. For example, use `start localhost:3000` to start a locally running development server. Commands like `echo` or `cat` that merely print text or open a file are not allowed. Ensure the command is properly formatted for user's OS and does not contain any harmful instructions"
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress with the latest status of each subtasks included previously, if any. If you are calling attempt completion, and all items in this list have been completed, they must be marked as completed in this response."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"result"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "plan_mode_respond",
|
||||
"description": "Respond to the user's inquiry in an effort to plan a solution to the user's task. This tool should ONLY be used when you have already explored the relevant files and are ready to present a concrete plan. DO NOT use this tool to announce what files you're going to read - just read them first. This tool is only available in PLAN MODE. The environment_details will specify the current mode; if it is not PLAN_MODE then you should not use this tool.\nHowever, if while writing your response you realize you actually need to do more exploration before providing a complete plan, you can add the optional needs_more_exploration parameter to indicate this. This allows you to acknowledge that you should have done more exploration first, and signals that your next message will use exploration tools instead.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"response": {
|
||||
"type": "string",
|
||||
"description": "The response to provide to the user."
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress with the latest status of each subtasks included previously if any."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"response"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "load_mcp_documentation",
|
||||
"description": "Load documentation about creating MCP servers. This tool should be used when the user requests to create or install an MCP server (the user may ask you something along the lines of \"add a tool\" that does some function, in other words to create an MCP server that provides tools and resources that may connect to external APIs for example. You have the ability to create an MCP server and add it to a configuration file that will then expose the tools and resources for you to use with `use_mcp_tool` and `access_mcp_resource`). The documentation provides detailed information about the MCP server creation process, including setup instructions, best practices, and examples.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "focus_chain",
|
||||
"description": "",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "generate_explanation",
|
||||
"description": "Opens a multi-file diff view and generates AI-powered inline comments explaining the changes between two git references. Use this tool to help users understand code changes from git commits, pull requests, branches, or any git refs. The tool uses git to retrieve file contents and displays a side-by-side diff view with explanatory comments.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "A descriptive title for the diff view (e.g., 'Changes in commit abc123', 'PR #42: Add authentication', 'Changes between main and feature-branch')"
|
||||
},
|
||||
"from_ref": {
|
||||
"type": "string",
|
||||
"description": "The git reference for the 'before' state. Can be a commit hash, branch name, tag, or relative reference like HEAD~1, HEAD^, origin/main, etc."
|
||||
},
|
||||
"to_ref": {
|
||||
"type": "string",
|
||||
"description": "The git reference for the 'after' state. Can be a commit hash, branch name, tag, or relative reference. If not provided, compares to the current working directory (including uncommitted changes)."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title",
|
||||
"from_ref"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "12345670mcp0test_tool",
|
||||
"description": "test-server: A test tool",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
+434
@@ -0,0 +1,434 @@
|
||||
[
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "execute_command",
|
||||
"description": "Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: {{CWD}}{{MULTI_ROOT_HINT}}",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions."
|
||||
},
|
||||
"requires_approval": {
|
||||
"type": "boolean",
|
||||
"description": "A boolean indicating whether this command requires explicit user approval before execution in case the user has auto-approve mode enabled. Set to 'true' for potentially impactful operations like installing/uninstalling packages, deleting/overwriting files, system configuration changes, network operations, or any commands that could have unintended side effects. Set to 'false' for safe operations like reading files/directories, running development servers, building projects, and other non-destructive operations."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"requires_approval"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"description": "Request to read the contents of a file at the specified path. Use this when you need to examine the contents of an existing file you do not know the contents of, for example to analyze code, review text files, or extract information from configuration files. Automatically extracts raw text from PDF and DOCX files. May not be suitable for other types of binary files, as it returns the raw content as a string. Do NOT use this tool to list the contents of a directory. Only use this tool on files.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "The path of the file to read (relative to the current working directory {{CWD}}){{MULTI_ROOT_HINT}}"
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress after this tool use is completed. The task_progress parameter must be included as a separate parameter inside of the parent tool call, it must be separate from other parameters such as content, arguments, etc. (See 'UPDATING TASK PROGRESS' section for more details)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "apply_patch",
|
||||
"description": "This is a custom utility that makes it more convenient to add, remove, move, or edit code in a single file. `apply_patch` effectively allows you to execute a diff/patch against a file, but the format of the diff specification is unique to this task, so pay careful attention to these instructions. To use the `apply_patch` command, you should pass a message of the following structure as \"input\":\n\n%%bash\napply_patch <<\"EOF\"\n*** Begin Patch\n[YOUR_PATCH]\n*** End Patch\nEOF\n\nWhere [YOUR_PATCH] is the actual content of your patch, specified in the following V4A diff format.\n\n*** [ACTION] File: [path/to/file] -> ACTION can be one of Add, Update, or Delete. \n\nIn a Add File section, every line of the new file (including blank/empty lines) MUST start with a `+` prefix. Do not include any unprefixed lines inside an Add section\nIn a Update/Delete section, repeat the following for each snippet of code that needs to be changed:\n[context_before] -> See below for further instructions on context.\n- [old_code] -> Precede the old code with a minus sign.\n+ [new_code] -> Precede the new, replacement code with a plus sign.\n[context_after] -> See below for further instructions on context.\n\nFor instructions on [context_before] and [context_after]:\n- By default, show 3 lines of code immediately above and 3 lines immediately below each change. If a change is within 3 lines of a previous change, do NOT duplicate the first change’s [context_after] lines in the second change’s [context_before] lines.\n- If 3 lines of context is insufficient to uniquely identify the snippet of code within the file, use the @@ operator to indicate the class or function to which the snippet belongs. For instance, we might have:\n@@ class BaseClass\n[3 lines of pre-context]\n- [old_code]\n+ [new_code]\n[3 lines of post-context]\n\n- If a code block is repeated so many times in a class or function such that even a single @@ statement and 3 lines of context cannot uniquely identify the snippet of code, you can use multiple `@@` statements to jump to the right context. For instance:\n\n@@ class BaseClass\n@@ \tdef method():\n[3 lines of pre-context]\n- [old_code]\n+ [new_code]\n[3 lines of post-context]\n\nNote, then, that we do not use line numbers in this diff format, as the context is enough to uniquely identify code. An example of a message that you might pass as \"input\" to this function, in order to apply a patch, is shown below.\n\n%%bash\napply_patch <<\"EOF\"\n*** Begin Patch\n*** Update File: pygorithm/searching/binary_search.py\n@@ class BaseClass\n@@ def search():\n- pass\n+ raise NotImplementedError()\n\n@@ class Subclass\n@@ def search():\n- pass\n+ raise NotImplementedError()\n\n*** End Patch\nEOF",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"input": {
|
||||
"type": "string",
|
||||
"description": "The apply_patch command that you wish to execute."
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress after this tool use is completed. The task_progress parameter must be included as a separate parameter inside of the parent tool call, it must be separate from other parameters such as content, arguments, etc. (See 'UPDATING TASK PROGRESS' section for more details)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"input"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "search_files",
|
||||
"description": "Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "The path of the directory to search in (relative to the current working directory {{CWD}}){{MULTI_ROOT_HINT}}. This directory will be recursively searched."
|
||||
},
|
||||
"regex": {
|
||||
"type": "string",
|
||||
"description": "The regular expression pattern to search for. Uses Rust regex syntax."
|
||||
},
|
||||
"file_pattern": {
|
||||
"type": "string",
|
||||
"description": "Glob pattern to filter files (e.g., '*.ts' for TypeScript files). If not provided, it will search all files (*)."
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress after this tool use is completed. The task_progress parameter must be included as a separate parameter inside of the parent tool call, it must be separate from other parameters such as content, arguments, etc. (See 'UPDATING TASK PROGRESS' section for more details)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"regex"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_files",
|
||||
"description": "Request to list files and directories within the specified directory. If recursive is true, it will list all files and directories recursively. If recursive is false or not provided, it will only list the top-level contents. Do not use this tool to confirm the existence of files you may have created, as the user will let you know if the files were created successfully or not.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "The path of the directory to list contents for (relative to the current working directory {{CWD}}){{MULTI_ROOT_HINT}}"
|
||||
},
|
||||
"recursive": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to list files recursively. Use true for recursive listing, false or omit for top-level only."
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress after this tool use is completed. The task_progress parameter must be included as a separate parameter inside of the parent tool call, it must be separate from other parameters such as content, arguments, etc. (See 'UPDATING TASK PROGRESS' section for more details)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_code_definition_names",
|
||||
"description": "Request to list definition names (classes, functions, methods, etc.) used in source code files at the top level of the specified directory. This tool provides insights into the codebase structure and important constructs, encapsulating high-level concepts and relationships that are crucial for understanding the overall architecture.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "The path of the directory (relative to the current working directory {{CWD}}){{MULTI_ROOT_HINT}} to list top level source code definitions for."
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress after this tool use is completed. The task_progress parameter must be included as a separate parameter inside of the parent tool call, it must be separate from other parameters such as content, arguments, etc. (See 'UPDATING TASK PROGRESS' section for more details)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "browser_action",
|
||||
"description": "Request to interact with a Puppeteer-controlled browser. Every action, except `close`, will be responded to with a screenshot of the browser's current state, along with any new console logs. You may only perform one browser action per message, and wait for the user's response including a screenshot and logs to determine the next action.\n- The sequence of actions **must always start with** launching the browser at a URL, and **must always end with** closing the browser. If you need to visit a new URL that is not possible to navigate to from the current webpage, you must first close the browser, then launch again at the new URL.\n- While the browser is active, only the `browser_action` tool can be used. No other tools should be called during this time. You may proceed to use other tools only after closing the browser. For example if you run into an error and need to fix a file, you must close the browser, then use other tools to make the necessary changes, then re-launch the browser to verify the result.\n- The browser window has a resolution of **1280x720** pixels. When performing any click actions, ensure the coordinates are within this resolution range.\n- Before clicking on any elements such as icons, links, or buttons, you must consult the provided screenshot of the page to determine the coordinates of the element. The click should be targeted at the **center of the element**, not on its edges.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"description": "The action to perform. The available actions are: \n\t* launch: Launch a new Puppeteer-controlled browser instance at the specified URL. This **must always be the first action**. \n\t\t- Use with the `url` parameter to provide the URL. \n\t\t- Ensure the URL is valid and includes the appropriate protocol (e.g. http://localhost:3000/page, file:///path/to/file.html, etc.) \n\t* click: Click at a specific x,y coordinate. \n\t\t- Use with the `coordinate` parameter to specify the location. \n\t\t- Always click in the center of an element (icon, button, link, etc.) based on coordinates derived from a screenshot. \n\t* type: Type a string of text on the keyboard. You might use this after clicking on a text field to input text. \n\t\t- Use with the `text` parameter to provide the string to type. \n\t* scroll_down: Scroll down the page by one page height. \n\t* scroll_up: Scroll up the page by one page height. \n\t* close: Close the Puppeteer-controlled browser instance. This **must always be the final browser action**. \n\t - Example: `<action>close</action>`"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "Use this for providing the URL for the `launch` action. \n\t* Example: <url>https://example.com</url>"
|
||||
},
|
||||
"coordinate": {
|
||||
"type": "string",
|
||||
"description": "The X and Y coordinates for the `click` action. Coordinates should be within the **1280x720** resolution. \n\t* Example: <coordinate>450,300</coordinate>"
|
||||
},
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "Use this for providing the text for the `type` action. \n\t* Example: <text>Hello, world!</text>"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"action"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "access_mcp_resource",
|
||||
"description": "Request to access a resource provided by a connected MCP server. Resources represent data sources that can be used as context, such as files, API responses, or system information.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"server_name": {
|
||||
"type": "string",
|
||||
"description": "The name of the MCP server providing the resource"
|
||||
},
|
||||
"uri": {
|
||||
"type": "string",
|
||||
"description": "The URI identifying the specific resource to access"
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress after this tool use is completed. The task_progress parameter must be included as a separate parameter inside of the parent tool call, it must be separate from other parameters such as content, arguments, etc. (See 'UPDATING TASK PROGRESS' section for more details)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"server_name",
|
||||
"uri"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "ask_followup_question",
|
||||
"description": "Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"question": {
|
||||
"type": "string",
|
||||
"description": "The question to ask the user. This should be a clear, specific question that addresses the information you need."
|
||||
},
|
||||
"options": {
|
||||
"type": "string",
|
||||
"description": "An array of 2-5 options for the user to choose from. Each option should be a string describing a possible answer. You may not always need to provide options, but it may be helpful in many cases where it can save the user from having to type out a response manually. IMPORTANT: NEVER include an option to toggle to Act mode, as this would be something you need to direct the user to do manually themselves if needed."
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress after this tool use is completed. The task_progress parameter must be included as a separate parameter inside of the parent tool call, it must be separate from other parameters such as content, arguments, etc. (See 'UPDATING TASK PROGRESS' section for more details)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"question"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "attempt_completion",
|
||||
"description": "After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. Optionally you may provide a CLI command to showcase the result of your work. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.\nIMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"result": {
|
||||
"type": "string",
|
||||
"description": "The result of the tool use. This should be a clear, specific description of the result."
|
||||
},
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "A CLI command to execute to show a live demo of the result to the user. For example, use `open index.html` to display a created html website, or `open localhost:3000` to display a locally running development server. But DO NOT use commands like `echo` or `cat` that merely print text. This command should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions"
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress after this tool use is completed. (See 'Updating Task Progress' section for more details)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"result"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "new_task",
|
||||
"description": "Request to create a new task with preloaded context covering the conversation with the user up to this point and key information for continuing with the new task. With this tool, you will create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions, with a focus on the most relevant information required for the new task.\nAmong other important areas of focus, this summary should be thorough in capturing technical details, code patterns, and architectural decisions that would be essential for continuing with the new task. The user will be presented with a preview of your generated context and can choose to create a new task or keep chatting in the current conversation. The user may choose to start a new task at any point.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"context": {
|
||||
"type": "string",
|
||||
"description": "The context to preload the new task with. If applicable based on the current task, this should include:\n 1. Current Work: Describe in detail what was being worked on prior to this request to create a new task. Pay special attention to the more recent messages / conversation.\n 2. Key Technical Concepts: List all important technical concepts, technologies, coding conventions, and frameworks discussed, which might be relevant for the new task.\n 3. Relevant Files and Code: If applicable, enumerate specific files and code sections examined, modified, or created for the task continuation. Pay special attention to the most recent messages and changes.\n 4. Problem Solving: Document problems solved thus far and any ongoing troubleshooting efforts.\n 5. Pending Tasks and Next Steps: Outline all pending tasks that you have explicitly been asked to work on, as well as list the next steps you will take for all outstanding work, if applicable. Include code snippets where they add clarity. For any next steps, include direct quotes from the most recent conversation showing exactly what task you were working on and where you left off. This should be verbatim to ensure there's no information loss in context between tasks. It's important to be detailed here."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"context"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "plan_mode_respond",
|
||||
"description": "Respond to the user's inquiry in an effort to plan a solution to the user's task. This tool should ONLY be used when you have already explored the relevant files and are ready to present a concrete plan. DO NOT use this tool to announce what files you're going to read - just read them first. This tool is only available in PLAN MODE. The environment_details will specify the current mode; if it is not PLAN_MODE then you should not use this tool.\nHowever, if while writing your response you realize you actually need to do more exploration before providing a complete plan, you can add the optional needs_more_exploration parameter to indicate this. This allows you to acknowledge that you should have done more exploration first, and signals that your next message will use exploration tools instead.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"response": {
|
||||
"type": "string",
|
||||
"description": "The response to provide to the user. Do not try to use tools in this parameter, this is simply a chat response. (You MUST use the response parameter, do not simply place the response text directly within <plan_mode_respond> tags.)"
|
||||
},
|
||||
"needs_more_exploration": {
|
||||
"type": "boolean",
|
||||
"description": "Set to true if while formulating your response that you found you need to do more exploration with tools, for example reading files. (Remember, you can explore the project with tools like read_file in PLAN MODE without the user having to toggle to ACT MODE.) Defaults to false if not specified."
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": " A checklist showing task progress after this tool use is completed. (See 'Updating Task Progress' section for more details)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"response"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "act_mode_respond",
|
||||
"description": "Provide a progress update or preamble to the user during ACT MODE execution. This tool allows you to communicate your thought process and planned actions without interrupting the execution flow. After displaying your message, execution automatically continues, allowing you to proceed with subsequent tool calls immediately. This tool is only available in ACT MODE. This tool may not be called immediately after a previous act_mode_respond call.\n\nIMPORTANT: Use this tool frequently to create a better user experience. Since it's non-blocking, there's no performance penalty for frequent use.\n\nUse this tool when:\n- After reading files and before making any edits - explain your analysis and what changes you plan to make\n- When starting a new phase of work (e.g., transitioning from backend to frontend, or from one feature to another)\n- During long sequences of operations to provide progress updates\n- When your approach or strategy changes mid-task\n- Before executing complex or potentially risky operations\n- To explain why you're choosing one approach over another\n\nDo NOT use this tool when you have completed all required actions and are ready to present the final output; in that case, use the attempt_completion tool instead.\n\nCRITICAL CONSTRAINT: You MUST NOT call this tool more than once in a row. After using act_mode_respond, your next assistant message MUST either call a different tool or perform additional work without using act_mode_respond again. If you attempt to call act_mode_respond consecutively, the tool call will fail with an explicit error.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"response": {
|
||||
"type": "string",
|
||||
"description": "The message to provide to the user. This should explain what you're about to do, your current progress, or your reasoning. The response should be brief and conversational in tone, aiming to keep the user informed without overwhelming them with details."
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress with the latest status of each subtasks included previously if any."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"response"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "load_mcp_documentation",
|
||||
"description": "Load documentation about creating MCP servers. This tool should be used when the user requests to create or install an MCP server (the user may ask you something along the lines of \"add a tool\" that does some function, in other words to create an MCP server that provides tools and resources that may connect to external APIs for example. You have the ability to create an MCP server and add it to a configuration file that will then expose the tools and resources for you to use with `use_mcp_tool` and `access_mcp_resource`). The documentation provides detailed information about the MCP server creation process, including setup instructions, best practices, and examples.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "focus_chain",
|
||||
"description": "",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "generate_explanation",
|
||||
"description": "Opens a multi-file diff view and generates AI-powered inline comments explaining the changes between two git references. Use this tool to help users understand code changes from git commits, pull requests, branches, or any git refs. The tool uses git to retrieve file contents and displays a side-by-side diff view with explanatory comments.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "A descriptive title for the diff view (e.g., 'Changes in commit abc123', 'PR #42: Add authentication', 'Changes between main and feature-branch')"
|
||||
},
|
||||
"from_ref": {
|
||||
"type": "string",
|
||||
"description": "The git reference for the 'before' state. Can be a commit hash, branch name, tag, or relative reference like HEAD~1, HEAD^, origin/main, etc."
|
||||
},
|
||||
"to_ref": {
|
||||
"type": "string",
|
||||
"description": "The git reference for the 'after' state. Can be a commit hash, branch name, tag, or relative reference. If not provided, compares to the current working directory (including uncommitted changes)."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title",
|
||||
"from_ref"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "12345670mcp0test_tool",
|
||||
"description": "test-server: A test tool",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
+420
@@ -0,0 +1,420 @@
|
||||
[
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "execute_command",
|
||||
"description": "Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "The CLI command to execute. This should be valid for the current operating system. Do not use the ~ character or $HOME to refer to the home directory. Always use absolute paths. The command will be executed from the current workspace, you do not need to cd to the workspace."
|
||||
},
|
||||
"requires_approval": {
|
||||
"type": "boolean",
|
||||
"description": "To indicate whether this command requires explicit user approval or interaction before it should be executed. For system/file altering operations like installing/uninstalling packages, removing/overwriting files, system configuration changes, network operations, or any commands that are considered potentially dangerous must be set to true. False for safe operations like running development servers, building projects, and other non-destructive operations."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"requires_approval"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"description": "Request to read the contents of a file at the specified path. Use this when you need to examine the contents of an existing file you do not know the contents of, for example to analyze code, review text files, or extract information from configuration files. Automatically extracts raw text from PDF and DOCX files. May not be suitable for other types of binary files, as it returns the raw content as a string. Do NOT use this tool to list the contents of a directory. Only use this tool on files.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "The path of the file to read (relative to the current working directory {{CWD}}){{MULTI_ROOT_HINT}}"
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress after this tool use is completed. The task_progress parameter must be included as a separate parameter inside of the parent tool call, it must be separate from other parameters such as content, arguments, etc. (See 'UPDATING TASK PROGRESS' section for more details)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "write_to_file",
|
||||
"description": "[IMPORTANT: Always output the absolutePath first] Request to write content to a file at the specified path. If the file exists, it will be overwritten with the provided content. If the file doesn't exist, it will be created. This tool will automatically create any directories needed to write the file.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"absolutePath": {
|
||||
"type": "string",
|
||||
"description": "The absolute path to the file to write to."
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "After providing the path so a file can be created, then use this to provide the content to write to the file."
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress after this tool use is completed. The task_progress parameter must be included as a separate parameter inside of the parent tool call, it must be separate from other parameters such as content, arguments, etc. (See 'UPDATING TASK PROGRESS' section for more details)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"absolutePath",
|
||||
"content"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "replace_in_file",
|
||||
"description": "[IMPORTANT: Always output the absolutePath first] Request to replace sections of content in an existing file using SEARCH/REPLACE blocks that define exact changes to specific parts of the file. This tool should be used when you need to make targeted changes to specific parts of a file.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"absolutePath": {
|
||||
"type": "string",
|
||||
"description": "The absolute path to the file to write to."
|
||||
},
|
||||
"diff": {
|
||||
"type": "string",
|
||||
"description": "One or more SEARCH/REPLACE blocks following this exact format:\n ```\n ------- SEARCH\n [exact content to find]\n =======\n [new content to replace with]\n +++++++ REPLACE\n ```\n Critical rules:\n 1. SEARCH content must match the associated file section to find EXACTLY:\n\t * Match character-for-character including whitespace, indentation, line endings\n\t * Include all comments, docstrings, etc.\n 2. SEARCH/REPLACE blocks will ONLY replace the first match occurrence.\n\t * Including multiple unique SEARCH/REPLACE blocks if you need to make multiple changes.\n\t * Include *just* enough lines in each SEARCH section to uniquely match each set of lines that need to change.\n\t * When using multiple SEARCH/REPLACE blocks, list them in the order they appear in the file.\n 3. Keep SEARCH/REPLACE blocks concise:\n\t * Break large SEARCH/REPLACE blocks into a series of smaller blocks that each change a small portion of the file.\n\t * Include just the changing lines, and a few surrounding lines if needed for uniqueness.\n\t * Do not include long runs of unchanging lines in SEARCH/REPLACE blocks.\n\t * Each line must be complete. Never truncate lines mid-way through as this can cause matching failures.\n 4. Special operations:\n\t * To move code: Use two SEARCH/REPLACE blocks (one to delete from original + one to insert at new location)\n\t * To delete code: Use empty REPLACE section"
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress after this tool use is completed. The task_progress parameter must be included as a separate parameter inside of the parent tool call, it must be separate from other parameters such as content, arguments, etc. (See 'UPDATING TASK PROGRESS' section for more details)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"absolutePath",
|
||||
"diff"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "search_files",
|
||||
"description": "Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "The path of the directory to search in (relative to the current working directory {{CWD}}){{MULTI_ROOT_HINT}}. This directory will be recursively searched."
|
||||
},
|
||||
"regex": {
|
||||
"type": "string",
|
||||
"description": "The regular expression pattern to search for. Uses Rust regex syntax."
|
||||
},
|
||||
"file_pattern": {
|
||||
"type": "string",
|
||||
"description": "Glob pattern to filter files (e.g., '*.ts' for TypeScript files). If not provided, it will search all files (*)."
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress after this tool use is completed. The task_progress parameter must be included as a separate parameter inside of the parent tool call, it must be separate from other parameters such as content, arguments, etc. (See 'UPDATING TASK PROGRESS' section for more details)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"regex"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_files",
|
||||
"description": "Request to list files and directories within the specified directory. If recursive is true, it will list all files and directories recursively. If recursive is false or not provided, it will only list the top-level contents. Do not use this tool to confirm the existence of files you may have created, as the user will let you know if the files were created successfully or not.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "The path of the directory to list contents for."
|
||||
},
|
||||
"recursive": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to list files recursively. Use true for recursive listing, false or omit for top-level only."
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress after this tool use is completed. The task_progress parameter must be included as a separate parameter inside of the parent tool call, it must be separate from other parameters such as content, arguments, etc. (See 'UPDATING TASK PROGRESS' section for more details)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_code_definition_names",
|
||||
"description": "Request to list definition names (classes, functions, methods, etc.) used in source code files at the top level of the specified directory. This tool provides insights into the codebase structure and important constructs, encapsulating high-level concepts and relationships that are crucial for understanding the overall architecture.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "The path of the directory (relative to the current working directory {{CWD}}){{MULTI_ROOT_HINT}} to list top level source code definitions for."
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress after this tool use is completed. The task_progress parameter must be included as a separate parameter inside of the parent tool call, it must be separate from other parameters such as content, arguments, etc. (See 'UPDATING TASK PROGRESS' section for more details)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "browser_action",
|
||||
"description": "Request to interact with a Puppeteer-controlled browser. Every action, except `close`, will be responded to with a screenshot of the browser's current state, along with any new console logs. You may only perform one browser action per message, and wait for the user's response including a screenshot and logs to determine the next action.\n- The sequence of actions **must always start with** launching the browser at a URL, and **must always end with** closing the browser. If you need to visit a new URL that is not possible to navigate to from the current webpage, you must first close the browser, then launch again at the new URL.\n- While the browser is active, only the `browser_action` tool can be used. No other tools should be called during this time. You may proceed to use other tools only after closing the browser. For example if you run into an error and need to fix a file, you must close the browser, then use other tools to make the necessary changes, then re-launch the browser to verify the result.\n- The browser window has a resolution of **1280x720** pixels. When performing any click actions, ensure the coordinates are within this resolution range.\n- Before clicking on any elements such as icons, links, or buttons, you must consult the provided screenshot of the page to determine the coordinates of the element. The click should be targeted at the **center of the element**, not on its edges.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"description": "The action to perform. The available actions are: \n\t* launch: Launch a new Puppeteer-controlled browser instance at the specified URL. This **must always be the first action**. \n\t\t- Use with the `url` parameter to provide the URL. \n\t\t- Ensure the URL is valid and includes the appropriate protocol (e.g. http://localhost:3000/page, file:///path/to/file.html, etc.) \n\t* click: Click at a specific x,y coordinate. \n\t\t- Use with the `coordinate` parameter to specify the location. \n\t\t- Always click in the center of an element (icon, button, link, etc.) based on coordinates derived from a screenshot. \n\t* type: Type a string of text on the keyboard. You might use this after clicking on a text field to input text. \n\t\t- Use with the `text` parameter to provide the string to type. \n\t* scroll_down: Scroll down the page by one page height. \n\t* scroll_up: Scroll up the page by one page height. \n\t* close: Close the Puppeteer-controlled browser instance. This **must always be the final browser action**. \n\t - Example: `<action>close</action>`"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "Use this for providing the URL for the `launch` action. \n\t* Example: <url>https://example.com</url>"
|
||||
},
|
||||
"coordinate": {
|
||||
"type": "string",
|
||||
"description": "The X and Y coordinates for the `click` action. Coordinates should be within the **1280x720** resolution. \n\t* Example: <coordinate>450,300</coordinate>"
|
||||
},
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "Use this for providing the text for the `type` action. \n\t* Example: <text>Hello, world!</text>"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"action"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "access_mcp_resource",
|
||||
"description": "Request to access a resource provided by a connected MCP server. Resources represent data sources that can be used as context, such as files, API responses, or system information. You must only use this tool if you have been informed of the MCP server and the resource you are trying to access.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"server_name": {
|
||||
"type": "string",
|
||||
"description": "The name of the MCP server providing the resource"
|
||||
},
|
||||
"uri": {
|
||||
"type": "string",
|
||||
"description": "The URI identifying the specific resource to access"
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress after this tool use is completed. The task_progress parameter must be included as a separate parameter inside of the parent tool call, it must be separate from other parameters such as content, arguments, etc. (See 'UPDATING TASK PROGRESS' section for more details)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"server_name",
|
||||
"uri"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "ask_followup_question",
|
||||
"description": "Ask user a question for clarifying or gathering information needed to complete the task. For example, ask the user clarifying questions about a key implementation decision. You should only ask one question.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"question": {
|
||||
"type": "string",
|
||||
"description": "The single question to ask the user. E.g. \"How can I help you?\""
|
||||
},
|
||||
"options": {
|
||||
"type": "string",
|
||||
"description": "An array of 2-5 options (e.x: \"[\"Option 1\", \"Option 2\", \"Option 3\"]\") for the user to choose from. Each option should be a string describing a possible answer to the single question. You may not always need to provide options, but it may be helpful in many cases where it can save the user from having to type out a response manually. IMPORTANT: NEVER include an option to toggle to Act mode, as this would be something you need to direct the user to do manually themselves if needed."
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress after this tool use is completed. The task_progress parameter must be included as a separate parameter inside of the parent tool call, it must be separate from other parameters such as content, arguments, etc. (See 'UPDATING TASK PROGRESS' section for more details)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"question",
|
||||
"options"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "attempt_completion",
|
||||
"description": "Once you've completed the user's task, use this tool to present the final result to the user, including a brief and very short (1-2 paragraph) summary of the task and what was done to resolve it. Provide the basics, hitting the highlights, but do delve into the specifics. You should only call this tool when you have completed all tasks in the task_progress list, and completed all changes that are necessary to satisfy the user's request. You should not provide the contents of the task_progress list in the result parameter, it must be included in the task_progress parameter.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"result": {
|
||||
"type": "string",
|
||||
"description": "A clear, brief and very short (1-2 paragraph) summary of the final result of the task."
|
||||
},
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "An actionable terminal command that is non-verbose that allows user to review the result of your work. For example, use `start localhost:3000` to start a locally running development server. Commands like `echo` or `cat` that merely print text or open a file are not allowed. Ensure the command is properly formatted for user's OS and does not contain any harmful instructions"
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress with the latest status of each subtasks included previously, if any. If you are calling attempt completion, and all items in this list have been completed, they must be marked as completed in this response."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"result"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "plan_mode_respond",
|
||||
"description": "Respond to the user's inquiry in an effort to plan a solution to the user's task. This tool should ONLY be used when you have already explored the relevant files and are ready to present a concrete plan. DO NOT use this tool to announce what files you're going to read - just read them first. This tool is only available in PLAN MODE. The environment_details will specify the current mode; if it is not PLAN_MODE then you should not use this tool.\nHowever, if while writing your response you realize you actually need to do more exploration before providing a complete plan, you can add the optional needs_more_exploration parameter to indicate this. This allows you to acknowledge that you should have done more exploration first, and signals that your next message will use exploration tools instead.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"response": {
|
||||
"type": "string",
|
||||
"description": "The response to provide to the user."
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "string",
|
||||
"description": "A checklist showing task progress with the latest status of each subtasks included previously if any."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"response"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "load_mcp_documentation",
|
||||
"description": "Load documentation about creating MCP servers. This tool should be used when the user requests to create or install an MCP server (the user may ask you something along the lines of \"add a tool\" that does some function, in other words to create an MCP server that provides tools and resources that may connect to external APIs for example. You have the ability to create an MCP server and add it to a configuration file that will then expose the tools and resources for you to use with `use_mcp_tool` and `access_mcp_resource`). The documentation provides detailed information about the MCP server creation process, including setup instructions, best practices, and examples.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "focus_chain",
|
||||
"description": "",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "generate_explanation",
|
||||
"description": "Opens a multi-file diff view and generates AI-powered inline comments explaining the changes between two git references. Use this tool to help users understand code changes from git commits, pull requests, branches, or any git refs. The tool uses git to retrieve file contents and displays a side-by-side diff view with explanatory comments.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "A descriptive title for the diff view (e.g., 'Changes in commit abc123', 'PR #42: Add authentication', 'Changes between main and feature-branch')"
|
||||
},
|
||||
"from_ref": {
|
||||
"type": "string",
|
||||
"description": "The git reference for the 'before' state. Can be a commit hash, branch name, tag, or relative reference like HEAD~1, HEAD^, origin/main, etc."
|
||||
},
|
||||
"to_ref": {
|
||||
"type": "string",
|
||||
"description": "The git reference for the 'after' state. Can be a commit hash, branch name, tag, or relative reference. If not provided, compares to the current working directory (including uncommitted changes)."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title",
|
||||
"from_ref"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "12345670mcp0test_tool",
|
||||
"description": "test-server: A test tool",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,364 @@
|
||||
[
|
||||
{
|
||||
"name": "execute_command",
|
||||
"description": "Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. When chaining commands, use the shell operator && (not the HTML entity &&). If using search/grep commands, be careful to not use vague search terms that may return thousands of results. When in PLAN MODE, you may use the execute_command tool, but only in a non-destructive manner and in a way that does not alter any files.",
|
||||
"parameters": {
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"requires_approval": {
|
||||
"type": "BOOLEAN"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"requires_approval"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "read_file",
|
||||
"description": "Request to read the contents of a file at the specified path. Use this when you need to examine the contents of an existing file you do not know the contents of, for example to analyze code, review text files, or extract information from configuration files. Automatically extracts raw text from PDF and DOCX files. May not be suitable for other types of binary files, as it returns the raw content as a string. Do NOT use this tool to list the contents of a directory. Only use this tool on files.",
|
||||
"parameters": {
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "STRING"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "write_to_file",
|
||||
"description": "Request to write content to a file at the specified path. If the file exists, it will be overwritten with the provided content. If the file doesn't exist, it will be created. This tool will automatically create any directories needed to write the file.",
|
||||
"parameters": {
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"content": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "STRING"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"content"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "replace_in_file",
|
||||
"description": "Request to replace sections of content in an existing file using SEARCH/REPLACE blocks that define exact changes to specific parts of the file. This tool should be used when you need to make targeted changes to specific parts of a file.",
|
||||
"parameters": {
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"diff": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "STRING"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"diff"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "search_files",
|
||||
"description": "Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context.",
|
||||
"parameters": {
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"regex": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"file_pattern": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "STRING"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"regex"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "list_files",
|
||||
"description": "Request to list files and directories within the specified directory. If recursive is true, it will list all files and directories recursively. If recursive is false or not provided, it will only list the top-level contents. Do not use this tool to confirm the existence of files you may have created, as the user will let you know if the files were created successfully or not.",
|
||||
"parameters": {
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"recursive": {
|
||||
"type": "BOOLEAN"
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "STRING"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "list_code_definition_names",
|
||||
"description": "Request to list definition names (classes, functions, methods, etc.) used in source code files at the top level of the specified directory. This tool provides insights into the codebase structure and important constructs, encapsulating high-level concepts and relationships that are crucial for understanding the overall architecture.",
|
||||
"parameters": {
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "STRING"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "browser_action",
|
||||
"description": "Request to interact with a Puppeteer-controlled browser. Every action, except `close`, will be responded to with a screenshot of the browser's current state, along with any new console logs. You may only perform one browser action per message, and wait for the user's response including a screenshot and logs to determine the next action.\n- The sequence of actions **must always start with** launching the browser at a URL, and **must always end with** closing the browser. If you need to visit a new URL that is not possible to navigate to from the current webpage, you must first close the browser, then launch again at the new URL.\n- While the browser is active, only the `browser_action` tool can be used. No other tools should be called during this time. You may proceed to use other tools only after closing the browser. For example if you run into an error and need to fix a file, you must close the browser, then use other tools to make the necessary changes, then re-launch the browser to verify the result.\n- The browser window has a resolution of **1280x720** pixels. When performing any click actions, ensure the coordinates are within this resolution range.\n- Before clicking on any elements such as icons, links, or buttons, you must consult the provided screenshot of the page to determine the coordinates of the element. The click should be targeted at the **center of the element**, not on its edges.",
|
||||
"parameters": {
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"url": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"coordinate": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"text": {
|
||||
"type": "STRING"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"action"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "use_mcp_tool",
|
||||
"description": "Request to use a tool provided by a connected MCP server. Each MCP server can provide multiple tools with different capabilities. Tools have defined input schemas that specify required and optional parameters.",
|
||||
"parameters": {
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
"server_name": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"tool_name": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"arguments": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "STRING"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"server_name",
|
||||
"tool_name",
|
||||
"arguments"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "access_mcp_resource",
|
||||
"description": "Request to access a resource provided by a connected MCP server. Resources represent data sources that can be used as context, such as files, API responses, or system information.",
|
||||
"parameters": {
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
"server_name": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"uri": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "STRING"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"server_name",
|
||||
"uri"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ask_followup_question",
|
||||
"description": "Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth.",
|
||||
"parameters": {
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
"question": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"options": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "STRING"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"question"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "attempt_completion",
|
||||
"description": "After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. Optionally you may provide a CLI command to showcase the result of your work. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.\nIMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.",
|
||||
"parameters": {
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
"result": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"command": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "STRING"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"result"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "new_task",
|
||||
"description": "Request to create a new task with preloaded context covering the conversation with the user up to this point and key information for continuing with the new task. With this tool, you will create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions, with a focus on the most relevant information required for the new task.\nAmong other important areas of focus, this summary should be thorough in capturing technical details, code patterns, and architectural decisions that would be essential for continuing with the new task. The user will be presented with a preview of your generated context and can choose to create a new task or keep chatting in the current conversation. The user may choose to start a new task at any point.",
|
||||
"parameters": {
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
"context": {
|
||||
"type": "STRING"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"context"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "plan_mode_respond",
|
||||
"description": "Respond with a plan that outlines a solution to the user's request. This tool should ONLY be used when you have already explored the relevant files and are ready to present a concrete plan. Only use this tool after you have explored relevant files and collected sufficient context to create a detailed, accurate plan. This tool is only available in PLAN MODE, as indicated by the environment_details.\nIf it becomes apparent that additional exploration is required while the plan_mode_respond response is being generated, the optional needs_more_exploration parameter can be toggled to enable further research. This allows you to acknowledge that more exploration is required before the final plan_mode_respond is generated, and signals that your next message will use exploration tools instead.",
|
||||
"parameters": {
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
"response": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"needs_more_exploration": {
|
||||
"type": "BOOLEAN"
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "STRING"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"response"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "act_mode_respond",
|
||||
"description": "Provide a progress update or preamble to the user during ACT MODE execution. This tool allows you to communicate your thought process and planned actions without interrupting the execution flow. After displaying your message, execution automatically continues, allowing you to proceed with subsequent tool calls immediately. This tool is only available in ACT MODE. This tool may not be called immediately after a previous act_mode_respond call.\n\nIMPORTANT: Use this tool frequently to create a better user experience. Since it's non-blocking, there's no performance penalty for frequent use.\n\nUse this tool when:\n- After reading files and before making any edits - explain your analysis and what changes you plan to make\n- When starting a new phase of work (e.g., transitioning from backend to frontend, or from one feature to another)\n- During long sequences of operations to provide progress updates\n- When your approach or strategy changes mid-task\n- Before executing complex or potentially risky operations\n- To explain why you're choosing one approach over another\n\nDo NOT use this tool when you have completed all required actions and are ready to present the final output; in that case, use the attempt_completion tool instead.\n\nCRITICAL CONSTRAINT: You MUST NOT call this tool more than once in a row. After using act_mode_respond, your next assistant message MUST either call a different tool or perform additional work without using act_mode_respond again. If you attempt to call act_mode_respond consecutively, the tool call will fail with an explicit error.",
|
||||
"parameters": {
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
"response": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"task_progress": {
|
||||
"type": "STRING"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"response"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "load_mcp_documentation",
|
||||
"description": "Load documentation about creating MCP servers. This tool should be used when the user requests to create or install an MCP server (the user may ask you something along the lines of \"add a tool\" that does some function, in other words to create an MCP server that provides tools and resources that may connect to external APIs for example. You have the ability to create an MCP server and add it to a configuration file that will then expose the tools and resources for you to use with `use_mcp_tool` and `access_mcp_resource`). The documentation provides detailed information about the MCP server creation process, including setup instructions, best practices, and examples.",
|
||||
"parameters": {
|
||||
"type": "OBJECT",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "focus_chain",
|
||||
"description": "",
|
||||
"parameters": {
|
||||
"type": "OBJECT",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "generate_explanation",
|
||||
"description": "Opens a multi-file diff view and generates AI-powered inline comments explaining the changes between two git references. Use this tool to help users understand code changes from git commits, pull requests, branches, or any git refs. The tool uses git to retrieve file contents and displays a side-by-side diff view with explanatory comments.",
|
||||
"parameters": {
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"from_ref": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"to_ref": {
|
||||
"type": "STRING"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title",
|
||||
"from_ref"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "12345670mcp0test_tool",
|
||||
"description": "test-server: A test tool",
|
||||
"parameters": {
|
||||
"type": "OBJECT",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -24,14 +24,14 @@ Plan Mode is for deep analysis and strategic planning before implementation. You
|
||||
|
||||
### Phase 1: Silent Investigation
|
||||
|
||||
Perform comprehensive research to build complete understanding of the codebase. Work silently - execute targetted searcg commands and read files without explaining what you're doing. Only ask questions when truly necessary for planning. You must strongly incrporate key words and principles from the user's input into your targetted search patterns and strategy.
|
||||
Perform comprehensive research to build complete understanding of the codebase. Work silently - execute targeted search commands and read files without explaining what you're doing. Only ask questions when truly necessary for planning. You must strongly incorporate key words and principles from the user's input into your targeted search patterns and strategy.
|
||||
|
||||
**Research Activities:**
|
||||
- Use read_file, search_files, and list_code_definition_names extensively to understand architecture, patterns, and conventions
|
||||
- Execute targetted terminal commands to search and gather information about structure and dependencies.
|
||||
- Execute targeted terminal commands to search and gather information about structure and dependencies.
|
||||
- Identify technical constraints, existing patterns, and potential risks
|
||||
- Ask targeted clarifying questions only when they will directly influence your implementation approach
|
||||
- Ensure complete converage- before presenting a plan, you should identify all related functions, classes, calls, and methods that are involved or affected by the proposed changes.
|
||||
- Ensure complete coverage - before presenting a plan, you should identify all related functions, classes, calls, and methods that are involved or affected by the proposed changes.
|
||||
|
||||
### Phase 2: Plan Presentation
|
||||
|
||||
@@ -66,7 +66,7 @@ Engage with the user to discuss the plan, answer questions, and incorporate feed
|
||||
|
||||
### Phase 4: Transition to Implementation
|
||||
|
||||
Once the plan is finalized and approved, you MUST direct the user to switch to ACT MODE. In Act Mode, you'll execute the plan step-by-step as outlined. If you not specifically ask the user to switch to ACT MODE, you will not be able to implemnent the planned changes.
|
||||
Once the plan is finalized and approved, you MUST direct the user to switch to ACT MODE. In Act Mode, you'll execute the plan step-by-step as outlined. If you not specifically ask the user to switch to ACT MODE, you will not be able to implement the planned changes.
|
||||
|
||||
## Act Mode Workflow
|
||||
|
||||
@@ -215,7 +215,7 @@ RULES
|
||||
|
||||
- The current working directory is `/test/project` - this is the directory where all the tools will be executed from.
|
||||
- When executing terminal commands, new terminals always open in the workspace directory. Use relative paths or chain commands with proper shell operators (e.g., `cd path && command` to change directory and run a command together).
|
||||
- Whean searching, prefer the search_files tool over using grep in the terminal. If you are directly instruted to use grep, ensure your search patterns are targetted and not too vague to prevent extremely large outputs.
|
||||
- When searching, prefer the search_files tool over using grep in the terminal. If you are directly instructed to use grep, ensure your search patterns are targeted and not too vague to prevent extremely large outputs.
|
||||
- When using replace_in_file, pay careful attention to the EDITING FILES section above. The most common errors are:
|
||||
- Not matching content exactly (every character, space, and newline must match)
|
||||
- Using incomplete lines in SEARCH blocks (always include complete lines from start to end)
|
||||
|
||||
+5
-5
@@ -24,14 +24,14 @@ Plan Mode is for deep analysis and strategic planning before implementation. You
|
||||
|
||||
### Phase 1: Silent Investigation
|
||||
|
||||
Perform comprehensive research to build complete understanding of the codebase. Work silently - execute targetted searcg commands and read files without explaining what you're doing. Only ask questions when truly necessary for planning. You must strongly incrporate key words and principles from the user's input into your targetted search patterns and strategy.
|
||||
Perform comprehensive research to build complete understanding of the codebase. Work silently - execute targeted search commands and read files without explaining what you're doing. Only ask questions when truly necessary for planning. You must strongly incorporate key words and principles from the user's input into your targeted search patterns and strategy.
|
||||
|
||||
**Research Activities:**
|
||||
- Use read_file, search_files, and list_code_definition_names extensively to understand architecture, patterns, and conventions
|
||||
- Execute targetted terminal commands to search and gather information about structure and dependencies.
|
||||
- Execute targeted terminal commands to search and gather information about structure and dependencies.
|
||||
- Identify technical constraints, existing patterns, and potential risks
|
||||
- Ask targeted clarifying questions only when they will directly influence your implementation approach
|
||||
- Ensure complete converage- before presenting a plan, you should identify all related functions, classes, calls, and methods that are involved or affected by the proposed changes.
|
||||
- Ensure complete coverage - before presenting a plan, you should identify all related functions, classes, calls, and methods that are involved or affected by the proposed changes.
|
||||
|
||||
### Phase 2: Plan Presentation
|
||||
|
||||
@@ -66,7 +66,7 @@ Engage with the user to discuss the plan, answer questions, and incorporate feed
|
||||
|
||||
### Phase 4: Transition to Implementation
|
||||
|
||||
Once the plan is finalized and approved, you MUST direct the user to switch to ACT MODE. In Act Mode, you'll execute the plan step-by-step as outlined. If you not specifically ask the user to switch to ACT MODE, you will not be able to implemnent the planned changes.
|
||||
Once the plan is finalized and approved, you MUST direct the user to switch to ACT MODE. In Act Mode, you'll execute the plan step-by-step as outlined. If you not specifically ask the user to switch to ACT MODE, you will not be able to implement the planned changes.
|
||||
|
||||
## Act Mode Workflow
|
||||
|
||||
@@ -213,7 +213,7 @@ RULES
|
||||
|
||||
- The current working directory is `/test/project` - this is the directory where all the tools will be executed from.
|
||||
- When executing terminal commands, new terminals always open in the workspace directory. Use relative paths or chain commands with proper shell operators (e.g., `cd path && command` to change directory and run a command together).
|
||||
- Whean searching, prefer the search_files tool over using grep in the terminal. If you are directly instruted to use grep, ensure your search patterns are targetted and not too vague to prevent extremely large outputs.
|
||||
- When searching, prefer the search_files tool over using grep in the terminal. If you are directly instructed to use grep, ensure your search patterns are targeted and not too vague to prevent extremely large outputs.
|
||||
- When using replace_in_file, pay careful attention to the EDITING FILES section above. The most common errors are:
|
||||
- Not matching content exactly (every character, space, and newline must match)
|
||||
- Using incomplete lines in SEARCH blocks (always include complete lines from start to end)
|
||||
|
||||
+5
-5
@@ -24,14 +24,14 @@ Plan Mode is for deep analysis and strategic planning before implementation. You
|
||||
|
||||
### Phase 1: Silent Investigation
|
||||
|
||||
Perform comprehensive research to build complete understanding of the codebase. Work silently - execute targetted searcg commands and read files without explaining what you're doing. Only ask questions when truly necessary for planning. You must strongly incrporate key words and principles from the user's input into your targetted search patterns and strategy.
|
||||
Perform comprehensive research to build complete understanding of the codebase. Work silently - execute targeted search commands and read files without explaining what you're doing. Only ask questions when truly necessary for planning. You must strongly incorporate key words and principles from the user's input into your targeted search patterns and strategy.
|
||||
|
||||
**Research Activities:**
|
||||
- Use read_file, search_files, and list_code_definition_names extensively to understand architecture, patterns, and conventions
|
||||
- Execute targetted terminal commands to search and gather information about structure and dependencies.
|
||||
- Execute targeted terminal commands to search and gather information about structure and dependencies.
|
||||
- Identify technical constraints, existing patterns, and potential risks
|
||||
- Ask targeted clarifying questions only when they will directly influence your implementation approach
|
||||
- Ensure complete converage- before presenting a plan, you should identify all related functions, classes, calls, and methods that are involved or affected by the proposed changes.
|
||||
- Ensure complete coverage - before presenting a plan, you should identify all related functions, classes, calls, and methods that are involved or affected by the proposed changes.
|
||||
|
||||
### Phase 2: Plan Presentation
|
||||
|
||||
@@ -66,7 +66,7 @@ Engage with the user to discuss the plan, answer questions, and incorporate feed
|
||||
|
||||
### Phase 4: Transition to Implementation
|
||||
|
||||
Once the plan is finalized and approved, you MUST direct the user to switch to ACT MODE. In Act Mode, you'll execute the plan step-by-step as outlined. If you not specifically ask the user to switch to ACT MODE, you will not be able to implemnent the planned changes.
|
||||
Once the plan is finalized and approved, you MUST direct the user to switch to ACT MODE. In Act Mode, you'll execute the plan step-by-step as outlined. If you not specifically ask the user to switch to ACT MODE, you will not be able to implement the planned changes.
|
||||
|
||||
## Act Mode Workflow
|
||||
|
||||
@@ -193,7 +193,7 @@ RULES
|
||||
|
||||
- The current working directory is `/test/project` - this is the directory where all the tools will be executed from.
|
||||
- When executing terminal commands, new terminals always open in the workspace directory. Use relative paths or chain commands with proper shell operators (e.g., `cd path && command` to change directory and run a command together).
|
||||
- Whean searching, prefer the search_files tool over using grep in the terminal. If you are directly instruted to use grep, ensure your search patterns are targetted and not too vague to prevent extremely large outputs.
|
||||
- When searching, prefer the search_files tool over using grep in the terminal. If you are directly instructed to use grep, ensure your search patterns are targeted and not too vague to prevent extremely large outputs.
|
||||
- When using replace_in_file, pay careful attention to the EDITING FILES section above. The most common errors are:
|
||||
- Not matching content exactly (every character, space, and newline must match)
|
||||
- Using incomplete lines in SEARCH blocks (always include complete lines from start to end)
|
||||
|
||||
@@ -24,14 +24,14 @@ Plan Mode is for deep analysis and strategic planning before implementation. You
|
||||
|
||||
### Phase 1: Silent Investigation
|
||||
|
||||
Perform comprehensive research to build complete understanding of the codebase. Work silently - execute targetted searcg commands and read files without explaining what you're doing. Only ask questions when truly necessary for planning. You must strongly incrporate key words and principles from the user's input into your targetted search patterns and strategy.
|
||||
Perform comprehensive research to build complete understanding of the codebase. Work silently - execute targeted search commands and read files without explaining what you're doing. Only ask questions when truly necessary for planning. You must strongly incorporate key words and principles from the user's input into your targeted search patterns and strategy.
|
||||
|
||||
**Research Activities:**
|
||||
- Use read_file, search_files, and list_code_definition_names extensively to understand architecture, patterns, and conventions
|
||||
- Execute targetted terminal commands to search and gather information about structure and dependencies.
|
||||
- Execute targeted terminal commands to search and gather information about structure and dependencies.
|
||||
- Identify technical constraints, existing patterns, and potential risks
|
||||
- Ask targeted clarifying questions only when they will directly influence your implementation approach
|
||||
- Ensure complete converage- before presenting a plan, you should identify all related functions, classes, calls, and methods that are involved or affected by the proposed changes.
|
||||
- Ensure complete coverage - before presenting a plan, you should identify all related functions, classes, calls, and methods that are involved or affected by the proposed changes.
|
||||
|
||||
### Phase 2: Plan Presentation
|
||||
|
||||
@@ -66,7 +66,7 @@ Engage with the user to discuss the plan, answer questions, and incorporate feed
|
||||
|
||||
### Phase 4: Transition to Implementation
|
||||
|
||||
Once the plan is finalized and approved, you MUST direct the user to switch to ACT MODE. In Act Mode, you'll execute the plan step-by-step as outlined. If you not specifically ask the user to switch to ACT MODE, you will not be able to implemnent the planned changes.
|
||||
Once the plan is finalized and approved, you MUST direct the user to switch to ACT MODE. In Act Mode, you'll execute the plan step-by-step as outlined. If you not specifically ask the user to switch to ACT MODE, you will not be able to implement the planned changes.
|
||||
|
||||
## Act Mode Workflow
|
||||
|
||||
@@ -215,7 +215,7 @@ RULES
|
||||
|
||||
- The current working directory is `/test/project` - this is the directory where all the tools will be executed from.
|
||||
- When executing terminal commands, new terminals always open in the workspace directory. Use relative paths or chain commands with proper shell operators (e.g., `cd path && command` to change directory and run a command together).
|
||||
- Whean searching, prefer the search_files tool over using grep in the terminal. If you are directly instruted to use grep, ensure your search patterns are targetted and not too vague to prevent extremely large outputs.
|
||||
- When searching, prefer the search_files tool over using grep in the terminal. If you are directly instructed to use grep, ensure your search patterns are targeted and not too vague to prevent extremely large outputs.
|
||||
- When using replace_in_file, pay careful attention to the EDITING FILES section above. The most common errors are:
|
||||
- Not matching content exactly (every character, space, and newline must match)
|
||||
- Using incomplete lines in SEARCH blocks (always include complete lines from start to end)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* model families and context configurations using snapshot testing.
|
||||
*
|
||||
* Usage:
|
||||
* - Run tests normally: `npm run test:unit -- --update-snapshots`
|
||||
* - Run tests normally: `npm run test:unit`
|
||||
* Tests will fail if generated prompts don't match existing snapshots
|
||||
*
|
||||
* - Update snapshots: `npm run test:unit -- --update-snapshots`
|
||||
@@ -26,40 +26,31 @@ import { ModelFamily } from "@/shared/prompts"
|
||||
import { getSystemPrompt } from "../index"
|
||||
import type { SystemPromptContext } from "../types"
|
||||
|
||||
// Check if snapshots should be updated via process argument
|
||||
const UPDATE_SNAPSHOTS = process.argv.includes("--update-snapshots") || process.env.UPDATE_SNAPSHOTS === "true"
|
||||
// ============================================================================
|
||||
// Configuration
|
||||
// ============================================================================
|
||||
|
||||
// Helper to format snapshot mismatch error messages
|
||||
const formatSnapshotError = (snapshotName: string, differences: string): string => {
|
||||
return `
|
||||
const UPDATE_SNAPSHOTS = process.argv.includes("--update-snapshots") || process.env.UPDATE_SNAPSHOTS === "true"
|
||||
const SNAPSHOTS_DIR = path.join(__dirname, "__snapshots__")
|
||||
const TEST_TIMEOUT = 30000
|
||||
const MAX_DIFF_LINES = 10
|
||||
|
||||
// ============================================================================
|
||||
// Snapshot Helpers
|
||||
// ============================================================================
|
||||
|
||||
const formatSnapshotError = (snapshotName: string, details: string): string => `
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
❌ SNAPSHOT MISMATCH: ${snapshotName}
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
${differences}
|
||||
${details}
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
🔧 HOW TO FIX:
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
1. 📋 Review the differences above to understand what changed
|
||||
2. 🤔 Determine if the changes are intentional:
|
||||
- ✅ Expected changes (prompt improvements, new features)
|
||||
- ❌ Unexpected changes (bugs, regressions)
|
||||
|
||||
3. 🔄 If changes are correct, update snapshots:
|
||||
npm run test:unit -- --update-snapshots
|
||||
|
||||
4. 🐛 If changes are unintentional, investigate:
|
||||
- Check recent changes to prompt generation logic
|
||||
- Verify context/configuration hasn't changed unexpectedly
|
||||
- Look for dependency updates that might affect output
|
||||
|
||||
🔧 To update snapshots: npm run test:unit -- --update-snapshots
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
`
|
||||
}
|
||||
|
||||
// Helper to compare two strings and return differences
|
||||
const compareStrings = (expected: string, actual: string): string | null => {
|
||||
if (expected === actual) {
|
||||
return null
|
||||
@@ -67,64 +58,69 @@ const compareStrings = (expected: string, actual: string): string | null => {
|
||||
|
||||
const expectedLines = expected.split("\n")
|
||||
const actualLines = actual.split("\n")
|
||||
const maxLines = Math.max(expectedLines.length, actualLines.length)
|
||||
const differences: string[] = []
|
||||
const diffs: string[] = []
|
||||
|
||||
for (let i = 0; i < maxLines; i++) {
|
||||
const expectedLine = expectedLines[i] || ""
|
||||
const actualLine = actualLines[i] || ""
|
||||
|
||||
if (expectedLine !== actualLine) {
|
||||
if (differences.length < 10) {
|
||||
// Limit to first 10 differences for readability
|
||||
differences.push(`Line ${i + 1}:`)
|
||||
if (expectedLine) {
|
||||
differences.push(` - Expected: ${expectedLine.substring(0, 100)}${expectedLine.length > 100 ? "..." : ""}`)
|
||||
}
|
||||
if (actualLine) {
|
||||
differences.push(` + Actual: ${actualLine.substring(0, 100)}${actualLine.length > 100 ? "..." : ""}`)
|
||||
}
|
||||
for (let i = 0; i < Math.max(expectedLines.length, actualLines.length) && diffs.length < MAX_DIFF_LINES; i++) {
|
||||
const exp = expectedLines[i] || ""
|
||||
const act = actualLines[i] || ""
|
||||
if (exp !== act) {
|
||||
diffs.push(`Line ${i + 1}:`)
|
||||
if (exp) {
|
||||
diffs.push(` - Expected: ${exp.substring(0, 100)}${exp.length > 100 ? "..." : ""}`)
|
||||
}
|
||||
if (act) {
|
||||
diffs.push(` + Actual: ${act.substring(0, 100)}${act.length > 100 ? "..." : ""}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (differences.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const summary = [
|
||||
`Expected length: ${expected.length} characters`,
|
||||
`Actual length: ${actual.length} characters`,
|
||||
`Line count difference: ${expectedLines.length} vs ${actualLines.length}`,
|
||||
return [
|
||||
`Expected: ${expected.length} chars, ${expectedLines.length} lines`,
|
||||
`Actual: ${actual.length} chars, ${actualLines.length} lines`,
|
||||
"",
|
||||
"First differences:",
|
||||
...differences,
|
||||
]
|
||||
...diffs,
|
||||
diffs.length >= MAX_DIFF_LINES ? "... and more differences" : "",
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
if (differences.length >= 10) {
|
||||
summary.push("... and more differences")
|
||||
async function assertSnapshot(name: string, content: string): Promise<void> {
|
||||
const snapshotPath = path.join(SNAPSHOTS_DIR, name)
|
||||
|
||||
if (UPDATE_SNAPSHOTS) {
|
||||
await fs.writeFile(snapshotPath, content, "utf-8")
|
||||
console.log(`Updated snapshot: ${name} (${content.length} chars)`)
|
||||
return
|
||||
}
|
||||
|
||||
return summary.join("\n")
|
||||
try {
|
||||
const existing = await fs.readFile(snapshotPath, "utf-8")
|
||||
const diff = compareStrings(existing, content)
|
||||
if (diff) {
|
||||
throw new Error(formatSnapshotError(name, diff))
|
||||
}
|
||||
console.log(`✓ Snapshot matches: ${name}`)
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
throw new Error(formatSnapshotError(name, `Snapshot does not exist. Run with --update-snapshots to create it.`))
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Test Context Helpers
|
||||
// ============================================================================
|
||||
|
||||
export const mockProviderInfo = {
|
||||
providerId: "test",
|
||||
model: {
|
||||
id: "fast",
|
||||
info: {
|
||||
supportsPromptCache: false,
|
||||
},
|
||||
},
|
||||
model: { id: "fast", info: { supportsPromptCache: false } },
|
||||
mode: "act" as const,
|
||||
}
|
||||
|
||||
const makeMockProviderInfo = (modelId: string, providerId: string = "test") => ({
|
||||
const makeProviderInfo = (modelId: string, providerId: string = "test") => ({
|
||||
providerId: modelId.includes("ollama") ? "ollama" : providerId,
|
||||
model: {
|
||||
...mockProviderInfo.model,
|
||||
id: modelId,
|
||||
},
|
||||
model: { ...mockProviderInfo.model, id: modelId },
|
||||
mode: "act" as const,
|
||||
customPrompt: providerId.includes("lmstudio") || providerId.includes("ollama") ? "compact" : undefined,
|
||||
})
|
||||
|
||||
@@ -136,31 +132,18 @@ const baseContext: SystemPromptContext = {
|
||||
mcpHub: {
|
||||
getServers: () => [
|
||||
{
|
||||
uid: "1234567",
|
||||
name: "test-server",
|
||||
status: "connected",
|
||||
config: '{"command": "test"}',
|
||||
tools: [
|
||||
{
|
||||
name: "test_tool",
|
||||
description: "A test tool",
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
},
|
||||
],
|
||||
tools: [{ name: "test_tool", description: "A test tool", inputSchema: { type: "object", properties: {} } }],
|
||||
resources: [],
|
||||
resourceTemplates: [],
|
||||
},
|
||||
],
|
||||
} as unknown as McpHub,
|
||||
focusChainSettings: {
|
||||
enabled: true,
|
||||
remindClineInterval: 6,
|
||||
},
|
||||
browserSettings: {
|
||||
viewport: {
|
||||
width: 1280,
|
||||
height: 720,
|
||||
},
|
||||
},
|
||||
focusChainSettings: { enabled: true, remindClineInterval: 6 },
|
||||
browserSettings: { viewport: { width: 1280, height: 720 } },
|
||||
globalClineRulesFileInstructions: "Follow global rules",
|
||||
localClineRulesFileInstructions: "Follow local rules",
|
||||
preferredLanguageInstructions: "Prefer TypeScript",
|
||||
@@ -169,276 +152,151 @@ const baseContext: SystemPromptContext = {
|
||||
enableNativeToolCalls: false,
|
||||
}
|
||||
|
||||
describe("Prompt System Integration Tests", () => {
|
||||
beforeEach(() => {
|
||||
// Reset any necessary state before each test
|
||||
})
|
||||
const isNativeToolsFamily = (family: ModelFamily) =>
|
||||
[ModelFamily.NATIVE_NEXT_GEN, ModelFamily.NATIVE_GPT_5, ModelFamily.NATIVE_GPT_5_1, ModelFamily.GEMINI_3].includes(family)
|
||||
|
||||
// Show helpful information about snapshot testing mode
|
||||
before(() => {
|
||||
if (UPDATE_SNAPSHOTS) {
|
||||
console.log("🔄 SNAPSHOT UPDATE MODE: Will update all snapshot files with current output")
|
||||
type TestRunner = Mocha.Context & { skip(): void; timeout(ms: number): void }
|
||||
|
||||
async function runPromptTest(
|
||||
testCtx: TestRunner,
|
||||
context: SystemPromptContext,
|
||||
modelId: string,
|
||||
handler: (result: Awaited<ReturnType<typeof getSystemPrompt>>) => Promise<void>,
|
||||
): Promise<void> {
|
||||
testCtx.timeout(TEST_TIMEOUT)
|
||||
try {
|
||||
const result = await getSystemPrompt(context)
|
||||
await handler(result)
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes("No prompt variant found")) {
|
||||
console.log(`Skipping ${modelId} - no variant available (expected)`)
|
||||
testCtx.skip()
|
||||
} else {
|
||||
console.log("✅ SNAPSHOT TEST MODE: Will compare against existing snapshots")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Test Data
|
||||
// ============================================================================
|
||||
|
||||
const contextVariations: Array<{ name: string; override: Partial<SystemPromptContext> }> = [
|
||||
{ name: "basic", override: {} },
|
||||
{ name: "no-browser", override: { supportsBrowserUse: false } },
|
||||
{ name: "no-mcp", override: { mcpHub: { getServers: () => [] } as unknown as McpHub } },
|
||||
{ name: "no-focus-chain", override: { focusChainSettings: { enabled: false, remindClineInterval: 0 } } },
|
||||
]
|
||||
|
||||
const modelTestCases = [
|
||||
{ family: ModelFamily.GENERIC, modelId: "gpt-3", providerId: "openai" },
|
||||
{ family: ModelFamily.GLM, modelId: "glm-4.6", providerId: "zai" },
|
||||
{ family: ModelFamily.HERMES, modelId: "hermes-4", providerId: "test" },
|
||||
{ family: ModelFamily.DEVSTRAL, modelId: "devstral", providerId: "cline" },
|
||||
{ family: ModelFamily.NEXT_GEN, modelId: "claude-sonnet-4", providerId: "anthropic" },
|
||||
{ family: ModelFamily.XS, modelId: "qwen3_coder", providerId: "lmstudio" },
|
||||
{ family: ModelFamily.NATIVE_NEXT_GEN, modelId: "claude-4-5-sonnet", providerId: "cline" },
|
||||
{ family: ModelFamily.GPT_5, modelId: "gpt-5", providerId: "openai" },
|
||||
{ family: ModelFamily.NATIVE_GPT_5, modelId: "gpt-5-codex", providerId: "openai" },
|
||||
{ family: ModelFamily.NATIVE_GPT_5_1, modelId: "gpt-5-1", providerId: "openai" },
|
||||
{ family: ModelFamily.GEMINI_3, modelId: "gemini-3", providerId: "vertex" },
|
||||
]
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
describe("Prompt System Integration Tests", () => {
|
||||
before(async () => {
|
||||
console.log(UPDATE_SNAPSHOTS ? "🔄 SNAPSHOT UPDATE MODE" : "✅ SNAPSHOT TEST MODE")
|
||||
await fs.mkdir(SNAPSHOTS_DIR, { recursive: true }).catch(() => {})
|
||||
})
|
||||
const contextVariations = [
|
||||
{ name: "basic", baseContext: { ...baseContext } },
|
||||
{
|
||||
name: "no-browser",
|
||||
baseContext: { ...baseContext, supportsBrowserUse: false },
|
||||
},
|
||||
{
|
||||
name: "no-mcp",
|
||||
baseContext: { ...baseContext, mcpHub: { getServers: () => [] } },
|
||||
},
|
||||
{
|
||||
name: "no-focus-chain",
|
||||
baseContext: { ...baseContext, focusChainSettings: { enabled: false } },
|
||||
},
|
||||
]
|
||||
|
||||
// Table-driven test cases for different model families
|
||||
const modelTestCases = [
|
||||
{
|
||||
modelGroup: ModelFamily.GENERIC,
|
||||
modelIds: ["gpt-3"],
|
||||
providerId: "openai",
|
||||
contextVariations,
|
||||
},
|
||||
{
|
||||
modelGroup: ModelFamily.GLM,
|
||||
modelIds: ["glm-4.6"],
|
||||
providerId: "zai",
|
||||
contextVariations,
|
||||
},
|
||||
{
|
||||
modelGroup: ModelFamily.HERMES,
|
||||
modelIds: ["hermes-4"],
|
||||
providerId: "test",
|
||||
contextVariations,
|
||||
},
|
||||
{
|
||||
modelGroup: ModelFamily.DEVSTRAL,
|
||||
modelIds: ["devstral"],
|
||||
providerId: "cline",
|
||||
contextVariations,
|
||||
},
|
||||
{
|
||||
modelGroup: ModelFamily.NEXT_GEN,
|
||||
modelIds: ["claude-sonnet-4"],
|
||||
providerId: "anthropic",
|
||||
contextVariations,
|
||||
},
|
||||
{
|
||||
modelGroup: ModelFamily.XS,
|
||||
modelIds: ["qwen3_coder"],
|
||||
providerId: "lmstudio",
|
||||
contextVariations,
|
||||
},
|
||||
{
|
||||
modelGroup: ModelFamily.NATIVE_NEXT_GEN,
|
||||
modelIds: ["claude-4-5-sonnet"],
|
||||
providerId: "cline",
|
||||
contextVariations,
|
||||
},
|
||||
{
|
||||
modelGroup: ModelFamily.GPT_5,
|
||||
modelIds: ["gpt-5"],
|
||||
providerId: "openai",
|
||||
contextVariations,
|
||||
},
|
||||
{
|
||||
modelGroup: ModelFamily.NATIVE_GPT_5,
|
||||
modelIds: ["gpt-5-codex"],
|
||||
providerId: "openai",
|
||||
contextVariations,
|
||||
},
|
||||
{
|
||||
modelGroup: ModelFamily.NATIVE_GPT_5_1,
|
||||
modelIds: ["gpt-5-1"],
|
||||
providerId: "openai",
|
||||
contextVariations,
|
||||
},
|
||||
{
|
||||
modelGroup: ModelFamily.GEMINI_3,
|
||||
modelIds: ["gemini-3"],
|
||||
providerId: "vertex",
|
||||
contextVariations,
|
||||
},
|
||||
]
|
||||
|
||||
// Generate snapshots for all model/context combinations
|
||||
describe("Snapshot Testing", () => {
|
||||
const snapshotsDir = path.join(__dirname, "__snapshots__")
|
||||
for (const { family, modelId, providerId } of modelTestCases) {
|
||||
describe(`${family} Model Group`, () => {
|
||||
const enableNativeToolCalls = isNativeToolsFamily(family)
|
||||
|
||||
before(async () => {
|
||||
// Ensure snapshots directory exists
|
||||
try {
|
||||
await fs.mkdir(snapshotsDir, { recursive: true })
|
||||
} catch {
|
||||
// Directory might already exist
|
||||
}
|
||||
})
|
||||
|
||||
for (const { modelGroup, modelIds, providerId, contextVariations } of modelTestCases) {
|
||||
describe(`${modelGroup} Model Group`, () => {
|
||||
for (const modelId of modelIds) {
|
||||
for (const { name: contextName, baseContext } of contextVariations) {
|
||||
const context = {
|
||||
...baseContext,
|
||||
providerInfo: makeMockProviderInfo(modelId, providerId),
|
||||
isTesting: true,
|
||||
enableNativeToolCalls:
|
||||
modelGroup === ModelFamily.NATIVE_NEXT_GEN ||
|
||||
modelGroup === ModelFamily.NATIVE_GPT_5 ||
|
||||
modelGroup === ModelFamily.NATIVE_GPT_5_1 ||
|
||||
modelGroup === ModelFamily.GEMINI_3,
|
||||
}
|
||||
it(`should generate consistent prompt for ${providerId}/${modelId} with ${contextName} context`, async function () {
|
||||
this.timeout(30000) // Allow more time for prompt generation
|
||||
|
||||
try {
|
||||
const { systemPrompt } = await getSystemPrompt(context as SystemPromptContext)
|
||||
|
||||
// Basic structure assertions
|
||||
expect(systemPrompt).to.be.a("string")
|
||||
expect(systemPrompt.length).to.be.greaterThan(100)
|
||||
expect(systemPrompt).to.not.include("{{TOOL_USE_SECTION}}") // Tools placeholder should be removed
|
||||
|
||||
// Snapshot testing logic
|
||||
const snapshotName = `${providerId}_${modelId.replace(/[^a-zA-Z0-9]/g, "_")}-${contextName}.snap`
|
||||
const snapshotPath = path.join(snapshotsDir, snapshotName)
|
||||
|
||||
if (UPDATE_SNAPSHOTS) {
|
||||
// Update mode: write new snapshot
|
||||
await fs.writeFile(snapshotPath, systemPrompt, "utf-8")
|
||||
console.log(`Updated snapshot: ${snapshotName} (${systemPrompt.length} chars)`)
|
||||
} else {
|
||||
// Test mode: compare with existing snapshot
|
||||
try {
|
||||
const existingSnapshot = await fs.readFile(snapshotPath, "utf-8")
|
||||
const differences = compareStrings(existingSnapshot, systemPrompt)
|
||||
|
||||
if (differences) {
|
||||
throw new Error(formatSnapshotError(snapshotName, differences))
|
||||
}
|
||||
|
||||
console.log(`✓ Snapshot matches: ${snapshotName}`)
|
||||
} catch (error) {
|
||||
if (error instanceof Error && (error as any).code === "ENOENT") {
|
||||
// Snapshot doesn't exist
|
||||
throw new Error(
|
||||
formatSnapshotError(
|
||||
snapshotName,
|
||||
`Snapshot file does not exist: ${snapshotPath}\n` +
|
||||
`This is a new test case. Run with --update-snapshots to create the initial snapshot.`,
|
||||
),
|
||||
)
|
||||
}
|
||||
// Re-throw comparison errors
|
||||
throw error
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// For missing variants, we expect errors - that's okay
|
||||
if (error instanceof Error && error.message.includes("No prompt variant found")) {
|
||||
console.log(`Skipping ${modelId} - no variant available (expected)`)
|
||||
this.skip()
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
})
|
||||
it(`should generate consistent native tools object when enabled`, async function () {
|
||||
const context: SystemPromptContext = {
|
||||
...baseContext,
|
||||
providerInfo: makeProviderInfo(modelId, providerId),
|
||||
enableNativeToolCalls,
|
||||
}
|
||||
|
||||
await runPromptTest(this, context, modelId, async ({ tools }) => {
|
||||
if (!enableNativeToolCalls) {
|
||||
expect(tools).to.be.undefined
|
||||
return
|
||||
}
|
||||
|
||||
expect(tools).to.be.an("array").that.is.not.empty
|
||||
const snapshotName = `${providerId}_${family.replace(/[^a-zA-Z0-9]/g, "_")}.tools.snap`
|
||||
await assertSnapshot(snapshotName, JSON.stringify(tools, null, 2))
|
||||
})
|
||||
})
|
||||
|
||||
for (const { name: contextName, override } of contextVariations) {
|
||||
it(`should generate consistent prompt for ${providerId}/${modelId} with ${contextName} context`, async function () {
|
||||
const context: SystemPromptContext = {
|
||||
...baseContext,
|
||||
...override,
|
||||
providerInfo: makeProviderInfo(modelId, providerId),
|
||||
enableNativeToolCalls,
|
||||
}
|
||||
|
||||
await runPromptTest(this, context, modelId, async ({ systemPrompt, tools }) => {
|
||||
if (enableNativeToolCalls) {
|
||||
expect(tools).to.be.an("array").that.is.not.empty
|
||||
} else {
|
||||
expect(tools).to.be.undefined
|
||||
}
|
||||
|
||||
expect(systemPrompt).to.be.a("string").with.length.greaterThan(100)
|
||||
expect(systemPrompt).to.not.include("{{TOOL_USE_SECTION}}")
|
||||
|
||||
const snapshotName = `${providerId}_${modelId.replace(/[^a-zA-Z0-9]/g, "_")}-${contextName}.snap`
|
||||
await assertSnapshot(snapshotName, systemPrompt)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe("Context-Specific Features", () => {
|
||||
it("should include browser-specific content when browser is enabled", async function () {
|
||||
this.timeout(30000)
|
||||
const featureTests = [
|
||||
{ name: "browser-specific content when browser is enabled", context: { supportsBrowserUse: true }, check: "browser" },
|
||||
{ name: "MCP content when MCP servers are present", context: {}, check: "MCP" },
|
||||
{ name: "TODO content when focus chain is enabled", context: {}, check: "TODO" },
|
||||
{ name: "user instructions when provided", context: {}, check: "USER'S CUSTOM INSTRUCTIONS" },
|
||||
]
|
||||
|
||||
const contextWithBrowser = { ...baseContext, supportsBrowserUse: true }
|
||||
|
||||
try {
|
||||
const { systemPrompt } = await getSystemPrompt(contextWithBrowser)
|
||||
expect(systemPrompt.toLowerCase()).to.include("browser")
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes("No prompt variant found")) {
|
||||
this.skip()
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it("should include MCP content when MCP servers are present", async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
try {
|
||||
const { systemPrompt } = await getSystemPrompt(baseContext)
|
||||
expect(systemPrompt).to.include("MCP")
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes("No prompt variant found")) {
|
||||
this.skip()
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it("should include TODO content when focus chain is enabled", async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
try {
|
||||
const { systemPrompt } = await getSystemPrompt(baseContext)
|
||||
expect(systemPrompt).to.include("TODO")
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes("No prompt variant found")) {
|
||||
this.skip()
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it("should include user instructions when provided", async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
try {
|
||||
const { systemPrompt } = await getSystemPrompt(baseContext)
|
||||
expect(systemPrompt).to.include("USER'S CUSTOM INSTRUCTIONS")
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes("No prompt variant found")) {
|
||||
this.skip()
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
})
|
||||
for (const { name, context, check } of featureTests) {
|
||||
it(`should include ${name}`, async function () {
|
||||
await runPromptTest(this, { ...baseContext, ...context }, "default", async ({ systemPrompt }) => {
|
||||
expect(systemPrompt.toLowerCase()).to.include(check.toLowerCase())
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe("Error Handling", () => {
|
||||
it("should handle completely invalid context gracefully", async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
const invalidContext = {} as SystemPromptContext
|
||||
const { systemPrompt } = await getSystemPrompt(invalidContext)
|
||||
this.timeout(TEST_TIMEOUT)
|
||||
const { systemPrompt } = await getSystemPrompt({} as SystemPromptContext)
|
||||
expect(systemPrompt).to.be.a("string")
|
||||
})
|
||||
|
||||
it("should handle undefined context properties", async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
this.timeout(TEST_TIMEOUT)
|
||||
const contextWithNulls: SystemPromptContext = {
|
||||
cwd: undefined,
|
||||
ide: "",
|
||||
supportsBrowserUse: undefined,
|
||||
mcpHub: undefined,
|
||||
focusChainSettings: undefined,
|
||||
providerInfo: baseContext.providerInfo,
|
||||
providerInfo: mockProviderInfo,
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -446,7 +304,6 @@ describe("Prompt System Integration Tests", () => {
|
||||
expect(systemPrompt).to.be.a("string")
|
||||
expect(systemPrompt).to.include("{{TOOL_USE_SECTION}}")
|
||||
} catch (error) {
|
||||
// Error is acceptable for invalid context
|
||||
expect(error).to.be.instanceOf(Error)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ClineDefaultTool } from "@/shared/tools"
|
||||
import { ClineToolSet } from "../registry/ClineToolSet"
|
||||
import { type ClineToolSpec } from "../spec"
|
||||
import { type ClineToolSpec, resolveInstruction } from "../spec"
|
||||
import { STANDARD_PLACEHOLDERS } from "../templates/placeholders"
|
||||
import { TemplateEngine } from "../templates/TemplateEngine"
|
||||
import type { ComponentRegistry, PromptVariant, SystemPromptContext } from "../types"
|
||||
@@ -207,21 +207,22 @@ export class PromptBuilder {
|
||||
const sections = [
|
||||
title,
|
||||
description.join("\n"),
|
||||
PromptBuilder.buildParametersSection(filteredParams),
|
||||
PromptBuilder.buildParametersSection(filteredParams, context),
|
||||
PromptBuilder.buildUsageSection(config.id, filteredParams),
|
||||
]
|
||||
|
||||
return sections.filter(Boolean).join("\n")
|
||||
}
|
||||
|
||||
private static buildParametersSection(params: any[]): string {
|
||||
private static buildParametersSection(params: any[], context: SystemPromptContext): string {
|
||||
if (!params.length) {
|
||||
return "Parameters: None"
|
||||
}
|
||||
|
||||
const paramList = params.map((p) => {
|
||||
const requiredText = p.required ? "required" : "optional"
|
||||
return `- ${p.name}: (${requiredText}) ${p.instruction}`
|
||||
const instruction = resolveInstruction(p.instruction, context)
|
||||
return `- ${p.name}: (${requiredText}) ${instruction}`
|
||||
})
|
||||
|
||||
return ["Parameters:", ...paramList].join("\n")
|
||||
|
||||
@@ -19,7 +19,7 @@ export interface ClineToolSpec {
|
||||
interface ClineToolSpecParameter {
|
||||
name: string
|
||||
required: boolean
|
||||
instruction: string
|
||||
instruction: string | ((context: SystemPromptContext) => string)
|
||||
usage?: string
|
||||
dependencies?: ClineDefaultTool[]
|
||||
description?: string
|
||||
@@ -77,7 +77,7 @@ export function toolSpecFunctionDefinition(tool: ClineToolSpec, context: SystemP
|
||||
// Build parameter schema
|
||||
const paramSchema: any = {
|
||||
type: paramType,
|
||||
description: replacer(param.instruction, context),
|
||||
description: replacer(resolveInstruction(param.instruction, context), context),
|
||||
}
|
||||
|
||||
// Add items for array types
|
||||
@@ -170,7 +170,7 @@ export function toolSpecInputSchema(tool: ClineToolSpec, context: SystemPromptCo
|
||||
// Build parameter schema
|
||||
const paramSchema: any = {
|
||||
type: paramType,
|
||||
description: replacer(param.instruction, context),
|
||||
description: replacer(resolveInstruction(param.instruction, context), context),
|
||||
}
|
||||
|
||||
// Add items for array types
|
||||
@@ -278,7 +278,7 @@ export function toolSpecFunctionDeclarations(tool: ClineToolSpec, context: Syste
|
||||
}
|
||||
paramSchema.properties[key] = {
|
||||
type: GOOGLE_TOOL_PARAM_MAP[prop.type || "string"] || GoogleToolParamType.OBJECT,
|
||||
description: replacer(param.instruction, context),
|
||||
description: replacer(resolveInstruction(param.instruction, context), context),
|
||||
}
|
||||
|
||||
// Handle enum values
|
||||
@@ -399,3 +399,13 @@ function replacer(description: string, context: SystemPromptContext): string {
|
||||
|
||||
return description.replace("{{BROWSER_VIEWPORT_WIDTH}}", String(width)).replace("{{BROWSER_VIEWPORT_HEIGHT}}", String(height))
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves an instruction that may be a string or a function.
|
||||
*/
|
||||
export function resolveInstruction(
|
||||
instruction: string | ((context: SystemPromptContext) => string),
|
||||
context: SystemPromptContext,
|
||||
): string {
|
||||
return typeof instruction === "function" ? instruction(context) : instruction
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ const GEMINI_3_RULES_TEMPLATE = (_context: SystemPromptContext) => `RULES
|
||||
|
||||
- The current working directory is \`{{CWD}}\` - this is the directory where all the tools will be executed from.
|
||||
- When executing terminal commands, new terminals always open in the workspace directory. Use relative paths or chain commands with proper shell operators (e.g., \`cd path && command\` to change directory and run a command together).
|
||||
- Whean searching, prefer the search_files tool over using grep in the terminal. If you are directly instruted to use grep, ensure your search patterns are targetted and not too vague to prevent extremely large outputs.
|
||||
- When searching, prefer the search_files tool over using grep in the terminal. If you are directly instructed to use grep, ensure your search patterns are targeted and not too vague to prevent extremely large outputs.
|
||||
- When using replace_in_file, pay careful attention to the EDITING FILES section above. The most common errors are:
|
||||
- Not matching content exactly (every character, space, and newline must match)
|
||||
- Using incomplete lines in SEARCH blocks (always include complete lines from start to end)
|
||||
@@ -157,13 +157,13 @@ Plan Mode is for deep analysis and strategic planning before implementation. You
|
||||
|
||||
### Phase 1: Silent Investigation
|
||||
|
||||
Perform comprehensive research to build complete understanding of the codebase. Work silently - execute targetted searcg commands and read files without explaining what you're doing. Only ask questions when truly necessary for planning. You must strongly incrporate key words and principles from the user's input into your targetted search patterns and strategy.
|
||||
Perform comprehensive research to build complete understanding of the codebase. Work silently - execute targeted search commands and read files without explaining what you're doing. Only ask questions when truly necessary for planning. You must strongly incorporate key words and principles from the user's input into your targeted search patterns and strategy.
|
||||
|
||||
**Research Activities:**
|
||||
- Use read_file, search_files, and list_code_definition_names extensively to understand architecture, patterns, and conventions
|
||||
- Execute targetted terminal commands to search and gather information about structure and dependencies.
|
||||
- Execute targeted terminal commands to search and gather information about structure and dependencies.
|
||||
- Identify technical constraints, existing patterns, and potential risks${context.yoloModeToggled !== true ? "\n- Ask targeted clarifying questions only when they will directly influence your implementation approach" : ""}
|
||||
- Ensure complete converage- before presenting a plan, you should identify all related functions, classes, calls, and methods that are involved or affected by the proposed changes.
|
||||
- Ensure complete coverage - before presenting a plan, you should identify all related functions, classes, calls, and methods that are involved or affected by the proposed changes.
|
||||
|
||||
### Phase 2: Plan Presentation
|
||||
|
||||
@@ -198,7 +198,7 @@ Engage with the user to discuss the plan, answer questions, and incorporate feed
|
||||
|
||||
### Phase 4: Transition to Implementation
|
||||
|
||||
Once the plan is finalized and approved, you MUST direct the user to switch to ACT MODE. In Act Mode, you'll execute the plan step-by-step as outlined. If you not specifically ask the user to switch to ACT MODE, you will not be able to implemnent the planned changes.
|
||||
Once the plan is finalized and approved, you MUST direct the user to switch to ACT MODE. In Act Mode, you'll execute the plan step-by-step as outlined. If you not specifically ask the user to switch to ACT MODE, you will not be able to implement the planned changes.
|
||||
|
||||
## Act Mode Workflow
|
||||
|
||||
|
||||
@@ -10,6 +10,10 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
|
||||
|
||||
====
|
||||
|
||||
{{${SystemPromptSection.CLI_SUBAGENTS}}}
|
||||
|
||||
====
|
||||
|
||||
{{${SystemPromptSection.ACT_VS_PLAN}}}
|
||||
|
||||
====
|
||||
|
||||
@@ -53,9 +53,7 @@ export const config = createVariant(ModelFamily.NATIVE_GPT_5_1)
|
||||
ClineDefaultTool.BASH,
|
||||
ClineDefaultTool.FILE_READ,
|
||||
// Should disable FILE_NEW and FILE_EDIT when enabled
|
||||
// ClineDefaultTool.APPLY_PATCH,
|
||||
ClineDefaultTool.FILE_NEW,
|
||||
ClineDefaultTool.FILE_EDIT,
|
||||
ClineDefaultTool.APPLY_PATCH,
|
||||
ClineDefaultTool.SEARCH,
|
||||
ClineDefaultTool.LIST_FILES,
|
||||
ClineDefaultTool.LIST_CODE_DEF,
|
||||
|
||||
@@ -156,12 +156,7 @@ export class VariantValidator {
|
||||
|
||||
private validateBestPractices(variant: PromptVariant, warnings: string[]): void {
|
||||
// Check for recommended components
|
||||
const recommendedComponents = [
|
||||
SystemPromptSection.AGENT_ROLE,
|
||||
SystemPromptSection.TOOL_USE,
|
||||
SystemPromptSection.RULES,
|
||||
SystemPromptSection.SYSTEM_INFO,
|
||||
]
|
||||
const recommendedComponents = [SystemPromptSection.AGENT_ROLE, SystemPromptSection.RULES, SystemPromptSection.SYSTEM_INFO]
|
||||
|
||||
const missingRecommended = recommendedComponents.filter((c) => !variant.componentOrder.includes(c))
|
||||
if (missingRecommended.length > 0) {
|
||||
|
||||
@@ -1143,8 +1143,14 @@ export class StateManager {
|
||||
awsProfile: this.taskStateCache["awsProfile"] || this.globalStateCache["awsProfile"],
|
||||
awsUseProfile: this.taskStateCache["awsUseProfile"] || this.globalStateCache["awsUseProfile"],
|
||||
awsAuthentication: this.taskStateCache["awsAuthentication"] || this.globalStateCache["awsAuthentication"],
|
||||
vertexProjectId: this.taskStateCache["vertexProjectId"] || this.globalStateCache["vertexProjectId"],
|
||||
vertexRegion: this.taskStateCache["vertexRegion"] || this.globalStateCache["vertexRegion"],
|
||||
vertexProjectId:
|
||||
this.remoteConfigCache["vertexProjectId"] ||
|
||||
this.taskStateCache["vertexProjectId"] ||
|
||||
this.globalStateCache["vertexProjectId"],
|
||||
vertexRegion:
|
||||
this.remoteConfigCache["vertexRegion"] ||
|
||||
this.taskStateCache["vertexRegion"] ||
|
||||
this.globalStateCache["vertexRegion"],
|
||||
requestyBaseUrl: this.taskStateCache["requestyBaseUrl"] || this.globalStateCache["requestyBaseUrl"],
|
||||
openAiBaseUrl:
|
||||
this.remoteConfigCache["openAiBaseUrl"] ||
|
||||
@@ -1168,7 +1174,10 @@ export class StateManager {
|
||||
this.globalStateCache["azureApiVersion"],
|
||||
openRouterProviderSorting:
|
||||
this.taskStateCache["openRouterProviderSorting"] || this.globalStateCache["openRouterProviderSorting"],
|
||||
liteLlmBaseUrl: this.taskStateCache["liteLlmBaseUrl"] || this.globalStateCache["liteLlmBaseUrl"],
|
||||
liteLlmBaseUrl:
|
||||
this.remoteConfigCache["liteLlmBaseUrl"] ||
|
||||
this.taskStateCache["liteLlmBaseUrl"] ||
|
||||
this.globalStateCache["liteLlmBaseUrl"],
|
||||
liteLlmUsePromptCache: this.taskStateCache["liteLlmUsePromptCache"] || this.globalStateCache["liteLlmUsePromptCache"],
|
||||
qwenApiLine: this.taskStateCache["qwenApiLine"] || this.globalStateCache["qwenApiLine"],
|
||||
moonshotApiLine: this.taskStateCache["moonshotApiLine"] || this.globalStateCache["moonshotApiLine"],
|
||||
|
||||
@@ -143,10 +143,10 @@ async function fetchRemoteConfigForOrganization(organizationId: string): Promise
|
||||
async function fetchApiKeysForOrganization(organizationId: string): Promise<APIKeySettings> {
|
||||
try {
|
||||
// Fetch API keys string using helper
|
||||
const apiKeysString = await makeAuthenticatedRequest<string>(CLINE_API_ENDPOINT.API_KEYS, organizationId)
|
||||
const response = await makeAuthenticatedRequest<{ providerApiKeys: string }>(CLINE_API_ENDPOINT.API_KEYS, organizationId)
|
||||
|
||||
// Parse and return API keys
|
||||
return parseApiKeys(apiKeysString)
|
||||
return parseApiKeys(response?.providerApiKeys)
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch API keys for organization ${organizationId}:`, error)
|
||||
return {}
|
||||
@@ -230,7 +230,7 @@ async function ensureUserInOrgWithRemoteConfig(controller: Controller): Promise<
|
||||
|
||||
// Cache and apply the remote config
|
||||
await writeRemoteConfigToCache(organizationId, config)
|
||||
applyRemoteConfig(config)
|
||||
await applyRemoteConfig(config)
|
||||
controller.postStateToWebview()
|
||||
|
||||
return config
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { synchronizeRemoteRuleToggles } from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
import { RemoteConfig } from "@shared/remote-config/schema"
|
||||
import { RemoteConfigFields } from "@shared/storage/state-keys"
|
||||
import { getTelemetryService } from "@/services/telemetry"
|
||||
import { OpenTelemetryClientProvider } from "@/services/telemetry/providers/opentelemetry/OpenTelemetryClientProvider"
|
||||
import { OpenTelemetryTelemetryProvider } from "@/services/telemetry/providers/opentelemetry/OpenTelemetryTelemetryProvider"
|
||||
import { type TelemetryService } from "@/services/telemetry/TelemetryService"
|
||||
import { OpenTelemetryClientValidConfig, remoteConfigToOtelConfig } from "@/shared/services/config/otel-config"
|
||||
import { StateManager } from "../StateManager"
|
||||
|
||||
/**
|
||||
@@ -21,6 +26,12 @@ export function transformRemoteConfigToStateShape(remoteConfig: RemoteConfig): P
|
||||
if (remoteConfig.allowedMCPServers !== undefined) {
|
||||
transformed.allowedMCPServers = remoteConfig.allowedMCPServers
|
||||
}
|
||||
if (remoteConfig.blockPersonalRemoteMCPServers !== undefined) {
|
||||
transformed.blockPersonalRemoteMCPServers = remoteConfig.blockPersonalRemoteMCPServers
|
||||
}
|
||||
if (remoteConfig.remoteMCPServers !== undefined) {
|
||||
transformed.remoteMCPServers = remoteConfig.remoteMCPServers
|
||||
}
|
||||
if (remoteConfig.yoloModeAllowed !== undefined) {
|
||||
// only set the yoloModeToggled field if yolo mode is not allowed. Otherwise, we let the user toggle it.
|
||||
if (remoteConfig.yoloModeAllowed === false) {
|
||||
@@ -71,6 +82,9 @@ export function transformRemoteConfigToStateShape(remoteConfig: RemoteConfig): P
|
||||
if (remoteConfig.openTelemetryLogMaxQueueSize !== undefined) {
|
||||
transformed.openTelemetryLogMaxQueueSize = remoteConfig.openTelemetryLogMaxQueueSize
|
||||
}
|
||||
if (remoteConfig.openTelemetryOtlpHeaders !== undefined) {
|
||||
transformed.openTelemetryOtlpHeaders = remoteConfig.openTelemetryOtlpHeaders
|
||||
}
|
||||
|
||||
// Map provider settings
|
||||
|
||||
@@ -168,16 +182,39 @@ export function transformRemoteConfigToStateShape(remoteConfig: RemoteConfig): P
|
||||
return transformed
|
||||
}
|
||||
|
||||
const REMOTE_CONFIG_OTEL_PROVIDER_ID = "OpenTelemetryRemoteConfiguredProvider"
|
||||
async function applyRemoteOTELConfig(transformed: Partial<RemoteConfigFields>, telemetryService: TelemetryService) {
|
||||
try {
|
||||
const otelConfig = remoteConfigToOtelConfig(transformed)
|
||||
if (otelConfig.enabled) {
|
||||
const client = new OpenTelemetryClientProvider(otelConfig as OpenTelemetryClientValidConfig)
|
||||
|
||||
if (client.meterProvider || client.loggerProvider) {
|
||||
telemetryService.addProvider(
|
||||
await new OpenTelemetryTelemetryProvider(client.meterProvider, client.loggerProvider, {
|
||||
name: REMOTE_CONFIG_OTEL_PROVIDER_ID,
|
||||
bypassUserSettings: true,
|
||||
}).initialize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[REMOTE CONFIG DEBUG] Failed to apply remote OTEL config", err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies remote config to the StateManager's remote config cache
|
||||
* @param remoteConfig The remote configuration object to apply
|
||||
*/
|
||||
export function applyRemoteConfig(remoteConfig?: RemoteConfig): void {
|
||||
export async function applyRemoteConfig(remoteConfig?: RemoteConfig): Promise<void> {
|
||||
const stateManager = StateManager.get()
|
||||
const telemetryService = await getTelemetryService()
|
||||
|
||||
// If no remote config provided, clear the cache and relevant state
|
||||
if (!remoteConfig) {
|
||||
stateManager.clearRemoteConfig()
|
||||
telemetryService.removeProvider(REMOTE_CONFIG_OTEL_PROVIDER_ID)
|
||||
// the remote config cline rules toggle state is stored in global state
|
||||
stateManager.setGlobalState("remoteRulesToggles", {})
|
||||
stateManager.setGlobalState("remoteWorkflowToggles", {})
|
||||
@@ -185,6 +222,8 @@ export function applyRemoteConfig(remoteConfig?: RemoteConfig): void {
|
||||
}
|
||||
|
||||
// Transform remote config to state shape
|
||||
// These are then set to the remote config cache in the StateManager
|
||||
// We need to ensure the cache is checked for new fields
|
||||
const transformed = transformRemoteConfigToStateShape(remoteConfig)
|
||||
|
||||
// Synchronize toggle state
|
||||
@@ -199,9 +238,12 @@ export function applyRemoteConfig(remoteConfig?: RemoteConfig): void {
|
||||
|
||||
// Clear existing remote config cache
|
||||
stateManager.clearRemoteConfig()
|
||||
telemetryService.removeProvider(REMOTE_CONFIG_OTEL_PROVIDER_ID)
|
||||
|
||||
// Populate remote config cache with transformed values
|
||||
for (const [key, value] of Object.entries(transformed)) {
|
||||
stateManager.setRemoteConfigField(key as keyof RemoteConfigFields, value)
|
||||
}
|
||||
|
||||
await applyRemoteOTELConfig(transformed, telemetryService)
|
||||
}
|
||||
|
||||
@@ -322,6 +322,8 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
|
||||
const openTelemetryLogMaxQueueSize =
|
||||
context.globalState.get<GlobalStateAndSettings["openTelemetryLogMaxQueueSize"]>("openTelemetryLogMaxQueueSize")
|
||||
const subagentsEnabled = context.globalState.get<GlobalStateAndSettings["subagentsEnabled"]>("subagentsEnabled")
|
||||
const backgroundEditEnabled =
|
||||
context.globalState.get<GlobalStateAndSettings["backgroundEditEnabled"]>("backgroundEditEnabled")
|
||||
|
||||
// Get mode-related configurations
|
||||
const mode = context.globalState.get<GlobalStateAndSettings["mode"]>("mode")
|
||||
@@ -682,6 +684,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
|
||||
qwenCodeOauthPath,
|
||||
customPrompt,
|
||||
autoCondenseThreshold: autoCondenseThreshold || 0.75, // default to 0.75 if not set
|
||||
backgroundEditEnabled: backgroundEditEnabled ?? false,
|
||||
// Hooks require explicit user opt-in and are only supported on macOS/Linux
|
||||
hooksEnabled: getHooksEnabledSafe(hooksEnabled),
|
||||
subagentsEnabled: subagentsEnabled ?? false,
|
||||
|
||||
+28
-22
@@ -69,12 +69,16 @@ import Mutex from "p-mutex"
|
||||
import pWaitFor from "p-wait-for"
|
||||
import * as path from "path"
|
||||
import { ulid } from "ulid"
|
||||
import * as vscode from "vscode"
|
||||
import type { SystemPromptContext } from "@/core/prompts/system-prompt"
|
||||
import { getSystemPrompt } from "@/core/prompts/system-prompt"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { CommandExecutorCallbacks, StandaloneTerminalManager } from "@/integrations/terminal"
|
||||
import { CommandExecutor, FullCommandExecutorConfig } from "@/integrations/terminal/CommandExecutor"
|
||||
import { FileEditProvider } from "@/integrations/editor/FileEditProvider"
|
||||
import {
|
||||
CommandExecutor,
|
||||
CommandExecutorCallbacks,
|
||||
FullCommandExecutorConfig,
|
||||
StandaloneTerminalManager,
|
||||
} from "@/integrations/terminal"
|
||||
import { ClineError, ClineErrorType, ErrorService } from "@/services/error"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import {
|
||||
@@ -272,7 +276,6 @@ export class Task {
|
||||
this.cancelTask = cancelTask
|
||||
this.clineIgnoreController = new ClineIgnoreController(cwd)
|
||||
this.taskLockAcquired = taskLockAcquired
|
||||
|
||||
// Determine terminal execution mode and create appropriate terminal manager
|
||||
this.terminalExecutionMode = vscodeTerminalExecutionMode || "vscodeTerminal"
|
||||
|
||||
@@ -296,12 +299,16 @@ export class Task {
|
||||
this.urlContentFetcher = new UrlContentFetcher(controller.context)
|
||||
this.browserSession = new BrowserSession(stateManager)
|
||||
this.contextManager = new ContextManager()
|
||||
this.diffViewProvider = HostProvider.get().createDiffViewProvider()
|
||||
this.streamHandler = new StreamResponseHandler()
|
||||
this.cwd = cwd
|
||||
this.stateManager = stateManager
|
||||
this.workspaceManager = workspaceManager
|
||||
|
||||
// DiffViewProvider opens Diff Editor during edits while FileEditProvider performs
|
||||
// edits in the background without stealing user's editor's focus.
|
||||
const backgroundEditEnabled = this.stateManager.getGlobalSettingsKey("backgroundEditEnabled")
|
||||
this.diffViewProvider = backgroundEditEnabled ? new FileEditProvider() : HostProvider.get().createDiffViewProvider()
|
||||
|
||||
// Set up MCP notification callback for real-time notifications
|
||||
this.mcpHub.setNotificationCallback(async (serverName: string, _level: string, message: string) => {
|
||||
// Display notification in chat immediately
|
||||
@@ -1600,21 +1607,6 @@ export class Task {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates the disableBrowserTool setting from VSCode configuration to browserSettings
|
||||
*/
|
||||
private async migrateDisableBrowserToolSetting(): Promise<void> {
|
||||
const config = vscode.workspace.getConfiguration("cline")
|
||||
const disableBrowserTool = config.get<boolean>("disableBrowserTool")
|
||||
|
||||
if (disableBrowserTool !== undefined) {
|
||||
const browserSettings = this.stateManager.getGlobalSettingsKey("browserSettings")
|
||||
browserSettings.disableToolUse = disableBrowserTool
|
||||
// Remove from VSCode configuration
|
||||
await config.update("disableBrowserTool", undefined, true)
|
||||
}
|
||||
}
|
||||
|
||||
private getCurrentProviderInfo(): ApiProviderInfo {
|
||||
const model = this.api.getModel()
|
||||
const apiConfig = this.stateManager.getApiConfiguration()
|
||||
@@ -1705,7 +1697,6 @@ export class Task {
|
||||
|
||||
const providerInfo = this.getCurrentProviderInfo()
|
||||
const ide = (await HostProvider.env.getHostVersion({})).platform || "Unknown"
|
||||
await this.migrateDisableBrowserToolSetting()
|
||||
const browserSettings = this.stateManager.getGlobalSettingsKey("browserSettings")
|
||||
const disableBrowserTool = browserSettings.disableToolUse ?? false
|
||||
// cline browser tool uses image recognition for navigation (requires model image support).
|
||||
@@ -2142,6 +2133,16 @@ export class Task {
|
||||
}
|
||||
|
||||
if (this.taskState.consecutiveMistakeCount >= this.stateManager.getGlobalSettingsKey("maxConsecutiveMistakes")) {
|
||||
// In yolo mode, don't wait for user input - fail the task
|
||||
if (this.stateManager.getGlobalSettingsKey("yoloModeToggled")) {
|
||||
const errorMessage =
|
||||
`[YOLO MODE] Task failed: Too many consecutive mistakes (${this.taskState.consecutiveMistakeCount}). ` +
|
||||
`The model may not be capable enough for this task. Consider using a more capable model.`
|
||||
await this.say("error", errorMessage)
|
||||
// End the task loop with failure
|
||||
return true // didEndLoop = true, signals task completion/failure
|
||||
}
|
||||
|
||||
const autoApprovalSettings = this.stateManager.getGlobalSettingsKey("autoApprovalSettings")
|
||||
if (autoApprovalSettings.enableNotifications) {
|
||||
showSystemNotification({
|
||||
@@ -2215,7 +2216,12 @@ export class Task {
|
||||
|
||||
// Now, if it's the first request AND checkpoints are enabled AND tracker was successfully initialized,
|
||||
// then say "checkpoint_created" and perform the commit.
|
||||
if (isFirstRequest && this.stateManager.getGlobalSettingsKey("enableCheckpointsSetting") && this.checkpointManager) {
|
||||
if (
|
||||
isFirstRequest &&
|
||||
this.stateManager.getGlobalSettingsKey("enableCheckpointsSetting") &&
|
||||
this.checkpointManager &&
|
||||
!this.taskState.checkpointManagerErrorMessage
|
||||
) {
|
||||
await this.say("checkpoint_created") // Now this is conditional
|
||||
const lastCheckpointMessageIndex = findLastIndex(
|
||||
this.messageStateHandler.getClineMessages(),
|
||||
|
||||
@@ -43,10 +43,6 @@ export class ApplyPatchHandler implements IFullyManagedTool {
|
||||
private config?: TaskConfig
|
||||
private pathResolver?: PathResolver
|
||||
private providerOps?: FileProviderOperations
|
||||
private partialPreviewState?: {
|
||||
originalFiles: Record<string, string>
|
||||
currentPreviewPath?: string
|
||||
}
|
||||
|
||||
constructor(private validator: ToolValidator) {}
|
||||
|
||||
@@ -85,19 +81,11 @@ export class ApplyPatchHandler implements IFullyManagedTool {
|
||||
}
|
||||
}
|
||||
|
||||
private ensurePartialPreviewState(): { originalFiles: Record<string, string>; currentPreviewPath?: string } {
|
||||
if (!this.partialPreviewState) {
|
||||
this.partialPreviewState = { originalFiles: {} }
|
||||
}
|
||||
return this.partialPreviewState
|
||||
}
|
||||
|
||||
private async previewPatchStream(rawInput: string, uiHelpers: StronglyTypedUIHelpers): Promise<void> {
|
||||
const config = uiHelpers.getConfig()
|
||||
const provider = config.services.diffViewProvider
|
||||
this.initializeHelpers(config)
|
||||
|
||||
const state = this.ensurePartialPreviewState()
|
||||
const lines = this.stripBashWrapper(rawInput.split("\n"))
|
||||
|
||||
// Extract the first operation path and type
|
||||
@@ -171,17 +159,6 @@ export class ApplyPatchHandler implements IFullyManagedTool {
|
||||
)
|
||||
.catch(() => {}) // sending true for partial even though it's not a partial, this shows the edit row before the content is streamed into the editor
|
||||
|
||||
const requiresOpen =
|
||||
!provider.isEditing || state.currentPreviewPath !== targetResolution.resolvedPath || provider.editType === undefined
|
||||
|
||||
const needsCreateEditor = actionType === PatchActionType.ADD || (actionType === PatchActionType.UPDATE && !!movePath)
|
||||
|
||||
if (requiresOpen) {
|
||||
provider.editType = needsCreateEditor ? "create" : "modify"
|
||||
await provider.open(targetResolution.absolutePath, { displayPath: targetResolution.resolvedPath })
|
||||
state.currentPreviewPath = targetResolution.resolvedPath
|
||||
}
|
||||
|
||||
const stream: { content: string | undefined } = { content: undefined }
|
||||
|
||||
switch (actionType) {
|
||||
@@ -222,12 +199,6 @@ export class ApplyPatchHandler implements IFullyManagedTool {
|
||||
if (stream.content === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await provider.update(stream.content, false)
|
||||
} catch {
|
||||
// Ignore streaming errors
|
||||
}
|
||||
}
|
||||
|
||||
async execute(config: TaskConfig, block: ToolUse): Promise<ToolResponse> {
|
||||
@@ -249,7 +220,6 @@ export class ApplyPatchHandler implements IFullyManagedTool {
|
||||
// Ignore reset errors
|
||||
}
|
||||
}
|
||||
this.partialPreviewState = undefined
|
||||
|
||||
try {
|
||||
const lines = this.preprocessLines(rawInput)
|
||||
@@ -336,7 +306,7 @@ export class ApplyPatchHandler implements IFullyManagedTool {
|
||||
if (result.finalContent) {
|
||||
responseLines.push(`\n<final_file_content path="${path}">`)
|
||||
responseLines.push(result.finalContent)
|
||||
responseLines.push(`\n</final_file_content>`)
|
||||
responseLines.push(`</final_file_content>`)
|
||||
}
|
||||
if (result.newProblemsMessage) {
|
||||
responseLines.push(`\n\n${result.newProblemsMessage}`)
|
||||
@@ -498,7 +468,7 @@ export class ApplyPatchHandler implements IFullyManagedTool {
|
||||
changes[path] = {
|
||||
type: PatchActionType.UPDATE,
|
||||
oldContent: originalFiles[path],
|
||||
newContent: this.applyChunks(originalFiles[path]!, action.chunks, path),
|
||||
newContent: this.applyChunks(originalFiles[path]!, action.chunks, path).trimEnd(),
|
||||
movePath: action.movePath,
|
||||
}
|
||||
break
|
||||
@@ -522,7 +492,6 @@ export class ApplyPatchHandler implements IFullyManagedTool {
|
||||
return content
|
||||
}
|
||||
|
||||
const endsWithNewline = content.endsWith("\n")
|
||||
const lines = content.split("\n")
|
||||
const result: string[] = []
|
||||
let currentIndex = 0
|
||||
@@ -558,9 +527,8 @@ export class ApplyPatchHandler implements IFullyManagedTool {
|
||||
|
||||
// Copy remaining lines
|
||||
result.push(...lines.slice(currentIndex))
|
||||
const joined = result.join("\n")
|
||||
|
||||
return endsWithNewline && !joined.endsWith("\n") ? `${joined}\n` : joined
|
||||
return result.join("\n")
|
||||
}
|
||||
|
||||
private async applyCommit(commit: Commit): Promise<Record<string, FileOpsResult>> {
|
||||
|
||||
@@ -40,6 +40,19 @@ export class AskFollowupQuestionToolHandler implements IToolHandler, IPartialBlo
|
||||
}
|
||||
config.taskState.consecutiveMistakeCount = 0
|
||||
|
||||
// In yolo mode, don't wait for user input - instruct AI to use tools instead
|
||||
if (config.yoloModeToggled) {
|
||||
// Log the question that was asked but auto-respond
|
||||
await config.callbacks.say(
|
||||
"info",
|
||||
`[YOLO MODE] Auto-responding to question: "${question.substring(0, 100)}${question.length > 100 ? "..." : ""}"`,
|
||||
)
|
||||
|
||||
return formatResponse.toolResult(
|
||||
`[YOLO MODE: User input is not available in non-interactive mode. You must use available tools (read_file, list_files, search_files, etc.) to gather the information you need instead of asking the user. Proceed with using tools to find the answer to your question: "${question}"]`,
|
||||
)
|
||||
}
|
||||
|
||||
// Show notification if enabled
|
||||
if (config.autoApprovalSettings.enableNotifications) {
|
||||
showSystemNotification({
|
||||
|
||||
@@ -415,6 +415,11 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
|
||||
!block.partial, // Pass the partial flag correctly
|
||||
)
|
||||
} catch (error) {
|
||||
// As we set the didAlreadyUseTool flag when the tool has failed once, we don't want to add the error message to the
|
||||
// userMessages array again on each new streaming chunk received.
|
||||
if (!config.enableParallelToolCalling && config.taskState.didAlreadyUseTool) {
|
||||
return
|
||||
}
|
||||
// Full original behavior - comprehensive error handling even for partial blocks
|
||||
await config.callbacks.say("diff_error", relPath)
|
||||
|
||||
|
||||
Vendored
+2
-2
@@ -228,7 +228,7 @@ function createAuthSucceededHtml(redirectUri?: string): string {
|
||||
<title>Cline - Authentication Success</title>
|
||||
${redirect}
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Azeret+Mono:wght@300;400;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Azeret:wght@300;400;700&display=swap');
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
@@ -237,7 +237,7 @@ function createAuthSucceededHtml(redirectUri?: string): string {
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Azeret Mono', monospace;
|
||||
font-family: 'Azeret', sans-serif;
|
||||
background-color: #ffffff;
|
||||
color: #333333;
|
||||
height: 100vh;
|
||||
|
||||
+6
-2
@@ -1,11 +1,15 @@
|
||||
import * as vscode from "vscode"
|
||||
import { ErrorSettings } from "@/services/error"
|
||||
import { EmptyRequest } from "@/shared/proto/index.cline"
|
||||
import { GetTelemetrySettingsResponse, Setting } from "@/shared/proto/index.host"
|
||||
|
||||
export async function getTelemetrySettings(_: EmptyRequest): Promise<GetTelemetrySettingsResponse> {
|
||||
const config = vscode.workspace.getConfiguration("telemetry")
|
||||
const errorLevel = config?.get<ErrorSettings["level"]>("telemetryLevel") || "all"
|
||||
|
||||
if (vscode.env.isTelemetryEnabled) {
|
||||
return { isEnabled: Setting.ENABLED }
|
||||
return { isEnabled: Setting.ENABLED, errorLevel }
|
||||
} else {
|
||||
return { isEnabled: Setting.DISABLED }
|
||||
return { isEnabled: Setting.DISABLED, errorLevel }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,16 @@ import { EventEmitter } from "events"
|
||||
import * as vscode from "vscode"
|
||||
import { stripAnsi } from "@/hosts/vscode/terminal/ansiUtils"
|
||||
import { getLatestTerminalOutput } from "@/hosts/vscode/terminal/get-latest-output"
|
||||
import {
|
||||
isCompilingOutput,
|
||||
MAX_FULL_OUTPUT_SIZE,
|
||||
MAX_UNRETRIEVED_LINES,
|
||||
PROCESS_HOT_TIMEOUT_COMPILING,
|
||||
PROCESS_HOT_TIMEOUT_NORMAL,
|
||||
TRUNCATE_KEEP_LINES,
|
||||
} from "@/integrations/terminal/constants"
|
||||
import type { ITerminalProcess, TerminalProcessEvents } from "@/integrations/terminal/types"
|
||||
|
||||
// how long to wait after a process outputs anything before we consider it "cool" again
|
||||
const PROCESS_HOT_TIMEOUT_NORMAL = 2_000
|
||||
const PROCESS_HOT_TIMEOUT_COMPILING = 15_000
|
||||
|
||||
/**
|
||||
* VscodeTerminalProcess - Manages command execution in VSCode's integrated terminal.
|
||||
*
|
||||
@@ -156,24 +160,7 @@ export class VscodeTerminalProcess extends EventEmitter<TerminalProcessEvents> i
|
||||
clearTimeout(this.hotTimer)
|
||||
}
|
||||
// these markers indicate the command is some kind of local dev server recompiling the app, which we want to wait for output of before sending request to cline
|
||||
const compilingMarkers = ["compiling", "building", "bundling", "transpiling", "generating", "starting"]
|
||||
const markerNullifiers = [
|
||||
"compiled",
|
||||
"success",
|
||||
"finish",
|
||||
"complete",
|
||||
"succeed",
|
||||
"done",
|
||||
"end",
|
||||
"stop",
|
||||
"exit",
|
||||
"terminate",
|
||||
"error",
|
||||
"fail",
|
||||
]
|
||||
const isCompiling =
|
||||
compilingMarkers.some((marker) => data.toLowerCase().includes(marker.toLowerCase())) &&
|
||||
!markerNullifiers.some((nullifier) => data.toLowerCase().includes(nullifier.toLowerCase()))
|
||||
const isCompiling = isCompilingOutput(data)
|
||||
this.hotTimer = setTimeout(
|
||||
() => {
|
||||
this.isHot = false
|
||||
@@ -189,6 +176,15 @@ export class VscodeTerminalProcess extends EventEmitter<TerminalProcessEvents> i
|
||||
}
|
||||
|
||||
this.fullOutput += data
|
||||
|
||||
// Cap fullOutput at MAX_FULL_OUTPUT_SIZE to prevent memory exhaustion
|
||||
if (this.fullOutput.length > MAX_FULL_OUTPUT_SIZE) {
|
||||
// Keep last half of max size
|
||||
this.fullOutput = this.fullOutput.slice(-MAX_FULL_OUTPUT_SIZE / 2)
|
||||
// Reset lastRetrievedIndex since we truncated the beginning
|
||||
this.lastRetrievedIndex = 0
|
||||
}
|
||||
|
||||
if (this.isListening) {
|
||||
this.emitIfEol(data)
|
||||
this.lastRetrievedIndex = this.fullOutput.length - this.buffer.length
|
||||
@@ -285,9 +281,24 @@ export class VscodeTerminalProcess extends EventEmitter<TerminalProcessEvents> i
|
||||
this.emit("continue")
|
||||
}
|
||||
|
||||
/**
|
||||
* Get output that hasn't been retrieved yet.
|
||||
* Truncates if output is too large to prevent context window overflow.
|
||||
* @returns The unretrieved output (truncated if necessary)
|
||||
*/
|
||||
getUnretrievedOutput(): string {
|
||||
const unretrieved = this.fullOutput.slice(this.lastRetrievedIndex)
|
||||
this.lastRetrievedIndex = this.fullOutput.length
|
||||
|
||||
// Truncate if too many lines to prevent context overflow
|
||||
const lines = unretrieved.split("\n")
|
||||
if (lines.length > MAX_UNRETRIEVED_LINES) {
|
||||
const first = lines.slice(0, TRUNCATE_KEEP_LINES)
|
||||
const last = lines.slice(-TRUNCATE_KEEP_LINES)
|
||||
const skipped = lines.length - first.length - last.length
|
||||
return this.removeLastLineArtifacts([...first, `\n... (${skipped} lines truncated) ...\n`, ...last].join("\n"))
|
||||
}
|
||||
|
||||
return this.removeLastLineArtifacts(unretrieved)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,25 +21,14 @@ import { telemetryService } from "@services/telemetry"
|
||||
import { ClineToolResponseContent } from "@shared/messages"
|
||||
import { orchestrateCommandExecution } from "./CommandOrchestrator"
|
||||
import { StandaloneTerminalManager } from "./standalone/StandaloneTerminalManager"
|
||||
import {
|
||||
ActiveBackgroundCommand,
|
||||
import type {
|
||||
CommandExecutorCallbacks,
|
||||
CommandExecutorConfig,
|
||||
ITerminalManager,
|
||||
ShellIntegrationWarningTracker,
|
||||
TerminalProcessResultPromise,
|
||||
} from "./types"
|
||||
|
||||
// Re-export types for convenience
|
||||
export type { CommandExecutorCallbacks, CommandExecutorConfig, FullCommandExecutorConfig } from "./types"
|
||||
|
||||
/**
|
||||
* Tracker for shell integration warnings to determine when to show background terminal suggestion
|
||||
*/
|
||||
interface ShellIntegrationWarningTracker {
|
||||
timestamps: number[]
|
||||
lastSuggestionShown?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* CommandExecutor - Unified command executor for all terminal modes.
|
||||
*
|
||||
@@ -55,19 +44,18 @@ export class CommandExecutor {
|
||||
private standaloneManager: StandaloneTerminalManager
|
||||
private callbacks: CommandExecutorCallbacks
|
||||
|
||||
// Track the currently executing foreground process for cancellation
|
||||
private currentProcess: TerminalProcessResultPromise | null = null
|
||||
|
||||
// Flag to track if the current command was cancelled externally
|
||||
private wasCancelledExternally = false
|
||||
|
||||
// Track shell integration warnings to determine when to show background terminal suggestion
|
||||
private shellIntegrationWarningTracker: ShellIntegrationWarningTracker = {
|
||||
timestamps: [],
|
||||
lastSuggestionShown: undefined,
|
||||
}
|
||||
|
||||
// Track active background command for cancellation (standalone mode only)
|
||||
private activeBackgroundCommand?: {
|
||||
process: TerminalProcessResultPromise & { terminate?: () => void }
|
||||
command: string
|
||||
outputLines: string[]
|
||||
}
|
||||
|
||||
constructor(config: CommandExecutorConfig, callbacks: CommandExecutorCallbacks) {
|
||||
this.cwd = config.cwd
|
||||
this.taskId = config.taskId
|
||||
@@ -76,16 +64,27 @@ export class CommandExecutor {
|
||||
this.terminalManager = config.terminalManager
|
||||
this.callbacks = callbacks
|
||||
|
||||
// Always create StandaloneTerminalManager for subagents (even in VSCode mode)
|
||||
this.standaloneManager = new StandaloneTerminalManager()
|
||||
// When in backgroundExec mode, the terminalManager is already a StandaloneTerminalManager
|
||||
// created by Task. We should reuse it so that Task.getEnvironmentDetails() can see
|
||||
// the terminals and processes we create (for isHot logic, busy terminals, etc.)
|
||||
if (config.terminalExecutionMode === "backgroundExec" && config.terminalManager instanceof StandaloneTerminalManager) {
|
||||
// Reuse the same instance that Task is using
|
||||
this.standaloneManager = config.terminalManager
|
||||
Logger.info(`[CommandExecutor] Reusing Task's StandaloneTerminalManager for backgroundExec mode`)
|
||||
} else {
|
||||
// Create new StandaloneTerminalManager for subagents (even in VSCode mode)
|
||||
// This ensures subagents run in hidden terminals, not cluttering the user's VSCode terminal
|
||||
this.standaloneManager = new StandaloneTerminalManager()
|
||||
Logger.info(`[CommandExecutor] Created new StandaloneTerminalManager for subagents`)
|
||||
|
||||
// Copy settings from the provided terminalManager to ensure consistency
|
||||
if ("shellIntegrationTimeout" in config.terminalManager) {
|
||||
const tm = config.terminalManager as any
|
||||
this.standaloneManager.setShellIntegrationTimeout(tm.shellIntegrationTimeout || 4000)
|
||||
this.standaloneManager.setTerminalReuseEnabled(tm.terminalReuseEnabled ?? true)
|
||||
this.standaloneManager.setTerminalOutputLineLimit(tm.terminalOutputLineLimit || 500)
|
||||
this.standaloneManager.setSubagentTerminalOutputLineLimit(tm.subagentTerminalOutputLineLimit || 2000)
|
||||
// Copy settings from the provided terminalManager to ensure consistency
|
||||
if ("shellIntegrationTimeout" in config.terminalManager) {
|
||||
const tm = config.terminalManager as any
|
||||
this.standaloneManager.setShellIntegrationTimeout(tm.shellIntegrationTimeout || 4000)
|
||||
this.standaloneManager.setTerminalReuseEnabled(tm.terminalReuseEnabled ?? true)
|
||||
this.standaloneManager.setTerminalOutputLineLimit(tm.terminalOutputLineLimit || 500)
|
||||
this.standaloneManager.setSubagentTerminalOutputLineLimit(tm.subagentTerminalOutputLineLimit || 2000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +119,6 @@ export class CommandExecutor {
|
||||
// Subagents always use standalone manager (hidden terminal)
|
||||
const useStandalone = isSubagent || this.terminalExecutionMode === "backgroundExec"
|
||||
const manager = useStandalone ? this.standaloneManager : this.terminalManager
|
||||
|
||||
Logger.info(`Executing command in ${useStandalone ? "standalone" : "VSCode"} terminal: ${command}`)
|
||||
|
||||
// Get terminal and run command
|
||||
@@ -128,146 +126,110 @@ export class CommandExecutor {
|
||||
terminalInfo.terminal.show()
|
||||
const process = manager.runCommand(terminalInfo, command)
|
||||
|
||||
// Track background command for standalone mode (enables cancellation)
|
||||
if (useStandalone) {
|
||||
this.activeBackgroundCommand = {
|
||||
process: process as any,
|
||||
command,
|
||||
outputLines: [],
|
||||
}
|
||||
// Reset cancellation flag and track the current process
|
||||
this.wasCancelledExternally = false
|
||||
this.currentProcess = process
|
||||
const clearCurrentProcess = () => {
|
||||
this.currentProcess = null
|
||||
}
|
||||
process.once("completed", clearCurrentProcess)
|
||||
process.once("error", clearCurrentProcess)
|
||||
|
||||
// Use shared orchestration logic
|
||||
// The StandaloneTerminalManager handles background command tracking internally
|
||||
const result = await orchestrateCommandExecution(process, manager, this.callbacks, {
|
||||
command,
|
||||
timeoutSeconds,
|
||||
onOutputLine: useStandalone
|
||||
? (line) => {
|
||||
if (this.activeBackgroundCommand) {
|
||||
this.activeBackgroundCommand.outputLines.push(line)
|
||||
}
|
||||
// When "Proceed While Running" is triggered, track the command in the manager
|
||||
// Returns the log file path so the orchestrator can send it to the UI
|
||||
// existingOutput contains all output lines captured so far
|
||||
onProceedWhileRunning: useStandalone
|
||||
? (existingOutput: string[]) => {
|
||||
const backgroundCmd = this.standaloneManager.trackBackgroundCommand(process, command, existingOutput)
|
||||
return { logFilePath: backgroundCmd.logFilePath }
|
||||
}
|
||||
: undefined,
|
||||
showShellIntegrationSuggestion: this.shouldShowBackgroundTerminalSuggestion(),
|
||||
})
|
||||
|
||||
// Clear background command tracking if completed
|
||||
if (result.completed && useStandalone) {
|
||||
this.activeBackgroundCommand = undefined
|
||||
}
|
||||
|
||||
// Capture subagent telemetry
|
||||
if (isSubagent && subAgentStartTime > 0) {
|
||||
const durationMs = Math.round(performance.now() - subAgentStartTime)
|
||||
telemetryService.captureSubagentExecution(this.ulid, durationMs, result.outputLines.length, result.completed)
|
||||
}
|
||||
|
||||
// If the command was cancelled externally (via cancel button), return a clear cancellation message
|
||||
// This ensures the AI agent knows the command was cancelled by the user
|
||||
if (this.wasCancelledExternally) {
|
||||
const outputSoFar =
|
||||
result.outputLines.length > 0
|
||||
? `\nOutput captured before cancellation:\n${manager.processOutput(result.outputLines)}`
|
||||
: ""
|
||||
return [true, `Command was cancelled by the user.${outputSoFar}`]
|
||||
}
|
||||
|
||||
return [result.userRejected, result.result]
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the currently running background command.
|
||||
* Only works in standalone/backgroundExec mode.
|
||||
* Cancel all running commands (both foreground and background).
|
||||
*
|
||||
* @returns true if a command was cancelled, false otherwise
|
||||
* This method cancels:
|
||||
* 1. All detached background commands (those that were "proceeded while running")
|
||||
* 2. The current foreground process (if one is actively running)
|
||||
*
|
||||
* @returns true if any commands were cancelled, false otherwise
|
||||
*/
|
||||
async cancelBackgroundCommand(): Promise<boolean> {
|
||||
if (!this.activeBackgroundCommand) {
|
||||
return false
|
||||
let cancelled = false
|
||||
|
||||
// 1. Cancel all detached background commands
|
||||
const runningCommands = this.standaloneManager.getRunningBackgroundCommands()
|
||||
for (const cmd of runningCommands) {
|
||||
if (this.standaloneManager.cancelBackgroundCommand(cmd.id)) {
|
||||
cancelled = true
|
||||
Logger.info(`Cancelled background command: ${cmd.command}`)
|
||||
}
|
||||
}
|
||||
|
||||
const { process, command, outputLines } = this.activeBackgroundCommand
|
||||
this.activeBackgroundCommand = undefined
|
||||
this.callbacks.updateBackgroundCommandState(false)
|
||||
// 2. Cancel the current foreground process (if any)
|
||||
if (this.currentProcess && typeof (this.currentProcess as any).terminate === "function") {
|
||||
// Set flag so execute() knows the command was cancelled externally
|
||||
this.wasCancelledExternally = true
|
||||
;(this.currentProcess as any).terminate()
|
||||
this.currentProcess = null
|
||||
cancelled = true
|
||||
Logger.info("Cancelled foreground command")
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to terminate the process if the method exists
|
||||
if (typeof process.terminate === "function") {
|
||||
try {
|
||||
await process.terminate()
|
||||
Logger.info(`Terminated background command: ${command}`)
|
||||
} catch (error) {
|
||||
Logger.error(`Error terminating background command: ${command}`, error)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure any pending operations complete
|
||||
if (typeof process.continue === "function") {
|
||||
try {
|
||||
process.continue()
|
||||
} catch (error) {
|
||||
Logger.error(`Error continuing background command: ${command}`, error)
|
||||
}
|
||||
}
|
||||
|
||||
// Mark the command message as completed in the UI
|
||||
const clineMessages = this.callbacks.getClineMessages()
|
||||
const lastCommandIndex = this.findLastIndex(clineMessages, (m) => m.ask === "command" || m.say === "command")
|
||||
if (lastCommandIndex !== -1) {
|
||||
await this.callbacks.updateClineMessage(lastCommandIndex, {
|
||||
commandCompleted: true,
|
||||
})
|
||||
}
|
||||
|
||||
// Process the captured output to include in the cancellation message
|
||||
const processedOutput = this.standaloneManager.processOutput(outputLines, undefined, false)
|
||||
|
||||
// Add cancellation information to the API conversation history
|
||||
let cancellationMessage = `Command "${command}" was cancelled by the user.`
|
||||
if (processedOutput.length > 0) {
|
||||
cancellationMessage += `\n\nOutput captured before cancellation:\n${processedOutput}`
|
||||
}
|
||||
|
||||
this.callbacks.addToUserMessageContent({
|
||||
type: "text",
|
||||
text: cancellationMessage,
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
Logger.error("Error in cancelBackgroundCommand", error)
|
||||
return false
|
||||
} finally {
|
||||
// 3. Update UI state and notify user
|
||||
if (cancelled) {
|
||||
this.callbacks.updateBackgroundCommandState(false)
|
||||
try {
|
||||
await this.callbacks.say("command_output", "Command execution has been cancelled.")
|
||||
await this.callbacks.say("command_output", "Command(s) cancelled by user.")
|
||||
} catch (error) {
|
||||
Logger.error("Failed to send cancellation notification", error)
|
||||
}
|
||||
}
|
||||
|
||||
return cancelled
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there's an active background command
|
||||
* Check if there are any active background commands.
|
||||
* Delegates to StandaloneTerminalManager.
|
||||
*/
|
||||
hasActiveBackgroundCommand(): boolean {
|
||||
return !!this.activeBackgroundCommand
|
||||
return this.standaloneManager.hasActiveBackgroundCommands()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the active background command info (for external access)
|
||||
*/
|
||||
getActiveBackgroundCommand(): ActiveBackgroundCommand | undefined {
|
||||
return this.activeBackgroundCommand
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a summary of background commands for environment details
|
||||
* Get a summary of background commands for environment details.
|
||||
* Delegates to StandaloneTerminalManager which tracks multiple commands.
|
||||
*/
|
||||
getBackgroundCommandSummary(): string | undefined {
|
||||
if (!this.activeBackgroundCommand) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const { command, outputLines } = this.activeBackgroundCommand
|
||||
const recentOutput = outputLines.slice(-10).join("\n")
|
||||
|
||||
let summary = "# Background Commands\n"
|
||||
summary += `## Running: \`${command}\`\n`
|
||||
if (recentOutput) {
|
||||
summary += `### Recent Output\n${recentOutput}`
|
||||
}
|
||||
|
||||
return summary
|
||||
const summary = this.standaloneManager.getBackgroundCommandsSummary()
|
||||
return summary || undefined
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -304,16 +266,4 @@ export class CommandExecutor {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to find last index matching a predicate
|
||||
*/
|
||||
private findLastIndex<T>(array: T[], predicate: (item: T) => boolean): number {
|
||||
for (let i = array.length - 1; i >= 0; i--) {
|
||||
if (predicate(array[i])) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,19 @@ import { processFilesIntoText } from "@integrations/misc/extract-text"
|
||||
import { Logger } from "@services/logging/Logger"
|
||||
import { TerminalHangStage, TerminalUserInterventionAction, telemetryService } from "@services/telemetry"
|
||||
import { COMMAND_CANCEL_TOKEN } from "@shared/ExtensionMessage"
|
||||
import * as fs from "fs"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import {
|
||||
BUFFER_STUCK_TIMEOUT_MS,
|
||||
CHUNK_BYTE_SIZE,
|
||||
CHUNK_DEBOUNCE_MS,
|
||||
CHUNK_LINE_COUNT,
|
||||
COMPLETION_TIMEOUT_MS,
|
||||
MAX_BYTES_BEFORE_FILE,
|
||||
MAX_LINES_BEFORE_FILE,
|
||||
SUMMARY_LINES_TO_KEEP,
|
||||
} from "./constants"
|
||||
import type {
|
||||
CommandExecutorCallbacks,
|
||||
ITerminalManager,
|
||||
@@ -27,16 +40,6 @@ import type {
|
||||
TerminalProcessResultPromise,
|
||||
} from "./types"
|
||||
|
||||
// Chunked terminal output buffering constants
|
||||
export const CHUNK_LINE_COUNT = 20
|
||||
export const CHUNK_BYTE_SIZE = 2048 // 2KB
|
||||
export const CHUNK_DEBOUNCE_MS = 100
|
||||
export const BUFFER_STUCK_TIMEOUT_MS = 6000 // 6 seconds
|
||||
export const COMPLETION_TIMEOUT_MS = 6000 // 6 seconds
|
||||
|
||||
// Re-export types for convenience
|
||||
export type { OrchestrationOptions, OrchestrationResult } from "./types"
|
||||
|
||||
/**
|
||||
* Orchestrate command execution with shared logic for buffering, user interaction, and result formatting.
|
||||
*
|
||||
@@ -52,7 +55,7 @@ export async function orchestrateCommandExecution(
|
||||
callbacks: CommandExecutorCallbacks,
|
||||
options: OrchestrationOptions,
|
||||
): Promise<OrchestrationResult> {
|
||||
const { command, timeoutSeconds, onOutputLine, showShellIntegrationSuggestion } = options
|
||||
const { timeoutSeconds, onOutputLine, showShellIntegrationSuggestion, onProceedWhileRunning } = options
|
||||
|
||||
// Track command execution state
|
||||
callbacks.updateBackgroundCommandState(true)
|
||||
@@ -79,6 +82,7 @@ export async function orchestrateCommandExecution(
|
||||
let userFeedback: { text?: string; images?: string[]; files?: string[] } | undefined
|
||||
let didContinue = false
|
||||
let didCancelViaUi = false
|
||||
let backgroundTrackingResult: OrchestrationResult | null = null // Set when background tracking returns early
|
||||
|
||||
// Chunked terminal output buffering
|
||||
let outputBuffer: string[] = []
|
||||
@@ -121,16 +125,61 @@ export async function orchestrateCommandExecution(
|
||||
userFeedback = { text, images, files }
|
||||
}
|
||||
didContinue = true
|
||||
|
||||
// Notify caller to start background command tracking
|
||||
// Pass existing output lines so they can be written to the log file
|
||||
// and send log file path to UI if tracking was started
|
||||
if (onProceedWhileRunning) {
|
||||
const trackingResult = onProceedWhileRunning(outputLines)
|
||||
|
||||
// Clear timers first
|
||||
if (chunkTimer) {
|
||||
clearTimeout(chunkTimer)
|
||||
chunkTimer = null
|
||||
}
|
||||
if (completionTimer) {
|
||||
clearTimeout(completionTimer)
|
||||
completionTimer = null
|
||||
}
|
||||
|
||||
// Set early return result BEFORE resuming the process
|
||||
// This prevents the orchestrator's listener from processing new lines
|
||||
const result = terminalManager.processOutput(outputLines)
|
||||
const logMsg = trackingResult?.logFilePath ? `Log file: ${trackingResult.logFilePath}\n` : ""
|
||||
const outputMsg = result.length > 0 ? `Output so far:\n${result}` : ""
|
||||
|
||||
backgroundTrackingResult = {
|
||||
userRejected: false,
|
||||
result: `Command is running in the background. You can proceed with other tasks.\n${logMsg}${outputMsg}`,
|
||||
completed: false,
|
||||
outputLines,
|
||||
}
|
||||
|
||||
// Send log file message to UI BEFORE resuming the process
|
||||
// This ensures the message appears before any new output lines
|
||||
if (trackingResult?.logFilePath) {
|
||||
await callbacks.say("command_output", `\n📋 Output is being logged to: ${trackingResult.logFilePath}`)
|
||||
}
|
||||
|
||||
// Now resume the process - any new lines will be handled by the background tracker
|
||||
process.continue()
|
||||
return
|
||||
}
|
||||
|
||||
process.continue()
|
||||
} else if (response === "noButtonClicked" && text === COMMAND_CANCEL_TOKEN) {
|
||||
telemetryService.captureTerminalUserIntervention(TerminalUserInterventionAction.CANCELLED)
|
||||
// Set flags BEFORE resuming the process to prevent new lines from being processed
|
||||
didCancelViaUi = true
|
||||
userFeedback = undefined
|
||||
didContinue = true
|
||||
process.continue()
|
||||
outputBuffer = []
|
||||
outputBufferSize = 0
|
||||
// Send cancellation message BEFORE resuming the process
|
||||
// This ensures the message appears before any new output lines
|
||||
await callbacks.say("command_output", "Command cancelled")
|
||||
// Now resume the process
|
||||
process.continue()
|
||||
} else {
|
||||
userFeedback = { text, images, files }
|
||||
didContinue = true
|
||||
@@ -162,31 +211,134 @@ export async function orchestrateCommandExecution(
|
||||
chunkTimer = setTimeout(async () => await flushBuffer(), CHUNK_DEBOUNCE_MS)
|
||||
}
|
||||
|
||||
// Large output file-based logging state
|
||||
let isWritingToFile = false
|
||||
let largeOutputLogPath: string | null = null
|
||||
let largeOutputLogStream: fs.WriteStream | null = null
|
||||
let totalOutputBytes = 0
|
||||
let totalLineCount = 0
|
||||
let firstLines: string[] = [] // Keep first N lines for summary
|
||||
let lastLines: string[] = [] // Keep last N lines for summary (circular buffer)
|
||||
|
||||
/**
|
||||
* Switch to file-based logging when output is too large.
|
||||
* This protects against memory exhaustion from commands with huge output.
|
||||
*/
|
||||
const switchToFileBased = async () => {
|
||||
if (isWritingToFile) return
|
||||
|
||||
isWritingToFile = true
|
||||
|
||||
// FIRST: Flush any pending buffer to UI so the "writing to file" message appears at the end
|
||||
if (outputBuffer.length > 0) {
|
||||
const chunk = outputBuffer.join("\n")
|
||||
outputBuffer = []
|
||||
outputBufferSize = 0
|
||||
if (!didContinue) {
|
||||
// Use say() instead of ask() since we're transitioning to file mode
|
||||
await callbacks.say("command_output", chunk)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear any pending flush timer
|
||||
if (chunkTimer) {
|
||||
clearTimeout(chunkTimer)
|
||||
chunkTimer = null
|
||||
}
|
||||
|
||||
// Set up file logging
|
||||
largeOutputLogPath = path.join(os.tmpdir(), `cline-large-output-${Date.now()}.log`)
|
||||
largeOutputLogStream = fs.createWriteStream(largeOutputLogPath, { flags: "a" })
|
||||
|
||||
// Write all existing lines to file in a single batch to reduce I/O overhead
|
||||
if (outputLines.length > 0) {
|
||||
largeOutputLogStream.write(outputLines.join("\n") + "\n")
|
||||
}
|
||||
|
||||
// Keep first N lines for summary
|
||||
firstLines = outputLines.slice(0, SUMMARY_LINES_TO_KEEP)
|
||||
|
||||
// Keep last N lines for summary (will be updated as more lines come in)
|
||||
lastLines = outputLines.slice(-SUMMARY_LINES_TO_KEEP)
|
||||
|
||||
// FINALLY: Notify user (now this will appear at the end after all buffered output)
|
||||
await callbacks.say(
|
||||
"command_output",
|
||||
`\n📋 Output is large (${outputLines.length} lines, ${Math.round(totalOutputBytes / 1024)}KB). Writing to: ${largeOutputLogPath}`,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up file-based logging resources.
|
||||
*/
|
||||
const cleanupFileBased = () => {
|
||||
if (largeOutputLogStream) {
|
||||
largeOutputLogStream.end()
|
||||
largeOutputLogStream = null
|
||||
}
|
||||
}
|
||||
|
||||
const outputLines: string[] = []
|
||||
process.on("line", async (line: string) => {
|
||||
if (didCancelViaUi) {
|
||||
return
|
||||
}
|
||||
outputLines.push(line)
|
||||
|
||||
// If background tracking is active, don't process lines here
|
||||
// The background tracker's listener will handle them
|
||||
if (backgroundTrackingResult) {
|
||||
return
|
||||
}
|
||||
|
||||
const lineBytes = Buffer.byteLength(line, "utf8")
|
||||
totalOutputBytes += lineBytes
|
||||
totalLineCount++
|
||||
|
||||
// Check if we should switch to file-based logging
|
||||
if (!isWritingToFile && (outputLines.length >= MAX_LINES_BEFORE_FILE || totalOutputBytes >= MAX_BYTES_BEFORE_FILE)) {
|
||||
await switchToFileBased()
|
||||
}
|
||||
|
||||
if (isWritingToFile) {
|
||||
// Write to file instead of keeping in memory
|
||||
if (largeOutputLogStream) {
|
||||
largeOutputLogStream.write(line + "\n")
|
||||
}
|
||||
|
||||
// Update last lines circular buffer for summary
|
||||
lastLines.push(line)
|
||||
if (lastLines.length > SUMMARY_LINES_TO_KEEP) {
|
||||
lastLines.shift()
|
||||
}
|
||||
} else {
|
||||
// Normal behavior - keep in memory
|
||||
outputLines.push(line)
|
||||
}
|
||||
|
||||
// Notify caller about output line (for background command tracking)
|
||||
if (onOutputLine) {
|
||||
onOutputLine(line)
|
||||
}
|
||||
|
||||
// Apply buffered streaming
|
||||
// Apply buffered streaming (only if not in file mode or still showing initial output)
|
||||
if (!didContinue) {
|
||||
outputBuffer.push(line)
|
||||
outputBufferSize += Buffer.byteLength(line, "utf8")
|
||||
// Flush if buffer is large enough
|
||||
if (outputBuffer.length >= CHUNK_LINE_COUNT || outputBufferSize >= CHUNK_BYTE_SIZE) {
|
||||
await flushBuffer()
|
||||
} else {
|
||||
scheduleFlush()
|
||||
if (!isWritingToFile) {
|
||||
outputBuffer.push(line)
|
||||
outputBufferSize += lineBytes
|
||||
// Flush if buffer is large enough
|
||||
if (outputBuffer.length >= CHUNK_LINE_COUNT || outputBufferSize >= CHUNK_BYTE_SIZE) {
|
||||
await flushBuffer()
|
||||
} else {
|
||||
scheduleFlush()
|
||||
}
|
||||
}
|
||||
// When in file mode, we've already notified the user, so don't keep buffering
|
||||
} else {
|
||||
// After "Proceed While Running": stream output directly to UI
|
||||
await callbacks.say("command_output", line)
|
||||
// After "Proceed While Running" (without background tracking): stream output directly to UI
|
||||
// But throttle if we're in file mode to avoid flooding UI
|
||||
if (!isWritingToFile) {
|
||||
await callbacks.say("command_output", line)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -241,9 +393,8 @@ export async function orchestrateCommandExecution(
|
||||
if (error.message === "COMMAND_TIMEOUT") {
|
||||
// Timeout triggers "Proceed While Running" behavior
|
||||
didContinue = true
|
||||
process.continue()
|
||||
|
||||
// Clear all our timers
|
||||
// Clear all our timers first
|
||||
if (chunkTimer) {
|
||||
clearTimeout(chunkTimer)
|
||||
chunkTimer = null
|
||||
@@ -253,6 +404,41 @@ export async function orchestrateCommandExecution(
|
||||
completionTimer = null
|
||||
}
|
||||
|
||||
// If background tracking is available (standalone mode only), use it
|
||||
// This writes output to a log file and detaches the command
|
||||
if (onProceedWhileRunning) {
|
||||
const trackingResult = onProceedWhileRunning(outputLines)
|
||||
|
||||
// Set early return result BEFORE resuming the process
|
||||
// This prevents the orchestrator's listener from processing new lines
|
||||
const result = terminalManager.processOutput(outputLines)
|
||||
const logMsg = trackingResult?.logFilePath ? `Log file: ${trackingResult.logFilePath}\n` : ""
|
||||
const outputMsg = result.length > 0 ? `Output so far:\n${result}` : ""
|
||||
|
||||
backgroundTrackingResult = {
|
||||
userRejected: false,
|
||||
result: `Command timed out after ${timeoutSeconds} seconds. Running in background.\n${logMsg}${outputMsg}`,
|
||||
completed: false,
|
||||
outputLines,
|
||||
}
|
||||
|
||||
// Send log file message to UI BEFORE resuming the process
|
||||
if (trackingResult?.logFilePath) {
|
||||
await callbacks.say(
|
||||
"command_output",
|
||||
`\n⏱️ Command timed out. Output is being logged to: ${trackingResult.logFilePath}`,
|
||||
)
|
||||
}
|
||||
|
||||
// Now resume the process - any new lines will be handled by the background tracker
|
||||
process.continue()
|
||||
return backgroundTrackingResult
|
||||
}
|
||||
|
||||
// VSCode terminal mode: no background tracking available
|
||||
// Just continue the process and return timeout result
|
||||
process.continue()
|
||||
|
||||
// Process any output we captured before timeout
|
||||
await setTimeoutPromise(50)
|
||||
const result = terminalManager.processOutput(outputLines)
|
||||
@@ -274,6 +460,12 @@ export async function orchestrateCommandExecution(
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we returned early due to background tracking
|
||||
// This happens when user clicks "Proceed While Running" with background tracking enabled
|
||||
if (backgroundTrackingResult) {
|
||||
return backgroundTrackingResult
|
||||
}
|
||||
|
||||
// Clear timer if process completes normally
|
||||
if (completionTimer) {
|
||||
clearTimeout(completionTimer)
|
||||
@@ -283,7 +475,23 @@ export async function orchestrateCommandExecution(
|
||||
// Wait for a short delay to ensure all messages are sent to the webview
|
||||
await setTimeoutPromise(50)
|
||||
|
||||
const result = terminalManager.processOutput(outputLines)
|
||||
// Clean up file-based logging if active
|
||||
cleanupFileBased()
|
||||
|
||||
// Build result based on whether we used file-based logging
|
||||
let result: string
|
||||
let resultOutputLines: string[]
|
||||
|
||||
if (isWritingToFile) {
|
||||
// Build summary from first and last lines
|
||||
const skippedLines = totalLineCount - firstLines.length - lastLines.length
|
||||
const summaryLines = [...firstLines, `\n... (${skippedLines} lines written to ${largeOutputLogPath}) ...\n`, ...lastLines]
|
||||
result = terminalManager.processOutput(summaryLines)
|
||||
resultOutputLines = summaryLines
|
||||
} else {
|
||||
result = terminalManager.processOutput(outputLines)
|
||||
resultOutputLines = outputLines
|
||||
}
|
||||
|
||||
if (didCancelViaUi) {
|
||||
return {
|
||||
@@ -292,7 +500,8 @@ export async function orchestrateCommandExecution(
|
||||
`Command cancelled. ${result.length > 0 ? `\nOutput captured before cancellation:\n${result}` : ""}`,
|
||||
),
|
||||
completed: false,
|
||||
outputLines,
|
||||
outputLines: resultOutputLines,
|
||||
logFilePath: largeOutputLogPath || undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,25 +523,30 @@ export async function orchestrateCommandExecution(
|
||||
fileContentString,
|
||||
),
|
||||
completed: false,
|
||||
outputLines,
|
||||
outputLines: resultOutputLines,
|
||||
logFilePath: largeOutputLogPath || undefined,
|
||||
}
|
||||
}
|
||||
|
||||
if (completed) {
|
||||
const logFileMsg = largeOutputLogPath ? `\nFull output saved to: ${largeOutputLogPath}` : ""
|
||||
return {
|
||||
userRejected: false,
|
||||
result: `Command executed.${result.length > 0 ? `\nOutput:\n${result}` : ""}`,
|
||||
result: `Command executed.${result.length > 0 ? `\nOutput:\n${result}` : ""}${logFileMsg}`,
|
||||
completed: true,
|
||||
outputLines,
|
||||
outputLines: resultOutputLines,
|
||||
logFilePath: largeOutputLogPath || undefined,
|
||||
}
|
||||
} else {
|
||||
const logFileMsg = largeOutputLogPath ? `\nFull output saved to: ${largeOutputLogPath}` : ""
|
||||
return {
|
||||
userRejected: false,
|
||||
result: `Command is still running in the user's terminal.${
|
||||
result.length > 0 ? `\nHere's the output so far:\n${result}` : ""
|
||||
}\n\nYou will be updated on the terminal status and new output in the future.`,
|
||||
}${logFileMsg}\n\nYou will be updated on the terminal status and new output in the future.`,
|
||||
completed: false,
|
||||
outputLines,
|
||||
outputLines: resultOutputLines,
|
||||
logFilePath: largeOutputLogPath || undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Terminal Constants
|
||||
*
|
||||
* Central location for all terminal-related constants.
|
||||
* This makes it easy to understand and tune terminal behavior.
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// Process "Hot" State Timeouts
|
||||
// =============================================================================
|
||||
// How long to wait after output before considering the process "cool"
|
||||
// This stalls API requests to let terminal output settle
|
||||
|
||||
/** Normal timeout after last output (2 seconds) */
|
||||
export const PROCESS_HOT_TIMEOUT_NORMAL = 2_000
|
||||
|
||||
/** Extended timeout for compilation/build commands (15 seconds) */
|
||||
export const PROCESS_HOT_TIMEOUT_COMPILING = 15_000
|
||||
|
||||
// =============================================================================
|
||||
// Output Buffering (CommandOrchestrator)
|
||||
// =============================================================================
|
||||
// Controls how output is chunked and sent to the UI
|
||||
|
||||
/** Lines to buffer before flushing to UI */
|
||||
export const CHUNK_LINE_COUNT = 20
|
||||
|
||||
/** Bytes to buffer before flushing to UI */
|
||||
export const CHUNK_BYTE_SIZE = 2048 // 2KB
|
||||
|
||||
/** Debounce time for buffer flush */
|
||||
export const CHUNK_DEBOUNCE_MS = 100
|
||||
|
||||
/** Timeout to detect stuck buffer */
|
||||
export const BUFFER_STUCK_TIMEOUT_MS = 6000 // 6 seconds
|
||||
|
||||
/** Timeout to detect stuck completion */
|
||||
export const COMPLETION_TIMEOUT_MS = 6000 // 6 seconds
|
||||
|
||||
// =============================================================================
|
||||
// Large Output Protection
|
||||
// =============================================================================
|
||||
// Prevents memory exhaustion and context window overflow
|
||||
|
||||
/** Switch to file-based logging after this many lines */
|
||||
export const MAX_LINES_BEFORE_FILE = 1000
|
||||
|
||||
/** Switch to file-based logging after this many bytes */
|
||||
export const MAX_BYTES_BEFORE_FILE = 512 * 1024 // 512KB
|
||||
|
||||
/** Lines to keep at start/end for summary when truncating */
|
||||
export const SUMMARY_LINES_TO_KEEP = 100
|
||||
|
||||
/** Maximum size for fullOutput storage (memory protection) */
|
||||
export const MAX_FULL_OUTPUT_SIZE = 1024 * 1024 // 1MB
|
||||
|
||||
/** Maximum lines to return from getUnretrievedOutput */
|
||||
export const MAX_UNRETRIEVED_LINES = 500
|
||||
|
||||
/** Lines to keep at start/end when truncating unretrieved output */
|
||||
export const TRUNCATE_KEEP_LINES = 100
|
||||
|
||||
// =============================================================================
|
||||
// Output Line Limits (processOutput)
|
||||
// =============================================================================
|
||||
// Controls truncation when returning output to AI
|
||||
|
||||
/** Default max lines for command output */
|
||||
export const DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT = 500
|
||||
|
||||
/** Max lines for subagent commands (more context needed) */
|
||||
export const DEFAULT_SUBAGENT_TERMINAL_OUTPUT_LINE_LIMIT = 2000
|
||||
|
||||
// =============================================================================
|
||||
// Background Command Tracking
|
||||
// =============================================================================
|
||||
// Controls background command behavior for "Proceed While Running"
|
||||
|
||||
/** Hard timeout for background commands to prevent zombie processes (10 minutes) */
|
||||
export const BACKGROUND_COMMAND_TIMEOUT_MS = 10 * 60 * 1000
|
||||
|
||||
// =============================================================================
|
||||
// Compilation Detection Markers
|
||||
// =============================================================================
|
||||
// Used to detect if a command is compiling/building
|
||||
|
||||
/** Markers that indicate compilation is starting */
|
||||
export const COMPILING_MARKERS = ["compiling", "building", "bundling", "transpiling", "generating", "starting"]
|
||||
|
||||
/** Markers that indicate compilation is done (nullify extended timeout) */
|
||||
export const COMPILING_NULLIFIERS = [
|
||||
"compiled",
|
||||
"success",
|
||||
"finish",
|
||||
"complete",
|
||||
"succeed",
|
||||
"done",
|
||||
"end",
|
||||
"stop",
|
||||
"exit",
|
||||
"terminate",
|
||||
"error",
|
||||
"fail",
|
||||
]
|
||||
|
||||
/**
|
||||
* Check if terminal output indicates compilation/building.
|
||||
* Matches markers anywhere in the output.
|
||||
*/
|
||||
export function isCompilingOutput(data: string): boolean {
|
||||
const lowerData = data.toLowerCase()
|
||||
const hasMarker = COMPILING_MARKERS.some((marker) => lowerData.includes(marker.toLowerCase()))
|
||||
const hasNullifier = COMPILING_NULLIFIERS.some((nullifier) => lowerData.includes(nullifier.toLowerCase()))
|
||||
return hasMarker && !hasNullifier
|
||||
}
|
||||
@@ -23,17 +23,7 @@
|
||||
export { CommandExecutor } from "./CommandExecutor"
|
||||
|
||||
// Export command orchestrator (shared logic)
|
||||
export {
|
||||
BUFFER_STUCK_TIMEOUT_MS,
|
||||
CHUNK_BYTE_SIZE,
|
||||
CHUNK_DEBOUNCE_MS,
|
||||
CHUNK_LINE_COUNT,
|
||||
COMPLETION_TIMEOUT_MS,
|
||||
findLastIndex,
|
||||
orchestrateCommandExecution,
|
||||
} from "./CommandOrchestrator"
|
||||
|
||||
// Export terminal process interface
|
||||
export { findLastIndex, orchestrateCommandExecution } from "./CommandOrchestrator"
|
||||
|
||||
// Export standalone terminal implementations
|
||||
export { StandaloneTerminal } from "./standalone/StandaloneTerminal"
|
||||
|
||||
@@ -4,12 +4,29 @@
|
||||
* This class provides the same interface as VSCode's TerminalManager but works
|
||||
* in CLI and JetBrains environments by using subprocess management instead of
|
||||
* VSCode's terminal API.
|
||||
*
|
||||
* Also handles background command tracking for "Proceed While Running" functionality:
|
||||
* - Logs output to temp files for later retrieval
|
||||
* - Tracks command status (running, completed, error, timed_out)
|
||||
* - Implements 10-minute hard timeout to prevent zombie processes
|
||||
* - Provides summary for environment details
|
||||
*/
|
||||
|
||||
import type { ITerminalManager, TerminalInfo, TerminalProcessResultPromise } from "../types"
|
||||
import * as fs from "fs"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import {
|
||||
BACKGROUND_COMMAND_TIMEOUT_MS,
|
||||
DEFAULT_SUBAGENT_TERMINAL_OUTPUT_LINE_LIMIT,
|
||||
DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT,
|
||||
} from "../constants"
|
||||
import type { BackgroundCommand, ITerminalManager, TerminalInfo, TerminalProcessResultPromise } from "../types"
|
||||
import { StandaloneTerminalProcess } from "./StandaloneTerminalProcess"
|
||||
import { StandaloneTerminalRegistry } from "./StandaloneTerminalRegistry"
|
||||
|
||||
// Re-export BackgroundCommand for backwards compatibility
|
||||
export type { BackgroundCommand }
|
||||
|
||||
/**
|
||||
* Helper function to merge a process with a promise for the TerminalProcessResultPromise type.
|
||||
* This allows the returned object to be both awaitable and have event methods.
|
||||
@@ -63,14 +80,27 @@ export class StandaloneTerminalManager implements ITerminalManager {
|
||||
private terminalReuseEnabled: boolean = true
|
||||
|
||||
/** Maximum output lines to keep */
|
||||
private terminalOutputLineLimit: number = 500
|
||||
private terminalOutputLineLimit: number = DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT
|
||||
|
||||
/** Maximum output lines for subagent commands */
|
||||
private subagentTerminalOutputLineLimit: number = 2000
|
||||
private subagentTerminalOutputLineLimit: number = DEFAULT_SUBAGENT_TERMINAL_OUTPUT_LINE_LIMIT
|
||||
|
||||
/** Default terminal profile */
|
||||
private defaultTerminalProfile: string = "default"
|
||||
|
||||
// =========================================================================
|
||||
// Background Command Tracking
|
||||
// =========================================================================
|
||||
|
||||
/** Map of background command ID to command info */
|
||||
private backgroundCommands: Map<string, BackgroundCommand> = new Map()
|
||||
|
||||
/** Map of background command ID to log file write stream */
|
||||
private logStreams: Map<string, fs.WriteStream> = new Map()
|
||||
|
||||
/** Map of background command ID to timeout handle */
|
||||
private backgroundTimeouts: Map<string, NodeJS.Timeout> = new Map()
|
||||
|
||||
/**
|
||||
* Run a command in the specified terminal.
|
||||
* @param terminalInfo The terminal to run the command in
|
||||
@@ -88,9 +118,8 @@ export class StandaloneTerminalManager implements ITerminalManager {
|
||||
terminalInfo.busy = false
|
||||
})
|
||||
|
||||
process.once("error", (error: Error) => {
|
||||
process.once("error", (_error: Error) => {
|
||||
terminalInfo.busy = false
|
||||
console.error(`[StandaloneTerminalManager] Command error on terminal ${terminalInfo.id}:`, error)
|
||||
})
|
||||
|
||||
// Create promise for the process
|
||||
@@ -138,7 +167,6 @@ export class StandaloneTerminalManager implements ITerminalManager {
|
||||
availableTerminal.terminal.shellIntegration.cwd.fsPath = cwd
|
||||
}
|
||||
this.terminalIds.add(availableTerminal.id)
|
||||
console.log(`[StandaloneTerminalManager] Reused terminal ${availableTerminal.id} with cd`)
|
||||
return availableTerminal
|
||||
}
|
||||
}
|
||||
@@ -158,10 +186,19 @@ export class StandaloneTerminalManager implements ITerminalManager {
|
||||
* @returns Array of terminal info with id and last command
|
||||
*/
|
||||
getTerminals(busy: boolean): { id: number; lastCommand: string }[] {
|
||||
return Array.from(this.terminalIds)
|
||||
const allTerminalIds = Array.from(this.terminalIds)
|
||||
|
||||
const terminals = allTerminalIds
|
||||
.map((id) => this.registry.getTerminal(id))
|
||||
.filter((t): t is TerminalInfo => t !== undefined && t.busy === busy)
|
||||
.filter((t): t is TerminalInfo => {
|
||||
if (t === undefined) {
|
||||
return false
|
||||
}
|
||||
return t.busy === busy
|
||||
})
|
||||
.map((t) => ({ id: t.id, lastCommand: t.lastCommand }))
|
||||
|
||||
return terminals
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -213,6 +250,9 @@ export class StandaloneTerminalManager implements ITerminalManager {
|
||||
* Dispose of all terminals and clean up resources.
|
||||
*/
|
||||
disposeAll(): void {
|
||||
// Dispose background commands first
|
||||
this.disposeBackgroundCommands()
|
||||
|
||||
// Terminate all processes
|
||||
for (const [_terminalId, process] of this.processes) {
|
||||
if (process && process.terminate) {
|
||||
@@ -369,4 +409,212 @@ export class StandaloneTerminalManager implements ITerminalManager {
|
||||
closeAllTerminals(): number {
|
||||
return this.closeTerminals(() => true, true)
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Background Command Tracking Methods
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Track a command that will continue running in the background.
|
||||
* Called when user clicks "Proceed While Running".
|
||||
* Creates a log file and pipes output to it.
|
||||
* Sets up a 10-minute hard timeout to prevent zombie processes.
|
||||
*
|
||||
* @param process The terminal process to track
|
||||
* @param command The command string being executed
|
||||
* @param existingOutput Output lines already captured before tracking started
|
||||
* @returns The background command info with log file path
|
||||
*/
|
||||
trackBackgroundCommand(
|
||||
process: TerminalProcessResultPromise,
|
||||
command: string,
|
||||
existingOutput: string[] = [],
|
||||
): BackgroundCommand {
|
||||
const id = `background-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
const logFilePath = path.join(os.tmpdir(), `cline-${id}.log`)
|
||||
|
||||
const backgroundCommand: BackgroundCommand = {
|
||||
id,
|
||||
command,
|
||||
startTime: Date.now(),
|
||||
status: "running",
|
||||
logFilePath,
|
||||
lineCount: existingOutput.length,
|
||||
process,
|
||||
}
|
||||
|
||||
// Create write stream for log file
|
||||
const logStream = fs.createWriteStream(logFilePath, { flags: "a" })
|
||||
this.logStreams.set(id, logStream)
|
||||
|
||||
// Write existing output that was captured before tracking started
|
||||
if (existingOutput.length > 0) {
|
||||
logStream.write(existingOutput.join("\n") + "\n")
|
||||
}
|
||||
|
||||
// Pipe future process output to log file
|
||||
process.on("line", (line: string) => {
|
||||
backgroundCommand.lineCount++
|
||||
logStream.write(line + "\n")
|
||||
})
|
||||
|
||||
// Set up 10-minute hard timeout to prevent zombie processes
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (backgroundCommand.status === "running") {
|
||||
backgroundCommand.status = "timed_out"
|
||||
logStream.write("\n[TIMEOUT] Process killed after 10 minutes\n")
|
||||
logStream.end()
|
||||
|
||||
// Terminate the process if it has a terminate method
|
||||
if (process && typeof (process as any).terminate === "function") {
|
||||
;(process as any).terminate()
|
||||
}
|
||||
}
|
||||
}, BACKGROUND_COMMAND_TIMEOUT_MS)
|
||||
this.backgroundTimeouts.set(id, timeoutId)
|
||||
|
||||
// Listen for completion - clear timeout
|
||||
process.on("completed", () => {
|
||||
// Guard: Skip if already handled by timeout
|
||||
if (backgroundCommand.status !== "running") {
|
||||
return
|
||||
}
|
||||
const timeout = this.backgroundTimeouts.get(id)
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
this.backgroundTimeouts.delete(id)
|
||||
}
|
||||
backgroundCommand.status = "completed"
|
||||
logStream.end()
|
||||
})
|
||||
|
||||
// Listen for errors - clear timeout
|
||||
process.on("error", (error: Error) => {
|
||||
// Guard: Skip if already handled by timeout
|
||||
if (backgroundCommand.status !== "running") {
|
||||
return
|
||||
}
|
||||
const timeout = this.backgroundTimeouts.get(id)
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
this.backgroundTimeouts.delete(id)
|
||||
}
|
||||
backgroundCommand.status = "error"
|
||||
// Try to extract exit code from error message if available
|
||||
const exitCodeMatch = error.message.match(/exit code (\d+)/)
|
||||
if (exitCodeMatch) {
|
||||
backgroundCommand.exitCode = parseInt(exitCodeMatch[1], 10)
|
||||
}
|
||||
logStream.end()
|
||||
})
|
||||
|
||||
this.backgroundCommands.set(id, backgroundCommand)
|
||||
return backgroundCommand
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific background command by ID.
|
||||
*/
|
||||
getBackgroundCommand(id: string): BackgroundCommand | undefined {
|
||||
return this.backgroundCommands.get(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tracked background commands.
|
||||
*/
|
||||
getAllBackgroundCommands(): BackgroundCommand[] {
|
||||
return Array.from(this.backgroundCommands.values())
|
||||
}
|
||||
|
||||
/**
|
||||
* Get only running background commands.
|
||||
*/
|
||||
getRunningBackgroundCommands(): BackgroundCommand[] {
|
||||
return this.getAllBackgroundCommands().filter((c) => c.status === "running")
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are any active background commands.
|
||||
*/
|
||||
hasActiveBackgroundCommands(): boolean {
|
||||
return this.getRunningBackgroundCommands().length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel/terminate a specific background command.
|
||||
* @param id The background command ID to cancel
|
||||
* @returns true if cancelled, false if not found or already completed
|
||||
*/
|
||||
cancelBackgroundCommand(id: string): boolean {
|
||||
const command = this.backgroundCommands.get(id)
|
||||
if (!command || command.status !== "running") {
|
||||
return false
|
||||
}
|
||||
|
||||
// Clear timeout
|
||||
const timeout = this.backgroundTimeouts.get(id)
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
this.backgroundTimeouts.delete(id)
|
||||
}
|
||||
|
||||
// Close log stream
|
||||
const logStream = this.logStreams.get(id)
|
||||
if (logStream) {
|
||||
logStream.write("\n[CANCELLED] Command cancelled by user\n")
|
||||
logStream.end()
|
||||
this.logStreams.delete(id)
|
||||
}
|
||||
|
||||
// Terminate process
|
||||
if (command.process && typeof (command.process as any).terminate === "function") {
|
||||
;(command.process as any).terminate()
|
||||
}
|
||||
|
||||
command.status = "error"
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a summary string for environment details.
|
||||
* Shows running background commands with duration, line count, and log paths.
|
||||
*/
|
||||
getBackgroundCommandsSummary(): string {
|
||||
const running = this.getRunningBackgroundCommands()
|
||||
if (running.length === 0) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const lines = [`# Background Commands (${running.length} running)`]
|
||||
for (const c of running) {
|
||||
const duration = Math.round((Date.now() - c.startTime) / 1000 / 60)
|
||||
lines.push(`- ${c.command} (running ${duration}m, ${c.lineCount} lines, log: ${c.logFilePath})`)
|
||||
}
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all background command resources.
|
||||
* Called when disposing the manager.
|
||||
*/
|
||||
disposeBackgroundCommands(): void {
|
||||
// Clear all timeouts
|
||||
for (const [_id, timeout] of this.backgroundTimeouts) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
this.backgroundTimeouts.clear()
|
||||
|
||||
// Close all log streams
|
||||
for (const [_id, logStream] of this.logStreams) {
|
||||
try {
|
||||
logStream.end()
|
||||
} catch (_error) {
|
||||
// Ignore errors when closing log streams
|
||||
}
|
||||
}
|
||||
this.logStreams.clear()
|
||||
|
||||
// Clear command tracking
|
||||
this.backgroundCommands.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,16 @@
|
||||
import { ChildProcess, spawn } from "child_process"
|
||||
import { EventEmitter } from "events"
|
||||
|
||||
import { terminateProcessTree } from "@/utils/process-termination"
|
||||
|
||||
import {
|
||||
isCompilingOutput,
|
||||
MAX_FULL_OUTPUT_SIZE,
|
||||
MAX_UNRETRIEVED_LINES,
|
||||
PROCESS_HOT_TIMEOUT_COMPILING,
|
||||
PROCESS_HOT_TIMEOUT_NORMAL,
|
||||
TRUNCATE_KEEP_LINES,
|
||||
} from "../constants"
|
||||
import type { ITerminal, ITerminalProcess, TerminalProcessEvents } from "../types"
|
||||
|
||||
/**
|
||||
@@ -67,8 +77,6 @@ export class StandaloneTerminalProcess extends EventEmitter<TerminalProcessEvent
|
||||
* @param command The command to execute
|
||||
*/
|
||||
async run(terminal: ITerminal, command: string): Promise<void> {
|
||||
console.log(`[StandaloneTerminal] Running command: ${command}`)
|
||||
|
||||
// Get shell and working directory from terminal
|
||||
const shell = (terminal as any)._shellPath || this.getDefaultShell()
|
||||
const cwd = (terminal as any)._cwd || process.cwd()
|
||||
@@ -104,8 +112,12 @@ export class StandaloneTerminalProcess extends EventEmitter<TerminalProcessEvent
|
||||
// Spawn the process with special handling for "cmd.exe"
|
||||
this.childProcess = spawn("cmd.exe", shellArgs, shellOptions)
|
||||
} else {
|
||||
// Spawn the process
|
||||
this.childProcess = spawn(shell, shellArgs, shellOptions)
|
||||
// Spawn the process with detached: true to create a process group
|
||||
// This allows us to kill the entire process tree when terminating
|
||||
this.childProcess = spawn(shell, shellArgs, {
|
||||
...shellOptions,
|
||||
detached: true,
|
||||
})
|
||||
}
|
||||
|
||||
// Track process state
|
||||
@@ -132,8 +144,7 @@ export class StandaloneTerminalProcess extends EventEmitter<TerminalProcessEvent
|
||||
})
|
||||
|
||||
// Handle process completion
|
||||
this.childProcess.on("close", (code: number | null, signal: NodeJS.Signals | null) => {
|
||||
console.log(`[StandaloneTerminal] Process closed with code ${code}, signal ${signal}`)
|
||||
this.childProcess.on("close", (code: number | null, _signal: NodeJS.Signals | null) => {
|
||||
this.exitCode = code
|
||||
this.isCompleted = true
|
||||
this.emitRemainingBuffer()
|
||||
@@ -150,7 +161,6 @@ export class StandaloneTerminalProcess extends EventEmitter<TerminalProcessEvent
|
||||
|
||||
// Handle process errors
|
||||
this.childProcess.on("error", (error: Error) => {
|
||||
console.error(`[StandaloneTerminal] Process error:`, error)
|
||||
this.emit("error", error)
|
||||
})
|
||||
|
||||
@@ -158,7 +168,6 @@ export class StandaloneTerminalProcess extends EventEmitter<TerminalProcessEvent
|
||||
;(terminal as any)._process = this.childProcess
|
||||
;(terminal as any)._processId = this.childProcess.pid
|
||||
} catch (error) {
|
||||
console.error(`[StandaloneTerminal] Failed to spawn process:`, error)
|
||||
this.emit("error", error)
|
||||
}
|
||||
}
|
||||
@@ -176,37 +185,25 @@ export class StandaloneTerminalProcess extends EventEmitter<TerminalProcessEvent
|
||||
}
|
||||
|
||||
// Check for compilation markers to adjust hot timeout
|
||||
const compilingMarkers = ["compiling", "building", "bundling", "transpiling", "generating", "starting"]
|
||||
const markerNullifiers = [
|
||||
"compiled",
|
||||
"success",
|
||||
"finish",
|
||||
"complete",
|
||||
"succeed",
|
||||
"done",
|
||||
"end",
|
||||
"stop",
|
||||
"exit",
|
||||
"terminate",
|
||||
"error",
|
||||
"fail",
|
||||
]
|
||||
|
||||
const isCompiling =
|
||||
compilingMarkers.some((marker) => data.toLowerCase().includes(marker.toLowerCase())) &&
|
||||
!markerNullifiers.some((nullifier) => data.toLowerCase().includes(nullifier.toLowerCase()))
|
||||
|
||||
const hotTimeout = isCompiling ? 15000 : 2000
|
||||
const isCompiling = isCompilingOutput(data)
|
||||
const hotTimeout = isCompiling ? PROCESS_HOT_TIMEOUT_COMPILING : PROCESS_HOT_TIMEOUT_NORMAL
|
||||
this.hotTimer = setTimeout(() => {
|
||||
this.isHot = false
|
||||
}, hotTimeout)
|
||||
|
||||
// Store full output
|
||||
// Store full output with size cap to prevent memory exhaustion
|
||||
this.fullOutput += data
|
||||
|
||||
// Cap fullOutput at MAX_FULL_OUTPUT_SIZE to prevent memory exhaustion
|
||||
if (this.fullOutput.length > MAX_FULL_OUTPUT_SIZE) {
|
||||
// Keep last half of max size
|
||||
this.fullOutput = this.fullOutput.slice(-MAX_FULL_OUTPUT_SIZE / 2)
|
||||
// Reset lastRetrievedIndex since we truncated the beginning
|
||||
this.lastRetrievedIndex = 0
|
||||
}
|
||||
|
||||
if (this.isListening) {
|
||||
this.emitLines(data)
|
||||
this.lastRetrievedIndex = this.fullOutput.length - this.buffer.length
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,22 +237,37 @@ export class StandaloneTerminalProcess extends EventEmitter<TerminalProcessEvent
|
||||
|
||||
/**
|
||||
* Continue execution without waiting for completion.
|
||||
* Stops event emission and resolves the promise.
|
||||
* Emits "continue" event but keeps emitting "line" events for background tracking.
|
||||
*
|
||||
* Note: We intentionally do NOT call removeAllListeners("line") or set isListening=false
|
||||
* because background command tracking needs to continue receiving output lines
|
||||
* after the user clicks "Proceed While Running".
|
||||
*/
|
||||
continue(): void {
|
||||
this.emitRemainingBuffer()
|
||||
this.isListening = false
|
||||
this.removeAllListeners("line")
|
||||
// Keep isListening = true so we continue emitting "line" events
|
||||
// This is needed for background command tracking to log output to file
|
||||
this.emit("continue")
|
||||
}
|
||||
|
||||
/**
|
||||
* Get output that hasn't been retrieved yet.
|
||||
* @returns The unretrieved output
|
||||
* Truncates if output is too large to prevent context window overflow.
|
||||
* @returns The unretrieved output (truncated if necessary)
|
||||
*/
|
||||
getUnretrievedOutput(): string {
|
||||
const unretrieved = this.fullOutput.slice(this.lastRetrievedIndex)
|
||||
this.lastRetrievedIndex = this.fullOutput.length
|
||||
|
||||
// Truncate if too many lines to prevent context overflow
|
||||
const lines = unretrieved.split("\n")
|
||||
if (lines.length > MAX_UNRETRIEVED_LINES) {
|
||||
const first = lines.slice(0, TRUNCATE_KEEP_LINES)
|
||||
const last = lines.slice(-TRUNCATE_KEEP_LINES)
|
||||
const skipped = lines.length - first.length - last.length
|
||||
return this.removeLastLineArtifacts([...first, `\n... (${skipped} lines truncated) ...\n`, ...last].join("\n"))
|
||||
}
|
||||
|
||||
return this.removeLastLineArtifacts(unretrieved)
|
||||
}
|
||||
|
||||
@@ -305,41 +317,29 @@ export class StandaloneTerminalProcess extends EventEmitter<TerminalProcessEvent
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminate the process if it's still running.
|
||||
* Terminate the process and all its children.
|
||||
*
|
||||
* Uses terminateProcessTree utility which handles:
|
||||
* - Cross-platform process tree termination via tree-kill
|
||||
* - Graceful shutdown with SIGTERM
|
||||
* - SIGKILL fallback after 2 second timeout
|
||||
*/
|
||||
terminate(): void {
|
||||
async terminate(): Promise<void> {
|
||||
if (!this.childProcess || this.isCompleted) {
|
||||
console.log(`[StandaloneTerminal] Process already completed or doesn't exist, skipping termination`)
|
||||
return
|
||||
}
|
||||
|
||||
const pid = this.childProcess.pid
|
||||
console.log(`[StandaloneTerminal] Terminating process ${pid} with SIGTERM`)
|
||||
|
||||
try {
|
||||
if (!pid) {
|
||||
// Fallback: try to kill the process directly if PID is unavailable
|
||||
this.childProcess.kill("SIGTERM")
|
||||
|
||||
// Force kill after timeout if process doesn't exit gracefully
|
||||
setTimeout(() => {
|
||||
if (!this.isCompleted && this.childProcess) {
|
||||
console.log(`[StandaloneTerminal] Process ${pid} did not exit gracefully, force killing with SIGKILL`)
|
||||
try {
|
||||
this.childProcess.kill("SIGKILL")
|
||||
} catch (killError) {
|
||||
console.error(`[StandaloneTerminal] Failed to force kill process ${pid}:`, killError)
|
||||
}
|
||||
} else {
|
||||
console.log(`[StandaloneTerminal] Process ${pid} exited gracefully`)
|
||||
}
|
||||
}, 5000)
|
||||
} catch (error) {
|
||||
console.error(`[StandaloneTerminal] Failed to send SIGTERM to process ${pid}:`, error)
|
||||
// Try SIGKILL immediately if SIGTERM fails
|
||||
try {
|
||||
this.childProcess.kill("SIGKILL")
|
||||
} catch (killError) {
|
||||
console.error(`[StandaloneTerminal] Failed to send SIGKILL to process ${pid}:`, killError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
await terminateProcessTree({
|
||||
pid,
|
||||
childProcess: this.childProcess,
|
||||
isCompleted: () => this.isCompleted,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user