mirror of
https://github.com/cline/cline.git
synced 2026-09-07 22:16:30 +08:00
Compare commits
64
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba464b2845 | ||
|
|
df1d33c751 | ||
|
|
361494d18f | ||
|
|
dca0a8fa3e | ||
|
|
6d7213dc6a | ||
|
|
97a35d3868 | ||
|
|
4ec9155c46 | ||
|
|
bffca989a1 | ||
|
|
0133b5d030 | ||
|
|
a94c4be438 | ||
|
|
d70792e539 | ||
|
|
e4ddaac627 | ||
|
|
9478b600aa | ||
|
|
d194e47bf6 | ||
|
|
c9ff9cf1d5 | ||
|
|
d7fa6b33c1 | ||
|
|
8f521e7ea3 | ||
|
|
3cb8d0fbcf | ||
|
|
5f2bf6329f | ||
|
|
963abc190e | ||
|
|
631a7d6566 | ||
|
|
e43ab0ea7a | ||
|
|
242e3321a2 | ||
|
|
ea6cb4b29e | ||
|
|
2c75285566 | ||
|
|
8279f2e145 | ||
|
|
5b94ba3ef9 | ||
|
|
703146182a | ||
|
|
9603643b77 | ||
|
|
c6dce7fb17 | ||
|
|
cc0d4ae6cb | ||
|
|
a9365e30e9 | ||
|
|
efe468d9b1 | ||
|
|
1b6202604d | ||
|
|
ea1dbd8bea | ||
|
|
e43517519e | ||
|
|
520d08c5f2 | ||
|
|
31a55ac87e | ||
|
|
a442983742 | ||
|
|
7b71eff294 | ||
|
|
6d1890f8bb | ||
|
|
11d17fc17e | ||
|
|
42a3dc6150 | ||
|
|
4032e51e8d | ||
|
|
1bbc90487c | ||
|
|
d422ebbb27 | ||
|
|
7470d234ef | ||
|
|
21b81f1844 | ||
|
|
362429a317 | ||
|
|
09cb9ac9ac | ||
|
|
94160faeef | ||
|
|
f526f70e3a | ||
|
|
d9b47378c6 | ||
|
|
0671c59e6d | ||
|
|
bf87887501 | ||
|
|
66a81a6efa | ||
|
|
eee64c5204 | ||
|
|
748ba99c1c | ||
|
|
82b1a01644 | ||
|
|
1c6307e8ad | ||
|
|
58d9c0af18 | ||
|
|
bf213c24ea | ||
|
|
80cceaa3ae | ||
|
|
359e088eb6 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add Skills system for reusable, on-demand agent instructions.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add zai-glm-4.7 to Cerebras model list
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add claude 4.5 haiku
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Adding support for responses api to OCA provider
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Log Persistence errors to PostHog
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix issue where tool call ids are invalid when switching between models using the chat completion format and the responses api format.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
add bash command permission system to cline
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Revert #8341 (0d04205dc) due to regressions in diff view/document truncation (see #8423, #8429).
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
feat(vercel-ai-gateway): add model refresh and improve reasoning support
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: removes retry message from UI after retry succeeds
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
show cline command permission denials in the CLI
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
add claude 4.5 opus into sap provider.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Throttle the remote config fetch
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Improve history view filter menu
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Reduce the number of network requests for the users profile
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: Verify selected index is not -1 when checking if an option is selectable in the context menu
|
||||
@@ -0,0 +1,196 @@
|
||||
---
|
||||
name: create-pull-request
|
||||
description: Create a GitHub pull request following project conventions. Use when the user asks to create a PR, submit changes for review, or open a pull request. Handles commit analysis, branch management, and PR creation using the gh CLI tool.
|
||||
---
|
||||
|
||||
# Create Pull Request
|
||||
|
||||
This skill guides you through creating a well-structured GitHub pull request that follows project conventions and best practices.
|
||||
|
||||
## Prerequisites Check
|
||||
|
||||
Before proceeding, verify the following:
|
||||
|
||||
### 1. Check if `gh` CLI is installed
|
||||
|
||||
```bash
|
||||
gh --version
|
||||
```
|
||||
|
||||
If not installed, inform the user:
|
||||
> The GitHub CLI (`gh`) is required but not installed. Please install it:
|
||||
> - macOS: `brew install gh`
|
||||
> - Other: https://cli.github.com/
|
||||
|
||||
### 2. Check if authenticated with GitHub
|
||||
|
||||
```bash
|
||||
gh auth status
|
||||
```
|
||||
|
||||
If not authenticated, guide the user to run `gh auth login`.
|
||||
|
||||
### 3. Verify clean working directory
|
||||
|
||||
```bash
|
||||
git status
|
||||
```
|
||||
|
||||
If there are uncommitted changes, ask the user whether to:
|
||||
- Commit them as part of this PR
|
||||
- Stash them temporarily
|
||||
- Discard them (with caution)
|
||||
|
||||
## Gather Context
|
||||
|
||||
### 1. Identify the current branch
|
||||
|
||||
```bash
|
||||
git branch --show-current
|
||||
```
|
||||
|
||||
Ensure you're not on `main` or `master`. If so, ask the user to create or switch to a feature branch.
|
||||
|
||||
### 2. Find the base branch
|
||||
|
||||
```bash
|
||||
git remote show origin | grep "HEAD branch"
|
||||
```
|
||||
|
||||
This is typically `main` or `master`.
|
||||
|
||||
### 3. Analyze recent commits relevant to this PR
|
||||
|
||||
```bash
|
||||
git log origin/main..HEAD --oneline --no-decorate
|
||||
```
|
||||
|
||||
Review these commits to understand:
|
||||
- What changes are being introduced
|
||||
- The scope of the PR (single feature/fix or multiple changes)
|
||||
- Whether commits should be squashed or reorganized
|
||||
|
||||
### 4. Review the diff
|
||||
|
||||
```bash
|
||||
git diff origin/main..HEAD --stat
|
||||
```
|
||||
|
||||
This shows which files changed and helps identify the type of change.
|
||||
|
||||
## Information Gathering
|
||||
|
||||
Before creating the PR, you need the following information. Check if it can be inferred from:
|
||||
- Commit messages
|
||||
- Branch name (e.g., `fix/issue-123`, `feature/new-login`)
|
||||
- Changed files and their content
|
||||
|
||||
If any critical information is missing, use `ask_followup_question` to ask the user:
|
||||
|
||||
### Required Information
|
||||
|
||||
1. **Related Issue Number**: Look for patterns like `#123`, `fixes #123`, or `closes #123` in commit messages
|
||||
2. **Description**: What problem does this solve? Why were these changes made?
|
||||
3. **Type of Change**: Bug fix, new feature, breaking change, refactor, cosmetic, documentation, or workflow
|
||||
4. **Test Procedure**: How was this tested? What could break?
|
||||
|
||||
### Example clarifying question
|
||||
|
||||
If the issue number is not found:
|
||||
> I couldn't find a related issue number in the commit messages or branch name. What GitHub issue does this PR address? (Enter the issue number, e.g., "123" or "N/A" for small fixes)
|
||||
|
||||
## Git Best Practices
|
||||
|
||||
Before creating the PR, consider these best practices:
|
||||
|
||||
### Commit Hygiene
|
||||
|
||||
1. **Atomic commits**: Each commit should represent a single logical change
|
||||
2. **Clear commit messages**: Follow conventional commit format when possible
|
||||
3. **No merge commits**: Prefer rebasing over merging to keep history clean
|
||||
|
||||
### Branch Management
|
||||
|
||||
1. **Rebase on latest main** (if needed):
|
||||
```bash
|
||||
git fetch origin
|
||||
git rebase origin/main
|
||||
```
|
||||
|
||||
2. **Squash if appropriate**: If there are many small "WIP" commits, consider interactive rebase:
|
||||
```bash
|
||||
git rebase -i origin/main
|
||||
```
|
||||
Only suggest this if commits appear messy and the user is comfortable with rebasing.
|
||||
|
||||
### Push Changes
|
||||
|
||||
Ensure all commits are pushed:
|
||||
```bash
|
||||
git push origin HEAD
|
||||
```
|
||||
|
||||
If the branch was rebased, you may need:
|
||||
```bash
|
||||
git push origin HEAD --force-with-lease
|
||||
```
|
||||
|
||||
## Create the Pull Request
|
||||
|
||||
**IMPORTANT**: Read and use the PR template at `.github/pull_request_template.md`. The PR body format must **strictly match** the template structure. Do not deviate from the template format.
|
||||
|
||||
When filling out the template:
|
||||
- Replace `#XXXX` with the actual issue number, or keep as `#XXXX` if no issue exists (for small fixes)
|
||||
- Fill in all sections with relevant information gathered from commits and context
|
||||
- Mark the appropriate "Type of Change" checkbox(es)
|
||||
- Complete the "Pre-flight Checklist" items that apply
|
||||
|
||||
### Create PR with gh CLI
|
||||
|
||||
```bash
|
||||
gh pr create --title "PR_TITLE" --body "PR_BODY" --base main
|
||||
```
|
||||
|
||||
Alternatively, create as draft if the user wants review before marking ready:
|
||||
```bash
|
||||
gh pr create --title "PR_TITLE" --body "PR_BODY" --base main --draft
|
||||
```
|
||||
|
||||
## Post-Creation
|
||||
|
||||
After creating the PR:
|
||||
|
||||
1. **Display the PR URL** so the user can review it
|
||||
2. **Remind about CI checks**: Tests and linting will run automatically
|
||||
3. **Suggest next steps**:
|
||||
- Add reviewers if needed: `gh pr edit --add-reviewer USERNAME`
|
||||
- Add labels if needed: `gh pr edit --add-label "bug"`
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **No commits ahead of main**: The branch has no changes to submit
|
||||
- Ask if the user meant to work on a different branch
|
||||
|
||||
2. **Branch not pushed**: Remote doesn't have the branch
|
||||
- Push the branch first: `git push -u origin HEAD`
|
||||
|
||||
3. **PR already exists**: A PR for this branch already exists
|
||||
- Show the existing PR: `gh pr view`
|
||||
- Ask if they want to update it instead
|
||||
|
||||
4. **Merge conflicts**: Branch conflicts with base
|
||||
- Guide user through resolving conflicts or rebasing
|
||||
|
||||
## Summary Checklist
|
||||
|
||||
Before finalizing, ensure:
|
||||
- [ ] `gh` CLI is installed and authenticated
|
||||
- [ ] Working directory is clean
|
||||
- [ ] All commits are pushed
|
||||
- [ ] Branch is up-to-date with base branch
|
||||
- [ ] Related issue number is identified, or placeholder is used
|
||||
- [ ] PR description follows the template exactly
|
||||
- [ ] Appropriate type of change is selected
|
||||
- [ ] Pre-flight checklist items are addressed
|
||||
+3
-3
@@ -72,12 +72,12 @@ POSTHOG_TELEMETRY_ENABLED=true # Enable PostHog telemetry (default: tru
|
||||
# Example configurations:
|
||||
#
|
||||
# Console debugging (logs only):
|
||||
# OTEL_TELEMETRY_ENABLED=1
|
||||
# OTEL_TELEMETRY_ENABLED=true
|
||||
# OTEL_LOGS_EXPORTER=console
|
||||
# TEL_DEBUG_DIAGNOSTICS=true
|
||||
#
|
||||
# OTLP with gRPC (insecure, for local testing):
|
||||
# OTEL_TELEMETRY_ENABLED=1
|
||||
# OTEL_TELEMETRY_ENABLED=true
|
||||
# OTEL_LOGS_EXPORTER=otlp
|
||||
# OTEL_EXPORTER_OTLP_PROTOCOL=grpc
|
||||
# OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317
|
||||
@@ -85,7 +85,7 @@ POSTHOG_TELEMETRY_ENABLED=true # Enable PostHog telemetry (default: tru
|
||||
# OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer your-token
|
||||
#
|
||||
# OTLP with HTTP/JSON (production):
|
||||
# OTEL_TELEMETRY_ENABLED=1
|
||||
# OTEL_TELEMETRY_ENABLED=true
|
||||
# OTEL_LOGS_EXPORTER=otlp
|
||||
# OTEL_EXPORTER_OTLP_PROTOCOL=http/json
|
||||
# OTEL_EXPORTER_OTLP_ENDPOINT=https://otel.example.com
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
name: Cline PR Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
[opened, ready_for_review]
|
||||
# Manual trigger for backfilling existing PRs. Run from terminal:
|
||||
# gh workflow run cline-pr-review.yml -f pr_number=1234
|
||||
# Or batch process open PRs:
|
||||
# gh pr list --state open --limit 10 --json number --jq '.[].number' | while read num; do
|
||||
# gh workflow run cline-pr-review.yml -f pr_number=$num
|
||||
# sleep 60
|
||||
# done
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: "PR number to review"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: pr-review-${{ github.event.pull_request.number || inputs.pr_number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
cline-pr-review:
|
||||
# Runs on PR opened/ready_for_review (skips drafts) or manual trigger for backfilling
|
||||
if: |
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
|
||||
github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
# SECURITY: These permissions are intentionally restrictive.
|
||||
# - contents: read -> cline can read the codebase but CANNOT write/push any code
|
||||
# - pull-requests: write -> cline can post reviews and inline suggestions
|
||||
# - issues: read -> cline can search for related issues
|
||||
# NOTE: Even with pull-requests: write, cline CANNOT merge PRs because branch protection
|
||||
# requires 1 approval from a Code Owner. The GITHUB_TOKEN cannot bypass this.
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
|
||||
- name: Install and Verify Cline CLI
|
||||
run: |
|
||||
npx cline version # verify installation
|
||||
|
||||
- name: Configure Cline with Anthropic
|
||||
run: |
|
||||
npx cline auth --provider anthropic \
|
||||
--apikey "${{ secrets.ANTHROPIC_API_KEY }}" \
|
||||
--modelid claude-opus-4-5-20251101
|
||||
|
||||
- name: Get PR number
|
||||
id: pr
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
echo "number=${{ inputs.pr_number }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Review PR with Cline
|
||||
env:
|
||||
PR_NUMBER: ${{ steps.pr.outputs.number }}
|
||||
GITHUB_REPO: ${{ github.repository }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
CLINE_COMMAND_PERMISSIONS: |
|
||||
{
|
||||
"allow": [
|
||||
"gh pr diff *",
|
||||
"gh pr view *",
|
||||
"gh pr checks *",
|
||||
"gh pr list *",
|
||||
"gh label list *",
|
||||
"gh issue list *",
|
||||
"gh issue view *",
|
||||
"git log *",
|
||||
"gh pr comment ${{ steps.pr.outputs.number }} *",
|
||||
"gh pr edit ${{ steps.pr.outputs.number }} *",
|
||||
"gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/comments *",
|
||||
"gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/reviews *"
|
||||
]
|
||||
}
|
||||
run: |
|
||||
npx cline --yolo 'You'\''re a GitHub PR reviewer for the open source Cline repository. Your goal is to give the PR author helpful feedback and give maintainers the context they need to review efficiently.
|
||||
|
||||
PR: #'"${PR_NUMBER}"'
|
||||
|
||||
## Gather context
|
||||
|
||||
```bash
|
||||
# Get full PR details
|
||||
gh pr view '"${PR_NUMBER}"' --json number,title,body,author,createdAt,updatedAt,isDraft,labels,commits,files,additions,deletions,changedFiles,baseRefName,headRefName,mergeable,reviewDecision
|
||||
|
||||
# Get the diff
|
||||
gh pr diff '"${PR_NUMBER}"'
|
||||
|
||||
# Check CI status
|
||||
gh pr checks '"${PR_NUMBER}"'
|
||||
|
||||
# Get existing review comments (to understand context and your previous feedback)
|
||||
gh api repos/'"${GITHUB_REPO}"'/pulls/'"${PR_NUMBER}"'/comments --jq '\''.[] | {user: .user.login, body: .body, path: .path, created_at: .created_at}'\''
|
||||
|
||||
# Get conversation comments
|
||||
gh pr view '"${PR_NUMBER}"' --comments
|
||||
```
|
||||
|
||||
If this is a re-review (workflow_dispatch event):
|
||||
Read your previous comments carefully. Understand what you asked for before.
|
||||
Check if new commits or comments address your previous feedback.
|
||||
|
||||
## Check contributing guidelines
|
||||
|
||||
Flag (but don'\''t block) if:
|
||||
- Missing changeset - For user-facing changes, check if there'\''s a `.changeset/` file:
|
||||
```bash
|
||||
gh pr diff '"${PR_NUMBER}"' --name-only | grep '\''.changeset/'\'' || echo '\''No changeset found'\''
|
||||
```
|
||||
If missing, ask them to run `npm run changeset`
|
||||
- Missing tests - New features should have tests
|
||||
|
||||
## Find related issues and PRs
|
||||
|
||||
Search thoroughly for context that might help with the review:
|
||||
|
||||
```bash
|
||||
# Find related issues for context
|
||||
gh issue list --search '\''<keywords from the PR>'\'' --state all --limit 30
|
||||
gh issue list --search '\''<error messages or feature names>'\'' --state all --limit 20
|
||||
|
||||
# Find similar PRs for reference
|
||||
gh pr list --search '\''<keywords>'\'' --state all --limit 30
|
||||
```
|
||||
|
||||
For each relevant issue or PR you find, read it including comments:
|
||||
```bash
|
||||
gh issue view <number> --comments
|
||||
gh pr view <number> --comments
|
||||
```
|
||||
|
||||
Look for:
|
||||
- Open issues this PR might fix that weren'\''t linked in the description
|
||||
- Similar PRs that went through review - what feedback did they get? What patterns did they follow?
|
||||
- Context from maintainer discussions that could inform your review
|
||||
|
||||
## Find subject matter experts
|
||||
|
||||
For files changed in this PR, find who knows the code best:
|
||||
```bash
|
||||
# Get files changed
|
||||
gh pr diff '"${PR_NUMBER}"' --name-only
|
||||
|
||||
# For each relevant path, find contributors
|
||||
git log --since='\''6 months ago'\'' --format='\''%an'\'' -- <path> | sort | uniq -c | sort -rn | head -5
|
||||
```
|
||||
|
||||
Cross-reference git authors with GitHub usernames. Include an SME table in your response:
|
||||
|
||||
| SME | Reason |
|
||||
|-----|--------|
|
||||
| @username1 | Authored PR #X which modified this area |
|
||||
| @username2 | Primary contributor to affected file (15 commits in 6 months) |
|
||||
| @username3 | Reviewed similar PR #Y with extensive feedback |
|
||||
|
||||
## Bash command usage
|
||||
|
||||
Don'\''t use operators like `|`, `&&`, or `;` - run each command separately and analyze the output.
|
||||
|
||||
When referencing command outputs, quote them properly to avoid formatting issues.
|
||||
|
||||
## Deep code review
|
||||
|
||||
This is the most important part. Don'\''t just look for syntax issues - understand what the PR is trying to achieve and whether the implementation is the right approach.
|
||||
|
||||
Step 1: Understand the intent
|
||||
Read the PR description and understand what the author is trying to accomplish. What problem are they solving? What feature are they adding?
|
||||
|
||||
Step 2: Form your own opinion first
|
||||
Before analyzing their code, think about how YOU would implement this feature or fix. What files would you touch? What patterns would you follow? What edge cases would you handle?
|
||||
|
||||
Step 3: Compare approaches
|
||||
Now look at their implementation. How does it compare to what you would have done?
|
||||
- Is their approach better in some ways? Note what they did well.
|
||||
- Is their approach missing something? Be specific about what and why.
|
||||
- Are there edge cases they haven'\''t considered?
|
||||
- Does it follow the patterns established in similar parts of the codebase?
|
||||
|
||||
Step 4: Look at the bigger picture
|
||||
- What other files or systems does this change interact with?
|
||||
- Could this break anything else?
|
||||
- Is there additional work needed beyond this PR to complete the feature?
|
||||
- Does this fit well with the overall architecture?
|
||||
|
||||
Step 5: Find reference implementations
|
||||
Look for similar changes in the codebase:
|
||||
```bash
|
||||
git log --oneline --all --grep='\''<relevant keywords>'\'' | head -20
|
||||
git log --oneline -- <similar files> | head -20
|
||||
```
|
||||
|
||||
If this is adding a new API provider, look at how other providers are implemented.
|
||||
If this is adding a new feature, look at how similar features were added.
|
||||
Note where their implementation aligns with or diverges from established patterns.
|
||||
|
||||
Step 6: Standard code review checks
|
||||
- DRY: Is there duplicated code that could be extracted?
|
||||
- Error handling: Are errors handled appropriately?
|
||||
- Security: Any injection risks, credential exposure, unsafe dependencies?
|
||||
- Performance: Any obvious inefficiencies, memory leaks, N+1 patterns?
|
||||
- Types: Is TypeScript used correctly? Any unsafe type assertions?
|
||||
- Naming: Are variables and functions named clearly?
|
||||
- Comments: Is complex logic explained? Are there outdated comments?
|
||||
|
||||
## Inline code suggestions
|
||||
|
||||
For specific code improvements, use GitHub'\''s suggestion syntax via `gh api`.
|
||||
This creates suggestions the author can commit with one click.
|
||||
|
||||
Single-line suggestion:
|
||||
```bash
|
||||
gh api repos/'"${GITHUB_REPO}"'/pulls/'"${PR_NUMBER}"'/reviews \
|
||||
-X POST \
|
||||
-f commit_id="$(gh pr view '"${PR_NUMBER}"' --json headRefOid -q .headRefOid)" \
|
||||
-f event='\''COMMENT'\'' \
|
||||
-f body='\'''\'' \
|
||||
-F comments='\''[
|
||||
{
|
||||
"path": "src/example.ts",
|
||||
"line": 42,
|
||||
"body": "Consider simplifying:\n\n```suggestion\nconst result = items.filter(Boolean);\n```"
|
||||
}
|
||||
]'\''
|
||||
```
|
||||
|
||||
Multi-line suggestion (replacing lines 40-45):
|
||||
```bash
|
||||
gh api repos/'"${GITHUB_REPO}"'/pulls/'"${PR_NUMBER}"'/reviews \
|
||||
-X POST \
|
||||
-f commit_id="$(gh pr view '"${PR_NUMBER}"' --json headRefOid -q .headRefOid)" \
|
||||
-f event='\''COMMENT'\'' \
|
||||
-f body='\'''\'' \
|
||||
-F comments='\''[
|
||||
{
|
||||
"path": "src/example.ts",
|
||||
"start_line": 40,
|
||||
"line": 45,
|
||||
"body": "This can be simplified:\n\n```suggestion\nconst simplified = doThing();\n```"
|
||||
}
|
||||
]'\''
|
||||
```
|
||||
|
||||
Use inline suggestions for concrete improvements. Use regular comments for questions or broader feedback.
|
||||
|
||||
## Post your review
|
||||
|
||||
After your investigation, post a single helpful comment that helps the author and gives maintainers context.
|
||||
|
||||
Start with a warm thank you for their contribution. Be conversational, not robotic.
|
||||
|
||||
Include what'\''s relevant:
|
||||
- In-depth explanation of what the PR does - Be comprehensive. A maintainer should be able to read this section and fully understand the author'\''s intent, why they made the changes, how they implemented it, and what files/systems are affected. Don'\''t just summarize - explain.
|
||||
- Related issues/PRs you found that provide useful context (link to them)
|
||||
- Your review findings (issues to address, suggestions, etc.)
|
||||
- Clear next steps for the author
|
||||
|
||||
Include a '\''For Maintainers'\'' section with:
|
||||
- Anything else useful to help the maintainer resolve this PR
|
||||
- Related issues/PRs with context on why they'\''re relevant
|
||||
- Open issues this PR might fix that weren'\''t linked in the description
|
||||
- Your recommendation: merge as-is, needs changes, needs discussion, close, etc.
|
||||
- SME table - who should review this and why
|
||||
|
||||
For the SME table:
|
||||
| SME | Reason |
|
||||
|-----|--------|
|
||||
| @username | Primary contributor to affected files |
|
||||
|
||||
## Update labels
|
||||
|
||||
Add appropriate labels based on your analysis:
|
||||
```bash
|
||||
gh label list --json name,description --limit 100
|
||||
gh pr edit '"${PR_NUMBER}"' --add-label '\''label1,label2'\''
|
||||
```
|
||||
|
||||
When done, add the reviewed label:
|
||||
```bash
|
||||
gh pr edit '"${PR_NUMBER}"' --add-label '\''Bot Reviewed'\''
|
||||
```
|
||||
|
||||
## Remember
|
||||
|
||||
- This is a one-time automated response - you will NOT see their reply or respond again. Never say things like '\''let me know if you have questions'\'', '\''I can help you with'\'', or '\''feel free to ask'\'' - you won'\''t be there to follow up. Frame any questions for the maintainers who will follow up.
|
||||
- Be helpful and welcoming - Many contributors are new to the project
|
||||
- Be specific - Point to exact lines and suggest fixes, don'\''t give vague feedback
|
||||
- Think deeply - Don'\''t just surface-level review, understand the intent and evaluate the approach
|
||||
- Use inline suggestions - Make it easy for authors to accept changes
|
||||
- You'\''re a first-pass reviewer - A human maintainer will do final approval'
|
||||
@@ -41,3 +41,8 @@ webview-ui/src/services/grpc-client.ts
|
||||
|
||||
# E2E Tests
|
||||
test-results
|
||||
|
||||
/.github/act
|
||||
/pkg
|
||||
.secrets
|
||||
|
||||
|
||||
+58
-1
@@ -1,8 +1,65 @@
|
||||
# Changelog
|
||||
|
||||
## [3.51.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Adding OpenAI gpt-5.2-codex model to the model picker
|
||||
|
||||
## [3.50.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Add gpt-5.2-codex OpenAI model support
|
||||
- Add create-pull-request skill
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix the selection of remotely configured providers
|
||||
- Fix act_mode_respond to prevent consecutive calls
|
||||
- Fix invalid tool call IDs when switching between model formats
|
||||
|
||||
## [3.49.1]
|
||||
|
||||
### Added
|
||||
|
||||
- Add telemetry to track usage of skills feature
|
||||
- Add version headers to Cline backend requests
|
||||
- Phase in Responses API usage instead of defaulting for every supported model
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix workflow slash command search to be case-insensitive
|
||||
- Fix model display in ModelPickerModal when using LiteLLM
|
||||
- Fix LiteLLM model fetching with default base URL
|
||||
- Fix crash when OpenAI-compatible APIs send usage chunks with empty or null choices arrays at end of streaming
|
||||
- Fix model ID for Kat Coder Pro Free model
|
||||
|
||||
## [3.49.0]
|
||||
|
||||
- Enable configuring an OTEL collector at runtime
|
||||
- Removing Minimax-2.1 from free model list as the free trial has ended
|
||||
- Improved image display in MCP responses
|
||||
- Auto-sync remote MCP servers from remote config to local settings
|
||||
|
||||
## [3.48.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Add Skills system for reusable, on-demand agent instructions
|
||||
- Add new websearch tooling in Cline provider
|
||||
- Add zai-glm-4.7 to Cerebras model list
|
||||
- Add model refresh and improve reasoning support for Vercel AI Gateway
|
||||
|
||||
### Fixed
|
||||
|
||||
- Revert #8341 due to regressions in diff view/document truncation (see #8423, #8429)
|
||||
- Fixed extension crash when using context menu selector
|
||||
|
||||
## [3.47.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Added experimental support for Background Edits (allows editing files in background without opening the diff view)
|
||||
- Updated free model to MiniMax M2.1 (replacing MiniMax M2)
|
||||
- Added support for Azure based identity authentication in OpenAI Compatible provider and Azure OpenAI
|
||||
@@ -1680,4 +1737,4 @@ Add Opus 4.1 through Claude Code
|
||||
|
||||
## [0.0.6]
|
||||
|
||||
- Initial release
|
||||
- Initial release
|
||||
@@ -127,3 +127,32 @@ const isGenerating = explanationInfo.status === "generating" && !wasCancelled
|
||||
**See also:** `BrowserSessionRow.tsx` uses similar pattern with `isLastApiReqInterrupted` and `isLastMessageResume`.
|
||||
|
||||
**Backend side:** When streaming is cancelled, clean up properly (close tabs, clear comments, etc.) by checking `taskState.abort` after the streaming function returns.
|
||||
|
||||
## Parallel Command Execution
|
||||
When multiple `execute_command` tools run in parallel (via `PARALLEL_SAFE_TOOLS`), commands cannot use `ask()` for interactive output handling—multiple concurrent `ask()` calls cause conflicts because they share single state variables in TaskState.
|
||||
|
||||
**The Solution - Queue-Based Ask/Response System:**
|
||||
- `PendingAskQueue` (`src/core/task/PendingAskQueue.ts`) - Manages concurrent ask operations with unique IDs per ask
|
||||
- `TaskState.pendingAskQueue` - Replaces single `askResponse`/`askResponseText`/`askResponseImages`/`askResponseFiles`
|
||||
- `Task.ask()` - Refactored to create unique ask IDs and wait for specific responses
|
||||
- `Task.handleWebviewAskResponse()` - Resolves asks in FIFO order (first pending ask gets the response)
|
||||
|
||||
**Concurrent Command Orchestration:**
|
||||
- `ConcurrentCommandOrchestrator` (`src/integrations/terminal/ConcurrentCommandOrchestrator.ts`) - Alternative to `CommandOrchestrator`
|
||||
- Does NOT call `ask()` on each output chunk—streams via `say()` instead
|
||||
- No "Proceed While Running" button (not needed for parallel execution)
|
||||
- Used when `taskState.isExecutingInParallel = true`
|
||||
|
||||
**How It Works:**
|
||||
1. When parallel tools execute, flags are set: `taskState.isExecutingInParallel = true` and `commandExecutor.setParallelExecution(true)`
|
||||
2. Each command's orchestrator is selected based on these flags
|
||||
3. In parallel mode, output is streamed directly without asking for user input on each chunk
|
||||
4. Single user response (if needed) is queued and distributed to waiting asks via `PendingAskQueue`
|
||||
5. Flags are cleared in a `finally` block to ensure proper cleanup
|
||||
|
||||
**Key Files:**
|
||||
- `src/core/task/PendingAskQueue.ts` - Queue implementation
|
||||
- `src/integrations/terminal/ConcurrentCommandOrchestrator.ts` - Parallel-safe orchestrator
|
||||
- `src/core/task/TaskState.ts` - Added `pendingAskQueue` and `isExecutingInParallel`
|
||||
- `src/core/task/index.ts` - Refactored `ask()`, `handleWebviewAskResponse()`, and parallel execution logic
|
||||
- `src/integrations/terminal/CommandExecutor.ts` - Added `setParallelExecution()` and orchestrator selection
|
||||
|
||||
@@ -358,6 +358,9 @@ func AddProviderPartial(ctx context.Context, manager *task.Manager, provider cli
|
||||
if openRouterInfo, ok := modelInfo.(*cline.OpenRouterModelInfo); ok {
|
||||
apiConfig.PlanModeOpenRouterModelInfo = openRouterInfo
|
||||
apiConfig.ActModeOpenRouterModelInfo = openRouterInfo
|
||||
} else if ocaInfo, ok := modelInfo.(*cline.OcaModelInfo); ok {
|
||||
apiConfig.PlanModeOcaModelInfo = ocaInfo
|
||||
apiConfig.ActModeOcaModelInfo = ocaInfo
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,6 +429,9 @@ func UpdateProviderPartial(ctx context.Context, manager *task.Manager, provider
|
||||
if openRouterInfo, ok := updates.ModelInfo.(*cline.OpenRouterModelInfo); ok {
|
||||
apiConfig.PlanModeOpenRouterModelInfo = openRouterInfo
|
||||
apiConfig.ActModeOpenRouterModelInfo = openRouterInfo
|
||||
} else if ocaInfo, ok := updates.ModelInfo.(*cline.OcaModelInfo); ok {
|
||||
apiConfig.PlanModeOcaModelInfo = ocaInfo
|
||||
apiConfig.ActModeOcaModelInfo = ocaInfo
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -190,7 +190,7 @@ func (pw *ProviderWizard) handleAddOcaProvider() error {
|
||||
}
|
||||
|
||||
// Step 3: Select model
|
||||
modelID, _, err := pw.selectModel(cline.ApiProvider_OCA, "")
|
||||
modelID, modelInfo, err := pw.selectModel(cline.ApiProvider_OCA, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("model selection failed: %w", err)
|
||||
}
|
||||
@@ -198,7 +198,7 @@ func (pw *ProviderWizard) handleAddOcaProvider() error {
|
||||
// Step 4: Apply the OCA model configuration and set as active
|
||||
updates := ProviderUpdatesPartial{
|
||||
ModelID: &modelID,
|
||||
ModelInfo: nil,
|
||||
ModelInfo: modelInfo,
|
||||
}
|
||||
|
||||
if err := UpdateProviderPartial(pw.ctx, pw.manager, cline.ApiProvider_OCA, updates, true); err != nil {
|
||||
|
||||
@@ -37,11 +37,16 @@ var (
|
||||
|
||||
func InitializeGlobalConfig(cfg *GlobalConfig) error {
|
||||
if cfg.ConfigPath == "" {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get home directory: %w", err)
|
||||
// Check CLINE_DIR environment variable first
|
||||
if clineDir := os.Getenv("CLINE_DIR"); clineDir != "" {
|
||||
cfg.ConfigPath = clineDir
|
||||
} else {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get home directory: %w", err)
|
||||
}
|
||||
cfg.ConfigPath = filepath.Join(homeDir, ".cline")
|
||||
}
|
||||
cfg.ConfigPath = filepath.Join(homeDir, ".cline")
|
||||
}
|
||||
|
||||
// Ensure .cline directory exists
|
||||
|
||||
@@ -58,59 +58,59 @@ Enable OpenTelemetry and configure an OTLP endpoint:
|
||||
|
||||
```bash
|
||||
# Enable OpenTelemetry
|
||||
export OTEL_TELEMETRY_ENABLED=1
|
||||
export CLINE_OTEL_TELEMETRY_ENABLED=true
|
||||
|
||||
# Configure metrics and logs export
|
||||
export OTEL_METRICS_EXPORTER=otlp
|
||||
export OTEL_LOGS_EXPORTER=otlp
|
||||
export CLINE_OTEL_METRICS_EXPORTER=otlp
|
||||
export CLINE_OTEL_LOGS_EXPORTER=otlp
|
||||
|
||||
# Set your OTLP endpoint
|
||||
export OTEL_EXPORTER_OTLP_ENDPOINT=https://your-collector:4317
|
||||
export CLINE_OTEL_EXPORTER_OTLP_ENDPOINT=https://your-collector:4317
|
||||
|
||||
# Optional: Set protocol (default is grpc)
|
||||
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
|
||||
export CLINE_OTEL_EXPORTER_OTLP_PROTOCOL=grpc
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `OTEL_TELEMETRY_ENABLED` | Enable OpenTelemetry (`1` or `true`) | Disabled |
|
||||
| `OTEL_METRICS_EXPORTER` | Metrics exporter type (`console`, `otlp`, or both) | None |
|
||||
| `OTEL_LOGS_EXPORTER` | Logs exporter type (`console`, `otlp`, or both) | None |
|
||||
| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP endpoint URL | None |
|
||||
| `OTEL_EXPORTER_OTLP_PROTOCOL` | Protocol (`grpc`, `http/protobuf`, `http/json`) | `grpc` |
|
||||
| `OTEL_EXPORTER_OTLP_INSECURE` | Allow insecure connections | `false` |
|
||||
| `OTEL_EXPORTER_OTLP_HEADERS` | Custom headers (comma-separated `key=value` pairs) | None |
|
||||
| `CLINE_OTEL_TELEMETRY_ENABLED` | Enable OpenTelemetry (`true`) | Disabled |
|
||||
| `CLINE_OTEL_METRICS_EXPORTER` | Metrics exporter type (`console`, `otlp`, or both) | None |
|
||||
| `CLINE_OTEL_LOGS_EXPORTER` | Logs exporter type (`console`, `otlp`, or both) | None |
|
||||
| `CLINE_OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP endpoint URL | None |
|
||||
| `CLINE_OTEL_EXPORTER_OTLP_PROTOCOL` | Protocol (`grpc`, `http/protobuf`, `http/json`) | `grpc` |
|
||||
| `CLINE_OTEL_EXPORTER_OTLP_INSECURE` | Allow insecure connections | `false` |
|
||||
| `CLINE_OTEL_EXPORTER_OTLP_HEADERS` | Custom headers (comma-separated `key=value` pairs) | None |
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
**Separate endpoints for metrics and logs:**
|
||||
```bash
|
||||
export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=https://metrics-collector:4317
|
||||
export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://logs-collector:4317
|
||||
export CLINE_OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=https://metrics-collector:4317
|
||||
export CLINE_OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://logs-collector:4317
|
||||
```
|
||||
|
||||
**Custom headers for authentication:**
|
||||
```bash
|
||||
export OTEL_EXPORTER_OTLP_HEADERS="api-key=your-key,x-custom-header=value"
|
||||
export CLINE_OTEL_EXPORTER_OTLP_HEADERS="api-key=your-key,x-custom-header=value"
|
||||
```
|
||||
|
||||
**Multiple exporters (console + OTLP):**
|
||||
```bash
|
||||
export OTEL_METRICS_EXPORTER=console,otlp
|
||||
export OTEL_LOGS_EXPORTER=console,otlp
|
||||
export CLINE_OTEL_METRICS_EXPORTER=console,otlp
|
||||
export CLINE_OTEL_LOGS_EXPORTER=console,otlp
|
||||
```
|
||||
|
||||
**Export intervals:**
|
||||
```bash
|
||||
# Metrics export interval in milliseconds (default: 60000)
|
||||
export OTEL_METRIC_EXPORT_INTERVAL=30000
|
||||
export CLINE_OTEL_METRIC_EXPORT_INTERVAL=30000
|
||||
|
||||
# Logs batch size and timeout
|
||||
export OTEL_LOG_BATCH_SIZE=512
|
||||
export OTEL_LOG_BATCH_TIMEOUT=5000
|
||||
export OTEL_LOG_MAX_QUEUE_SIZE=2048
|
||||
export CLINE_OTEL_LOG_BATCH_SIZE=512
|
||||
export CLINE_OTEL_LOG_BATCH_TIMEOUT=5000
|
||||
export CLINE_OTEL_LOG_MAX_QUEUE_SIZE=2048
|
||||
```
|
||||
|
||||
## Integration Examples
|
||||
@@ -120,11 +120,11 @@ export OTEL_LOG_MAX_QUEUE_SIZE=2048
|
||||
Export to Datadog using their OTLP endpoint:
|
||||
|
||||
```bash
|
||||
export OTEL_TELEMETRY_ENABLED=1
|
||||
export OTEL_METRICS_EXPORTER=otlp
|
||||
export OTEL_LOGS_EXPORTER=otlp
|
||||
export OTEL_EXPORTER_OTLP_ENDPOINT=https://api.datadoghq.com
|
||||
export OTEL_EXPORTER_OTLP_HEADERS="dd-api-key=YOUR_DD_API_KEY"
|
||||
export CLINE_OTEL_TELEMETRY_ENABLED=true
|
||||
export CLINE_OTEL_METRICS_EXPORTER=otlp
|
||||
export CLINE_OTEL_LOGS_EXPORTER=otlp
|
||||
export CLINE_OTEL_EXPORTER_OTLP_ENDPOINT=https://api.datadoghq.com
|
||||
export CLINE_OTEL_EXPORTER_OTLP_HEADERS="dd-api-key=YOUR_DD_API_KEY"
|
||||
```
|
||||
|
||||
### New Relic
|
||||
@@ -132,11 +132,11 @@ export OTEL_EXPORTER_OTLP_HEADERS="dd-api-key=YOUR_DD_API_KEY"
|
||||
Export to New Relic:
|
||||
|
||||
```bash
|
||||
export OTEL_TELEMETRY_ENABLED=1
|
||||
export OTEL_METRICS_EXPORTER=otlp
|
||||
export OTEL_LOGS_EXPORTER=otlp
|
||||
export OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.nr-data.net:4317
|
||||
export OTEL_EXPORTER_OTLP_HEADERS="api-key=YOUR_NEW_RELIC_LICENSE_KEY"
|
||||
export CLINE_OTEL_TELEMETRY_ENABLED=true
|
||||
export CLINE_OTEL_METRICS_EXPORTER=otlp
|
||||
export CLINE_OTEL_LOGS_EXPORTER=otlp
|
||||
export CLINE_OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.nr-data.net:4317
|
||||
export CLINE_OTEL_EXPORTER_OTLP_HEADERS="api-key=YOUR_NEW_RELIC_LICENSE_KEY"
|
||||
```
|
||||
|
||||
### Grafana Cloud
|
||||
@@ -144,11 +144,11 @@ export OTEL_EXPORTER_OTLP_HEADERS="api-key=YOUR_NEW_RELIC_LICENSE_KEY"
|
||||
Export to Grafana Cloud:
|
||||
|
||||
```bash
|
||||
export OTEL_TELEMETRY_ENABLED=1
|
||||
export OTEL_METRICS_EXPORTER=otlp
|
||||
export OTEL_LOGS_EXPORTER=otlp
|
||||
export OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp-gateway-prod-us-central-0.grafana.net/otlp
|
||||
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic YOUR_BASE64_CREDENTIALS"
|
||||
export CLINE_OTEL_TELEMETRY_ENABLED=true
|
||||
export CLINE_OTEL_METRICS_EXPORTER=otlp
|
||||
export CLINE_OTEL_LOGS_EXPORTER=otlp
|
||||
export CLINE_OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp-gateway-prod-us-central-0.grafana.net/otlp
|
||||
export CLINE_OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic YOUR_BASE64_CREDENTIALS"
|
||||
```
|
||||
|
||||
|
||||
@@ -158,9 +158,9 @@ Test your configuration with console output before sending to a real endpoint:
|
||||
|
||||
```bash
|
||||
# Enable console output to see what data would be exported
|
||||
export OTEL_TELEMETRY_ENABLED=1
|
||||
export OTEL_METRICS_EXPORTER=console
|
||||
export OTEL_LOGS_EXPORTER=console
|
||||
export CLINE_OTEL_TELEMETRY_ENABLED=true
|
||||
export CLINE_OTEL_METRICS_EXPORTER=console
|
||||
export CLINE_OTEL_LOGS_EXPORTER=console
|
||||
```
|
||||
|
||||
Then launch Cline and check the console output for metrics and logs.
|
||||
@@ -171,20 +171,20 @@ Then launch Cline and check the console output for metrics and logs.
|
||||
|
||||
1. **Verify OpenTelemetry is enabled:**
|
||||
```bash
|
||||
echo $OTEL_TELEMETRY_ENABLED
|
||||
echo $CLINE_OTEL_TELEMETRY_ENABLED
|
||||
```
|
||||
Should output `1` or `true`
|
||||
Should output `true`
|
||||
|
||||
2. **Check exporters are configured:**
|
||||
```bash
|
||||
echo $OTEL_METRICS_EXPORTER
|
||||
echo $OTEL_LOGS_EXPORTER
|
||||
echo $CLINE_OTEL_METRICS_EXPORTER
|
||||
echo $CLINE_OTEL_LOGS_EXPORTER
|
||||
```
|
||||
|
||||
3. **Test with console exporter first:**
|
||||
```bash
|
||||
export OTEL_METRICS_EXPORTER=console
|
||||
export OTEL_LOGS_EXPORTER=console
|
||||
export CLINE_OTEL_METRICS_EXPORTER=console
|
||||
export CLINE_OTEL_LOGS_EXPORTER=console
|
||||
```
|
||||
|
||||
### Connection Errors
|
||||
@@ -196,7 +196,7 @@ Then launch Cline and check the console output for metrics and logs.
|
||||
|
||||
2. **Check if insecure mode is needed:**
|
||||
```bash
|
||||
export OTEL_EXPORTER_OTLP_INSECURE=true
|
||||
export CLINE_OTEL_EXPORTER_OTLP_INSECURE=true
|
||||
```
|
||||
|
||||
3. **Verify authentication headers:**
|
||||
|
||||
@@ -120,8 +120,38 @@ Controls a built-in browser to interact with websites or local servers. Useful f
|
||||
</browser_action>
|
||||
```
|
||||
|
||||
### Leverage MCP Tools
|
||||
You can use Model Context Protocol (MCP) tools within your workflows to interact with external services like GitHub, Slack, or databases. This allows you to create powerful end-to-end automations.
|
||||
### Leveraging MCP Tools
|
||||
|
||||
MCP tools allow Cline to interact with external services like GitHub, Slack, or databases. You can reference them in your workflows using natural language or explicit XML tags for deterministic control.
|
||||
|
||||
#### Natural Language (Heuristic)
|
||||
|
||||
Most of the time, the simplest way to use an MCP tool is to describe the action you want Cline to take.
|
||||
|
||||
```markdown
|
||||
1. Fetch the latest issues from the github-repo MCP server.
|
||||
2. Summarize the critical bugs.
|
||||
3. Post the summary to the #engineering channel using the slack-notifications MCP.
|
||||
```
|
||||
|
||||
#### Explicit XML Tag (Deterministic)
|
||||
|
||||
For critical automation where you need exact control over parameters, use the `use_mcp_tool` tag.
|
||||
|
||||
```xml
|
||||
<use_mcp_tool>
|
||||
<server_name>github-repo-manager</server_name>
|
||||
<tool_name>create_issue</tool_name>
|
||||
<arguments>
|
||||
{
|
||||
"owner": "cline",
|
||||
"repo": "cline",
|
||||
"title": "Automated Bug Report",
|
||||
"body": "Found a regression in the latest build."
|
||||
}
|
||||
</arguments>
|
||||
</use_mcp_tool>
|
||||
```
|
||||
|
||||
### Manage Context Window
|
||||
Be mindful of Cline's context window. If a workflow is too long or processes too much data, it might exceed the token limit.
|
||||
|
||||
@@ -16,20 +16,17 @@ Cline supports accessing models directly through the official OpenAI API.
|
||||
|
||||
### Supported Models
|
||||
|
||||
Cline is compatible with a variety of OpenAI models, including but not limited to:
|
||||
Cline is compatible with a variety of OpenAI models, including common choices from OpenAI's featured/frontier lists:
|
||||
|
||||
- 'o3'
|
||||
- `o3-mini` (medium reasoning effort)
|
||||
- 'o4-mini'
|
||||
- `o3-mini-high` (high reasoning effort)
|
||||
- `o3-mini-low` (low reasoning effort)
|
||||
- `o1`
|
||||
- `o1-preview`
|
||||
- `o1-mini`
|
||||
- `gpt-5.2`
|
||||
- `gpt-5.2-codex`
|
||||
- `gpt-5-mini`
|
||||
- `gpt-5-nano`
|
||||
- `gpt-4.1`
|
||||
- `gpt-4o`
|
||||
- `gpt-4o-mini`
|
||||
- 'gpt-4.1'
|
||||
- 'gpt-4.1-mini'
|
||||
- `o3`
|
||||
- `o4-mini`
|
||||
|
||||
For the most current list of available models and their capabilities, please refer to the official [OpenAI Models documentation](https://platform.openai.com/docs/models).
|
||||
|
||||
|
||||
Generated
+57
-39
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.47.0",
|
||||
"version": "3.51.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.47.0",
|
||||
"version": "3.51.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
@@ -1182,7 +1182,6 @@
|
||||
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.3",
|
||||
@@ -2645,7 +2644,6 @@
|
||||
"node_modules/@grpc/grpc-js": {
|
||||
"version": "1.9.15",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@grpc/proto-loader": "^0.7.8",
|
||||
"@types/node": ">=12.12.47"
|
||||
@@ -3229,7 +3227,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz",
|
||||
"integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.19.7",
|
||||
"ajv": "^8.17.1",
|
||||
@@ -3298,7 +3295,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
@@ -4914,7 +4910,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4927,7 +4924,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4940,7 +4938,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4953,7 +4952,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4966,7 +4966,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4979,7 +4980,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.52.4",
|
||||
@@ -4992,7 +4994,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.52.4",
|
||||
@@ -5005,7 +5008,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5018,7 +5022,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.52.4",
|
||||
@@ -5031,7 +5036,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5044,7 +5050,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5057,7 +5064,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5070,7 +5078,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.52.4",
|
||||
@@ -5083,7 +5092,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5096,7 +5106,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5109,7 +5120,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.52.4",
|
||||
@@ -5122,7 +5134,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -5135,7 +5148,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.52.4",
|
||||
@@ -5148,7 +5162,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.52.4",
|
||||
@@ -5161,7 +5176,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5174,7 +5190,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.52.4",
|
||||
@@ -5187,7 +5204,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@sap-ai-sdk/ai-api": {
|
||||
"version": "2.1.0",
|
||||
@@ -6750,7 +6768,8 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/get-folder-size": {
|
||||
"version": "3.0.4",
|
||||
@@ -6782,7 +6801,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.21.tgz",
|
||||
"integrity": "sha512-CsGG2P3I5y48RPMfprQGfy4JPRZ6csfC3ltBZSRItG3ngggmNY/qs2uZKp4p9VbrpqNNSMzUZNFZKzgOGnd/VA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
@@ -7486,7 +7504,6 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -8253,7 +8270,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.3",
|
||||
"caniuse-lite": "^1.0.30001741",
|
||||
@@ -9467,8 +9483,7 @@
|
||||
},
|
||||
"node_modules/devtools-protocol": {
|
||||
"version": "0.0.1342118",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "5.2.0",
|
||||
@@ -12444,7 +12459,6 @@
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
@@ -12678,7 +12692,6 @@
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
||||
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
|
||||
"license": "MPL-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
},
|
||||
@@ -15559,6 +15572,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -15579,6 +15593,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
@@ -16246,6 +16261,7 @@
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz",
|
||||
"integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -17671,6 +17687,7 @@
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3"
|
||||
@@ -17687,6 +17704,7 @@
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
@@ -18050,7 +18068,6 @@
|
||||
"version": "5.5.3",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -18310,6 +18327,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
|
||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -18384,6 +18402,7 @@
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
@@ -19079,7 +19098,6 @@
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
@@ -19094,4 +19112,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -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.47.0",
|
||||
"version": "3.51.0",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -403,6 +403,10 @@
|
||||
"storybook": "cd webview-ui && npm run storybook"
|
||||
},
|
||||
"lint-staged": {
|
||||
"src/shared/storage/state-keys.ts": [
|
||||
"node scripts/generate-state-proto.mjs",
|
||||
"git add proto/cline/state.proto"
|
||||
],
|
||||
"*": [
|
||||
"biome check --write --staged --no-errors-on-unmatched --files-ignore-unknown=true"
|
||||
]
|
||||
|
||||
+217
-170
@@ -54,183 +54,229 @@ message AutoApprovalSettings {
|
||||
optional bool enable_notifications = 3;
|
||||
}
|
||||
|
||||
// NOTE: Add the new secret fields under SECRETS_KEYS in src/shared/storage/state-keys.ts
|
||||
// and use the scripts/generate-state-proto.mjs script to regenerate this list.
|
||||
message Secrets {
|
||||
optional string api_key = 1;
|
||||
optional string open_router_api_key = 4;
|
||||
optional string aws_access_key = 5;
|
||||
optional string aws_secret_key = 6;
|
||||
optional string aws_session_token = 7;
|
||||
optional string aws_bedrock_api_key = 8;
|
||||
optional string open_ai_api_key = 9;
|
||||
optional string gemini_api_key = 10;
|
||||
optional string open_ai_native_api_key = 11;
|
||||
optional string ollama_api_key = 12;
|
||||
optional string deep_seek_api_key = 13;
|
||||
optional string requesty_api_key = 14;
|
||||
optional string together_api_key = 15;
|
||||
optional string fireworks_api_key = 16;
|
||||
optional string qwen_api_key = 17;
|
||||
optional string doubao_api_key = 18;
|
||||
optional string mistral_api_key = 19;
|
||||
optional string lite_llm_api_key = 20;
|
||||
optional string auth_nonce = 21;
|
||||
optional string asksage_api_key = 22;
|
||||
optional string xai_api_key = 23;
|
||||
optional string moonshot_api_key = 24;
|
||||
optional string zai_api_key = 25;
|
||||
optional string hugging_face_api_key = 26;
|
||||
optional string nebius_api_key = 27;
|
||||
optional string sambanova_api_key = 28;
|
||||
optional string cerebras_api_key = 29;
|
||||
optional string sap_ai_core_client_id = 30;
|
||||
optional string sap_ai_core_client_secret = 31;
|
||||
optional string groq_api_key = 32;
|
||||
optional string huawei_cloud_maas_api_key = 33;
|
||||
optional string baseten_api_key = 34;
|
||||
optional string vercel_ai_gateway_api_key = 35;
|
||||
optional string dify_api_key = 36;
|
||||
optional string oca_api_key = 37;
|
||||
optional string oca_refresh_token = 38;
|
||||
optional string hicap_api_key = 39;
|
||||
optional string mcp_oauth_secrets = 40;
|
||||
optional string cline_account_id = 2;
|
||||
optional string open_router_api_key = 3;
|
||||
optional string aws_access_key = 4;
|
||||
optional string aws_secret_key = 5;
|
||||
optional string aws_session_token = 6;
|
||||
optional string aws_bedrock_api_key = 7;
|
||||
optional string open_ai_api_key = 8;
|
||||
optional string gemini_api_key = 9;
|
||||
optional string open_ai_native_api_key = 10;
|
||||
optional string ollama_api_key = 11;
|
||||
optional string deep_seek_api_key = 12;
|
||||
optional string requesty_api_key = 13;
|
||||
optional string together_api_key = 14;
|
||||
optional string fireworks_api_key = 15;
|
||||
optional string qwen_api_key = 16;
|
||||
optional string doubao_api_key = 17;
|
||||
optional string mistral_api_key = 18;
|
||||
optional string lite_llm_api_key = 19;
|
||||
optional string auth_nonce = 20;
|
||||
optional string asksage_api_key = 21;
|
||||
optional string xai_api_key = 22;
|
||||
optional string moonshot_api_key = 23;
|
||||
optional string zai_api_key = 24;
|
||||
optional string hugging_face_api_key = 25;
|
||||
optional string nebius_api_key = 26;
|
||||
optional string sambanova_api_key = 27;
|
||||
optional string cerebras_api_key = 28;
|
||||
optional string sap_ai_core_client_id = 29;
|
||||
optional string sap_ai_core_client_secret = 30;
|
||||
optional string groq_api_key = 31;
|
||||
optional string huawei_cloud_maas_api_key = 32;
|
||||
optional string baseten_api_key = 33;
|
||||
optional string vercel_ai_gateway_api_key = 34;
|
||||
optional string dify_api_key = 35;
|
||||
optional string minimax_api_key = 36;
|
||||
optional string hicap_api_key = 37;
|
||||
optional string aihubmix_api_key = 38;
|
||||
optional string nous_research_api_key = 39;
|
||||
optional string remote_lite_llm_api_key = 40;
|
||||
optional string oca_api_key = 41;
|
||||
optional string oca_refresh_token = 42;
|
||||
optional string mcp_o_auth_secrets = 43;
|
||||
}
|
||||
|
||||
// NOTE: Add new fields under API_HANDLER_SETTINGS_FIELDS or USER_SETTINGS_FIELDS
|
||||
// in src/shared/storage/state-keys.ts and use the scripts/generate-state-proto.mjs
|
||||
// script to regenerate this list.
|
||||
message Settings {
|
||||
optional string aws_region = 1;
|
||||
optional bool aws_use_cross_region_inference = 2;
|
||||
optional bool aws_bedrock_use_prompt_cache = 3;
|
||||
optional string aws_bedrock_endpoint = 4;
|
||||
optional string aws_profile = 5;
|
||||
optional string aws_authentication = 6;
|
||||
optional bool aws_use_profile = 7;
|
||||
optional string vertex_project_id = 8;
|
||||
optional string vertex_region = 9;
|
||||
optional string requesty_base_url = 10;
|
||||
optional string open_ai_base_url = 11;
|
||||
// map<string, string> open_ai_headers = 12;
|
||||
optional string ollama_base_url = 13;
|
||||
optional string ollama_api_options_ctx_num = 14;
|
||||
optional string lm_studio_base_url = 15;
|
||||
optional string lm_studio_max_tokens = 16;
|
||||
optional string anthropic_base_url = 17;
|
||||
optional string gemini_base_url = 18;
|
||||
optional string azure_api_version = 19;
|
||||
optional string open_router_provider_sorting = 20;
|
||||
optional AutoApprovalSettings auto_approval_settings = 21;
|
||||
optional BrowserSettings browser_settings = 24;
|
||||
optional string lite_llm_base_url = 25;
|
||||
optional bool lite_llm_use_prompt_cache = 26;
|
||||
optional int32 fireworks_model_max_completion_tokens = 27;
|
||||
optional int32 fireworks_model_max_tokens = 28;
|
||||
optional string lite_llm_base_url = 1;
|
||||
optional bool lite_llm_use_prompt_cache = 2;
|
||||
map<string, string> open_ai_headers = 3;
|
||||
optional string anthropic_base_url = 4;
|
||||
optional string open_router_provider_sorting = 5;
|
||||
optional string aws_region = 6;
|
||||
optional bool aws_use_cross_region_inference = 7;
|
||||
optional bool aws_use_global_inference = 8;
|
||||
optional bool aws_bedrock_use_prompt_cache = 9;
|
||||
optional string aws_authentication = 10;
|
||||
optional bool aws_use_profile = 11;
|
||||
optional string aws_profile = 12;
|
||||
optional string aws_bedrock_endpoint = 13;
|
||||
optional string claude_code_path = 14;
|
||||
optional string vertex_project_id = 15;
|
||||
optional string vertex_region = 16;
|
||||
optional string open_ai_base_url = 17;
|
||||
optional string ollama_base_url = 18;
|
||||
optional string ollama_api_options_ctx_num = 19;
|
||||
optional string lm_studio_base_url = 20;
|
||||
optional string lm_studio_max_tokens = 21;
|
||||
optional string gemini_base_url = 22;
|
||||
optional string requesty_base_url = 23;
|
||||
optional int32 fireworks_model_max_completion_tokens = 24;
|
||||
optional int32 fireworks_model_max_tokens = 25;
|
||||
optional string qwen_code_oauth_path = 26;
|
||||
optional string azure_api_version = 27;
|
||||
optional bool azure_identity = 28;
|
||||
optional string qwen_api_line = 29;
|
||||
optional string moonshot_api_line = 30;
|
||||
optional string zai_api_line = 31;
|
||||
optional string telemetry_setting = 32;
|
||||
optional string asksage_api_url = 33;
|
||||
optional bool plan_act_separate_models_setting = 34;
|
||||
optional bool enable_checkpoints_setting = 35;
|
||||
optional int32 request_timeout_ms = 36;
|
||||
optional int32 shell_integration_timeout = 37;
|
||||
optional string default_terminal_profile = 38;
|
||||
optional int32 terminal_output_line_limit = 39;
|
||||
optional string sap_ai_core_token_url = 40;
|
||||
optional string sap_ai_core_base_url = 41;
|
||||
optional string sap_ai_resource_group = 42;
|
||||
optional bool sap_ai_core_use_orchestration_mode = 43;
|
||||
optional string claude_code_path = 44;
|
||||
optional string qwen_code_oauth_path = 45;
|
||||
optional bool strict_plan_mode_enabled = 46;
|
||||
optional bool yolo_mode_toggled = 47;
|
||||
optional bool use_auto_condense = 48;
|
||||
optional string preferred_language = 49;
|
||||
optional OpenaiReasoningEffort openai_reasoning_effort = 50;
|
||||
optional PlanActMode mode = 51;
|
||||
optional DictationSettings dictation_settings = 52;
|
||||
optional FocusChainSettings focus_chain_settings = 53;
|
||||
optional string custom_prompt = 54;
|
||||
optional string dify_base_url = 55;
|
||||
optional double auto_condense_threshold = 56;
|
||||
optional string oca_base_url = 57;
|
||||
optional ApiProvider plan_mode_api_provider = 58;
|
||||
optional string plan_mode_api_model_id = 59;
|
||||
optional int64 plan_mode_thinking_budget_tokens = 60;
|
||||
optional string plan_mode_reasoning_effort = 61;
|
||||
optional LanguageModelChatSelector plan_mode_vs_code_lm_model_selector = 62;
|
||||
optional bool plan_mode_aws_bedrock_custom_selected = 63;
|
||||
optional string plan_mode_aws_bedrock_custom_model_base_id = 64;
|
||||
optional string plan_mode_open_router_model_id = 65;
|
||||
optional OpenRouterModelInfo plan_mode_open_router_model_info = 66;
|
||||
optional string plan_mode_open_ai_model_id = 67;
|
||||
optional OpenAiCompatibleModelInfo plan_mode_open_ai_model_info = 68;
|
||||
optional string plan_mode_ollama_model_id = 69;
|
||||
optional string plan_mode_lm_studio_model_id = 70;
|
||||
optional string plan_mode_lite_llm_model_id = 71;
|
||||
optional LiteLLMModelInfo plan_mode_lite_llm_model_info = 72;
|
||||
optional string plan_mode_requesty_model_id = 73;
|
||||
optional OpenRouterModelInfo plan_mode_requesty_model_info = 74;
|
||||
optional string plan_mode_together_model_id = 75;
|
||||
optional string plan_mode_fireworks_model_id = 76;
|
||||
optional string plan_mode_sap_ai_core_model_id = 77;
|
||||
optional string plan_mode_sap_ai_core_deployment_id = 78;
|
||||
optional string plan_mode_groq_model_id = 79;
|
||||
optional OpenRouterModelInfo plan_mode_groq_model_info = 80;
|
||||
optional string plan_mode_baseten_model_id = 81;
|
||||
optional OpenRouterModelInfo plan_mode_baseten_model_info = 82;
|
||||
optional string plan_mode_hugging_face_model_id = 83;
|
||||
optional OpenRouterModelInfo plan_mode_hugging_face_model_info = 84;
|
||||
optional string plan_mode_huawei_cloud_maas_model_id = 85;
|
||||
optional OpenRouterModelInfo plan_mode_huawei_cloud_maas_model_info = 86;
|
||||
optional string plan_mode_oca_model_id = 87;
|
||||
optional OcaModelInfo plan_mode_oca_model_info = 88;
|
||||
optional ApiProvider act_mode_api_provider = 89;
|
||||
optional string act_mode_api_model_id = 90;
|
||||
optional int64 act_mode_thinking_budget_tokens = 91;
|
||||
optional string act_mode_reasoning_effort = 92;
|
||||
optional LanguageModelChatSelector act_mode_vs_code_lm_model_selector = 93;
|
||||
optional bool act_mode_aws_bedrock_custom_selected = 94;
|
||||
optional string act_mode_aws_bedrock_custom_model_base_id = 95;
|
||||
optional string act_mode_open_router_model_id = 96;
|
||||
optional OpenRouterModelInfo act_mode_open_router_model_info = 97;
|
||||
optional string act_mode_open_ai_model_id = 98;
|
||||
optional OpenAiCompatibleModelInfo act_mode_open_ai_model_info = 99;
|
||||
optional string act_mode_ollama_model_id = 100;
|
||||
optional string act_mode_lm_studio_model_id = 101;
|
||||
optional string act_mode_lite_llm_model_id = 102;
|
||||
optional LiteLLMModelInfo act_mode_lite_llm_model_info = 103;
|
||||
optional string act_mode_requesty_model_id = 104;
|
||||
optional OpenRouterModelInfo act_mode_requesty_model_info = 105;
|
||||
optional string act_mode_together_model_id = 106;
|
||||
optional string act_mode_fireworks_model_id = 107;
|
||||
optional string act_mode_sap_ai_core_model_id = 108;
|
||||
optional string act_mode_sap_ai_core_deployment_id = 109;
|
||||
optional string act_mode_groq_model_id = 110;
|
||||
optional OpenRouterModelInfo act_mode_groq_model_info = 111;
|
||||
optional string act_mode_baseten_model_id = 112;
|
||||
optional OpenRouterModelInfo act_mode_baseten_model_info = 113;
|
||||
optional string act_mode_hugging_face_model_id = 114;
|
||||
optional OpenRouterModelInfo act_mode_hugging_face_model_info = 115;
|
||||
optional string act_mode_huawei_cloud_maas_model_id = 116;
|
||||
optional OpenRouterModelInfo act_mode_huawei_cloud_maas_model_info = 117;
|
||||
optional string plan_mode_vercel_ai_gateway_model_id = 118;
|
||||
optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 119;
|
||||
optional string act_mode_vercel_ai_gateway_model_id = 120;
|
||||
optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 121;
|
||||
optional string act_mode_oca_model_id = 122;
|
||||
optional OcaModelInfo act_mode_oca_model_info = 123;
|
||||
optional int32 max_consecutive_mistakes = 124;
|
||||
optional bool subagents_enabled = 125;
|
||||
optional int32 subagent_terminal_output_line_limit = 126;
|
||||
optional string aihubmix_api_key = 127;
|
||||
optional string aihubmix_base_url = 128;
|
||||
optional string aihubmix_app_code = 129;
|
||||
optional string plan_mode_aihubmix_model_id = 130;
|
||||
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 131;
|
||||
optional string act_mode_aihubmix_model_id = 132;
|
||||
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 133;
|
||||
optional bool cline_web_tools_enabled = 134;
|
||||
optional bool hooks_enabled = 135;
|
||||
optional bool azure_identity = 136;
|
||||
optional bool skills_enabled = 137;
|
||||
optional string asksage_api_url = 31;
|
||||
optional int32 request_timeout_ms = 32;
|
||||
optional string sap_ai_resource_group = 33;
|
||||
optional string sap_ai_core_token_url = 34;
|
||||
optional string sap_ai_core_base_url = 35;
|
||||
optional bool sap_ai_core_use_orchestration_mode = 36;
|
||||
optional string dify_base_url = 37;
|
||||
optional string zai_api_line = 38;
|
||||
optional string oca_base_url = 39;
|
||||
optional string minimax_api_line = 40;
|
||||
optional string oca_mode = 41;
|
||||
optional string aihubmix_base_url = 42;
|
||||
optional string aihubmix_app_code = 43;
|
||||
optional string plan_mode_api_model_id = 44;
|
||||
optional int64 plan_mode_thinking_budget_tokens = 45;
|
||||
optional string gemini_plan_mode_thinking_level = 46;
|
||||
optional string plan_mode_reasoning_effort = 47;
|
||||
optional string plan_mode_verbosity = 48;
|
||||
optional LanguageModelChatSelector plan_mode_vs_code_lm_model_selector = 49;
|
||||
optional bool plan_mode_aws_bedrock_custom_selected = 50;
|
||||
optional string plan_mode_aws_bedrock_custom_model_base_id = 51;
|
||||
optional string plan_mode_open_router_model_id = 52;
|
||||
optional OpenRouterModelInfo plan_mode_open_router_model_info = 53;
|
||||
optional string plan_mode_open_ai_model_id = 54;
|
||||
optional OpenAiCompatibleModelInfo plan_mode_open_ai_model_info = 55;
|
||||
optional string plan_mode_ollama_model_id = 56;
|
||||
optional string plan_mode_lm_studio_model_id = 57;
|
||||
optional string plan_mode_lite_llm_model_id = 58;
|
||||
optional LiteLLMModelInfo plan_mode_lite_llm_model_info = 59;
|
||||
optional string plan_mode_requesty_model_id = 60;
|
||||
optional OpenRouterModelInfo plan_mode_requesty_model_info = 61;
|
||||
optional string plan_mode_together_model_id = 62;
|
||||
optional string plan_mode_fireworks_model_id = 63;
|
||||
optional string plan_mode_sap_ai_core_model_id = 64;
|
||||
optional string plan_mode_sap_ai_core_deployment_id = 65;
|
||||
optional string plan_mode_groq_model_id = 66;
|
||||
optional OpenRouterModelInfo plan_mode_groq_model_info = 67;
|
||||
optional string plan_mode_baseten_model_id = 68;
|
||||
optional OpenRouterModelInfo plan_mode_baseten_model_info = 69;
|
||||
optional string plan_mode_hugging_face_model_id = 70;
|
||||
optional OpenRouterModelInfo plan_mode_hugging_face_model_info = 71;
|
||||
optional string plan_mode_huawei_cloud_maas_model_id = 72;
|
||||
optional OpenRouterModelInfo plan_mode_huawei_cloud_maas_model_info = 73;
|
||||
optional string plan_mode_oca_model_id = 74;
|
||||
optional OcaModelInfo plan_mode_oca_model_info = 75;
|
||||
optional string plan_mode_oca_reasoning_effort = 76;
|
||||
optional string plan_mode_aihubmix_model_id = 77;
|
||||
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 78;
|
||||
optional string plan_mode_hicap_model_id = 79;
|
||||
optional OpenRouterModelInfo plan_mode_hicap_model_info = 80;
|
||||
optional string plan_mode_nous_research_model_id = 81;
|
||||
optional string plan_mode_vercel_ai_gateway_model_id = 82;
|
||||
optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 83;
|
||||
optional string act_mode_api_model_id = 84;
|
||||
optional int64 act_mode_thinking_budget_tokens = 85;
|
||||
optional string gemini_act_mode_thinking_level = 86;
|
||||
optional string act_mode_reasoning_effort = 87;
|
||||
optional string act_mode_verbosity = 88;
|
||||
optional LanguageModelChatSelector act_mode_vs_code_lm_model_selector = 89;
|
||||
optional bool act_mode_aws_bedrock_custom_selected = 90;
|
||||
optional string act_mode_aws_bedrock_custom_model_base_id = 91;
|
||||
optional string act_mode_open_router_model_id = 92;
|
||||
optional OpenRouterModelInfo act_mode_open_router_model_info = 93;
|
||||
optional string act_mode_open_ai_model_id = 94;
|
||||
optional OpenAiCompatibleModelInfo act_mode_open_ai_model_info = 95;
|
||||
optional string act_mode_ollama_model_id = 96;
|
||||
optional string act_mode_lm_studio_model_id = 97;
|
||||
optional string act_mode_lite_llm_model_id = 98;
|
||||
optional LiteLLMModelInfo act_mode_lite_llm_model_info = 99;
|
||||
optional string act_mode_requesty_model_id = 100;
|
||||
optional OpenRouterModelInfo act_mode_requesty_model_info = 101;
|
||||
optional string act_mode_together_model_id = 102;
|
||||
optional string act_mode_fireworks_model_id = 103;
|
||||
optional string act_mode_sap_ai_core_model_id = 104;
|
||||
optional string act_mode_sap_ai_core_deployment_id = 105;
|
||||
optional string act_mode_groq_model_id = 106;
|
||||
optional OpenRouterModelInfo act_mode_groq_model_info = 107;
|
||||
optional string act_mode_baseten_model_id = 108;
|
||||
optional OpenRouterModelInfo act_mode_baseten_model_info = 109;
|
||||
optional string act_mode_hugging_face_model_id = 110;
|
||||
optional OpenRouterModelInfo act_mode_hugging_face_model_info = 111;
|
||||
optional string act_mode_huawei_cloud_maas_model_id = 112;
|
||||
optional OpenRouterModelInfo act_mode_huawei_cloud_maas_model_info = 113;
|
||||
optional string act_mode_oca_model_id = 114;
|
||||
optional OcaModelInfo act_mode_oca_model_info = 115;
|
||||
optional string act_mode_oca_reasoning_effort = 116;
|
||||
optional string act_mode_aihubmix_model_id = 117;
|
||||
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 118;
|
||||
optional string act_mode_hicap_model_id = 119;
|
||||
optional OpenRouterModelInfo act_mode_hicap_model_info = 120;
|
||||
optional string act_mode_nous_research_model_id = 121;
|
||||
optional string act_mode_vercel_ai_gateway_model_id = 122;
|
||||
optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 123;
|
||||
optional ApiProvider plan_mode_api_provider = 124;
|
||||
optional ApiProvider act_mode_api_provider = 125;
|
||||
optional string hicap_model_id = 126;
|
||||
optional string lm_studio_model_id = 127;
|
||||
optional AutoApprovalSettings auto_approval_settings = 128;
|
||||
optional string global_cline_rules_toggles = 129;
|
||||
optional string global_workflow_toggles = 130;
|
||||
optional string global_skills_toggles = 131;
|
||||
optional BrowserSettings browser_settings = 132;
|
||||
optional string telemetry_setting = 133;
|
||||
optional bool plan_act_separate_models_setting = 134;
|
||||
optional bool enable_checkpoints_setting = 135;
|
||||
optional int32 shell_integration_timeout = 136;
|
||||
optional string default_terminal_profile = 137;
|
||||
optional int32 terminal_output_line_limit = 138;
|
||||
optional int32 max_consecutive_mistakes = 139;
|
||||
optional int32 subagent_terminal_output_line_limit = 140;
|
||||
optional bool strict_plan_mode_enabled = 141;
|
||||
optional bool yolo_mode_toggled = 142;
|
||||
optional bool use_auto_condense = 143;
|
||||
optional bool cline_web_tools_enabled = 144;
|
||||
optional string preferred_language = 145;
|
||||
optional OpenaiReasoningEffort openai_reasoning_effort = 146;
|
||||
optional PlanActMode mode = 147;
|
||||
optional DictationSettings dictation_settings = 148;
|
||||
optional FocusChainSettings focus_chain_settings = 149;
|
||||
optional string custom_prompt = 150;
|
||||
optional double auto_condense_threshold = 151;
|
||||
optional bool hooks_enabled = 152;
|
||||
optional bool subagents_enabled = 153;
|
||||
optional bool enable_parallel_tool_calling = 154;
|
||||
optional bool background_edit_enabled = 155;
|
||||
optional bool skills_enabled = 156;
|
||||
optional bool opt_out_of_remote_config = 157;
|
||||
optional bool open_telemetry_enabled = 158;
|
||||
optional string open_telemetry_metrics_exporter = 159;
|
||||
optional string open_telemetry_logs_exporter = 160;
|
||||
optional string open_telemetry_otlp_protocol = 161;
|
||||
optional string open_telemetry_otlp_endpoint = 162;
|
||||
optional string open_telemetry_otlp_metrics_protocol = 163;
|
||||
optional string open_telemetry_otlp_metrics_endpoint = 164;
|
||||
optional string open_telemetry_otlp_logs_protocol = 165;
|
||||
optional string open_telemetry_otlp_logs_endpoint = 166;
|
||||
optional int32 open_telemetry_metric_export_interval = 167;
|
||||
optional bool open_telemetry_otlp_insecure = 168;
|
||||
optional int32 open_telemetry_log_batch_size = 169;
|
||||
optional int32 open_telemetry_log_batch_timeout = 170;
|
||||
optional int32 open_telemetry_log_max_queue_size = 171;
|
||||
}
|
||||
|
||||
message DictationSettings {
|
||||
@@ -374,6 +420,7 @@ message UpdateSettingsRequest {
|
||||
optional bool background_edit_enabled = 36;
|
||||
optional string oca_reasoning_effort = 37;
|
||||
optional bool skills_enabled = 38;
|
||||
optional bool opt_out_of_remote_config = 39;
|
||||
}
|
||||
|
||||
message UpdateTerminalConnectionTimeoutRequest {
|
||||
|
||||
@@ -0,0 +1,413 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Generates proto message definitions from TypeScript source of truth.
|
||||
*
|
||||
* This script reads the field definitions from src/shared/storage/state-keys.ts
|
||||
* and generates the corresponding proto message definitions for Secrets and Settings.
|
||||
*
|
||||
* Usage: node scripts/generate-state-proto.mjs
|
||||
*
|
||||
* The generated proto content is written to proto/cline/state.proto,
|
||||
* replacing only the Secrets and Settings messages while preserving
|
||||
* the rest of the file (services, enums, other messages).
|
||||
*/
|
||||
|
||||
import * as fs from "node:fs/promises"
|
||||
import { Project, SyntaxKind } from "ts-morph"
|
||||
|
||||
const STATE_KEYS_PATH = "src/shared/storage/state-keys.ts"
|
||||
const STATE_PROTO_PATH = "proto/cline/state.proto"
|
||||
|
||||
/**
|
||||
* Convert camelCase to snake_case for proto field names
|
||||
*/
|
||||
function camelToSnake(str) {
|
||||
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
|
||||
}
|
||||
|
||||
// Fields that should use int64 instead of int32
|
||||
const INT64_FIELDS = new Set(["planModeThinkingBudgetTokens", "actModeThinkingBudgetTokens"])
|
||||
|
||||
// Fields that should use double instead of int32
|
||||
const DOUBLE_FIELDS = new Set(["autoCondenseThreshold"])
|
||||
|
||||
/**
|
||||
* Infer proto type from TypeScript type expression
|
||||
* @param {string} typeText - The TypeScript type expression
|
||||
* @param {string} [fieldName] - Optional field name for field-specific overrides
|
||||
*/
|
||||
function inferProtoType(typeText, fieldName) {
|
||||
// Remove 'undefined' from union types
|
||||
const cleanType = typeText
|
||||
.replace(/\s*\|\s*undefined/g, "")
|
||||
.replace(/undefined\s*\|\s*/g, "")
|
||||
.trim()
|
||||
|
||||
// Handle common types
|
||||
if (cleanType === "string") {
|
||||
return "string"
|
||||
}
|
||||
if (cleanType === "boolean") {
|
||||
return "bool"
|
||||
}
|
||||
if (cleanType === "number") {
|
||||
// Some number fields need specific numeric types
|
||||
if (fieldName && INT64_FIELDS.has(fieldName)) {
|
||||
return "int64"
|
||||
}
|
||||
if (fieldName && DOUBLE_FIELDS.has(fieldName)) {
|
||||
return "double"
|
||||
}
|
||||
return "int32"
|
||||
}
|
||||
|
||||
// Handle Record<string, string> as map<string, string>
|
||||
if (/Record\s*<\s*string\s*,\s*string\s*>/.test(cleanType)) {
|
||||
return "map<string, string>"
|
||||
}
|
||||
|
||||
// Handle specific known types that map to proto messages/enums
|
||||
// Order matters! More specific types must come before generic ones
|
||||
// (e.g., OpenAiCompatibleModelInfo before ModelInfo)
|
||||
// Check known types BEFORE string literals, since types like `"act" as Mode`
|
||||
// contain quotes but should map to proto enums
|
||||
const knownTypes = [
|
||||
// Specific model info types first
|
||||
["OpenAiCompatibleModelInfo", "OpenAiCompatibleModelInfo"],
|
||||
["LiteLLMModelInfo", "LiteLLMModelInfo"],
|
||||
["OcaModelInfo", "OcaModelInfo"],
|
||||
// Generic ModelInfo last (catches OpenRouterModelInfo, etc.)
|
||||
["ModelInfo", "OpenRouterModelInfo"],
|
||||
// Other types - order matters for substring matching
|
||||
["AutoApprovalSettings", "AutoApprovalSettings"],
|
||||
["BrowserSettings", "BrowserSettings"],
|
||||
["DictationSettings", "DictationSettings"],
|
||||
["FocusChainSettings", "FocusChainSettings"],
|
||||
["OpenaiReasoningEffort", "OpenaiReasoningEffort"],
|
||||
["PlanActMode", "PlanActMode"],
|
||||
["ApiProvider", "ApiProvider"],
|
||||
["LanguageModelChatSelector", "LanguageModelChatSelector"], // Must come before "Mode" check
|
||||
]
|
||||
|
||||
for (const [tsType, protoType] of knownTypes) {
|
||||
if (cleanType.includes(tsType)) {
|
||||
return protoType
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Mode type separately with word boundary to avoid matching "VsCodeLmModelSelector"
|
||||
// This handles TS `Mode` type which maps to proto `PlanActMode`
|
||||
if (/\bMode\b/.test(cleanType)) {
|
||||
return "PlanActMode"
|
||||
}
|
||||
|
||||
// Handle specific string literal unions (treat as string)
|
||||
// This comes after known types check since some types like `"act" as Mode` contain quotes
|
||||
if (cleanType.includes('"') || cleanType.includes("'")) {
|
||||
return "string"
|
||||
}
|
||||
|
||||
// Default to string for complex types we can't map
|
||||
return "string"
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the SECRETS_KEYS array from state-keys.ts
|
||||
*/
|
||||
function parseSecretsKeys(sourceFile) {
|
||||
const secretsDecl = sourceFile.getVariableDeclaration("SECRETS_KEYS")
|
||||
if (!secretsDecl) {
|
||||
throw new Error("Could not find SECRETS_KEYS declaration")
|
||||
}
|
||||
|
||||
let initializer = secretsDecl.getInitializer()
|
||||
if (!initializer) {
|
||||
throw new Error("SECRETS_KEYS has no initializer")
|
||||
}
|
||||
|
||||
// Handle 'as const' expression
|
||||
if (initializer.getKind() === SyntaxKind.AsExpression) {
|
||||
initializer = initializer.getExpression()
|
||||
}
|
||||
|
||||
if (initializer.getKind() !== SyntaxKind.ArrayLiteralExpression) {
|
||||
throw new Error(`SECRETS_KEYS is not an array literal (got ${SyntaxKind[initializer.getKind()]})`)
|
||||
}
|
||||
|
||||
const keys = []
|
||||
for (const element of initializer.getElements()) {
|
||||
const text = element.getText()
|
||||
// Remove quotes and handle special prefixes
|
||||
const key = text.replace(/^['"]|['"]$/g, "")
|
||||
// Skip prefixed keys like "cline:clineAccountId"
|
||||
if (!key.includes(":")) {
|
||||
keys.push(key)
|
||||
}
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse field definitions from an object literal in state-keys.ts
|
||||
*/
|
||||
function parseFieldDefinitions(sourceFile, variableName) {
|
||||
const decl = sourceFile.getVariableDeclaration(variableName)
|
||||
if (!decl) {
|
||||
throw new Error(`Could not find ${variableName} declaration`)
|
||||
}
|
||||
|
||||
const initializer = decl.getInitializer()
|
||||
if (!initializer) {
|
||||
throw new Error(`${variableName} has no initializer`)
|
||||
}
|
||||
|
||||
// Handle 'satisfies' expression
|
||||
let objectLiteral = initializer
|
||||
if (initializer.getKind() === SyntaxKind.SatisfiesExpression) {
|
||||
objectLiteral = initializer.getExpression()
|
||||
}
|
||||
|
||||
if (objectLiteral.getKind() !== SyntaxKind.ObjectLiteralExpression) {
|
||||
throw new Error(`${variableName} is not an object literal`)
|
||||
}
|
||||
|
||||
const fields = []
|
||||
for (const prop of objectLiteral.getProperties()) {
|
||||
if (prop.getKind() !== SyntaxKind.PropertyAssignment) {
|
||||
continue
|
||||
}
|
||||
|
||||
const name = prop.getName()
|
||||
const propInit = prop.getInitializer()
|
||||
|
||||
if (!propInit || propInit.getKind() !== SyntaxKind.ObjectLiteralExpression) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the 'default' property to infer the type
|
||||
const defaultProp = propInit.getProperty("default")
|
||||
if (!defaultProp) {
|
||||
continue
|
||||
}
|
||||
|
||||
let typeText = "string"
|
||||
const defaultInit = defaultProp.getInitializer()
|
||||
if (defaultInit) {
|
||||
// Check for 'as' expression to get the type
|
||||
if (defaultInit.getKind() === SyntaxKind.AsExpression) {
|
||||
const typeNode = defaultInit.getTypeNode()
|
||||
if (typeNode) {
|
||||
typeText = typeNode.getText()
|
||||
}
|
||||
} else {
|
||||
// Infer from literal
|
||||
const text = defaultInit.getText()
|
||||
if (text === "true" || text === "false") {
|
||||
typeText = "boolean"
|
||||
} else if (/^\d+$/.test(text)) {
|
||||
typeText = "number"
|
||||
} else if (/^\d+\.\d+$/.test(text)) {
|
||||
typeText = "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fields.push({
|
||||
name,
|
||||
tsType: typeText,
|
||||
protoType: inferProtoType(typeText, name),
|
||||
})
|
||||
}
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert snake_case to camelCase for mapping proto fields back to TS keys
|
||||
*/
|
||||
function snakeToCamel(str) {
|
||||
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse field numbers from an existing proto message definition
|
||||
* Returns a map of camelCase field names to their field numbers
|
||||
*/
|
||||
function parseProtoMessageFieldNumbers(protoContent, messageName) {
|
||||
const fieldNumbers = {}
|
||||
|
||||
// Match the message block (handles single-level nesting for now)
|
||||
const messageRegex = new RegExp(`message\\s+${messageName}\\s*\\{([^}]*(?:\\{[^}]*\\}[^}]*)*)\\}`, "s")
|
||||
const match = protoContent.match(messageRegex)
|
||||
|
||||
if (!match) {
|
||||
return fieldNumbers
|
||||
}
|
||||
|
||||
const messageBody = match[1]
|
||||
|
||||
// Match field definitions: optional/required/repeated type name = number;
|
||||
const fieldRegex = /(?:optional|required|repeated)?\s*\w+\s+(\w+)\s*=\s*(\d+)\s*;/g
|
||||
const matches = messageBody.matchAll(fieldRegex)
|
||||
|
||||
for (const fieldMatch of matches) {
|
||||
const snakeName = fieldMatch[1]
|
||||
const fieldNum = parseInt(fieldMatch[2], 10)
|
||||
const camelName = snakeToCamel(snakeName)
|
||||
fieldNumbers[camelName] = fieldNum
|
||||
}
|
||||
|
||||
return fieldNumbers
|
||||
}
|
||||
|
||||
/**
|
||||
* Load field number mappings from existing proto file
|
||||
*/
|
||||
async function loadFieldNumbersFromProto() {
|
||||
try {
|
||||
const protoContent = await fs.readFile(STATE_PROTO_PATH, "utf-8")
|
||||
const secrets = parseProtoMessageFieldNumbers(protoContent, "Secrets")
|
||||
const settings = parseProtoMessageFieldNumbers(protoContent, "Settings")
|
||||
|
||||
console.log(` Found ${Object.keys(secrets).length} existing Secrets fields`)
|
||||
console.log(` Found ${Object.keys(settings).length} existing Settings fields`)
|
||||
|
||||
return { Secrets: secrets, Settings: settings }
|
||||
} catch {
|
||||
// Proto file doesn't exist, start fresh
|
||||
return { Secrets: {}, Settings: {} }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign field numbers, preserving existing assignments and adding new ones
|
||||
*/
|
||||
function assignFieldNumbers(fields, existingNumbers, startNumber = 1) {
|
||||
const result = {}
|
||||
let nextNumber = startNumber
|
||||
|
||||
// Find the highest existing number
|
||||
for (const num of Object.values(existingNumbers)) {
|
||||
if (num >= nextNumber) {
|
||||
nextNumber = num + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve existing assignments
|
||||
for (const field of fields) {
|
||||
if (existingNumbers[field.name] !== undefined) {
|
||||
result[field.name] = existingNumbers[field.name]
|
||||
}
|
||||
}
|
||||
|
||||
// Assign new numbers for new fields
|
||||
for (const field of fields) {
|
||||
if (result[field.name] === undefined) {
|
||||
result[field.name] = nextNumber++
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate proto message definition
|
||||
*/
|
||||
function generateProtoMessage(messageName, fields, fieldNumbers) {
|
||||
const lines = [`message ${messageName} {`]
|
||||
|
||||
// Sort fields by field number for consistent output
|
||||
const sortedFields = [...fields].sort((a, b) => fieldNumbers[a.name] - fieldNumbers[b.name])
|
||||
|
||||
for (const field of sortedFields) {
|
||||
const snakeName = camelToSnake(field.name)
|
||||
const fieldNum = fieldNumbers[field.name]
|
||||
// Map types cannot have the 'optional' modifier in proto3
|
||||
const prefix = field.protoType.startsWith("map<") ? "" : "optional "
|
||||
lines.push(` ${prefix}${field.protoType} ${snakeName} = ${fieldNum};`)
|
||||
}
|
||||
|
||||
lines.push("}")
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Secrets message from SECRETS_KEYS
|
||||
*/
|
||||
function generateSecretsMessage(secretsKeys, fieldNumbers) {
|
||||
const fields = secretsKeys.map((key) => ({
|
||||
name: key,
|
||||
protoType: "string",
|
||||
}))
|
||||
|
||||
return generateProtoMessage("Secrets", fields, fieldNumbers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace a message in the proto file content
|
||||
*/
|
||||
function replaceMessage(protoContent, messageName, newMessageContent) {
|
||||
// Match the message definition including nested braces
|
||||
const messageRegex = new RegExp(`message\\s+${messageName}\\s*\\{[^}]*(?:\\{[^}]*\\}[^}]*)*\\}`, "g")
|
||||
|
||||
if (messageRegex.test(protoContent)) {
|
||||
return protoContent.replace(messageRegex, newMessageContent)
|
||||
} else {
|
||||
// Message doesn't exist, append before the first message or at end
|
||||
console.warn(`Warning: ${messageName} message not found in proto file, appending`)
|
||||
return protoContent + "\n\n" + newMessageContent
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log("Generating proto definitions from TypeScript source...")
|
||||
|
||||
// Parse TypeScript source
|
||||
const project = new Project({
|
||||
tsConfigFilePath: "tsconfig.json",
|
||||
})
|
||||
const sourceFile = project.addSourceFileAtPath(STATE_KEYS_PATH)
|
||||
|
||||
// Parse definitions
|
||||
const secretsKeys = parseSecretsKeys(sourceFile)
|
||||
console.log(`Found ${secretsKeys.length} secret keys`)
|
||||
|
||||
const apiHandlerFields = parseFieldDefinitions(sourceFile, "API_HANDLER_SETTINGS_FIELDS")
|
||||
const userSettingsFields = parseFieldDefinitions(sourceFile, "USER_SETTINGS_FIELDS")
|
||||
const settingsFields = [...apiHandlerFields, ...userSettingsFields]
|
||||
console.log(`Found ${settingsFields.length} settings fields`)
|
||||
|
||||
// Load existing field numbers from proto file
|
||||
const existingFieldNumbers = await loadFieldNumbersFromProto()
|
||||
|
||||
// Assign field numbers (preserving existing, adding new ones)
|
||||
const secretsFieldNumbers = assignFieldNumbers(
|
||||
secretsKeys.map((k) => ({ name: k })),
|
||||
existingFieldNumbers.Secrets,
|
||||
1,
|
||||
)
|
||||
const settingsFieldNumbers = assignFieldNumbers(settingsFields, existingFieldNumbers.Settings, 1)
|
||||
|
||||
// Generate messages
|
||||
const secretsMessage = generateSecretsMessage(secretsKeys, secretsFieldNumbers)
|
||||
const settingsMessage = generateProtoMessage("Settings", settingsFields, settingsFieldNumbers)
|
||||
|
||||
// Read existing proto file
|
||||
let protoContent = await fs.readFile(STATE_PROTO_PATH, "utf-8")
|
||||
|
||||
// Replace messages
|
||||
protoContent = replaceMessage(protoContent, "Secrets", secretsMessage)
|
||||
protoContent = replaceMessage(protoContent, "Settings", settingsMessage)
|
||||
|
||||
// Write updated proto file
|
||||
await fs.writeFile(STATE_PROTO_PATH, protoContent)
|
||||
console.log(`Updated ${STATE_PROTO_PATH}`)
|
||||
|
||||
console.log("\nGeneration complete! Run 'npm run protos' to regenerate TypeScript from protos.")
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Error:", error)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
/**
|
||||
* NPM Package Builder for Cline CLI
|
||||
*
|
||||
*
|
||||
* This script builds the Cline CLI NPM package (dist-standalone/).
|
||||
* It is completely independent from package-standalone.mjs (JetBrains build).
|
||||
*
|
||||
*
|
||||
* Usage: node scripts/package-npm.mjs
|
||||
*
|
||||
*
|
||||
* Prerequisites:
|
||||
* - npm run protos && npm run protos-go
|
||||
* - npm run compile-cli
|
||||
|
||||
+2
-12
@@ -76,19 +76,9 @@ export async function initialize(context: vscode.ExtensionContext): Promise<Webv
|
||||
|
||||
await showVersionUpdateAnnouncement(context)
|
||||
|
||||
// Initialize banner service
|
||||
// Initialize banner service (TEMPORARILY DISABLED - not fetching banners to prevent API hammering)
|
||||
BannerService.initialize(webview.controller)
|
||||
BannerService.get()
|
||||
.fetchActiveBanners()
|
||||
.then((banners) => {
|
||||
if (banners.length > 0) {
|
||||
Logger.log(`BannerService: ${banners.length} active banner(s) fetched.`)
|
||||
// Banners are now cached and can be accessed by the frontend when needed
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
Logger.error("BannerService: Failed to fetch banners on startup", error)
|
||||
})
|
||||
// DISABLED: .getActiveBanners(true)
|
||||
|
||||
telemetryService.captureExtensionActivated()
|
||||
|
||||
|
||||
@@ -259,7 +259,7 @@ export class AIhubmixHandler implements ApiHandler {
|
||||
const stream = await client.chat.completions.create(fixedRequestBody)
|
||||
|
||||
for await (const chunk of stream as any) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -199,7 +199,7 @@ export class ClineHandler implements ApiHandler {
|
||||
// @ts-ignore-next-line
|
||||
let totalCost = (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0)
|
||||
|
||||
if (["x-ai/grok-code-fast-1", "minimax/minimax-m2.1"].includes(this.getModel().id)) {
|
||||
if (["x-ai/grok-code-fast-1", "kwaipilot/kat-coder-pro"].includes(this.getModel().id)) {
|
||||
totalCost = 0
|
||||
}
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ export class DeepSeekHandler implements ApiHandler {
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -67,7 +67,7 @@ export class DoubaoHandler implements ApiHandler {
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -60,7 +60,7 @@ export class FireworksHandler implements ApiHandler {
|
||||
|
||||
let reasoning: string | null = null
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (reasoning || delta?.content?.includes("<think>")) {
|
||||
reasoning = (reasoning || "") + (delta.content ?? "")
|
||||
}
|
||||
|
||||
@@ -229,7 +229,7 @@ export class GroqHandler implements ApiHandler {
|
||||
const stream = await client.chat.completions.create(requestParams)
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
|
||||
// Handle reasoning field if present (for reasoning models with parsed output)
|
||||
if ((delta as any)?.reasoning) {
|
||||
|
||||
@@ -66,7 +66,7 @@ export class HicapHandler implements ApiHandler {
|
||||
stream_options: { include_usage: true },
|
||||
})
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -86,7 +86,7 @@ export class HuaweiCloudMaaSHandler implements ApiHandler {
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
|
||||
// Handle reasoning content detection
|
||||
if (delta?.content) {
|
||||
|
||||
@@ -97,7 +97,7 @@ export class HuggingFaceHandler implements ApiHandler {
|
||||
|
||||
for await (const chunk of stream) {
|
||||
_chunkCount++
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
_totalContent += delta.content
|
||||
|
||||
|
||||
@@ -307,7 +307,7 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
} as LiteLlmChatCompletionCreateParams)
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
|
||||
// Handle normal text content
|
||||
if (delta?.content) {
|
||||
|
||||
@@ -60,7 +60,7 @@ export class LmStudioHandler implements ApiHandler {
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const choice = chunk.choices[0]
|
||||
const choice = chunk.choices?.[0]
|
||||
const delta = choice?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
|
||||
@@ -62,7 +62,7 @@ export class MoonshotHandler implements ApiHandler {
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -57,7 +57,7 @@ export class NebiusHandler implements ApiHandler {
|
||||
})
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -55,7 +55,7 @@ export class NousResearchHandler implements ApiHandler {
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -241,7 +241,7 @@ export class OcaHandler implements ApiHandler {
|
||||
const stream = await client.chat.completions.create(chatCompletionsParams)
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
|
||||
// Handle normal text content
|
||||
if (delta?.content) {
|
||||
@@ -303,7 +303,6 @@ export class OcaHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
async *createMessageResponsesApi(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
|
||||
console.log("Uses Responses API")
|
||||
const client = this.ensureClient()
|
||||
|
||||
// Convert messages to Responses API input format
|
||||
|
||||
@@ -123,7 +123,7 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -132,7 +132,7 @@ export class OpenAiHandler implements ApiHandler {
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -113,7 +113,7 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
this.lastGenerationId = chunk.id
|
||||
}
|
||||
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -122,7 +122,7 @@ export class QwenHandler implements ApiHandler {
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -99,7 +99,7 @@ export class RequestyHandler implements ApiHandler {
|
||||
let lastUsage: any
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -68,7 +68,7 @@ export class SambanovaHandler implements ApiHandler {
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -601,6 +601,8 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
const anthropicModels = [
|
||||
"anthropic--claude-4.5-haiku",
|
||||
"anthropic--claude-4.5-opus",
|
||||
"anthropic--claude-4.5-sonnet",
|
||||
"anthropic--claude-4-sonnet",
|
||||
"anthropic--claude-4-opus",
|
||||
@@ -649,7 +651,9 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
|
||||
|
||||
if (
|
||||
model.id === "anthropic--claude-4.5-opus" ||
|
||||
model.id === "anthropic--claude-4.5-sonnet" ||
|
||||
model.id === "anthropic--claude-4.5-haiku" ||
|
||||
model.id === "anthropic--claude-4-sonnet" ||
|
||||
model.id === "anthropic--claude-4-opus" ||
|
||||
model.id === "anthropic--claude-3.7-sonnet"
|
||||
@@ -779,7 +783,9 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
} else if (openAIModels.includes(model.id) || perplexityModels.includes(model.id)) {
|
||||
yield* this.streamCompletionGPT(response.data, model)
|
||||
} else if (
|
||||
model.id === "anthropic--claude-4.5-opus" ||
|
||||
model.id === "anthropic--claude-4.5-sonnet" ||
|
||||
model.id === "anthropic--claude-4.5-haiku" ||
|
||||
model.id === "anthropic--claude-4-sonnet" ||
|
||||
model.id === "anthropic--claude-4-opus" ||
|
||||
model.id === "anthropic--claude-3.7-sonnet"
|
||||
|
||||
@@ -66,7 +66,7 @@ export class TogetherHandler implements ApiHandler {
|
||||
})
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -71,7 +71,7 @@ export class VercelAIGatewayHandler implements ApiHandler {
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
|
||||
@@ -69,7 +69,7 @@ export class XAIHandler implements ApiHandler {
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -95,7 +95,7 @@ export class ZAiHandler implements ApiHandler {
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -10,6 +10,43 @@ import {
|
||||
ClineUserToolResultContentBlock,
|
||||
} from "@/shared/messages/content"
|
||||
|
||||
// OpenAI API has a maximum tool call ID length of 40 characters
|
||||
const MAX_TOOL_CALL_ID_LENGTH = 40
|
||||
|
||||
/**
|
||||
* Determines if a given tool ID follows the OpenAI Responses API format for tool calls.
|
||||
* OpenAI tool call IDs start with "fc_" and are exactly 53 characters long.
|
||||
*
|
||||
* @param callId - The tool ID to check
|
||||
* @returns True if the tool ID matches the OpenAI Responses API format, false otherwise
|
||||
*/
|
||||
function isOpenAIResponseToolId(callId: string): boolean {
|
||||
return callId.startsWith("fc_") && callId.length === 53
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a tool ID to a consistent format for OpenAI's Chat Completions API.
|
||||
* This function MUST be used for both tool_calls[].id (assistant) and tool_call_id (tool result)
|
||||
* to ensure they match - otherwise OpenAI will reject the request with:
|
||||
* "Invalid parameter: 'tool_call_id' of 'xxx' not found in 'tool_calls' of previous message."
|
||||
*
|
||||
* @param toolId - The original tool ID from Cline/Anthropic format
|
||||
* @returns The transformed ID suitable for OpenAI API
|
||||
*/
|
||||
function transformToolCallId(toolId: string): string {
|
||||
// OpenAI Responses API uses "fc_" prefix with 53 char length
|
||||
// Convert these to "call_" prefix format for Chat Completions API
|
||||
if (isOpenAIResponseToolId(toolId)) {
|
||||
// Use the last 33 chars + "call_" (5 chars) to stay under the 40-char limit.
|
||||
return `call_${toolId.slice(toolId.length - (MAX_TOOL_CALL_ID_LENGTH - 5))}`
|
||||
}
|
||||
// Ensure ID doesn't exceed max length
|
||||
if (toolId.length > MAX_TOOL_CALL_ID_LENGTH) {
|
||||
return toolId.slice(0, MAX_TOOL_CALL_ID_LENGTH)
|
||||
}
|
||||
return toolId
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an array of ClineStorageMessage objects to OpenAI's Completions API format.
|
||||
*
|
||||
@@ -80,7 +117,9 @@ export function convertToOpenAiMessages(
|
||||
}
|
||||
openAiMessages.push({
|
||||
role: "tool",
|
||||
tool_call_id: toolMessage.tool_use_id,
|
||||
// The tool_call_id must match the id used in the assistant's tool_calls array.
|
||||
// Use the same transformation logic as tool_calls to ensure IDs match.
|
||||
tool_call_id: transformToolCallId(toolMessage.tool_use_id),
|
||||
content: content,
|
||||
})
|
||||
})
|
||||
@@ -171,23 +210,29 @@ export function convertToOpenAiMessages(
|
||||
// Process tool use messages
|
||||
const tool_calls: OpenAI.Chat.ChatCompletionMessageToolCall[] = toolMessages.map((toolMessage) => {
|
||||
const toolDetails = toolMessage.reasoning_details
|
||||
const toolId = toolMessage.id
|
||||
if (toolDetails) {
|
||||
if (Array.isArray(toolDetails)) {
|
||||
// For Gemini: reasoning details must be linkable back to the tool call.
|
||||
// Sometimes OpenRouter/Gemini returns entries without `id`; those poison the next request.
|
||||
// Keep only entries with an id matching the tool call id.
|
||||
// See: https://github.com/cline/cline/issues/8214
|
||||
const validDetails = toolDetails.filter((detail: any) => detail?.id === toolMessage.id)
|
||||
if (validDetails.length > 0) reasoningDetails.push(...validDetails)
|
||||
const validDetails = toolDetails.filter((detail: any) => detail?.id === toolId)
|
||||
if (validDetails.length > 0) {
|
||||
reasoningDetails.push(...validDetails)
|
||||
}
|
||||
} else {
|
||||
// Single reasoning detail - only include if it has matching id
|
||||
const detail = toolDetails as any
|
||||
if (detail?.id === toolMessage.id) reasoningDetails.push(toolDetails)
|
||||
if (detail?.id === toolId) {
|
||||
reasoningDetails.push(toolDetails)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: toolMessage.id,
|
||||
// Use the same transformation as tool_call_id to ensure IDs match
|
||||
id: transformToolCallId(toolId),
|
||||
type: "function",
|
||||
function: {
|
||||
name: toolMessage.name,
|
||||
|
||||
@@ -170,7 +170,8 @@ export function convertToOpenAIResponsesInput(messages: ClineStorageMessage[]):
|
||||
assistantItems.push({
|
||||
type: "function_call",
|
||||
call_id,
|
||||
id: part.id,
|
||||
// MAX 53 characters for OpenAI Responses API tool IDs
|
||||
id: !part.id.startsWith("fc_") ? `fc_${part.id.slice(0, 50)}` : part.id,
|
||||
name: part.name,
|
||||
arguments: JSON.stringify(part.input ?? {}),
|
||||
})
|
||||
|
||||
@@ -180,7 +180,7 @@ export async function createOpenRouterStream(
|
||||
thinkingBudgetTokens &&
|
||||
model.info?.thinkingConfig &&
|
||||
thinkingBudgetTokens > 0 &&
|
||||
!(model.id.includes("gemini") && geminiThinkingLevel)
|
||||
!(model.id.includes("gemini-3") && geminiThinkingLevel)
|
||||
) {
|
||||
temperature = undefined // extended thinking does not support non-1 temperature
|
||||
reasoning = { max_tokens: thinkingBudgetTokens }
|
||||
@@ -212,7 +212,7 @@ export async function createOpenRouterStream(
|
||||
...(providerPreferences ? { provider: providerPreferences } : {}),
|
||||
...(isClaudeSonnet1m ? { provider: { order: ["anthropic", "google-vertex/global"], allow_fallbacks: false } } : {}),
|
||||
...getOpenAIToolParams(tools),
|
||||
...(model.id.includes("gemini") && geminiThinkingLevel
|
||||
...(model.id.includes("gemini-3") && geminiThinkingLevel
|
||||
? { thinking_config: { thinking_level: geminiThinkingLevel, include_thoughts: true } }
|
||||
: {}),
|
||||
})
|
||||
|
||||
@@ -139,7 +139,7 @@ export async function createVercelAIGatewayStream(
|
||||
...(model.id.startsWith("openai/o") ? { reasoning_effort: reasoningEffort || "medium" } : {}),
|
||||
...(reasoning ? { reasoning } : {}),
|
||||
...getOpenAIToolParams(tools),
|
||||
...(model.id.includes("gemini") && geminiThinkingLevel
|
||||
...(model.id.includes("gemini-3") && geminiThinkingLevel
|
||||
? { thinking_config: { thinking_level: geminiThinkingLevel, include_thoughts: true } }
|
||||
: {}),
|
||||
})
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { expect } from "chai"
|
||||
import { parseYamlFrontmatter } from "../frontmatter"
|
||||
|
||||
describe("parseYamlFrontmatter", () => {
|
||||
it("returns original content when no frontmatter", () => {
|
||||
const input = "Just text"
|
||||
const result = parseYamlFrontmatter(input)
|
||||
expect(result.hadFrontmatter).to.equal(false)
|
||||
expect(result.data).to.deep.equal({})
|
||||
expect(result.body).to.equal(input)
|
||||
})
|
||||
|
||||
it("parses valid YAML frontmatter", () => {
|
||||
const input = `---\npaths:\n - "src/**"\n---\n\nHello`
|
||||
const result = parseYamlFrontmatter(input)
|
||||
expect(result.hadFrontmatter).to.equal(true)
|
||||
expect(result.parseError).to.equal(undefined)
|
||||
expect(result.data).to.deep.equal({ paths: ["src/**"] })
|
||||
expect(result.body.trim()).to.equal("Hello")
|
||||
})
|
||||
|
||||
it("fails open on malformed YAML", () => {
|
||||
const input = `---\npaths: [invalid\n---\nBody`
|
||||
const result = parseYamlFrontmatter(input)
|
||||
expect(result.hadFrontmatter).to.equal(true)
|
||||
expect(result.data).to.deep.equal({})
|
||||
expect(result.body).to.equal(input)
|
||||
expect(result.parseError).to.be.a("string")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,53 @@
|
||||
import * as yaml from "js-yaml"
|
||||
|
||||
export type FrontmatterParseResult = {
|
||||
data: Record<string, unknown>
|
||||
/**
|
||||
* The markdown content after stripping the `--- frontmatter ---` block.
|
||||
*
|
||||
* Named `body` (rather than `content`) to make it clear this is the remaining
|
||||
* document body and to keep this helper generic for multiple consumers.
|
||||
*/
|
||||
body: string
|
||||
|
||||
/**
|
||||
* True when the input contained a frontmatter block, even if parsing failed.
|
||||
*
|
||||
* This allows callers to distinguish:
|
||||
* - "no frontmatter provided" (baseline behavior), vs
|
||||
* - "frontmatter was provided" (may have semantic meaning in future consumers).
|
||||
*/
|
||||
hadFrontmatter: boolean
|
||||
/**
|
||||
* Present only when YAML frontmatter was detected but failed to parse.
|
||||
*
|
||||
* This helper is intentionally fail-open and does not log. Returning `parseError`
|
||||
* lets each caller decide whether to log, surface diagnostics, etc.
|
||||
*/
|
||||
parseError?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse YAML frontmatter from markdown content.
|
||||
*
|
||||
* Behavior is intentionally fail-open:
|
||||
* - If YAML fails to parse, returns data={} and body=original markdown.
|
||||
* - If no frontmatter exists, returns data={} and body=original markdown.
|
||||
*/
|
||||
export function parseYamlFrontmatter(markdown: string): FrontmatterParseResult {
|
||||
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/
|
||||
const match = markdown.match(frontmatterRegex)
|
||||
|
||||
if (!match) {
|
||||
return { data: {}, body: markdown, hadFrontmatter: false }
|
||||
}
|
||||
|
||||
const [, yamlContent, body] = match
|
||||
try {
|
||||
const data = (yaml.load(yamlContent) as Record<string, unknown>) || {}
|
||||
return { data, body, hadFrontmatter: true }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
return { data: {}, body: markdown, hadFrontmatter: true, parseError: message }
|
||||
}
|
||||
}
|
||||
@@ -2,28 +2,16 @@ import { ensureSkillsDirectoryExists, GlobalFileNames } from "@core/storage/disk
|
||||
import type { SkillContent, SkillMetadata } from "@shared/skills"
|
||||
import { fileExistsAtPath, isDirectory } from "@utils/fs"
|
||||
import * as fs from "fs/promises"
|
||||
import * as yaml from "js-yaml"
|
||||
import * as path from "path"
|
||||
import { parseYamlFrontmatter } from "./frontmatter"
|
||||
|
||||
/**
|
||||
* Parse YAML frontmatter from markdown content.
|
||||
*/
|
||||
/** Parse YAML frontmatter from markdown content (shared helper). */
|
||||
function parseFrontmatter(fileContent: string): { data: Record<string, unknown>; content: string } {
|
||||
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/
|
||||
const match = fileContent.match(frontmatterRegex)
|
||||
|
||||
if (!match) {
|
||||
return { data: {}, content: fileContent }
|
||||
}
|
||||
|
||||
const [, yamlContent, body] = match
|
||||
try {
|
||||
const data = yaml.load(yamlContent) as Record<string, unknown>
|
||||
return { data: data || {}, content: body }
|
||||
} catch (error) {
|
||||
console.warn("Failed to parse YAML frontmatter:", error)
|
||||
return { data: {}, content: fileContent }
|
||||
const result = parseYamlFrontmatter(fileContent)
|
||||
if (result.parseError) {
|
||||
console.warn("Failed to parse YAML frontmatter:", result.parseError)
|
||||
}
|
||||
return { data: result.data, content: result.body }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,14 +30,16 @@ import { ExtensionRegistryInfo } from "@/registry"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
import { OcaAuthService } from "@/services/auth/oca/OcaAuthService"
|
||||
import { LogoutReason } from "@/services/auth/types"
|
||||
import { BannerService } from "@/services/banner/BannerService"
|
||||
import { featureFlagsService } from "@/services/feature-flags"
|
||||
import { getDistinctId } from "@/services/logging/distinctId"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { BannerCardData } from "@/shared/cline/banner"
|
||||
import { getAxiosSettings } from "@/shared/net"
|
||||
import { ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { getLatestAnnouncementId } from "@/utils/announcements"
|
||||
import { getCwd, getDesktopDir } from "@/utils/path"
|
||||
import { BannerService } from "../../services/banner/BannerService"
|
||||
import { PromptRegistry } from "../prompts/system-prompt"
|
||||
import {
|
||||
ensureCacheDirectoryExists,
|
||||
@@ -47,6 +49,7 @@ import {
|
||||
writeMcpMarketplaceCatalogToCache,
|
||||
} from "../storage/disk"
|
||||
import { fetchRemoteConfig } from "../storage/remote-config/fetch"
|
||||
import { clearRemoteConfig } from "../storage/remote-config/utils"
|
||||
import { type PersistenceErrorEvent, StateManager } from "../storage/StateManager"
|
||||
import { Task } from "../task"
|
||||
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
|
||||
@@ -104,13 +107,13 @@ export class Controller {
|
||||
|
||||
/**
|
||||
* Starts the periodic remote config fetching timer
|
||||
* Fetches immediately and then every 30 seconds
|
||||
* Fetches immediately and then every hour
|
||||
*/
|
||||
private startRemoteConfigTimer() {
|
||||
// Initial fetch
|
||||
fetchRemoteConfig(this)
|
||||
// Set up 30-second interval
|
||||
this.remoteConfigTimer = setInterval(() => fetchRemoteConfig(this), 30000) // 30 seconds
|
||||
// Set up 1-hour interval
|
||||
this.remoteConfigTimer = setInterval(() => fetchRemoteConfig(this), 3600000) // 1 hour
|
||||
}
|
||||
|
||||
constructor(readonly context: vscode.ExtensionContext) {
|
||||
@@ -119,7 +122,7 @@ export class Controller {
|
||||
this.stateManager = StateManager.get()
|
||||
StateManager.get().registerCallbacks({
|
||||
onPersistenceError: async ({ error }: PersistenceErrorEvent) => {
|
||||
console.error("[Controller] Cache persistence failed, recovering:", error)
|
||||
Logger.error("[Controller] Cache persistence failed, recovering:", error)
|
||||
try {
|
||||
await StateManager.get().reInitialize(this.task?.taskId)
|
||||
await this.postStateToWebview()
|
||||
@@ -128,7 +131,7 @@ export class Controller {
|
||||
message: "Saving settings to storage failed.",
|
||||
})
|
||||
} catch (recoveryError) {
|
||||
console.error("[Controller] Cache recovery failed:", recoveryError)
|
||||
Logger.error("[Controller] Cache recovery failed:", recoveryError)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to save settings. Please restart the extension.",
|
||||
@@ -186,6 +189,7 @@ export class Controller {
|
||||
try {
|
||||
// AuthService now handles its own storage cleanup in handleDeauth()
|
||||
this.stateManager.setGlobalState("userInfo", undefined)
|
||||
clearRemoteConfig()
|
||||
|
||||
// Update API providers through cache service
|
||||
const apiConfiguration = this.stateManager.getApiConfiguration()
|
||||
@@ -535,6 +539,8 @@ export class Controller {
|
||||
// Mark welcome view as completed since user has successfully logged in
|
||||
this.stateManager.setGlobalState("welcomeViewCompleted", true)
|
||||
|
||||
await fetchRemoteConfig(this)
|
||||
|
||||
if (this.task) {
|
||||
this.task.api = buildApiHandler({ ...updatedConfig, ulid: this.task.ulid }, currentMode)
|
||||
}
|
||||
@@ -877,6 +883,7 @@ export class Controller {
|
||||
const distinctId = getDistinctId()
|
||||
const version = ExtensionRegistryInfo.version
|
||||
const environment = ClineEnv.config().environment
|
||||
const banners = await this.getBanners()
|
||||
|
||||
// Set feature flag in dictation settings based on platform
|
||||
const updatedDictationSettings = {
|
||||
@@ -961,6 +968,8 @@ export class Controller {
|
||||
enableParallelToolCalling: this.stateManager.getGlobalSettingsKey("enableParallelToolCalling"),
|
||||
backgroundEditEnabled: this.stateManager.getGlobalSettingsKey("backgroundEditEnabled"),
|
||||
skillsEnabled,
|
||||
optOutOfRemoteConfig: this.stateManager.getGlobalSettingsKey("optOutOfRemoteConfig"),
|
||||
banners,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1003,64 +1012,12 @@ export class Controller {
|
||||
return history
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the BannerService if not already initialized
|
||||
*/
|
||||
private async ensureBannerService() {
|
||||
if (!BannerService.isInitialized()) {
|
||||
try {
|
||||
BannerService.initialize(this)
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize BannerService:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches non-dismissed banners for display
|
||||
* @returns Array of banners that haven't been dismissed
|
||||
*/
|
||||
async fetchBannersForDisplay(): Promise<any[]> {
|
||||
async getBanners(): Promise<BannerCardData[]> {
|
||||
try {
|
||||
await this.ensureBannerService()
|
||||
if (BannerService.isInitialized()) {
|
||||
return await BannerService.get().getNonDismissedBanners()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch banners:", error)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismisses a banner and sends telemetry
|
||||
* @param bannerId The ID of the banner to dismiss
|
||||
*/
|
||||
async dismissBanner(bannerId: string): Promise<void> {
|
||||
try {
|
||||
await this.ensureBannerService()
|
||||
if (BannerService.isInitialized()) {
|
||||
await BannerService.get().dismissBanner(bannerId)
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to dismiss banner:", error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a banner event for telemetry tracking
|
||||
* @param bannerId The ID of the banner
|
||||
* @param eventType The type of event (seen, dismiss, click)
|
||||
*/
|
||||
async trackBannerEvent(bannerId: string, eventType: "dismiss"): Promise<void> {
|
||||
try {
|
||||
await this.ensureBannerService()
|
||||
if (BannerService.isInitialized()) {
|
||||
await BannerService.get().sendBannerEvent(bannerId, eventType)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to track banner event:", error)
|
||||
return BannerService.get().getActiveBanners()
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export async function refreshLiteLlmModels(): Promise<Record<string, ModelInfo>>
|
||||
try {
|
||||
// Get the LiteLLM configuration
|
||||
const apiConfiguration = stateManager.getApiConfiguration()
|
||||
const baseUrl = apiConfiguration.liteLlmBaseUrl || ""
|
||||
const baseUrl = apiConfiguration.liteLlmBaseUrl || "http://localhost:4000"
|
||||
const apiKey = apiConfiguration.liteLlmApiKey
|
||||
|
||||
if (!apiKey) {
|
||||
|
||||
@@ -63,10 +63,10 @@ export async function refreshOcaModels(controller: Controller, request: StringRe
|
||||
}
|
||||
const modelInfo = model.model_info
|
||||
const supportedApiList = modelInfo.supported_api_list ?? [CHAT_COMPLETIONS_API]
|
||||
const apiFormat: ApiFormat = supportedApiList.includes(RESPONSES_API)
|
||||
? ApiFormat.OPENAI_RESPONSES
|
||||
: ApiFormat.OPENAI_CHAT
|
||||
console.log(modelId, supportedApiList)
|
||||
const apiFormat: ApiFormat =
|
||||
supportedApiList.includes(RESPONSES_API) && !supportedApiList.includes(CHAT_COMPLETIONS_API)
|
||||
? ApiFormat.OPENAI_RESPONSES
|
||||
: ApiFormat.OPENAI_CHAT
|
||||
models[modelId] = OcaModelInfo.create({
|
||||
maxTokens: model.litellm_params?.max_tokens || -1,
|
||||
contextWindow: modelInfo.context_window,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Empty } from "@shared/proto/cline/common"
|
||||
import { convertProtoToApiProvider } from "@shared/proto-conversions/models/api-configuration-conversion"
|
||||
import { buildApiHandler } from "@/core/api"
|
||||
import { ApiHandlerOptions, ApiHandlerSecrets, ApiProvider } from "@/shared/api"
|
||||
import { ApiHandlerOptions, ApiProvider } from "@/shared/api"
|
||||
import { UpdateApiConfigurationRequestNew } from "@/shared/proto/index.cline"
|
||||
import { Secrets } from "@/shared/storage/state-keys"
|
||||
import type { Controller } from "../index"
|
||||
|
||||
/**
|
||||
@@ -69,7 +70,7 @@ export async function updateApiConfiguration(controller: Controller, request: Up
|
||||
const { options: maskOptionsFields, secrets: maskSecretsFields } = parseFieldMask(updateMask)
|
||||
|
||||
// Process secrets based on field mask
|
||||
const secrets: Partial<ApiHandlerSecrets> = {}
|
||||
const secrets: Partial<Secrets> = {}
|
||||
|
||||
if (protoSecrets && maskSecretsFields.size > 0) {
|
||||
// Validate all masked fields exist
|
||||
@@ -81,7 +82,7 @@ export async function updateApiConfiguration(controller: Controller, request: Up
|
||||
// Process entries that are in the mask
|
||||
for (const [key, value] of Object.entries(protoSecrets)) {
|
||||
if (maskSecretsFields.has(key)) {
|
||||
secrets[key as keyof ApiHandlerSecrets] = value
|
||||
secrets[key as keyof Secrets] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { BannerService } from "@/services/banner/BannerService"
|
||||
import type { StringRequest } from "@/shared/proto/cline/common"
|
||||
import { Empty } from "@/shared/proto/cline/common"
|
||||
import type { Controller } from ".."
|
||||
|
||||
/**
|
||||
* Dismisses a banner by ID
|
||||
* Dismisses a banner and sends telemetry
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the banner ID to dismiss
|
||||
* @returns Empty response
|
||||
@@ -11,9 +12,14 @@ import type { Controller } from ".."
|
||||
export async function dismissBanner(controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
const bannerId = request.value
|
||||
|
||||
if (bannerId) {
|
||||
await controller.dismissBanner(bannerId)
|
||||
if (!bannerId) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return Empty.create()
|
||||
try {
|
||||
await BannerService.get().dismissBanner(bannerId)
|
||||
await controller.postStateToWebview()
|
||||
} catch (error) {
|
||||
console.error("Failed to dismiss banner:", error)
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { BannerService } from "@/services/banner/BannerService"
|
||||
import { Empty } from "@/shared/proto/cline/common"
|
||||
import type { TrackBannerEventRequest } from "@/shared/proto/cline/state"
|
||||
import type { Controller } from ".."
|
||||
@@ -8,12 +9,19 @@ import type { Controller } from ".."
|
||||
* @param request The request containing banner ID and event type
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function trackBannerEvent(controller: Controller, request: TrackBannerEventRequest): Promise<Empty> {
|
||||
export async function trackBannerEvent(_controller: Controller, request: TrackBannerEventRequest): Promise<Empty> {
|
||||
const { bannerId, eventType } = request
|
||||
|
||||
if (bannerId && eventType) {
|
||||
await controller.trackBannerEvent(bannerId, eventType as "dismiss")
|
||||
if (!bannerId) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return Empty.create()
|
||||
if (eventType !== "dismiss") {
|
||||
console.error("Unsupported event type ", eventType)
|
||||
return {}
|
||||
}
|
||||
try {
|
||||
await BannerService.get().sendBannerEvent(bannerId, eventType)
|
||||
} catch (error) {
|
||||
console.error("Failed to track banner event:", error)
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import { convertProtoToApiProvider } from "@shared/proto-conversions/models/api-
|
||||
import { OpenaiReasoningEffort } from "@shared/storage/types"
|
||||
import { TelemetrySetting } from "@shared/TelemetrySetting"
|
||||
import { ClineEnv } from "@/config"
|
||||
import { fetchRemoteConfig } from "@/core/storage/remote-config/fetch"
|
||||
import { clearRemoteConfig } from "@/core/storage/remote-config/utils"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { McpDisplayMode } from "@/shared/McpDisplayMode"
|
||||
import { ShowMessageType } from "@/shared/proto/host/window"
|
||||
@@ -383,6 +385,25 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
controller.stateManager.setGlobalState("enableParallelToolCalling", !!request.enableParallelToolCalling)
|
||||
}
|
||||
|
||||
if (request.optOutOfRemoteConfig !== undefined) {
|
||||
const hadOptedOut = controller.stateManager.getGlobalSettingsKey("optOutOfRemoteConfig")
|
||||
const isOptingOut = !!request.optOutOfRemoteConfig
|
||||
const isReenablingRemoteConfig = !isOptingOut && hadOptedOut
|
||||
|
||||
// Update now so any subsequent function can access the updated value
|
||||
controller.stateManager.setGlobalState("optOutOfRemoteConfig", isOptingOut)
|
||||
|
||||
if (isOptingOut && !hadOptedOut) {
|
||||
clearRemoteConfig()
|
||||
} else if (isReenablingRemoteConfig) {
|
||||
// Fire-and-forget: We don't need to await here
|
||||
// The function catches any errors and posts the updated state to the webview
|
||||
// The immediate state update below shows the user's intent (opted-in),
|
||||
// and we apply the actual config afterwards without blocking the settings update
|
||||
fetchRemoteConfig(controller)
|
||||
}
|
||||
}
|
||||
|
||||
// Post updated state to webview
|
||||
await controller.postStateToWebview()
|
||||
|
||||
|
||||
+1
-1
@@ -338,7 +338,7 @@
|
||||
"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.",
|
||||
"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 when it adds value to the user experience, but always follow it with an actual tool call - never call it twice in a row.\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",
|
||||
|
||||
@@ -296,7 +296,7 @@
|
||||
},
|
||||
{
|
||||
"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.",
|
||||
"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 when it adds value to the user experience, but always follow it with an actual tool call - never call it twice in a row.\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": {
|
||||
|
||||
@@ -28,7 +28,7 @@ const NATIVE_GPT_5: ClineToolSpec = {
|
||||
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.
|
||||
|
||||
IMPORTANT: Use this tool frequently to create a better user experience. Since it's non-blocking, there's no performance penalty for frequent use.
|
||||
IMPORTANT: Use this tool when it adds value to the user experience, but always follow it with an actual tool call - never call it twice in a row.
|
||||
|
||||
Use this tool when:
|
||||
- After reading files and before making any edits - explain your analysis and what changes you plan to make
|
||||
|
||||
@@ -38,7 +38,6 @@ export const config = createVariant(ModelFamily.GEMINI_3)
|
||||
SystemPromptSection.EDITING_FILES,
|
||||
SystemPromptSection.FEEDBACK,
|
||||
SystemPromptSection.TODO,
|
||||
SystemPromptSection.MCP,
|
||||
SystemPromptSection.TASK_PROGRESS,
|
||||
SystemPromptSection.SYSTEM_INFO,
|
||||
SystemPromptSection.OBJECTIVE,
|
||||
|
||||
@@ -125,9 +125,14 @@ export class VariantValidator {
|
||||
|
||||
// Check component overrides reference valid components
|
||||
if (variant.componentOverrides) {
|
||||
const invalidOverrides = Object.keys(variant.componentOverrides).filter(
|
||||
(key) => !variant.componentOrder.includes(key as SystemPromptSection),
|
||||
)
|
||||
const invalidOverrides = Object.keys(variant.componentOverrides).filter((key) => {
|
||||
const override = variant.componentOverrides[key as SystemPromptSection]
|
||||
// Skip overrides that explicitly disable a component - these are valid even without being in componentOrder
|
||||
if (override?.enabled === false) {
|
||||
return false
|
||||
}
|
||||
return !variant.componentOrder.includes(key as SystemPromptSection)
|
||||
})
|
||||
if (invalidOverrides.length > 0) {
|
||||
warnings.push(`Component overrides for unused components: ${invalidOverrides.join(", ")}`)
|
||||
}
|
||||
@@ -147,7 +152,14 @@ export class VariantValidator {
|
||||
|
||||
// Check tool overrides reference valid tools
|
||||
if (variant.toolOverrides) {
|
||||
const invalidOverrides = Object.keys(variant.toolOverrides).filter((key) => !variant.tools?.includes(key as any))
|
||||
const invalidOverrides = Object.keys(variant.toolOverrides).filter((key) => {
|
||||
const override = variant.toolOverrides![key as keyof typeof variant.toolOverrides]
|
||||
// Skip overrides that explicitly disable a tool - these are valid even without being in tools list
|
||||
if (override?.enabled === false) {
|
||||
return false
|
||||
}
|
||||
return !variant.tools?.includes(key as any)
|
||||
})
|
||||
if (invalidOverrides.length > 0) {
|
||||
warnings.push(`Tool overrides for unused tools: ${invalidOverrides.join(", ")}`)
|
||||
}
|
||||
|
||||
@@ -94,6 +94,6 @@ export const xsComponentOverrides: PromptVariant["componentOverrides"] = {
|
||||
enabled: true, // Use default user instructions
|
||||
},
|
||||
[SystemPromptSection.FEEDBACK]: {
|
||||
enabled: true, // Use default feedback section
|
||||
enabled: false,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { ApiConfiguration, ModelInfo } from "@shared/api"
|
||||
import {
|
||||
ApiHandlerSettingsKeys,
|
||||
GlobalState,
|
||||
GlobalStateAndSettings,
|
||||
GlobalStateAndSettingsKey,
|
||||
GlobalStateKey,
|
||||
isSecretKey,
|
||||
isSettingsKey,
|
||||
LocalState,
|
||||
LocalStateKey,
|
||||
RemoteConfigFields,
|
||||
SecretKey,
|
||||
SecretKeys,
|
||||
Secrets,
|
||||
Settings,
|
||||
SettingsKey,
|
||||
@@ -15,6 +19,7 @@ import {
|
||||
import chokidar, { FSWatcher } from "chokidar"
|
||||
import type { ExtensionContext } from "vscode"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { ShowMessageType } from "@/shared/proto/index.host"
|
||||
import {
|
||||
getTaskHistoryStateFilePath,
|
||||
@@ -24,6 +29,7 @@ import {
|
||||
writeTaskSettingsToStorage,
|
||||
} from "./disk"
|
||||
import { STATE_MANAGER_NOT_INITIALIZED } from "./error-messages"
|
||||
import { filterAllowedRemoteConfigFields } from "./remote-config/utils"
|
||||
import { readGlobalStateFromDisk, readSecretsFromDisk, readWorkspaceStateFromDisk } from "./utils/state-helpers"
|
||||
export interface PersistenceErrorEvent {
|
||||
error: Error
|
||||
@@ -180,6 +186,18 @@ export class StateManager {
|
||||
this.scheduleDebouncedPersistence()
|
||||
}
|
||||
|
||||
private setRemoteConfigState(updates: Partial<GlobalStateAndSettings>): void {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(STATE_MANAGER_NOT_INITIALIZED)
|
||||
}
|
||||
|
||||
// Update cache in one go
|
||||
this.remoteConfigCache = {
|
||||
...this.remoteConfigCache,
|
||||
...filterAllowedRemoteConfigFields(updates),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set method for task settings keys - updates cache immediately and schedules debounced persistence
|
||||
*/
|
||||
@@ -477,353 +495,43 @@ export class StateManager {
|
||||
|
||||
/**
|
||||
* Convenience method for setting API configuration
|
||||
* Automatically categorizes keys based on STATE_DEFINITION and SecretKeys
|
||||
*/
|
||||
setApiConfiguration(apiConfiguration: ApiConfiguration): void {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(STATE_MANAGER_NOT_INITIALIZED)
|
||||
}
|
||||
|
||||
const {
|
||||
apiKey,
|
||||
openRouterApiKey,
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
awsRegion,
|
||||
awsUseCrossRegionInference,
|
||||
awsUseGlobalInference,
|
||||
awsBedrockUsePromptCache,
|
||||
awsBedrockEndpoint,
|
||||
awsBedrockApiKey,
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
openAiApiKey,
|
||||
openAiHeaders,
|
||||
ollamaBaseUrl,
|
||||
ollamaApiKey,
|
||||
ollamaApiOptionsCtxNum,
|
||||
lmStudioBaseUrl,
|
||||
lmStudioMaxTokens,
|
||||
anthropicBaseUrl,
|
||||
geminiApiKey,
|
||||
geminiBaseUrl,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
requestyBaseUrl,
|
||||
togetherApiKey,
|
||||
qwenApiKey,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
azureApiVersion,
|
||||
azureIdentity,
|
||||
openRouterProviderSorting,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmApiKey,
|
||||
liteLlmUsePromptCache,
|
||||
qwenApiLine,
|
||||
moonshotApiLine,
|
||||
zaiApiLine,
|
||||
asksageApiKey,
|
||||
asksageApiUrl,
|
||||
xaiApiKey,
|
||||
clineAccountId,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
fireworksApiKey,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
sapAiCoreUseOrchestrationMode,
|
||||
claudeCodePath,
|
||||
qwenCodeOauthPath,
|
||||
basetenApiKey,
|
||||
huggingFaceApiKey,
|
||||
huaweiCloudMaasApiKey,
|
||||
difyApiKey,
|
||||
difyBaseUrl,
|
||||
vercelAiGatewayApiKey,
|
||||
zaiApiKey,
|
||||
minimaxApiKey,
|
||||
minimaxApiLine,
|
||||
nousResearchApiKey,
|
||||
requestTimeoutMs,
|
||||
ocaBaseUrl,
|
||||
ocaMode,
|
||||
hicapApiKey,
|
||||
hicapModelId,
|
||||
aihubmixApiKey,
|
||||
aihubmixBaseUrl,
|
||||
aihubmixAppCode,
|
||||
// Plan mode configurations
|
||||
planModeApiProvider,
|
||||
planModeApiModelId,
|
||||
planModeThinkingBudgetTokens,
|
||||
planModeReasoningEffort,
|
||||
planModeVsCodeLmModelSelector,
|
||||
planModeAwsBedrockCustomSelected,
|
||||
planModeAwsBedrockCustomModelBaseId,
|
||||
planModeOpenRouterModelId,
|
||||
planModeOpenRouterModelInfo,
|
||||
planModeOpenAiModelId,
|
||||
planModeOpenAiModelInfo,
|
||||
planModeOllamaModelId,
|
||||
planModeLmStudioModelId,
|
||||
planModeLiteLlmModelId,
|
||||
planModeLiteLlmModelInfo,
|
||||
planModeRequestyModelId,
|
||||
planModeRequestyModelInfo,
|
||||
planModeTogetherModelId,
|
||||
planModeFireworksModelId,
|
||||
planModeSapAiCoreModelId,
|
||||
planModeSapAiCoreDeploymentId,
|
||||
planModeGroqModelId,
|
||||
planModeGroqModelInfo,
|
||||
planModeBasetenModelId,
|
||||
planModeBasetenModelInfo,
|
||||
planModeHuggingFaceModelId,
|
||||
planModeHuggingFaceModelInfo,
|
||||
planModeHuaweiCloudMaasModelId,
|
||||
planModeHuaweiCloudMaasModelInfo,
|
||||
planModeOcaModelId,
|
||||
planModeOcaModelInfo,
|
||||
planModeOcaReasoningEffort,
|
||||
planModeHicapModelId,
|
||||
planModeHicapModelInfo,
|
||||
planModeAihubmixModelId,
|
||||
planModeAihubmixModelInfo,
|
||||
planModeNousResearchModelId,
|
||||
planModeVercelAiGatewayModelId,
|
||||
planModeVercelAiGatewayModelInfo,
|
||||
geminiPlanModeThinkingLevel,
|
||||
// Act mode configurations
|
||||
actModeApiProvider,
|
||||
actModeApiModelId,
|
||||
actModeThinkingBudgetTokens,
|
||||
actModeReasoningEffort,
|
||||
actModeVsCodeLmModelSelector,
|
||||
actModeAwsBedrockCustomSelected,
|
||||
actModeAwsBedrockCustomModelBaseId,
|
||||
actModeOpenRouterModelId,
|
||||
actModeOpenRouterModelInfo,
|
||||
actModeOpenAiModelId,
|
||||
actModeOpenAiModelInfo,
|
||||
actModeOllamaModelId,
|
||||
actModeLmStudioModelId,
|
||||
actModeLiteLlmModelId,
|
||||
actModeLiteLlmModelInfo,
|
||||
actModeRequestyModelId,
|
||||
actModeRequestyModelInfo,
|
||||
actModeTogetherModelId,
|
||||
actModeFireworksModelId,
|
||||
actModeSapAiCoreModelId,
|
||||
actModeSapAiCoreDeploymentId,
|
||||
actModeGroqModelId,
|
||||
actModeGroqModelInfo,
|
||||
actModeBasetenModelId,
|
||||
actModeBasetenModelInfo,
|
||||
actModeHuggingFaceModelId,
|
||||
actModeHuggingFaceModelInfo,
|
||||
actModeHuaweiCloudMaasModelId,
|
||||
actModeHuaweiCloudMaasModelInfo,
|
||||
actModeOcaModelId,
|
||||
actModeOcaModelInfo,
|
||||
actModeOcaReasoningEffort,
|
||||
actModeHicapModelId,
|
||||
actModeHicapModelInfo,
|
||||
actModeAihubmixModelId,
|
||||
actModeAihubmixModelInfo,
|
||||
actModeNousResearchModelId,
|
||||
actModeVercelAiGatewayModelId,
|
||||
actModeVercelAiGatewayModelInfo,
|
||||
geminiActModeThinkingLevel,
|
||||
} = apiConfiguration
|
||||
// Automatically categorize the API configuration keys
|
||||
const { settingsUpdates, secretsUpdates } = Object.entries(apiConfiguration).reduce(
|
||||
(acc, [key, value]) => {
|
||||
if (key === undefined || value === undefined) {
|
||||
return acc // Skip undefined values
|
||||
}
|
||||
|
||||
// Batch update global state keys
|
||||
this.setGlobalStateBatch({
|
||||
// Plan mode configuration updates
|
||||
planModeApiProvider,
|
||||
planModeApiModelId,
|
||||
planModeThinkingBudgetTokens,
|
||||
planModeReasoningEffort,
|
||||
planModeVsCodeLmModelSelector,
|
||||
planModeAwsBedrockCustomSelected,
|
||||
planModeAwsBedrockCustomModelBaseId,
|
||||
planModeOpenRouterModelId,
|
||||
planModeOpenRouterModelInfo,
|
||||
planModeOpenAiModelId,
|
||||
planModeOpenAiModelInfo,
|
||||
planModeOllamaModelId,
|
||||
planModeLmStudioModelId,
|
||||
planModeLiteLlmModelId,
|
||||
planModeLiteLlmModelInfo,
|
||||
planModeRequestyModelId,
|
||||
planModeRequestyModelInfo,
|
||||
planModeTogetherModelId,
|
||||
planModeFireworksModelId,
|
||||
planModeSapAiCoreModelId,
|
||||
planModeSapAiCoreDeploymentId,
|
||||
planModeGroqModelId,
|
||||
planModeGroqModelInfo,
|
||||
planModeBasetenModelId,
|
||||
planModeBasetenModelInfo,
|
||||
planModeHuggingFaceModelId,
|
||||
planModeHuggingFaceModelInfo,
|
||||
planModeHuaweiCloudMaasModelId,
|
||||
planModeHuaweiCloudMaasModelInfo,
|
||||
planModeOcaModelId,
|
||||
planModeOcaModelInfo,
|
||||
planModeOcaReasoningEffort,
|
||||
planModeHicapModelId,
|
||||
planModeHicapModelInfo,
|
||||
planModeAihubmixModelId,
|
||||
planModeAihubmixModelInfo,
|
||||
planModeNousResearchModelId,
|
||||
planModeVercelAiGatewayModelId,
|
||||
planModeVercelAiGatewayModelInfo,
|
||||
geminiPlanModeThinkingLevel,
|
||||
if (isSecretKey(key)) {
|
||||
// This is a secret key
|
||||
acc.secretsUpdates[key as keyof Secrets] = value as any
|
||||
} else if (isSettingsKey(key)) {
|
||||
// This is a settings key
|
||||
acc.settingsUpdates[key as keyof Settings] = value as any
|
||||
}
|
||||
|
||||
// Act mode configuration updates
|
||||
actModeApiProvider,
|
||||
actModeApiModelId,
|
||||
actModeThinkingBudgetTokens,
|
||||
actModeReasoningEffort,
|
||||
actModeVsCodeLmModelSelector,
|
||||
actModeAwsBedrockCustomSelected,
|
||||
actModeAwsBedrockCustomModelBaseId,
|
||||
actModeOpenRouterModelId,
|
||||
actModeOpenRouterModelInfo,
|
||||
actModeOpenAiModelId,
|
||||
actModeOpenAiModelInfo,
|
||||
actModeOllamaModelId,
|
||||
actModeLmStudioModelId,
|
||||
actModeLiteLlmModelId,
|
||||
actModeLiteLlmModelInfo,
|
||||
actModeRequestyModelId,
|
||||
actModeRequestyModelInfo,
|
||||
actModeTogetherModelId,
|
||||
actModeFireworksModelId,
|
||||
actModeSapAiCoreModelId,
|
||||
actModeSapAiCoreDeploymentId,
|
||||
actModeGroqModelId,
|
||||
actModeGroqModelInfo,
|
||||
actModeBasetenModelId,
|
||||
actModeBasetenModelInfo,
|
||||
actModeHuggingFaceModelId,
|
||||
actModeHuggingFaceModelInfo,
|
||||
actModeHuaweiCloudMaasModelId,
|
||||
actModeHuaweiCloudMaasModelInfo,
|
||||
actModeOcaModelId,
|
||||
actModeOcaModelInfo,
|
||||
actModeOcaReasoningEffort,
|
||||
actModeHicapModelId,
|
||||
actModeHicapModelInfo,
|
||||
actModeAihubmixModelId,
|
||||
actModeAihubmixModelInfo,
|
||||
actModeNousResearchModelId,
|
||||
actModeVercelAiGatewayModelId,
|
||||
actModeVercelAiGatewayModelInfo,
|
||||
geminiActModeThinkingLevel,
|
||||
return acc
|
||||
},
|
||||
{ settingsUpdates: {} as Partial<Settings>, secretsUpdates: {} as Partial<Secrets> },
|
||||
)
|
||||
|
||||
// Global state updates
|
||||
awsRegion,
|
||||
awsUseCrossRegionInference,
|
||||
awsUseGlobalInference,
|
||||
awsBedrockUsePromptCache,
|
||||
awsBedrockEndpoint,
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
requestyBaseUrl,
|
||||
openAiBaseUrl,
|
||||
openAiHeaders,
|
||||
ollamaBaseUrl,
|
||||
ollamaApiOptionsCtxNum,
|
||||
lmStudioBaseUrl,
|
||||
lmStudioMaxTokens,
|
||||
anthropicBaseUrl,
|
||||
geminiBaseUrl,
|
||||
azureApiVersion,
|
||||
azureIdentity,
|
||||
openRouterProviderSorting,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmUsePromptCache,
|
||||
qwenApiLine,
|
||||
moonshotApiLine,
|
||||
zaiApiLine,
|
||||
asksageApiUrl,
|
||||
requestTimeoutMs,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
sapAiCoreUseOrchestrationMode,
|
||||
claudeCodePath,
|
||||
difyBaseUrl,
|
||||
qwenCodeOauthPath,
|
||||
ocaBaseUrl,
|
||||
minimaxApiLine,
|
||||
ocaMode,
|
||||
hicapModelId,
|
||||
aihubmixBaseUrl,
|
||||
aihubmixAppCode,
|
||||
})
|
||||
// Batch update settings (stored in global state)
|
||||
if (Object.keys(settingsUpdates).length > 0) {
|
||||
this.setRemoteConfigState(settingsUpdates)
|
||||
this.setGlobalStateBatch(settingsUpdates)
|
||||
}
|
||||
|
||||
// Batch update secrets
|
||||
this.setSecretsBatch({
|
||||
apiKey,
|
||||
openRouterApiKey,
|
||||
clineAccountId,
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
awsBedrockApiKey,
|
||||
openAiApiKey,
|
||||
ollamaApiKey,
|
||||
geminiApiKey,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
togetherApiKey,
|
||||
qwenApiKey,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
liteLlmApiKey,
|
||||
fireworksApiKey,
|
||||
asksageApiKey,
|
||||
xaiApiKey,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
basetenApiKey,
|
||||
huggingFaceApiKey,
|
||||
huaweiCloudMaasApiKey,
|
||||
difyApiKey,
|
||||
vercelAiGatewayApiKey,
|
||||
zaiApiKey,
|
||||
minimaxApiKey,
|
||||
hicapApiKey,
|
||||
aihubmixApiKey,
|
||||
nousResearchApiKey,
|
||||
})
|
||||
if (Object.keys(secretsUpdates).length > 0) {
|
||||
this.setSecretsBatch(secretsUpdates)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -984,7 +692,7 @@ export class StateManager {
|
||||
await this.persistPendingState()
|
||||
this.persistenceTimeout = null
|
||||
} catch (error) {
|
||||
console.error("[StateManager] Failed to persist pending changes:", error)
|
||||
Logger.error("[StateManager] Failed to persist pending changes:", error)
|
||||
this.persistenceTimeout = null
|
||||
|
||||
// Call persistence error callback for error recovery
|
||||
@@ -1091,292 +799,47 @@ export class StateManager {
|
||||
Object.assign(this.workspaceStateCache, workspaceState)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get a setting value with override support
|
||||
* Precedence: remote config > task settings > global settings
|
||||
*/
|
||||
private getSettingWithOverride<K extends keyof Settings>(key: K): Settings[K] {
|
||||
const remoteValue = this.remoteConfigCache[key]
|
||||
if (remoteValue !== undefined) {
|
||||
return remoteValue
|
||||
}
|
||||
const taskValue = this.taskStateCache[key]
|
||||
if (taskValue !== undefined) {
|
||||
return taskValue
|
||||
}
|
||||
return this.globalStateCache[key]
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get a secret value
|
||||
*/
|
||||
private getSecret<K extends keyof Secrets>(key: K): Secrets[K] {
|
||||
return this.secretsCache[key]
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct API configuration from cached component keys
|
||||
*/
|
||||
private constructApiConfigurationFromCache(): ApiConfiguration {
|
||||
return {
|
||||
// Secrets
|
||||
apiKey: this.secretsCache["apiKey"],
|
||||
openRouterApiKey: this.secretsCache["openRouterApiKey"],
|
||||
clineAccountId: this.secretsCache["clineAccountId"],
|
||||
awsAccessKey: this.secretsCache["awsAccessKey"],
|
||||
awsSecretKey: this.secretsCache["awsSecretKey"],
|
||||
awsSessionToken: this.secretsCache["awsSessionToken"],
|
||||
awsBedrockApiKey: this.secretsCache["awsBedrockApiKey"],
|
||||
openAiApiKey: this.secretsCache["openAiApiKey"],
|
||||
ollamaApiKey: this.secretsCache["ollamaApiKey"],
|
||||
geminiApiKey: this.secretsCache["geminiApiKey"],
|
||||
openAiNativeApiKey: this.secretsCache["openAiNativeApiKey"],
|
||||
deepSeekApiKey: this.secretsCache["deepSeekApiKey"],
|
||||
requestyApiKey: this.secretsCache["requestyApiKey"],
|
||||
togetherApiKey: this.secretsCache["togetherApiKey"],
|
||||
qwenApiKey: this.secretsCache["qwenApiKey"],
|
||||
doubaoApiKey: this.secretsCache["doubaoApiKey"],
|
||||
mistralApiKey: this.secretsCache["mistralApiKey"],
|
||||
liteLlmApiKey: this.secretsCache["remoteLiteLlmApiKey"] || this.secretsCache["liteLlmApiKey"],
|
||||
fireworksApiKey: this.secretsCache["fireworksApiKey"],
|
||||
asksageApiKey: this.secretsCache["asksageApiKey"],
|
||||
xaiApiKey: this.secretsCache["xaiApiKey"],
|
||||
sambanovaApiKey: this.secretsCache["sambanovaApiKey"],
|
||||
cerebrasApiKey: this.secretsCache["cerebrasApiKey"],
|
||||
groqApiKey: this.secretsCache["groqApiKey"],
|
||||
basetenApiKey: this.secretsCache["basetenApiKey"],
|
||||
moonshotApiKey: this.secretsCache["moonshotApiKey"],
|
||||
nebiusApiKey: this.secretsCache["nebiusApiKey"],
|
||||
sapAiCoreClientId: this.secretsCache["sapAiCoreClientId"],
|
||||
sapAiCoreClientSecret: this.secretsCache["sapAiCoreClientSecret"],
|
||||
huggingFaceApiKey: this.secretsCache["huggingFaceApiKey"],
|
||||
huaweiCloudMaasApiKey: this.secretsCache["huaweiCloudMaasApiKey"],
|
||||
difyApiKey: this.secretsCache["difyApiKey"],
|
||||
vercelAiGatewayApiKey: this.secretsCache["vercelAiGatewayApiKey"],
|
||||
zaiApiKey: this.secretsCache["zaiApiKey"],
|
||||
minimaxApiKey: this.secretsCache["minimaxApiKey"],
|
||||
hicapApiKey: this.secretsCache["hicapApiKey"],
|
||||
aihubmixApiKey: this.secretsCache["aihubmixApiKey"],
|
||||
// Build secrets object
|
||||
const secrets = Object.fromEntries(SecretKeys.map((key) => [key, this.getSecret(key)])) as Secrets
|
||||
|
||||
// Global state (with remote config precedence for applicable fields)
|
||||
awsRegion:
|
||||
this.remoteConfigCache["awsRegion"] || this.taskStateCache["awsRegion"] || this.globalStateCache["awsRegion"],
|
||||
awsUseCrossRegionInference:
|
||||
this.remoteConfigCache["awsUseCrossRegionInference"] ||
|
||||
this.taskStateCache["awsUseCrossRegionInference"] ||
|
||||
this.globalStateCache["awsUseCrossRegionInference"],
|
||||
awsUseGlobalInference:
|
||||
this.remoteConfigCache["awsUseGlobalInference"] ||
|
||||
this.taskStateCache["awsUseGlobalInference"] ||
|
||||
this.globalStateCache["awsUseGlobalInference"],
|
||||
awsBedrockUsePromptCache:
|
||||
this.remoteConfigCache["awsBedrockUsePromptCache"] ||
|
||||
this.taskStateCache["awsBedrockUsePromptCache"] ||
|
||||
this.globalStateCache["awsBedrockUsePromptCache"],
|
||||
awsBedrockEndpoint:
|
||||
this.remoteConfigCache["awsBedrockEndpoint"] ||
|
||||
this.taskStateCache["awsBedrockEndpoint"] ||
|
||||
this.globalStateCache["awsBedrockEndpoint"],
|
||||
awsProfile: this.taskStateCache["awsProfile"] || this.globalStateCache["awsProfile"],
|
||||
awsUseProfile: this.taskStateCache["awsUseProfile"] || this.globalStateCache["awsUseProfile"],
|
||||
awsAuthentication: this.taskStateCache["awsAuthentication"] || this.globalStateCache["awsAuthentication"],
|
||||
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"] ||
|
||||
this.taskStateCache["openAiBaseUrl"] ||
|
||||
this.globalStateCache["openAiBaseUrl"],
|
||||
openAiHeaders:
|
||||
this.remoteConfigCache["openAiHeaders"] ||
|
||||
this.taskStateCache["openAiHeaders"] ||
|
||||
this.globalStateCache["openAiHeaders"] ||
|
||||
{},
|
||||
ollamaBaseUrl: this.taskStateCache["ollamaBaseUrl"] || this.globalStateCache["ollamaBaseUrl"],
|
||||
ollamaApiOptionsCtxNum:
|
||||
this.taskStateCache["ollamaApiOptionsCtxNum"] || this.globalStateCache["ollamaApiOptionsCtxNum"],
|
||||
lmStudioBaseUrl: this.taskStateCache["lmStudioBaseUrl"] || this.globalStateCache["lmStudioBaseUrl"],
|
||||
lmStudioMaxTokens: this.taskStateCache["lmStudioMaxTokens"] || this.globalStateCache["lmStudioMaxTokens"],
|
||||
anthropicBaseUrl: this.taskStateCache["anthropicBaseUrl"] || this.globalStateCache["anthropicBaseUrl"],
|
||||
geminiBaseUrl: this.taskStateCache["geminiBaseUrl"] || this.globalStateCache["geminiBaseUrl"],
|
||||
azureApiVersion:
|
||||
this.remoteConfigCache["azureApiVersion"] ||
|
||||
this.taskStateCache["azureApiVersion"] ||
|
||||
this.globalStateCache["azureApiVersion"],
|
||||
azureIdentity:
|
||||
this.remoteConfigCache["azureIdentity"] ||
|
||||
this.taskStateCache["azureIdentity"] ||
|
||||
this.globalStateCache["azureIdentity"],
|
||||
openRouterProviderSorting:
|
||||
this.taskStateCache["openRouterProviderSorting"] || this.globalStateCache["openRouterProviderSorting"],
|
||||
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"],
|
||||
zaiApiLine: this.taskStateCache["zaiApiLine"] || this.globalStateCache["zaiApiLine"],
|
||||
asksageApiUrl: this.taskStateCache["asksageApiUrl"] || this.globalStateCache["asksageApiUrl"],
|
||||
requestTimeoutMs: this.taskStateCache["requestTimeoutMs"] || this.globalStateCache["requestTimeoutMs"],
|
||||
fireworksModelMaxCompletionTokens:
|
||||
this.taskStateCache["fireworksModelMaxCompletionTokens"] ||
|
||||
this.globalStateCache["fireworksModelMaxCompletionTokens"],
|
||||
fireworksModelMaxTokens:
|
||||
this.taskStateCache["fireworksModelMaxTokens"] || this.globalStateCache["fireworksModelMaxTokens"],
|
||||
sapAiCoreBaseUrl: this.taskStateCache["sapAiCoreBaseUrl"] || this.globalStateCache["sapAiCoreBaseUrl"],
|
||||
sapAiCoreTokenUrl: this.taskStateCache["sapAiCoreTokenUrl"] || this.globalStateCache["sapAiCoreTokenUrl"],
|
||||
sapAiResourceGroup: this.taskStateCache["sapAiResourceGroup"] || this.globalStateCache["sapAiResourceGroup"],
|
||||
sapAiCoreUseOrchestrationMode:
|
||||
this.taskStateCache["sapAiCoreUseOrchestrationMode"] || this.globalStateCache["sapAiCoreUseOrchestrationMode"],
|
||||
claudeCodePath: this.taskStateCache["claudeCodePath"] || this.globalStateCache["claudeCodePath"],
|
||||
qwenCodeOauthPath: this.taskStateCache["qwenCodeOauthPath"] || this.globalStateCache["qwenCodeOauthPath"],
|
||||
difyBaseUrl: this.taskStateCache["difyBaseUrl"] || this.globalStateCache["difyBaseUrl"],
|
||||
ocaBaseUrl: this.globalStateCache["ocaBaseUrl"],
|
||||
minimaxApiLine: this.taskStateCache["minimaxApiLine"] || this.globalStateCache["minimaxApiLine"],
|
||||
ocaMode: this.globalStateCache["ocaMode"],
|
||||
hicapModelId: this.globalStateCache["hicapModelId"],
|
||||
aihubmixBaseUrl: this.taskStateCache["aihubmixBaseUrl"] || this.globalStateCache["aihubmixBaseUrl"],
|
||||
aihubmixAppCode: this.taskStateCache["aihubmixAppCode"] || this.globalStateCache["aihubmixAppCode"],
|
||||
|
||||
// Plan mode configurations
|
||||
planModeApiProvider:
|
||||
this.remoteConfigCache["planModeApiProvider"] ||
|
||||
this.taskStateCache["planModeApiProvider"] ||
|
||||
this.globalStateCache["planModeApiProvider"],
|
||||
planModeApiModelId: this.taskStateCache["planModeApiModelId"] || this.globalStateCache["planModeApiModelId"],
|
||||
planModeThinkingBudgetTokens:
|
||||
this.taskStateCache["planModeThinkingBudgetTokens"] || this.globalStateCache["planModeThinkingBudgetTokens"],
|
||||
planModeReasoningEffort:
|
||||
this.taskStateCache["planModeReasoningEffort"] || this.globalStateCache["planModeReasoningEffort"],
|
||||
planModeVsCodeLmModelSelector:
|
||||
this.taskStateCache["planModeVsCodeLmModelSelector"] || this.globalStateCache["planModeVsCodeLmModelSelector"],
|
||||
planModeAwsBedrockCustomSelected:
|
||||
this.taskStateCache["planModeAwsBedrockCustomSelected"] ||
|
||||
this.globalStateCache["planModeAwsBedrockCustomSelected"],
|
||||
planModeAwsBedrockCustomModelBaseId:
|
||||
this.taskStateCache["planModeAwsBedrockCustomModelBaseId"] ||
|
||||
this.globalStateCache["planModeAwsBedrockCustomModelBaseId"],
|
||||
planModeOpenRouterModelId:
|
||||
this.taskStateCache["planModeOpenRouterModelId"] || this.globalStateCache["planModeOpenRouterModelId"],
|
||||
planModeOpenRouterModelInfo:
|
||||
this.taskStateCache["planModeOpenRouterModelInfo"] || this.globalStateCache["planModeOpenRouterModelInfo"],
|
||||
planModeOpenAiModelId: this.taskStateCache["planModeOpenAiModelId"] || this.globalStateCache["planModeOpenAiModelId"],
|
||||
planModeOpenAiModelInfo:
|
||||
this.taskStateCache["planModeOpenAiModelInfo"] || this.globalStateCache["planModeOpenAiModelInfo"],
|
||||
planModeOllamaModelId: this.taskStateCache["planModeOllamaModelId"] || this.globalStateCache["planModeOllamaModelId"],
|
||||
planModeLmStudioModelId:
|
||||
this.taskStateCache["planModeLmStudioModelId"] || this.globalStateCache["planModeLmStudioModelId"],
|
||||
planModeLiteLlmModelId:
|
||||
this.taskStateCache["planModeLiteLlmModelId"] || this.globalStateCache["planModeLiteLlmModelId"],
|
||||
planModeLiteLlmModelInfo:
|
||||
this.taskStateCache["planModeLiteLlmModelInfo"] || this.globalStateCache["planModeLiteLlmModelInfo"],
|
||||
planModeRequestyModelId:
|
||||
this.taskStateCache["planModeRequestyModelId"] || this.globalStateCache["planModeRequestyModelId"],
|
||||
planModeRequestyModelInfo:
|
||||
this.taskStateCache["planModeRequestyModelInfo"] || this.globalStateCache["planModeRequestyModelInfo"],
|
||||
planModeTogetherModelId:
|
||||
this.taskStateCache["planModeTogetherModelId"] || this.globalStateCache["planModeTogetherModelId"],
|
||||
planModeFireworksModelId:
|
||||
this.taskStateCache["planModeFireworksModelId"] || this.globalStateCache["planModeFireworksModelId"],
|
||||
planModeSapAiCoreModelId:
|
||||
this.taskStateCache["planModeSapAiCoreModelId"] || this.globalStateCache["planModeSapAiCoreModelId"],
|
||||
planModeSapAiCoreDeploymentId:
|
||||
this.taskStateCache["planModeSapAiCoreDeploymentId"] || this.globalStateCache["planModeSapAiCoreDeploymentId"],
|
||||
planModeGroqModelId: this.taskStateCache["planModeGroqModelId"] || this.globalStateCache["planModeGroqModelId"],
|
||||
planModeGroqModelInfo: this.taskStateCache["planModeGroqModelInfo"] || this.globalStateCache["planModeGroqModelInfo"],
|
||||
planModeBasetenModelId:
|
||||
this.taskStateCache["planModeBasetenModelId"] || this.globalStateCache["planModeBasetenModelId"],
|
||||
planModeBasetenModelInfo:
|
||||
this.taskStateCache["planModeBasetenModelInfo"] || this.globalStateCache["planModeBasetenModelInfo"],
|
||||
planModeHuggingFaceModelId:
|
||||
this.taskStateCache["planModeHuggingFaceModelId"] || this.globalStateCache["planModeHuggingFaceModelId"],
|
||||
planModeHuggingFaceModelInfo:
|
||||
this.taskStateCache["planModeHuggingFaceModelInfo"] || this.globalStateCache["planModeHuggingFaceModelInfo"],
|
||||
planModeHuaweiCloudMaasModelId:
|
||||
this.taskStateCache["planModeHuaweiCloudMaasModelId"] || this.globalStateCache["planModeHuaweiCloudMaasModelId"],
|
||||
planModeHuaweiCloudMaasModelInfo:
|
||||
this.taskStateCache["planModeHuaweiCloudMaasModelInfo"] ||
|
||||
this.globalStateCache["planModeHuaweiCloudMaasModelInfo"],
|
||||
planModeOcaModelId: this.globalStateCache["planModeOcaModelId"],
|
||||
planModeOcaModelInfo: this.globalStateCache["planModeOcaModelInfo"],
|
||||
planModeOcaReasoningEffort: this.globalStateCache["planModeOcaReasoningEffort"],
|
||||
planModeHicapModelId: this.taskStateCache["planModeHicapModelId"] || this.globalStateCache["planModeHicapModelId"],
|
||||
planModeHicapModelInfo:
|
||||
this.taskStateCache["planModeHicapModelInfo"] || this.globalStateCache["planModeHicapModelInfo"],
|
||||
planModeAihubmixModelId:
|
||||
this.taskStateCache["planModeAihubmixModelId"] || this.globalStateCache["planModeAihubmixModelId"],
|
||||
planModeAihubmixModelInfo:
|
||||
this.taskStateCache["planModeAihubmixModelInfo"] || this.globalStateCache["planModeAihubmixModelInfo"],
|
||||
planModeNousResearchModelId:
|
||||
this.taskStateCache["planModeNousResearchModelId"] || this.globalStateCache["planModeNousResearchModelId"],
|
||||
planModeVercelAiGatewayModelId:
|
||||
this.taskStateCache["planModeVercelAiGatewayModelId"] || this.globalStateCache["planModeVercelAiGatewayModelId"],
|
||||
planModeVercelAiGatewayModelInfo:
|
||||
this.taskStateCache["planModeVercelAiGatewayModelInfo"] ||
|
||||
this.globalStateCache["planModeVercelAiGatewayModelInfo"],
|
||||
geminiPlanModeThinkingLevel:
|
||||
this.taskStateCache["geminiPlanModeThinkingLevel"] || this.globalStateCache["geminiPlanModeThinkingLevel"],
|
||||
|
||||
// Act mode configurations
|
||||
actModeApiProvider:
|
||||
this.remoteConfigCache["actModeApiProvider"] ||
|
||||
this.taskStateCache["actModeApiProvider"] ||
|
||||
this.globalStateCache["actModeApiProvider"],
|
||||
actModeApiModelId: this.taskStateCache["actModeApiModelId"] || this.globalStateCache["actModeApiModelId"],
|
||||
actModeThinkingBudgetTokens:
|
||||
this.taskStateCache["actModeThinkingBudgetTokens"] || this.globalStateCache["actModeThinkingBudgetTokens"],
|
||||
actModeReasoningEffort:
|
||||
this.taskStateCache["actModeReasoningEffort"] || this.globalStateCache["actModeReasoningEffort"],
|
||||
actModeVsCodeLmModelSelector:
|
||||
this.taskStateCache["actModeVsCodeLmModelSelector"] || this.globalStateCache["actModeVsCodeLmModelSelector"],
|
||||
actModeAwsBedrockCustomSelected:
|
||||
this.taskStateCache["actModeAwsBedrockCustomSelected"] ||
|
||||
this.globalStateCache["actModeAwsBedrockCustomSelected"],
|
||||
actModeAwsBedrockCustomModelBaseId:
|
||||
this.taskStateCache["actModeAwsBedrockCustomModelBaseId"] ||
|
||||
this.globalStateCache["actModeAwsBedrockCustomModelBaseId"],
|
||||
actModeOpenRouterModelId:
|
||||
this.taskStateCache["actModeOpenRouterModelId"] || this.globalStateCache["actModeOpenRouterModelId"],
|
||||
actModeOpenRouterModelInfo:
|
||||
this.taskStateCache["actModeOpenRouterModelInfo"] || this.globalStateCache["actModeOpenRouterModelInfo"],
|
||||
actModeOpenAiModelId: this.taskStateCache["actModeOpenAiModelId"] || this.globalStateCache["actModeOpenAiModelId"],
|
||||
actModeOpenAiModelInfo:
|
||||
this.taskStateCache["actModeOpenAiModelInfo"] || this.globalStateCache["actModeOpenAiModelInfo"],
|
||||
actModeOllamaModelId: this.taskStateCache["actModeOllamaModelId"] || this.globalStateCache["actModeOllamaModelId"],
|
||||
actModeLmStudioModelId:
|
||||
this.taskStateCache["actModeLmStudioModelId"] || this.globalStateCache["actModeLmStudioModelId"],
|
||||
actModeLiteLlmModelId: this.taskStateCache["actModeLiteLlmModelId"] || this.globalStateCache["actModeLiteLlmModelId"],
|
||||
actModeLiteLlmModelInfo:
|
||||
this.taskStateCache["actModeLiteLlmModelInfo"] || this.globalStateCache["actModeLiteLlmModelInfo"],
|
||||
actModeRequestyModelId:
|
||||
this.taskStateCache["actModeRequestyModelId"] || this.globalStateCache["actModeRequestyModelId"],
|
||||
actModeRequestyModelInfo:
|
||||
this.taskStateCache["actModeRequestyModelInfo"] || this.globalStateCache["actModeRequestyModelInfo"],
|
||||
actModeTogetherModelId:
|
||||
this.taskStateCache["actModeTogetherModelId"] || this.globalStateCache["actModeTogetherModelId"],
|
||||
actModeFireworksModelId:
|
||||
this.taskStateCache["actModeFireworksModelId"] || this.globalStateCache["actModeFireworksModelId"],
|
||||
actModeSapAiCoreModelId:
|
||||
this.taskStateCache["actModeSapAiCoreModelId"] || this.globalStateCache["actModeSapAiCoreModelId"],
|
||||
actModeSapAiCoreDeploymentId:
|
||||
this.taskStateCache["actModeSapAiCoreDeploymentId"] || this.globalStateCache["actModeSapAiCoreDeploymentId"],
|
||||
actModeGroqModelId: this.taskStateCache["actModeGroqModelId"] || this.globalStateCache["actModeGroqModelId"],
|
||||
actModeGroqModelInfo: this.taskStateCache["actModeGroqModelInfo"] || this.globalStateCache["actModeGroqModelInfo"],
|
||||
actModeBasetenModelId: this.taskStateCache["actModeBasetenModelId"] || this.globalStateCache["actModeBasetenModelId"],
|
||||
actModeBasetenModelInfo:
|
||||
this.taskStateCache["actModeBasetenModelInfo"] || this.globalStateCache["actModeBasetenModelInfo"],
|
||||
actModeHuggingFaceModelId:
|
||||
this.taskStateCache["actModeHuggingFaceModelId"] || this.globalStateCache["actModeHuggingFaceModelId"],
|
||||
actModeHuggingFaceModelInfo:
|
||||
this.taskStateCache["actModeHuggingFaceModelInfo"] || this.globalStateCache["actModeHuggingFaceModelInfo"],
|
||||
actModeHuaweiCloudMaasModelId:
|
||||
this.taskStateCache["actModeHuaweiCloudMaasModelId"] || this.globalStateCache["actModeHuaweiCloudMaasModelId"],
|
||||
actModeHuaweiCloudMaasModelInfo:
|
||||
this.taskStateCache["actModeHuaweiCloudMaasModelInfo"] ||
|
||||
this.globalStateCache["actModeHuaweiCloudMaasModelInfo"],
|
||||
actModeOcaModelId: this.globalStateCache["actModeOcaModelId"],
|
||||
actModeOcaModelInfo: this.globalStateCache["actModeOcaModelInfo"],
|
||||
actModeOcaReasoningEffort: this.globalStateCache["actModeOcaReasoningEffort"],
|
||||
actModeHicapModelId: this.globalStateCache["actModeHicapModelId"],
|
||||
actModeHicapModelInfo: this.globalStateCache["actModeHicapModelInfo"],
|
||||
actModeAihubmixModelId:
|
||||
this.taskStateCache["actModeAihubmixModelId"] || this.globalStateCache["actModeAihubmixModelId"],
|
||||
actModeAihubmixModelInfo:
|
||||
this.taskStateCache["actModeAihubmixModelInfo"] || this.globalStateCache["actModeAihubmixModelInfo"],
|
||||
actModeNousResearchModelId:
|
||||
this.taskStateCache["actModeNousResearchModelId"] || this.globalStateCache["actModeNousResearchModelId"],
|
||||
actModeVercelAiGatewayModelId:
|
||||
this.taskStateCache["actModeVercelAiGatewayModelId"] || this.globalStateCache["actModeVercelAiGatewayModelId"],
|
||||
actModeVercelAiGatewayModelInfo:
|
||||
this.taskStateCache["actModeVercelAiGatewayModelInfo"] ||
|
||||
this.globalStateCache["actModeVercelAiGatewayModelInfo"],
|
||||
geminiActModeThinkingLevel:
|
||||
this.taskStateCache["geminiActModeThinkingLevel"] || this.globalStateCache["geminiActModeThinkingLevel"],
|
||||
nousResearchApiKey: this.secretsCache["nousResearchApiKey"],
|
||||
// Preserve legacy fallback behavior for LiteLLM API key:
|
||||
// if a remoteLiteLlmApiKey is set (via remote config), it should
|
||||
// take precedence over the local liteLlmApiKey.
|
||||
const remoteLiteLlmApiKey = this.secretsCache["remoteLiteLlmApiKey"]
|
||||
if (remoteLiteLlmApiKey !== undefined && remoteLiteLlmApiKey !== null && remoteLiteLlmApiKey !== "") {
|
||||
secrets.liteLlmApiKey = remoteLiteLlmApiKey
|
||||
}
|
||||
|
||||
// Build API handler settings object with task override support
|
||||
const settings = Object.fromEntries(ApiHandlerSettingsKeys.map((key) => [key, this.getSettingWithOverride(key)]))
|
||||
|
||||
return { ...secrets, ...settings } satisfies ApiConfiguration
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,6 +180,20 @@ export async function ensureSettingsDirectoryExists(): Promise<string> {
|
||||
return getGlobalStorageDir("settings")
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the path to the MCP settings file, creating it if it doesn't exist
|
||||
* @param settingsDirectoryPath Path to the settings directory
|
||||
* @returns Path to the MCP settings file
|
||||
*/
|
||||
export async function getMcpSettingsFilePath(settingsDirectoryPath: string): Promise<string> {
|
||||
const mcpSettingsFilePath = path.join(settingsDirectoryPath, GlobalFileNames.mcpSettings)
|
||||
const fileExists = await fileExistsAtPath(mcpSettingsFilePath)
|
||||
if (!fileExists) {
|
||||
await fs.writeFile(mcpSettingsFilePath, JSON.stringify({ mcpServers: {} }, null, 2))
|
||||
}
|
||||
return mcpSettingsFilePath
|
||||
}
|
||||
|
||||
export async function getSavedApiConversationHistory(taskId: string): Promise<Anthropic.MessageParam[]> {
|
||||
const filePath = path.join(await ensureTaskDirectoryExists(taskId), GlobalFileNames.apiConversationHistory)
|
||||
const fileExists = await fileExistsAtPath(filePath)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import axios, { AxiosRequestConfig, AxiosResponse } from "axios"
|
||||
import { Controller } from "@/core/controller"
|
||||
import { buildBasicClineHeaders } from "@/services/EnvUtils"
|
||||
import { getAxiosSettings } from "@/shared/net"
|
||||
import { ClineEnv } from "../../../config"
|
||||
import { AuthService } from "../../../services/auth/AuthService"
|
||||
import { CLINE_API_ENDPOINT } from "../../../shared/cline/api"
|
||||
import { APIKeySchema, type APIKeySettings, RemoteConfig, RemoteConfigSchema } from "../../../shared/remote-config/schema"
|
||||
import { deleteRemoteConfigFromCache, readRemoteConfigFromCache, writeRemoteConfigToCache } from "../disk"
|
||||
import { StateManager } from "../StateManager"
|
||||
import { applyRemoteConfig } from "./utils"
|
||||
import { applyRemoteConfig, clearRemoteConfig, isRemoteConfigEnabled } from "./utils"
|
||||
|
||||
/**
|
||||
* Parses API keys from a JSON string response
|
||||
@@ -51,6 +51,7 @@ async function makeAuthenticatedRequest<T>(endpoint: string, organizationId: str
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
...(await buildBasicClineHeaders()),
|
||||
},
|
||||
...getAxiosSettings(),
|
||||
}
|
||||
@@ -170,6 +171,10 @@ async function findOrganizationWithRemoteConfig(): Promise<{ organizationId: str
|
||||
|
||||
// Scan each organization for remote config
|
||||
for (const org of userOrganizations) {
|
||||
if (!isRemoteConfigEnabled(org.organizationId)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const remoteConfig = await fetchRemoteConfigForOrganization(org.organizationId)
|
||||
|
||||
if (remoteConfig) {
|
||||
@@ -198,7 +203,7 @@ async function ensureUserInOrgWithRemoteConfig(controller: Controller): Promise<
|
||||
const result = await findOrganizationWithRemoteConfig()
|
||||
|
||||
if (!result) {
|
||||
StateManager.get().clearRemoteConfig()
|
||||
clearRemoteConfig()
|
||||
controller.postStateToWebview()
|
||||
return undefined
|
||||
}
|
||||
@@ -230,7 +235,11 @@ async function ensureUserInOrgWithRemoteConfig(controller: Controller): Promise<
|
||||
|
||||
// Cache and apply the remote config
|
||||
await writeRemoteConfigToCache(organizationId, config)
|
||||
await applyRemoteConfig(config)
|
||||
if (isRemoteConfigEnabled(organizationId)) {
|
||||
await applyRemoteConfig(config, undefined, controller.mcpHub)
|
||||
} else {
|
||||
clearRemoteConfig()
|
||||
}
|
||||
controller.postStateToWebview()
|
||||
|
||||
return config
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { getMcpSettingsFilePath } from "@core/storage/disk"
|
||||
import { StateManager } from "@core/storage/StateManager"
|
||||
import { RemoteMCPServer } from "@shared/remote-config/schema"
|
||||
import * as fs from "fs/promises"
|
||||
import type { McpHub } from "@/services/mcp/McpHub"
|
||||
|
||||
/**
|
||||
* Synchronizes remote MCP servers from remote config to the local MCP settings file
|
||||
* This allows admins to centrally configure MCP servers that are automatically deployed to users
|
||||
*
|
||||
* Handles:
|
||||
* - Removing servers that were previously from remote config but are no longer present
|
||||
* - Adding new servers from remote config
|
||||
* - Preventing duplicates when re-adding servers
|
||||
*
|
||||
* @param remoteMCPServers Array of remote MCP servers from remote config
|
||||
* @param settingsDirectoryPath Path to the settings directory
|
||||
* @param mcpHub Optional McpHub instance to set flag preventing watcher triggers
|
||||
*/
|
||||
export async function syncRemoteMcpServersToSettings(
|
||||
remoteMCPServers: RemoteMCPServer[],
|
||||
settingsDirectoryPath: string,
|
||||
mcpHub?: McpHub,
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Get or create the MCP settings file
|
||||
const settingsPath = await getMcpSettingsFilePath(settingsDirectoryPath)
|
||||
|
||||
// Read current settings
|
||||
const content = await fs.readFile(settingsPath, "utf-8")
|
||||
const config = JSON.parse(content)
|
||||
|
||||
// Ensure mcpServers object exists
|
||||
if (!config.mcpServers || typeof config.mcpServers !== "object") {
|
||||
config.mcpServers = {}
|
||||
}
|
||||
|
||||
// Get previous remote servers from cache
|
||||
const stateManager = StateManager.get()
|
||||
const previousRemoteServers = (stateManager.getRemoteConfigSettings().previousRemoteMCPServers as RemoteMCPServer[]) || []
|
||||
|
||||
// Remove old remote servers that are no longer in the new list
|
||||
for (const prevServer of previousRemoteServers) {
|
||||
// Check if this server exists in current settings with same name and URL
|
||||
const existingServer = config.mcpServers[prevServer.name]
|
||||
if (existingServer && existingServer.url === prevServer.url) {
|
||||
// Check if it's still in the new remote config
|
||||
const stillInRemoteConfig = remoteMCPServers.some(
|
||||
(newServer) => newServer.name === prevServer.name && newServer.url === prevServer.url,
|
||||
)
|
||||
if (!stillInRemoteConfig) {
|
||||
// Remove it from settings
|
||||
delete config.mcpServers[prevServer.name]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add/update servers from new remote config
|
||||
for (const server of remoteMCPServers) {
|
||||
// Check if server with same name and URL already exists to skip duplicates
|
||||
const existingServer = config.mcpServers[server.name]
|
||||
if (existingServer && existingServer.url === server.url) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Add or update the server
|
||||
config.mcpServers[server.name] = {
|
||||
url: server.url,
|
||||
type: "streamableHttp",
|
||||
disabled: false,
|
||||
autoApprove: [],
|
||||
}
|
||||
}
|
||||
|
||||
// Set flag to prevent watcher from triggering
|
||||
if (mcpHub) {
|
||||
mcpHub.setIsUpdatingFromRemoteConfig(true)
|
||||
}
|
||||
|
||||
try {
|
||||
// Write back to file
|
||||
await fs.writeFile(settingsPath, JSON.stringify(config, null, 2))
|
||||
} finally {
|
||||
// Always clear flag, even if write fails
|
||||
if (mcpHub) {
|
||||
mcpHub.setIsUpdatingFromRemoteConfig(false)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[RemoteConfig] Failed to sync remote MCP servers:", error)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,17 @@
|
||||
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 { GlobalStateAndSettings, RemoteConfigFields } from "@shared/storage/state-keys"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { getTelemetryService, telemetryService } 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 { ApiProvider } from "@/shared/api"
|
||||
import { isOpenTelemetryConfigValid, remoteConfigToOtelConfig } from "@/shared/services/config/otel-config"
|
||||
import { ensureSettingsDirectoryExists } from "../disk"
|
||||
import { StateManager } from "../StateManager"
|
||||
import { syncRemoteMcpServersToSettings } from "./syncRemoteMcpServers"
|
||||
|
||||
/**
|
||||
* Transforms RemoteConfig schema to RemoteConfigFields shape
|
||||
@@ -88,7 +93,7 @@ export function transformRemoteConfigToStateShape(remoteConfig: RemoteConfig): P
|
||||
|
||||
// Map provider settings
|
||||
|
||||
const providers: string[] = []
|
||||
const providers: ApiProvider[] = []
|
||||
|
||||
// Map OpenAiCompatible provider settings
|
||||
const openAiSettings = remoteConfig.providerSettings?.OpenAiCompatible
|
||||
@@ -206,24 +211,46 @@ async function applyRemoteOTELConfig(transformed: Partial<RemoteConfigFields>, t
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies remote config to the StateManager's remote config cache
|
||||
* @param remoteConfig The remote configuration object to apply
|
||||
*/
|
||||
export async function applyRemoteConfig(remoteConfig?: RemoteConfig): Promise<void> {
|
||||
const stateManager = StateManager.get()
|
||||
const telemetryService = await getTelemetryService()
|
||||
export function clearRemoteConfig() {
|
||||
try {
|
||||
const stateManager = StateManager.get()
|
||||
|
||||
// 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", {})
|
||||
|
||||
// clear secrets
|
||||
stateManager.setSecret("remoteLiteLlmApiKey", undefined)
|
||||
} catch (err) {
|
||||
Logger.error("[REMOTE CONFIG] Failed to clear remote config", err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies remote config to the StateManager's remote config cache
|
||||
* @param remoteConfig The remote configuration object to apply
|
||||
* @param settingsDirectoryPath Path to the settings directory
|
||||
* @param mcpHub Optional McpHub instance to prevent watcher triggers during sync
|
||||
*/
|
||||
export async function applyRemoteConfig(
|
||||
remoteConfig?: RemoteConfig,
|
||||
settingsDirectoryPath?: string,
|
||||
mcpHub?: any,
|
||||
): Promise<void> {
|
||||
const stateManager = StateManager.get()
|
||||
const telemetryService = await getTelemetryService()
|
||||
|
||||
// If no remote config provided, clear the cache and relevant state
|
||||
if (!remoteConfig) {
|
||||
clearRemoteConfig()
|
||||
return
|
||||
}
|
||||
|
||||
// Save previousRemoteMCPServers before clearing cache, this is needed for next sync to detect removals)
|
||||
const previousRemoteMCPServers = stateManager.getRemoteConfigSettings().previousRemoteMCPServers
|
||||
|
||||
// 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
|
||||
@@ -248,5 +275,75 @@ export async function applyRemoteConfig(remoteConfig?: RemoteConfig): Promise<vo
|
||||
stateManager.setRemoteConfigField(key as keyof RemoteConfigFields, value)
|
||||
}
|
||||
|
||||
// Restore previousRemoteMCPServers across cache clears
|
||||
if (previousRemoteMCPServers !== undefined) {
|
||||
stateManager.setRemoteConfigField("previousRemoteMCPServers", previousRemoteMCPServers)
|
||||
}
|
||||
|
||||
// Sync remote MCP servers to settings file (AFTER cache is populated, so sync can read previous state)
|
||||
if (remoteConfig.remoteMCPServers !== undefined) {
|
||||
try {
|
||||
// Get settings directory path - use provided path or get it from disk helper
|
||||
const settingsPath = settingsDirectoryPath || (await ensureSettingsDirectoryExists())
|
||||
await syncRemoteMcpServersToSettings(remoteConfig.remoteMCPServers, settingsPath, mcpHub)
|
||||
// Store current remote servers list for next sync to detect removals
|
||||
stateManager.setRemoteConfigField("previousRemoteMCPServers", remoteConfig.remoteMCPServers)
|
||||
} catch (error) {
|
||||
console.error("[RemoteConfig] Failed to sync remote MCP servers to settings:", error)
|
||||
// Continue with other config application even if MCP sync fails
|
||||
}
|
||||
}
|
||||
await applyRemoteOTELConfig(transformed, telemetryService)
|
||||
}
|
||||
|
||||
const isProviderValid = (provider?: ApiProvider) => {
|
||||
const remoteConfiguredProviders = StateManager.get().getRemoteConfigSettings().remoteConfiguredProviders
|
||||
if (!remoteConfiguredProviders || !remoteConfiguredProviders.length) {
|
||||
return true
|
||||
}
|
||||
|
||||
return provider && remoteConfiguredProviders.includes(provider)
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives a config and returns the subset of fields that can be overriden in the cache
|
||||
*/
|
||||
export function filterAllowedRemoteConfigFields(config: Partial<GlobalStateAndSettings>): Partial<GlobalStateAndSettings> {
|
||||
const updatedFields: Partial<GlobalStateAndSettings> = {}
|
||||
|
||||
const actModeApiProvider = config.actModeApiProvider
|
||||
if (isProviderValid(actModeApiProvider)) {
|
||||
updatedFields.actModeApiProvider = actModeApiProvider
|
||||
}
|
||||
|
||||
const planModeApiProvider = config.planModeApiProvider
|
||||
if (isProviderValid(planModeApiProvider)) {
|
||||
updatedFields.planModeApiProvider = planModeApiProvider
|
||||
}
|
||||
|
||||
return updatedFields
|
||||
}
|
||||
|
||||
const canDisableRemoteConfig = (orgId: string) => {
|
||||
// Check if they're an admin/owner
|
||||
const authService = AuthService.getInstance()
|
||||
const userOrgs = authService.getUserOrganizations()
|
||||
|
||||
if (!userOrgs) {
|
||||
return false
|
||||
}
|
||||
|
||||
const org = userOrgs.find((org) => org.organizationId === orgId)
|
||||
const isAdminOrOwner = org?.roles?.some((role) => role === "admin" || role === "owner")
|
||||
|
||||
return isAdminOrOwner
|
||||
}
|
||||
|
||||
export const isRemoteConfigEnabled = (orgId: string) => {
|
||||
const stateManager = StateManager.get()
|
||||
const hasOptedOut = stateManager.getGlobalSettingsKey("optOutOfRemoteConfig")
|
||||
|
||||
const isDisabled = hasOptedOut && canDisableRemoteConfig(orgId)
|
||||
|
||||
return !isDisabled
|
||||
}
|
||||
|
||||
@@ -1,759 +1,125 @@
|
||||
import { ANTHROPIC_MIN_THINKING_BUDGET, ApiProvider, fireworksDefaultModelId, type OcaModelInfo } from "@shared/api"
|
||||
import { GlobalStateAndSettings, LocalState, SecretKey, Secrets } from "@shared/storage/state-keys"
|
||||
import { ApiProvider } from "@shared/api"
|
||||
import {
|
||||
applyTransform,
|
||||
GlobalStateAndSettingKeys,
|
||||
GlobalStateAndSettings,
|
||||
getDefaultValue,
|
||||
isAsyncProperty,
|
||||
isComputedProperty,
|
||||
LocalState,
|
||||
LocalStateKeys,
|
||||
SecretKeys,
|
||||
Secrets,
|
||||
} from "@shared/storage/state-keys"
|
||||
import { ExtensionContext } from "vscode"
|
||||
import { Controller } from "@/core/controller"
|
||||
import { getHooksEnabledSafe } from "@/core/hooks/hooks-utils"
|
||||
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@/shared/AutoApprovalSettings"
|
||||
import { DEFAULT_BROWSER_SETTINGS } from "@/shared/BrowserSettings"
|
||||
import { ClineRulesToggles } from "@/shared/cline-rules"
|
||||
import { DEFAULT_DICTATION_SETTINGS, DictationSettings } from "@/shared/DictationSettings"
|
||||
import { DEFAULT_FOCUS_CHAIN_SETTINGS } from "@/shared/FocusChainSettings"
|
||||
import { DEFAULT_MCP_DISPLAY_MODE } from "@/shared/McpDisplayMode"
|
||||
import { OpenaiReasoningEffort } from "@/shared/storage/types"
|
||||
import { readTaskHistoryFromState } from "../disk"
|
||||
export async function readSecretsFromDisk(context: ExtensionContext): Promise<Secrets> {
|
||||
const [
|
||||
apiKey,
|
||||
openRouterApiKey,
|
||||
firebaseClineAccountId,
|
||||
clineAccountId,
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
awsBedrockApiKey,
|
||||
openAiApiKey,
|
||||
geminiApiKey,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
togetherApiKey,
|
||||
qwenApiKey,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
fireworksApiKey,
|
||||
liteLlmApiKey,
|
||||
remoteLiteLlmApiKey,
|
||||
asksageApiKey,
|
||||
xaiApiKey,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
huggingFaceApiKey,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
huaweiCloudMaasApiKey,
|
||||
basetenApiKey,
|
||||
zaiApiKey,
|
||||
ollamaApiKey,
|
||||
vercelAiGatewayApiKey,
|
||||
difyApiKey,
|
||||
authNonce,
|
||||
ocaApiKey,
|
||||
ocaRefreshToken,
|
||||
minimaxApiKey,
|
||||
hicapApiKey,
|
||||
aihubmixApiKey,
|
||||
mcpOAuthSecrets,
|
||||
nousResearchApiKey,
|
||||
] = await Promise.all([
|
||||
context.secrets.get("apiKey") as Promise<Secrets["apiKey"]>,
|
||||
context.secrets.get("openRouterApiKey") as Promise<Secrets["openRouterApiKey"]>,
|
||||
context.secrets.get("clineAccountId") as Promise<Secrets["clineAccountId"]>,
|
||||
context.secrets.get("cline:clineAccountId") as Promise<Secrets["cline:clineAccountId"]>,
|
||||
context.secrets.get("awsAccessKey") as Promise<Secrets["awsAccessKey"]>,
|
||||
context.secrets.get("awsSecretKey") as Promise<Secrets["awsSecretKey"]>,
|
||||
context.secrets.get("awsSessionToken") as Promise<Secrets["awsSessionToken"]>,
|
||||
context.secrets.get("awsBedrockApiKey") as Promise<Secrets["awsBedrockApiKey"]>,
|
||||
context.secrets.get("openAiApiKey") as Promise<Secrets["openAiApiKey"]>,
|
||||
context.secrets.get("geminiApiKey") as Promise<Secrets["geminiApiKey"]>,
|
||||
context.secrets.get("openAiNativeApiKey") as Promise<Secrets["openAiNativeApiKey"]>,
|
||||
context.secrets.get("deepSeekApiKey") as Promise<Secrets["deepSeekApiKey"]>,
|
||||
context.secrets.get("requestyApiKey") as Promise<Secrets["requestyApiKey"]>,
|
||||
context.secrets.get("togetherApiKey") as Promise<Secrets["togetherApiKey"]>,
|
||||
context.secrets.get("qwenApiKey") as Promise<Secrets["qwenApiKey"]>,
|
||||
context.secrets.get("doubaoApiKey") as Promise<Secrets["doubaoApiKey"]>,
|
||||
context.secrets.get("mistralApiKey") as Promise<Secrets["mistralApiKey"]>,
|
||||
context.secrets.get("fireworksApiKey") as Promise<Secrets["fireworksApiKey"]>,
|
||||
context.secrets.get("liteLlmApiKey") as Promise<Secrets["liteLlmApiKey"]>,
|
||||
context.secrets.get("remoteLiteLlmApiKey") as Promise<Secrets["remoteLiteLlmApiKey"]>,
|
||||
context.secrets.get("asksageApiKey") as Promise<Secrets["asksageApiKey"]>,
|
||||
context.secrets.get("xaiApiKey") as Promise<Secrets["xaiApiKey"]>,
|
||||
context.secrets.get("sambanovaApiKey") as Promise<Secrets["sambanovaApiKey"]>,
|
||||
context.secrets.get("cerebrasApiKey") as Promise<Secrets["cerebrasApiKey"]>,
|
||||
context.secrets.get("groqApiKey") as Promise<Secrets["groqApiKey"]>,
|
||||
context.secrets.get("moonshotApiKey") as Promise<Secrets["moonshotApiKey"]>,
|
||||
context.secrets.get("nebiusApiKey") as Promise<Secrets["nebiusApiKey"]>,
|
||||
context.secrets.get("huggingFaceApiKey") as Promise<Secrets["huggingFaceApiKey"]>,
|
||||
context.secrets.get("sapAiCoreClientId") as Promise<Secrets["sapAiCoreClientId"]>,
|
||||
context.secrets.get("sapAiCoreClientSecret") as Promise<Secrets["sapAiCoreClientSecret"]>,
|
||||
context.secrets.get("huaweiCloudMaasApiKey") as Promise<Secrets["huaweiCloudMaasApiKey"]>,
|
||||
context.secrets.get("basetenApiKey") as Promise<Secrets["basetenApiKey"]>,
|
||||
context.secrets.get("zaiApiKey") as Promise<Secrets["zaiApiKey"]>,
|
||||
context.secrets.get("ollamaApiKey") as Promise<Secrets["ollamaApiKey"]>,
|
||||
context.secrets.get("vercelAiGatewayApiKey") as Promise<Secrets["vercelAiGatewayApiKey"]>,
|
||||
context.secrets.get("difyApiKey") as Promise<Secrets["difyApiKey"]>,
|
||||
context.secrets.get("authNonce") as Promise<Secrets["authNonce"]>,
|
||||
context.secrets.get("ocaApiKey") as Promise<string | undefined>,
|
||||
context.secrets.get("ocaRefreshToken") as Promise<string | undefined>,
|
||||
context.secrets.get("minimaxApiKey") as Promise<Secrets["minimaxApiKey"]>,
|
||||
context.secrets.get("hicapApiKey") as Promise<Secrets["hicapApiKey"]>,
|
||||
context.secrets.get("aihubmixApiKey") as Promise<Secrets["aihubmixApiKey"]>,
|
||||
context.secrets.get("mcpOAuthSecrets") as Promise<Secrets["mcpOAuthSecrets"]>,
|
||||
context.secrets.get("nousResearchApiKey") as Promise<Secrets["nousResearchApiKey"]>,
|
||||
])
|
||||
|
||||
return {
|
||||
authNonce,
|
||||
apiKey,
|
||||
openRouterApiKey,
|
||||
clineAccountId: firebaseClineAccountId,
|
||||
"cline:clineAccountId": clineAccountId,
|
||||
huggingFaceApiKey,
|
||||
huaweiCloudMaasApiKey,
|
||||
basetenApiKey,
|
||||
zaiApiKey,
|
||||
ollamaApiKey,
|
||||
vercelAiGatewayApiKey,
|
||||
difyApiKey,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
xaiApiKey,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
asksageApiKey,
|
||||
fireworksApiKey,
|
||||
liteLlmApiKey,
|
||||
remoteLiteLlmApiKey,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
togetherApiKey,
|
||||
qwenApiKey,
|
||||
geminiApiKey,
|
||||
openAiApiKey,
|
||||
awsBedrockApiKey,
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
ocaApiKey,
|
||||
ocaRefreshToken,
|
||||
minimaxApiKey,
|
||||
hicapApiKey,
|
||||
aihubmixApiKey,
|
||||
mcpOAuthSecrets,
|
||||
nousResearchApiKey,
|
||||
}
|
||||
export async function readSecretsFromDisk(context: ExtensionContext): Promise<Secrets> {
|
||||
const secrets = await Promise.all(SecretKeys.map((key) => context.secrets.get(key)))
|
||||
|
||||
return SecretKeys.reduce((acc, key, index) => {
|
||||
acc[key] = secrets[index]
|
||||
return acc
|
||||
}, {} as Secrets)
|
||||
}
|
||||
|
||||
export async function readWorkspaceStateFromDisk(context: ExtensionContext): Promise<LocalState> {
|
||||
const localClineRulesToggles = context.workspaceState.get("localClineRulesToggles") as ClineRulesToggles | undefined
|
||||
const localWindsurfRulesToggles = context.workspaceState.get("localWindsurfRulesToggles") as ClineRulesToggles | undefined
|
||||
const localCursorRulesToggles = context.workspaceState.get("localCursorRulesToggles") as ClineRulesToggles | undefined
|
||||
const localAgentsRulesToggles = context.workspaceState.get("localAgentsRulesToggles") as ClineRulesToggles | undefined
|
||||
const localWorkflowToggles = context.workspaceState.get("workflowToggles") as ClineRulesToggles | undefined
|
||||
const localSkillsToggles = context.workspaceState.get("localSkillsToggles") as ClineRulesToggles | undefined
|
||||
const states = LocalStateKeys.map((key) => context.workspaceState.get<ClineRulesToggles | undefined>(key))
|
||||
|
||||
return {
|
||||
localClineRulesToggles: localClineRulesToggles || {},
|
||||
localWindsurfRulesToggles: localWindsurfRulesToggles || {},
|
||||
localCursorRulesToggles: localCursorRulesToggles || {},
|
||||
localAgentsRulesToggles: localAgentsRulesToggles || {},
|
||||
workflowToggles: localWorkflowToggles || {},
|
||||
localSkillsToggles: localSkillsToggles || {},
|
||||
}
|
||||
return LocalStateKeys.reduce((acc, key, index) => {
|
||||
acc[key] = states[index] || {}
|
||||
return acc
|
||||
}, {} as LocalState)
|
||||
}
|
||||
|
||||
export async function readGlobalStateFromDisk(context: ExtensionContext): Promise<GlobalStateAndSettings> {
|
||||
try {
|
||||
// Get all global state values
|
||||
const strictPlanModeEnabled =
|
||||
context.globalState.get<GlobalStateAndSettings["strictPlanModeEnabled"]>("strictPlanModeEnabled")
|
||||
const yoloModeToggled = context.globalState.get<GlobalStateAndSettings["yoloModeToggled"]>("yoloModeToggled")
|
||||
const useAutoCondense = context.globalState.get<GlobalStateAndSettings["useAutoCondense"]>("useAutoCondense")
|
||||
const clineWebToolsEnabled =
|
||||
context.globalState.get<GlobalStateAndSettings["clineWebToolsEnabled"]>("clineWebToolsEnabled")
|
||||
const isNewUser = context.globalState.get<GlobalStateAndSettings["isNewUser"]>("isNewUser")
|
||||
const welcomeViewCompleted =
|
||||
context.globalState.get<GlobalStateAndSettings["welcomeViewCompleted"]>("welcomeViewCompleted")
|
||||
const awsRegion = context.globalState.get<GlobalStateAndSettings["awsRegion"]>("awsRegion")
|
||||
const awsUseCrossRegionInference =
|
||||
context.globalState.get<GlobalStateAndSettings["awsUseCrossRegionInference"]>("awsUseCrossRegionInference")
|
||||
const awsUseGlobalInference =
|
||||
context.globalState.get<GlobalStateAndSettings["awsUseGlobalInference"]>("awsUseGlobalInference")
|
||||
const awsBedrockUsePromptCache =
|
||||
context.globalState.get<GlobalStateAndSettings["awsBedrockUsePromptCache"]>("awsBedrockUsePromptCache")
|
||||
const awsBedrockEndpoint = context.globalState.get<GlobalStateAndSettings["awsBedrockEndpoint"]>("awsBedrockEndpoint")
|
||||
const awsProfile = context.globalState.get<GlobalStateAndSettings["awsProfile"]>("awsProfile")
|
||||
const awsUseProfile = context.globalState.get<GlobalStateAndSettings["awsUseProfile"]>("awsUseProfile")
|
||||
const awsAuthentication = context.globalState.get<GlobalStateAndSettings["awsAuthentication"]>("awsAuthentication")
|
||||
const vertexProjectId = context.globalState.get<GlobalStateAndSettings["vertexProjectId"]>("vertexProjectId")
|
||||
const vertexRegion = context.globalState.get<GlobalStateAndSettings["vertexRegion"]>("vertexRegion")
|
||||
const openAiBaseUrl = context.globalState.get<GlobalStateAndSettings["openAiBaseUrl"]>("openAiBaseUrl")
|
||||
const requestyBaseUrl = context.globalState.get<GlobalStateAndSettings["requestyBaseUrl"]>("requestyBaseUrl")
|
||||
const openAiHeaders = context.globalState.get<GlobalStateAndSettings["openAiHeaders"]>("openAiHeaders")
|
||||
const ollamaBaseUrl = context.globalState.get<GlobalStateAndSettings["ollamaBaseUrl"]>("ollamaBaseUrl")
|
||||
const ollamaApiOptionsCtxNum =
|
||||
context.globalState.get<GlobalStateAndSettings["ollamaApiOptionsCtxNum"]>("ollamaApiOptionsCtxNum")
|
||||
const lmStudioBaseUrl = context.globalState.get<GlobalStateAndSettings["lmStudioBaseUrl"]>("lmStudioBaseUrl")
|
||||
const lmStudioMaxTokens = context.globalState.get<GlobalStateAndSettings["lmStudioMaxTokens"]>("lmStudioMaxTokens")
|
||||
const anthropicBaseUrl = context.globalState.get<GlobalStateAndSettings["anthropicBaseUrl"]>("anthropicBaseUrl")
|
||||
const geminiBaseUrl = context.globalState.get<GlobalStateAndSettings["geminiBaseUrl"]>("geminiBaseUrl")
|
||||
const azureApiVersion = context.globalState.get<GlobalStateAndSettings["azureApiVersion"]>("azureApiVersion")
|
||||
const azureIdentity = context.globalState.get<GlobalStateAndSettings["azureIdentity"]>("azureIdentity")
|
||||
const openRouterProviderSorting =
|
||||
context.globalState.get<GlobalStateAndSettings["openRouterProviderSorting"]>("openRouterProviderSorting")
|
||||
const lastShownAnnouncementId =
|
||||
context.globalState.get<GlobalStateAndSettings["lastShownAnnouncementId"]>("lastShownAnnouncementId")
|
||||
const autoApprovalSettings =
|
||||
context.globalState.get<GlobalStateAndSettings["autoApprovalSettings"]>("autoApprovalSettings")
|
||||
const browserSettings = context.globalState.get<GlobalStateAndSettings["browserSettings"]>("browserSettings")
|
||||
const liteLlmBaseUrl = context.globalState.get<GlobalStateAndSettings["liteLlmBaseUrl"]>("liteLlmBaseUrl")
|
||||
const liteLlmUsePromptCache =
|
||||
context.globalState.get<GlobalStateAndSettings["liteLlmUsePromptCache"]>("liteLlmUsePromptCache")
|
||||
const fireworksModelMaxCompletionTokens = context.globalState.get<
|
||||
GlobalStateAndSettings["fireworksModelMaxCompletionTokens"]
|
||||
>("fireworksModelMaxCompletionTokens")
|
||||
const fireworksModelMaxTokens =
|
||||
context.globalState.get<GlobalStateAndSettings["fireworksModelMaxTokens"]>("fireworksModelMaxTokens")
|
||||
const userInfo = context.globalState.get<GlobalStateAndSettings["userInfo"]>("userInfo")
|
||||
const qwenApiLine = context.globalState.get<GlobalStateAndSettings["qwenApiLine"]>("qwenApiLine")
|
||||
const moonshotApiLine = context.globalState.get<GlobalStateAndSettings["moonshotApiLine"]>("moonshotApiLine")
|
||||
const zaiApiLine = context.globalState.get<GlobalStateAndSettings["zaiApiLine"]>("zaiApiLine")
|
||||
const minimaxApiLine = context.globalState.get<GlobalStateAndSettings["minimaxApiLine"]>("minimaxApiLine")
|
||||
const telemetrySetting = context.globalState.get<GlobalStateAndSettings["telemetrySetting"]>("telemetrySetting")
|
||||
const asksageApiUrl = context.globalState.get<GlobalStateAndSettings["asksageApiUrl"]>("asksageApiUrl")
|
||||
const planActSeparateModelsSettingRaw =
|
||||
context.globalState.get<GlobalStateAndSettings["planActSeparateModelsSetting"]>("planActSeparateModelsSetting")
|
||||
const favoritedModelIds = context.globalState.get<GlobalStateAndSettings["favoritedModelIds"]>("favoritedModelIds")
|
||||
const globalClineRulesToggles =
|
||||
context.globalState.get<GlobalStateAndSettings["globalClineRulesToggles"]>("globalClineRulesToggles")
|
||||
const requestTimeoutMs = context.globalState.get<GlobalStateAndSettings["requestTimeoutMs"]>("requestTimeoutMs")
|
||||
const shellIntegrationTimeout =
|
||||
context.globalState.get<GlobalStateAndSettings["shellIntegrationTimeout"]>("shellIntegrationTimeout")
|
||||
const enableCheckpointsSettingRaw =
|
||||
context.globalState.get<GlobalStateAndSettings["enableCheckpointsSetting"]>("enableCheckpointsSetting")
|
||||
const mcpMarketplaceEnabledRaw =
|
||||
context.globalState.get<GlobalStateAndSettings["mcpMarketplaceEnabled"]>("mcpMarketplaceEnabled")
|
||||
const mcpDisplayMode = context.globalState.get<GlobalStateAndSettings["mcpDisplayMode"]>("mcpDisplayMode")
|
||||
const mcpResponsesCollapsedRaw =
|
||||
context.globalState.get<GlobalStateAndSettings["mcpResponsesCollapsed"]>("mcpResponsesCollapsed")
|
||||
const globalWorkflowToggles =
|
||||
context.globalState.get<GlobalStateAndSettings["globalWorkflowToggles"]>("globalWorkflowToggles")
|
||||
const globalSkillsToggles = context.globalState.get<GlobalStateAndSettings["globalSkillsToggles"]>("globalSkillsToggles")
|
||||
const terminalReuseEnabled =
|
||||
context.globalState.get<GlobalStateAndSettings["terminalReuseEnabled"]>("terminalReuseEnabled")
|
||||
const vscodeTerminalExecutionMode =
|
||||
context.globalState.get<GlobalStateAndSettings["vscodeTerminalExecutionMode"]>("vscodeTerminalExecutionMode")
|
||||
const terminalOutputLineLimit =
|
||||
context.globalState.get<GlobalStateAndSettings["terminalOutputLineLimit"]>("terminalOutputLineLimit")
|
||||
const maxConsecutiveMistakes =
|
||||
context.globalState.get<GlobalStateAndSettings["maxConsecutiveMistakes"]>("maxConsecutiveMistakes")
|
||||
const subagentTerminalOutputLineLimit = context.globalState.get<
|
||||
GlobalStateAndSettings["subagentTerminalOutputLineLimit"]
|
||||
>("subagentTerminalOutputLineLimit")
|
||||
const defaultTerminalProfile =
|
||||
context.globalState.get<GlobalStateAndSettings["defaultTerminalProfile"]>("defaultTerminalProfile")
|
||||
const sapAiCoreBaseUrl = context.globalState.get<GlobalStateAndSettings["sapAiCoreBaseUrl"]>("sapAiCoreBaseUrl")
|
||||
const sapAiCoreTokenUrl = context.globalState.get<GlobalStateAndSettings["sapAiCoreTokenUrl"]>("sapAiCoreTokenUrl")
|
||||
const sapAiResourceGroup = context.globalState.get<GlobalStateAndSettings["sapAiResourceGroup"]>("sapAiResourceGroup")
|
||||
const claudeCodePath = context.globalState.get<GlobalStateAndSettings["claudeCodePath"]>("claudeCodePath")
|
||||
const difyBaseUrl = context.globalState.get<GlobalStateAndSettings["difyBaseUrl"]>("difyBaseUrl")
|
||||
const ocaBaseUrl = context.globalState.get("ocaBaseUrl") as string | undefined
|
||||
const ocaMode = context.globalState.get("ocaMode") as string | undefined
|
||||
const openaiReasoningEffort =
|
||||
context.globalState.get<GlobalStateAndSettings["openaiReasoningEffort"]>("openaiReasoningEffort")
|
||||
const preferredLanguage = context.globalState.get<GlobalStateAndSettings["preferredLanguage"]>("preferredLanguage")
|
||||
const focusChainSettings = context.globalState.get<GlobalStateAndSettings["focusChainSettings"]>("focusChainSettings")
|
||||
const dictationSettings = context.globalState.get<GlobalStateAndSettings["dictationSettings"]>("dictationSettings") as
|
||||
| DictationSettings
|
||||
| undefined
|
||||
const lastDismissedInfoBannerVersion =
|
||||
context.globalState.get<GlobalStateAndSettings["lastDismissedInfoBannerVersion"]>("lastDismissedInfoBannerVersion")
|
||||
const lastDismissedModelBannerVersion = context.globalState.get<
|
||||
GlobalStateAndSettings["lastDismissedModelBannerVersion"]
|
||||
>("lastDismissedModelBannerVersion")
|
||||
const lastDismissedCliBannerVersion =
|
||||
context.globalState.get<GlobalStateAndSettings["lastDismissedCliBannerVersion"]>("lastDismissedCliBannerVersion")
|
||||
const dismissedBanners = context.globalState.get<GlobalStateAndSettings["dismissedBanners"]>("dismissedBanners")
|
||||
const qwenCodeOauthPath = context.globalState.get<GlobalStateAndSettings["qwenCodeOauthPath"]>("qwenCodeOauthPath")
|
||||
const customPrompt = context.globalState.get<GlobalStateAndSettings["customPrompt"]>("customPrompt")
|
||||
const autoCondenseThreshold =
|
||||
context.globalState.get<GlobalStateAndSettings["autoCondenseThreshold"]>("autoCondenseThreshold") // number from 0 to 1
|
||||
const hooksEnabled = context.globalState.get<GlobalStateAndSettings["hooksEnabled"]>("hooksEnabled")
|
||||
const enableParallelToolCalling =
|
||||
context.globalState.get<GlobalStateAndSettings["enableParallelToolCalling"]>("enableParallelToolCalling")
|
||||
const hicapModelId = context.globalState.get<GlobalStateAndSettings["hicapModelId"]>("hicapModelId")
|
||||
const aihubmixBaseUrl = context.globalState.get<GlobalStateAndSettings["aihubmixBaseUrl"]>("aihubmixBaseUrl")
|
||||
const aihubmixAppCode = context.globalState.get<GlobalStateAndSettings["aihubmixAppCode"]>("aihubmixAppCode")
|
||||
|
||||
// OpenTelemetry configuration
|
||||
const openTelemetryEnabled =
|
||||
context.globalState.get<GlobalStateAndSettings["openTelemetryEnabled"]>("openTelemetryEnabled")
|
||||
const openTelemetryMetricsExporter =
|
||||
context.globalState.get<GlobalStateAndSettings["openTelemetryMetricsExporter"]>("openTelemetryMetricsExporter")
|
||||
const openTelemetryLogsExporter =
|
||||
context.globalState.get<GlobalStateAndSettings["openTelemetryLogsExporter"]>("openTelemetryLogsExporter")
|
||||
const openTelemetryOtlpProtocol =
|
||||
context.globalState.get<GlobalStateAndSettings["openTelemetryOtlpProtocol"]>("openTelemetryOtlpProtocol")
|
||||
const openTelemetryOtlpEndpoint =
|
||||
context.globalState.get<GlobalStateAndSettings["openTelemetryOtlpEndpoint"]>("openTelemetryOtlpEndpoint")
|
||||
const openTelemetryOtlpMetricsProtocol = context.globalState.get<
|
||||
GlobalStateAndSettings["openTelemetryOtlpMetricsProtocol"]
|
||||
>("openTelemetryOtlpMetricsProtocol")
|
||||
const openTelemetryOtlpMetricsEndpoint = context.globalState.get<
|
||||
GlobalStateAndSettings["openTelemetryOtlpMetricsEndpoint"]
|
||||
>("openTelemetryOtlpMetricsEndpoint")
|
||||
const openTelemetryOtlpLogsProtocol =
|
||||
context.globalState.get<GlobalStateAndSettings["openTelemetryOtlpLogsProtocol"]>("openTelemetryOtlpLogsProtocol")
|
||||
const openTelemetryOtlpLogsEndpoint =
|
||||
context.globalState.get<GlobalStateAndSettings["openTelemetryOtlpLogsEndpoint"]>("openTelemetryOtlpLogsEndpoint")
|
||||
const openTelemetryMetricExportInterval = context.globalState.get<
|
||||
GlobalStateAndSettings["openTelemetryMetricExportInterval"]
|
||||
>("openTelemetryMetricExportInterval")
|
||||
const openTelemetryOtlpInsecure =
|
||||
context.globalState.get<GlobalStateAndSettings["openTelemetryOtlpInsecure"]>("openTelemetryOtlpInsecure")
|
||||
const openTelemetryLogBatchSize =
|
||||
context.globalState.get<GlobalStateAndSettings["openTelemetryLogBatchSize"]>("openTelemetryLogBatchSize")
|
||||
const openTelemetryLogBatchTimeout =
|
||||
context.globalState.get<GlobalStateAndSettings["openTelemetryLogBatchTimeout"]>("openTelemetryLogBatchTimeout")
|
||||
const openTelemetryLogMaxQueueSize =
|
||||
context.globalState.get<GlobalStateAndSettings["openTelemetryLogMaxQueueSize"]>("openTelemetryLogMaxQueueSize")
|
||||
const subagentsEnabled = context.globalState.get<GlobalStateAndSettings["subagentsEnabled"]>("subagentsEnabled")
|
||||
const skillsEnabled = context.globalState.get<GlobalStateAndSettings["skillsEnabled"]>("skillsEnabled")
|
||||
const backgroundEditEnabled =
|
||||
context.globalState.get<GlobalStateAndSettings["backgroundEditEnabled"]>("backgroundEditEnabled")
|
||||
|
||||
// Get mode-related configurations
|
||||
const mode = context.globalState.get<GlobalStateAndSettings["mode"]>("mode")
|
||||
|
||||
// Plan mode configurations
|
||||
const planModeApiProvider = context.globalState.get<GlobalStateAndSettings["planModeApiProvider"]>("planModeApiProvider")
|
||||
const planModeApiModelId = context.globalState.get<GlobalStateAndSettings["planModeApiModelId"]>("planModeApiModelId")
|
||||
const planModeThinkingBudgetTokens =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeThinkingBudgetTokens"]>("planModeThinkingBudgetTokens")
|
||||
const geminiPlanModeThinkingLevel =
|
||||
context.globalState.get<GlobalStateAndSettings["geminiPlanModeThinkingLevel"]>("geminiPlanModeThinkingLevel")
|
||||
const planModeReasoningEffort =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeReasoningEffort"]>("planModeReasoningEffort")
|
||||
const planModeVsCodeLmModelSelector =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeVsCodeLmModelSelector"]>("planModeVsCodeLmModelSelector")
|
||||
const planModeAwsBedrockCustomSelected = context.globalState.get<
|
||||
GlobalStateAndSettings["planModeAwsBedrockCustomSelected"]
|
||||
>("planModeAwsBedrockCustomSelected")
|
||||
const planModeAwsBedrockCustomModelBaseId = context.globalState.get<
|
||||
GlobalStateAndSettings["planModeAwsBedrockCustomModelBaseId"]
|
||||
>("planModeAwsBedrockCustomModelBaseId")
|
||||
const planModeOpenRouterModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeOpenRouterModelId"]>("planModeOpenRouterModelId")
|
||||
const planModeOpenRouterModelInfo =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeOpenRouterModelInfo"]>("planModeOpenRouterModelInfo")
|
||||
const planModeOpenAiModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeOpenAiModelId"]>("planModeOpenAiModelId")
|
||||
const planModeOpenAiModelInfo =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeOpenAiModelInfo"]>("planModeOpenAiModelInfo")
|
||||
const planModeOllamaModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeOllamaModelId"]>("planModeOllamaModelId")
|
||||
const planModeLmStudioModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeLmStudioModelId"]>("planModeLmStudioModelId")
|
||||
const planModeLiteLlmModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeLiteLlmModelId"]>("planModeLiteLlmModelId")
|
||||
const planModeLiteLlmModelInfo =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeLiteLlmModelInfo"]>("planModeLiteLlmModelInfo")
|
||||
const planModeRequestyModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeRequestyModelId"]>("planModeRequestyModelId")
|
||||
const planModeRequestyModelInfo =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeRequestyModelInfo"]>("planModeRequestyModelInfo")
|
||||
const planModeTogetherModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeTogetherModelId"]>("planModeTogetherModelId")
|
||||
const planModeFireworksModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeFireworksModelId"]>("planModeFireworksModelId")
|
||||
const planModeSapAiCoreModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeSapAiCoreModelId"]>("planModeSapAiCoreModelId")
|
||||
const planModeSapAiCoreDeploymentId =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeSapAiCoreDeploymentId"]>("planModeSapAiCoreDeploymentId")
|
||||
const planModeGroqModelId = context.globalState.get<GlobalStateAndSettings["planModeGroqModelId"]>("planModeGroqModelId")
|
||||
const planModeGroqModelInfo =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeGroqModelInfo"]>("planModeGroqModelInfo")
|
||||
const planModeHuggingFaceModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeHuggingFaceModelId"]>("planModeHuggingFaceModelId")
|
||||
const planModeHuggingFaceModelInfo =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeHuggingFaceModelInfo"]>("planModeHuggingFaceModelInfo")
|
||||
const planModeHuaweiCloudMaasModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeHuaweiCloudMaasModelId"]>("planModeHuaweiCloudMaasModelId")
|
||||
const planModeHuaweiCloudMaasModelInfo = context.globalState.get<
|
||||
GlobalStateAndSettings["planModeHuaweiCloudMaasModelInfo"]
|
||||
>("planModeHuaweiCloudMaasModelInfo")
|
||||
const planModeBasetenModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeBasetenModelId"]>("planModeBasetenModelId")
|
||||
const planModeBasetenModelInfo =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeBasetenModelInfo"]>("planModeBasetenModelInfo")
|
||||
const planModeOcaModelId = context.globalState.get("planModeOcaModelId") as string | undefined
|
||||
const planModeOcaModelInfo = context.globalState.get("planModeOcaModelInfo") as OcaModelInfo | undefined
|
||||
const planModeOcaReasoningEffort = context.globalState.get("planModeOcaReasoningEffort") as string | undefined
|
||||
const planModeHicapModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeHicapModelId"]>("planModeHicapModelId")
|
||||
const planModeHicapModelInfo =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeHicapModelInfo"]>("planModeHicapModelInfo")
|
||||
const planModeAihubmixModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeAihubmixModelId"]>("planModeAihubmixModelId")
|
||||
const planModeAihubmixModelInfo =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeAihubmixModelInfo"]>("planModeAihubmixModelInfo")
|
||||
const planModeNousResearchModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeNousResearchModelId"]>("planModeNousResearchModelId")
|
||||
const planModeVercelAiGatewayModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["planModeVercelAiGatewayModelId"]>("planModeVercelAiGatewayModelId")
|
||||
const planModeVercelAiGatewayModelInfo = context.globalState.get<
|
||||
GlobalStateAndSettings["planModeVercelAiGatewayModelInfo"]
|
||||
>("planModeVercelAiGatewayModelInfo")
|
||||
// Act mode configurations
|
||||
const actModeApiProvider = context.globalState.get<GlobalStateAndSettings["actModeApiProvider"]>("actModeApiProvider")
|
||||
const actModeApiModelId = context.globalState.get<GlobalStateAndSettings["actModeApiModelId"]>("actModeApiModelId")
|
||||
const actModeThinkingBudgetTokens =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeThinkingBudgetTokens"]>("actModeThinkingBudgetTokens")
|
||||
const geminiActModeThinkingLevel =
|
||||
context.globalState.get<GlobalStateAndSettings["geminiActModeThinkingLevel"]>("geminiActModeThinkingLevel")
|
||||
const actModeReasoningEffort =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeReasoningEffort"]>("actModeReasoningEffort")
|
||||
const actModeVsCodeLmModelSelector =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeVsCodeLmModelSelector"]>("actModeVsCodeLmModelSelector")
|
||||
const actModeAwsBedrockCustomSelected = context.globalState.get<
|
||||
GlobalStateAndSettings["actModeAwsBedrockCustomSelected"]
|
||||
>("actModeAwsBedrockCustomSelected")
|
||||
const actModeAwsBedrockCustomModelBaseId = context.globalState.get<
|
||||
GlobalStateAndSettings["actModeAwsBedrockCustomModelBaseId"]
|
||||
>("actModeAwsBedrockCustomModelBaseId")
|
||||
const actModeOpenRouterModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeOpenRouterModelId"]>("actModeOpenRouterModelId")
|
||||
const actModeOpenRouterModelInfo =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeOpenRouterModelInfo"]>("actModeOpenRouterModelInfo")
|
||||
const actModeOpenAiModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeOpenAiModelId"]>("actModeOpenAiModelId")
|
||||
const actModeOpenAiModelInfo =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeOpenAiModelInfo"]>("actModeOpenAiModelInfo")
|
||||
const actModeOllamaModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeOllamaModelId"]>("actModeOllamaModelId")
|
||||
const actModeLmStudioModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeLmStudioModelId"]>("actModeLmStudioModelId")
|
||||
const actModeLiteLlmModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeLiteLlmModelId"]>("actModeLiteLlmModelId")
|
||||
const actModeLiteLlmModelInfo =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeLiteLlmModelInfo"]>("actModeLiteLlmModelInfo")
|
||||
const actModeRequestyModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeRequestyModelId"]>("actModeRequestyModelId")
|
||||
const actModeRequestyModelInfo =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeRequestyModelInfo"]>("actModeRequestyModelInfo")
|
||||
const actModeTogetherModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeTogetherModelId"]>("actModeTogetherModelId")
|
||||
const actModeFireworksModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeFireworksModelId"]>("actModeFireworksModelId")
|
||||
const actModeSapAiCoreModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeSapAiCoreModelId"]>("actModeSapAiCoreModelId")
|
||||
const actModeSapAiCoreDeploymentId =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeSapAiCoreDeploymentId"]>("actModeSapAiCoreDeploymentId")
|
||||
const actModeGroqModelId = context.globalState.get<GlobalStateAndSettings["actModeGroqModelId"]>("actModeGroqModelId")
|
||||
const actModeGroqModelInfo =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeGroqModelInfo"]>("actModeGroqModelInfo")
|
||||
const actModeHuggingFaceModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeHuggingFaceModelId"]>("actModeHuggingFaceModelId")
|
||||
const actModeHuggingFaceModelInfo =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeHuggingFaceModelInfo"]>("actModeHuggingFaceModelInfo")
|
||||
const actModeHuaweiCloudMaasModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeHuaweiCloudMaasModelId"]>("actModeHuaweiCloudMaasModelId")
|
||||
const actModeHuaweiCloudMaasModelInfo = context.globalState.get<
|
||||
GlobalStateAndSettings["actModeHuaweiCloudMaasModelInfo"]
|
||||
>("actModeHuaweiCloudMaasModelInfo")
|
||||
const actModeBasetenModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeBasetenModelId"]>("actModeBasetenModelId")
|
||||
const actModeBasetenModelInfo =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeBasetenModelInfo"]>("actModeBasetenModelInfo")
|
||||
const actModeOcaModelId = context.globalState.get("actModeOcaModelId") as string | undefined
|
||||
const actModeOcaModelInfo = context.globalState.get("actModeOcaModelInfo") as OcaModelInfo | undefined
|
||||
const actModeOcaReasoningEffort = context.globalState.get("actModeOcaReasoningEffort") as string | undefined
|
||||
const actModeNousResearchModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeNousResearchModelId"]>("actModeNousResearchModelId")
|
||||
const sapAiCoreUseOrchestrationMode =
|
||||
context.globalState.get<GlobalStateAndSettings["sapAiCoreUseOrchestrationMode"]>("sapAiCoreUseOrchestrationMode")
|
||||
const actModeHicapModelId = context.globalState.get<GlobalStateAndSettings["actModeHicapModelId"]>("actModeHicapModelId")
|
||||
const actModeHicapModelInfo =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeHicapModelInfo"]>("actModeHicapModelInfo")
|
||||
const actModeAihubmixModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeAihubmixModelId"]>("actModeAihubmixModelId")
|
||||
const actModeAihubmixModelInfo =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeAihubmixModelInfo"]>("actModeAihubmixModelInfo")
|
||||
const actModeVercelAiGatewayModelId =
|
||||
context.globalState.get<GlobalStateAndSettings["actModeVercelAiGatewayModelId"]>("actModeVercelAiGatewayModelId")
|
||||
const actModeVercelAiGatewayModelInfo = context.globalState.get<
|
||||
GlobalStateAndSettings["actModeVercelAiGatewayModelInfo"]
|
||||
>("actModeVercelAiGatewayModelInfo")
|
||||
|
||||
let apiProvider: ApiProvider
|
||||
if (planModeApiProvider) {
|
||||
apiProvider = planModeApiProvider
|
||||
} else {
|
||||
// New users should default to openrouter, since they've opted to use an API key instead of signing in
|
||||
apiProvider = "openrouter"
|
||||
// Batch read all state values in a single optimized pass
|
||||
const stateValues = new Map<string, any>()
|
||||
// Read all values at once for better performance
|
||||
for (const key of GlobalStateAndSettingKeys) {
|
||||
const value = context.globalState.get(key as string)
|
||||
stateValues.set(key, value)
|
||||
}
|
||||
|
||||
const mcpResponsesCollapsed = mcpResponsesCollapsedRaw ?? false
|
||||
// Build result object with proper typing
|
||||
const result = {} as any // Use any for assignment, but return proper type
|
||||
|
||||
// Plan/Act separate models setting is a boolean indicating whether the user wants to use different models for plan and act. Existing users expect this to be enabled, while we want new users to opt in to this being disabled by default.
|
||||
// On win11 state sometimes initializes as empty string instead of undefined
|
||||
let planActSeparateModelsSetting: boolean | undefined
|
||||
if (planActSeparateModelsSettingRaw === true || planActSeparateModelsSettingRaw === false) {
|
||||
planActSeparateModelsSetting = planActSeparateModelsSettingRaw
|
||||
} else {
|
||||
// default to false
|
||||
planActSeparateModelsSetting = false
|
||||
// Process each state property using optimized approach
|
||||
for (const key of GlobalStateAndSettingKeys) {
|
||||
const stateKey = key as keyof GlobalStateAndSettings
|
||||
let value = stateValues.get(stateKey)
|
||||
|
||||
// Skip async properties - they need special handling
|
||||
if (isAsyncProperty(stateKey)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip computed properties - they need special handling
|
||||
if (isComputedProperty(stateKey)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Apply default value if needed
|
||||
if (value === undefined) {
|
||||
const defaultValue = getDefaultValue(stateKey)
|
||||
if (defaultValue !== undefined) {
|
||||
value = defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
// Apply transformation if provided
|
||||
if (value !== undefined) {
|
||||
value = applyTransform(stateKey, value)
|
||||
}
|
||||
// Set the processed value
|
||||
result[stateKey] = value
|
||||
}
|
||||
|
||||
// Read task history from disk
|
||||
// Note: If this throws (e.g., filesystem I/O error), StateManager initialization will fail
|
||||
// and the extension will not start. This is intentional to prevent data loss - better to
|
||||
// fail visibly than silently wipe history. The readTaskHistoryFromState function handles:
|
||||
// - File doesn't exist → returns []
|
||||
// - Parse errors → attempts reconstruction, returns [] only if reconstruction fails
|
||||
// - I/O errors → throws (caught here, causing initialization to fail)
|
||||
// Handle computed properties with special logic
|
||||
await handleComputedProperties(result, stateValues)
|
||||
|
||||
// So, any errors thrown here are true IO errors, which should be exceptionally rare.
|
||||
// The state manager tries once more to start on any failure. So if there is truly an I/O error happening twice that is not due to the file not existing or being corrupted, then something is truly wrong and it is correct to not start the application.
|
||||
const taskHistory = await readTaskHistoryFromState()
|
||||
// Handle async properties
|
||||
await handleAsyncProperties(result)
|
||||
|
||||
// Multi-root workspace support
|
||||
const workspaceRoots = context.globalState.get<GlobalStateAndSettings["workspaceRoots"]>("workspaceRoots")
|
||||
/**
|
||||
* Get primary root index from global state.
|
||||
* The primary root is the main workspace folder that Cline focuses on when dealing with
|
||||
* multi-root workspaces. In VS Code, you can have multiple folders open in one workspace,
|
||||
* and the primary root index indicates which folder (by its position in the array, 0-based)
|
||||
* should be treated as the main/default working directory for operations.
|
||||
*/
|
||||
const primaryRootIndex = context.globalState.get<GlobalStateAndSettings["primaryRootIndex"]>("primaryRootIndex")
|
||||
const multiRootEnabled = context.globalState.get<GlobalStateAndSettings["multiRootEnabled"]>("multiRootEnabled")
|
||||
const nativeToolCallEnabled =
|
||||
context.globalState.get<GlobalStateAndSettings["nativeToolCallEnabled"]>("nativeToolCallEnabled")
|
||||
const remoteRulesToggles = context.globalState.get<GlobalStateAndSettings["remoteRulesToggles"]>("remoteRulesToggles")
|
||||
const remoteWorkflowToggles =
|
||||
context.globalState.get<GlobalStateAndSettings["remoteWorkflowToggles"]>("remoteWorkflowToggles")
|
||||
|
||||
return {
|
||||
// api configuration fields
|
||||
claudeCodePath,
|
||||
awsRegion,
|
||||
awsUseCrossRegionInference,
|
||||
awsUseGlobalInference,
|
||||
awsBedrockUsePromptCache,
|
||||
awsBedrockEndpoint,
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
requestyBaseUrl,
|
||||
openAiHeaders: openAiHeaders || {},
|
||||
ollamaBaseUrl,
|
||||
ollamaApiOptionsCtxNum,
|
||||
lmStudioBaseUrl,
|
||||
lmStudioMaxTokens,
|
||||
anthropicBaseUrl,
|
||||
geminiBaseUrl,
|
||||
qwenApiLine,
|
||||
moonshotApiLine,
|
||||
zaiApiLine,
|
||||
azureApiVersion,
|
||||
azureIdentity,
|
||||
openRouterProviderSorting,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmUsePromptCache,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
asksageApiUrl,
|
||||
favoritedModelIds: favoritedModelIds || [],
|
||||
requestTimeoutMs,
|
||||
sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
difyBaseUrl,
|
||||
sapAiCoreUseOrchestrationMode: sapAiCoreUseOrchestrationMode ?? true,
|
||||
ocaBaseUrl,
|
||||
minimaxApiLine,
|
||||
ocaMode: ocaMode || "internal",
|
||||
hicapModelId,
|
||||
aihubmixBaseUrl,
|
||||
aihubmixAppCode,
|
||||
// Plan mode configurations
|
||||
planModeApiProvider: planModeApiProvider || apiProvider,
|
||||
planModeApiModelId,
|
||||
// undefined means it was never modified, 0 means it was turned off
|
||||
// (having this on by default ensures that <thinking> text does not pollute the user's chat and is instead rendered as reasoning)
|
||||
planModeThinkingBudgetTokens: planModeThinkingBudgetTokens ?? ANTHROPIC_MIN_THINKING_BUDGET,
|
||||
planModeReasoningEffort,
|
||||
planModeVsCodeLmModelSelector,
|
||||
planModeAwsBedrockCustomSelected,
|
||||
planModeAwsBedrockCustomModelBaseId,
|
||||
planModeOpenRouterModelId,
|
||||
planModeOpenRouterModelInfo,
|
||||
planModeOpenAiModelId,
|
||||
planModeOpenAiModelInfo,
|
||||
planModeOllamaModelId,
|
||||
planModeLmStudioModelId,
|
||||
planModeLiteLlmModelId,
|
||||
planModeLiteLlmModelInfo,
|
||||
planModeRequestyModelId,
|
||||
planModeRequestyModelInfo,
|
||||
planModeTogetherModelId,
|
||||
planModeFireworksModelId: planModeFireworksModelId || fireworksDefaultModelId,
|
||||
planModeSapAiCoreModelId,
|
||||
planModeSapAiCoreDeploymentId,
|
||||
planModeGroqModelId,
|
||||
planModeGroqModelInfo,
|
||||
planModeHuggingFaceModelId,
|
||||
planModeHuggingFaceModelInfo,
|
||||
planModeHuaweiCloudMaasModelId,
|
||||
planModeHuaweiCloudMaasModelInfo,
|
||||
planModeBasetenModelId,
|
||||
planModeBasetenModelInfo,
|
||||
planModeOcaModelId,
|
||||
planModeOcaModelInfo,
|
||||
planModeOcaReasoningEffort,
|
||||
planModeHicapModelId,
|
||||
planModeHicapModelInfo,
|
||||
planModeAihubmixModelId,
|
||||
planModeAihubmixModelInfo,
|
||||
planModeNousResearchModelId,
|
||||
planModeVercelAiGatewayModelId,
|
||||
planModeVercelAiGatewayModelInfo,
|
||||
geminiPlanModeThinkingLevel,
|
||||
// Act mode configurations
|
||||
actModeApiProvider: actModeApiProvider || apiProvider,
|
||||
actModeApiModelId,
|
||||
actModeThinkingBudgetTokens: actModeThinkingBudgetTokens ?? ANTHROPIC_MIN_THINKING_BUDGET,
|
||||
actModeReasoningEffort,
|
||||
actModeVsCodeLmModelSelector,
|
||||
actModeAwsBedrockCustomSelected,
|
||||
actModeAwsBedrockCustomModelBaseId,
|
||||
actModeOpenRouterModelId,
|
||||
actModeOpenRouterModelInfo,
|
||||
actModeOpenAiModelId,
|
||||
actModeOpenAiModelInfo,
|
||||
actModeOllamaModelId,
|
||||
actModeLmStudioModelId,
|
||||
actModeLiteLlmModelId,
|
||||
actModeLiteLlmModelInfo,
|
||||
actModeRequestyModelId,
|
||||
actModeRequestyModelInfo,
|
||||
actModeTogetherModelId,
|
||||
actModeFireworksModelId: actModeFireworksModelId || fireworksDefaultModelId,
|
||||
actModeSapAiCoreModelId,
|
||||
actModeSapAiCoreDeploymentId,
|
||||
actModeGroqModelId,
|
||||
actModeGroqModelInfo,
|
||||
actModeHuggingFaceModelId,
|
||||
actModeHuggingFaceModelInfo,
|
||||
actModeHuaweiCloudMaasModelId,
|
||||
actModeHuaweiCloudMaasModelInfo,
|
||||
actModeBasetenModelId,
|
||||
actModeBasetenModelInfo,
|
||||
actModeOcaModelId,
|
||||
actModeOcaModelInfo,
|
||||
actModeOcaReasoningEffort,
|
||||
actModeHicapModelId,
|
||||
actModeHicapModelInfo,
|
||||
actModeAihubmixModelId,
|
||||
actModeAihubmixModelInfo,
|
||||
actModeNousResearchModelId,
|
||||
actModeVercelAiGatewayModelId,
|
||||
actModeVercelAiGatewayModelInfo,
|
||||
geminiActModeThinkingLevel,
|
||||
|
||||
// Other global fields
|
||||
focusChainSettings: focusChainSettings || DEFAULT_FOCUS_CHAIN_SETTINGS,
|
||||
dictationSettings: { ...DEFAULT_DICTATION_SETTINGS, ...dictationSettings },
|
||||
strictPlanModeEnabled: strictPlanModeEnabled ?? true,
|
||||
yoloModeToggled: yoloModeToggled ?? false,
|
||||
useAutoCondense: useAutoCondense ?? false,
|
||||
clineWebToolsEnabled: clineWebToolsEnabled ?? true,
|
||||
isNewUser: isNewUser ?? true,
|
||||
welcomeViewCompleted,
|
||||
lastShownAnnouncementId,
|
||||
taskHistory: taskHistory || [],
|
||||
autoApprovalSettings: autoApprovalSettings || DEFAULT_AUTO_APPROVAL_SETTINGS, // default value can be 0 or empty string
|
||||
globalClineRulesToggles: globalClineRulesToggles || {},
|
||||
browserSettings: { ...DEFAULT_BROWSER_SETTINGS, ...browserSettings }, // this will ensure that older versions of browserSettings (e.g. before remoteBrowserEnabled was added) are merged with the default values (false for remoteBrowserEnabled)
|
||||
preferredLanguage: preferredLanguage || "English",
|
||||
openaiReasoningEffort: (openaiReasoningEffort as OpenaiReasoningEffort) || "medium",
|
||||
mode: mode || "act",
|
||||
userInfo,
|
||||
mcpMarketplaceEnabled: mcpMarketplaceEnabledRaw ?? true,
|
||||
mcpDisplayMode: mcpDisplayMode ?? DEFAULT_MCP_DISPLAY_MODE,
|
||||
mcpResponsesCollapsed: mcpResponsesCollapsed,
|
||||
telemetrySetting: telemetrySetting || "unset",
|
||||
planActSeparateModelsSetting: planActSeparateModelsSetting ?? false,
|
||||
enableCheckpointsSetting: enableCheckpointsSettingRaw ?? true,
|
||||
shellIntegrationTimeout: shellIntegrationTimeout || 4000,
|
||||
terminalReuseEnabled: terminalReuseEnabled ?? true,
|
||||
vscodeTerminalExecutionMode: vscodeTerminalExecutionMode ?? "vscodeTerminal",
|
||||
terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
|
||||
maxConsecutiveMistakes: maxConsecutiveMistakes ?? 3,
|
||||
subagentTerminalOutputLineLimit: subagentTerminalOutputLineLimit ?? 2000,
|
||||
defaultTerminalProfile: defaultTerminalProfile ?? "default",
|
||||
globalWorkflowToggles: globalWorkflowToggles || {},
|
||||
globalSkillsToggles: globalSkillsToggles || {},
|
||||
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,
|
||||
skillsEnabled: skillsEnabled ?? false,
|
||||
enableParallelToolCalling: enableParallelToolCalling ?? false,
|
||||
lastDismissedInfoBannerVersion: lastDismissedInfoBannerVersion ?? 0,
|
||||
lastDismissedModelBannerVersion: lastDismissedModelBannerVersion ?? 0,
|
||||
lastDismissedCliBannerVersion: lastDismissedCliBannerVersion ?? 0,
|
||||
dismissedBanners: dismissedBanners || [],
|
||||
nativeToolCallEnabled: nativeToolCallEnabled ?? true,
|
||||
// Multi-root workspace support
|
||||
workspaceRoots,
|
||||
primaryRootIndex: primaryRootIndex ?? 0,
|
||||
// Feature flag - defaults to false
|
||||
// For now, always return false to disable multi-root support by default
|
||||
multiRootEnabled: !!multiRootEnabled,
|
||||
|
||||
// OpenTelemetry configuration
|
||||
openTelemetryEnabled: openTelemetryEnabled ?? true,
|
||||
openTelemetryMetricsExporter,
|
||||
openTelemetryLogsExporter,
|
||||
openTelemetryOtlpProtocol: openTelemetryOtlpProtocol ?? "http/json",
|
||||
openTelemetryOtlpEndpoint: openTelemetryOtlpEndpoint ?? "http://localhost:4318",
|
||||
openTelemetryOtlpMetricsProtocol,
|
||||
openTelemetryOtlpMetricsEndpoint,
|
||||
openTelemetryOtlpLogsProtocol,
|
||||
openTelemetryOtlpLogsEndpoint,
|
||||
openTelemetryMetricExportInterval: openTelemetryMetricExportInterval ?? 60000,
|
||||
openTelemetryOtlpInsecure: openTelemetryOtlpInsecure ?? false,
|
||||
openTelemetryLogBatchSize: openTelemetryLogBatchSize ?? 512,
|
||||
openTelemetryLogBatchTimeout: openTelemetryLogBatchTimeout ?? 5000,
|
||||
openTelemetryLogMaxQueueSize: openTelemetryLogMaxQueueSize ?? 2048,
|
||||
remoteRulesToggles: remoteRulesToggles || {},
|
||||
remoteWorkflowToggles: remoteWorkflowToggles || {},
|
||||
}
|
||||
return result as GlobalStateAndSettings
|
||||
} catch (error) {
|
||||
console.error("[StateHelpers] Failed to read global state:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle properties that require computed logic
|
||||
*/
|
||||
async function handleComputedProperties(result: any, stateValues: Map<string, any>): Promise<void> {
|
||||
// 1. API Provider logic - set defaults based on existing values
|
||||
const defaultApiProvider: ApiProvider = "openrouter"
|
||||
result.planModeApiProvider = result.planModeApiProvider || defaultApiProvider
|
||||
result.actModeApiProvider = result.actModeApiProvider || defaultApiProvider
|
||||
|
||||
// 2. Plan/Act separate models setting with special logic
|
||||
const planActSeparateModelsSettingRaw = stateValues.get("planActSeparateModelsSetting")
|
||||
if (planActSeparateModelsSettingRaw === true || planActSeparateModelsSettingRaw === false) {
|
||||
result.planActSeparateModelsSetting = planActSeparateModelsSettingRaw
|
||||
} else {
|
||||
// Default to false when not explicitly set
|
||||
result.planActSeparateModelsSetting = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle properties that require async operations
|
||||
*/
|
||||
async function handleAsyncProperties(result: any): Promise<void> {
|
||||
// Task history requires async disk read
|
||||
result.taskHistory = await readTaskHistoryFromState()
|
||||
}
|
||||
|
||||
export async function resetWorkspaceState(controller: Controller) {
|
||||
const context = controller.context
|
||||
await Promise.all(context.workspaceState.keys().map((key) => controller.context.workspaceState.update(key, undefined)))
|
||||
await Promise.all(LocalStateKeys.map((key) => controller.context.workspaceState.update(key, undefined)))
|
||||
|
||||
await controller.stateManager.reInitialize()
|
||||
}
|
||||
@@ -762,49 +128,9 @@ export async function resetGlobalState(controller: Controller) {
|
||||
// TODO: Reset all workspace states?
|
||||
const context = controller.context
|
||||
|
||||
await Promise.all(context.globalState.keys().map((key) => context.globalState.update(key, undefined)))
|
||||
const secretKeys: SecretKey[] = [
|
||||
"apiKey",
|
||||
"openRouterApiKey",
|
||||
"awsAccessKey",
|
||||
"awsSecretKey",
|
||||
"awsSessionToken",
|
||||
"awsBedrockApiKey",
|
||||
"openAiApiKey",
|
||||
"ollamaApiKey",
|
||||
"geminiApiKey",
|
||||
"openAiNativeApiKey",
|
||||
"deepSeekApiKey",
|
||||
"requestyApiKey",
|
||||
"togetherApiKey",
|
||||
"qwenApiKey",
|
||||
"doubaoApiKey",
|
||||
"mistralApiKey",
|
||||
"clineAccountId",
|
||||
"liteLlmApiKey",
|
||||
"remoteLiteLlmApiKey",
|
||||
"fireworksApiKey",
|
||||
"asksageApiKey",
|
||||
"xaiApiKey",
|
||||
"sambanovaApiKey",
|
||||
"cerebrasApiKey",
|
||||
"groqApiKey",
|
||||
"basetenApiKey",
|
||||
"moonshotApiKey",
|
||||
"nebiusApiKey",
|
||||
"huggingFaceApiKey",
|
||||
"huaweiCloudMaasApiKey",
|
||||
"vercelAiGatewayApiKey",
|
||||
"zaiApiKey",
|
||||
"difyApiKey",
|
||||
"ocaApiKey",
|
||||
"ocaRefreshToken",
|
||||
"minimaxApiKey",
|
||||
"hicapApiKey",
|
||||
"aihubmixApiKey",
|
||||
"mcpOAuthSecrets",
|
||||
"nousResearchApiKey",
|
||||
]
|
||||
await Promise.all(secretKeys.map((key) => context.secrets.delete(key)))
|
||||
await Promise.all(GlobalStateAndSettingKeys.map((key) => context.globalState.update(key, undefined)))
|
||||
|
||||
await Promise.all(SecretKeys.map((key) => context.secrets.delete(key)))
|
||||
|
||||
await controller.stateManager.reInitialize()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import { ClineAskResponse } from "@shared/WebviewMessage"
|
||||
|
||||
/**
|
||||
* Represents a pending ask operation waiting for user response
|
||||
*/
|
||||
interface PendingAsk {
|
||||
askId: string
|
||||
askTs: number
|
||||
response?: ClineAskResponse
|
||||
text?: string
|
||||
images?: string[]
|
||||
files?: string[]
|
||||
resolved: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages concurrent ask operations with queue-based tracking.
|
||||
* Each ask gets a unique ID, allowing multiple asks to be pending simultaneously.
|
||||
* Responses are matched back to their corresponding asks.
|
||||
*/
|
||||
export class PendingAskQueue {
|
||||
private queue: Map<string, PendingAsk> = new Map()
|
||||
private lastMessageTs?: number
|
||||
|
||||
/**
|
||||
* Create a new pending ask and add it to the queue
|
||||
* @returns The unique askId for this ask operation
|
||||
*/
|
||||
createPendingAsk(askTs: number): string {
|
||||
const askId = `ask-${askTs}-${Math.random().toString(36).substr(2, 9)}`
|
||||
this.queue.set(askId, {
|
||||
askId,
|
||||
askTs,
|
||||
resolved: false,
|
||||
})
|
||||
this.lastMessageTs = askTs
|
||||
return askId
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a pending ask with user response
|
||||
* @param askId The unique ID of the ask to resolve
|
||||
* @param response The user's response
|
||||
* @param text Optional text response
|
||||
* @param images Optional image attachments
|
||||
* @param files Optional file attachments
|
||||
* @returns true if ask was found and resolved, false otherwise
|
||||
*/
|
||||
resolvePendingAsk(askId: string, response: ClineAskResponse, text?: string, images?: string[], files?: string[]): boolean {
|
||||
const pendingAsk = this.queue.get(askId)
|
||||
if (!pendingAsk) {
|
||||
return false
|
||||
}
|
||||
|
||||
pendingAsk.response = response
|
||||
pendingAsk.text = text
|
||||
pendingAsk.images = images
|
||||
pendingAsk.files = files
|
||||
pendingAsk.resolved = true
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resolution status of a pending ask
|
||||
* @param askId The unique ID of the ask
|
||||
* @returns The ask object if found, undefined otherwise
|
||||
*/
|
||||
getPendingAsk(askId: string): PendingAsk | undefined {
|
||||
return this.queue.get(askId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a resolved ask from the queue
|
||||
* @param askId The unique ID of the ask
|
||||
*/
|
||||
removePendingAsk(askId: string): void {
|
||||
this.queue.delete(askId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all pending asks (those not yet resolved)
|
||||
*/
|
||||
getPendingAsks(): PendingAsk[] {
|
||||
return Array.from(this.queue.values()).filter((ask) => !ask.resolved)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an ask was interrupted (another message came after it)
|
||||
* @param askId The unique ID of the ask
|
||||
* @returns true if this ask is no longer the most recent, false otherwise
|
||||
*/
|
||||
wasAskInterrupted(askId: string, currentLastMessageTs?: number): boolean {
|
||||
const pendingAsk = this.queue.get(askId)
|
||||
if (!pendingAsk) {
|
||||
return true // Ask was removed or never existed
|
||||
}
|
||||
|
||||
// If currentLastMessageTs is provided and is different from this ask's ts,
|
||||
// then this ask was interrupted by another message
|
||||
if (currentLastMessageTs !== undefined && currentLastMessageTs !== pendingAsk.askTs) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all pending asks (used on task abort)
|
||||
*/
|
||||
clear(): void {
|
||||
this.queue.clear()
|
||||
this.lastMessageTs = undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last message timestamp
|
||||
*/
|
||||
getLastMessageTs(): number | undefined {
|
||||
return this.lastMessageTs
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the last message timestamp (for tracking if asks were interrupted)
|
||||
*/
|
||||
setLastMessageTs(ts: number): void {
|
||||
this.lastMessageTs = ts
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { AssistantMessageContent } from "@core/assistant-message"
|
||||
import { ClineAskResponse } from "@shared/WebviewMessage"
|
||||
import { PendingAskQueue } from "./PendingAskQueue"
|
||||
import type { HookExecution } from "./types/HookExecution"
|
||||
|
||||
export class TaskState {
|
||||
@@ -21,11 +21,8 @@ export class TaskState {
|
||||
presentAssistantMessageLocked = false
|
||||
presentAssistantMessageHasPendingUpdates = false
|
||||
|
||||
// Ask/Response handling
|
||||
askResponse?: ClineAskResponse
|
||||
askResponseText?: string
|
||||
askResponseImages?: string[]
|
||||
askResponseFiles?: string[]
|
||||
// Ask/Response handling - now queue-based for concurrent ask support
|
||||
pendingAskQueue = new PendingAskQueue()
|
||||
lastMessageTs?: number
|
||||
|
||||
// Plan mode specific state
|
||||
@@ -39,6 +36,8 @@ export class TaskState {
|
||||
didRejectTool = false
|
||||
didAlreadyUseTool = false
|
||||
didEditFile: boolean = false
|
||||
lastToolName: string = "" // Track last tool used for consecutive call detection
|
||||
isExecutingInParallel = false // Track if currently executing tools in parallel
|
||||
|
||||
// Error tracking
|
||||
consecutiveMistakeCount: number = 0
|
||||
|
||||
@@ -101,11 +101,13 @@ export class ToolExecutor {
|
||||
images?: string[],
|
||||
files?: string[],
|
||||
partial?: boolean,
|
||||
call_id?: string,
|
||||
) => Promise<number | undefined>,
|
||||
private ask: (
|
||||
type: ClineAsk,
|
||||
text?: string,
|
||||
partial?: boolean,
|
||||
call_id?: string,
|
||||
) => Promise<{
|
||||
response: ClineAskResponse
|
||||
text?: string
|
||||
@@ -114,7 +116,11 @@ export class ToolExecutor {
|
||||
}>,
|
||||
private saveCheckpoint: (isAttemptCompletionMessage?: boolean, completionMessageTs?: number) => Promise<void>,
|
||||
private sayAndCreateMissingParamError: (toolName: ClineDefaultTool, paramName: string, relPath?: string) => Promise<any>,
|
||||
private removeLastPartialMessageIfExistsWithType: (type: "ask" | "say", askOrSay: ClineAsk | ClineSay) => Promise<void>,
|
||||
private removeLastPartialMessageIfExistsWithType: (
|
||||
type: "ask" | "say",
|
||||
askOrSay: ClineAsk | ClineSay,
|
||||
call_id?: string,
|
||||
) => Promise<void>,
|
||||
private executeCommandTool: (command: string, timeoutSeconds: number | undefined) => Promise<[boolean, any]>,
|
||||
private doesLatestTaskCompletionHaveNewChanges: () => Promise<boolean>,
|
||||
private updateFCListFromToolResponse: (taskProgress: string | undefined) => Promise<void>,
|
||||
@@ -294,7 +300,6 @@ export class ToolExecutor {
|
||||
block,
|
||||
this.taskState.userMessageContent,
|
||||
(block: ToolUse) => ToolDisplayUtils.getToolDescription(block),
|
||||
this.api,
|
||||
this.coordinator,
|
||||
this.taskState.toolUseIdMap,
|
||||
)
|
||||
@@ -548,7 +553,7 @@ export class ToolExecutor {
|
||||
|
||||
// Check if handler supports partial blocks with proper typing
|
||||
if (handler && "handlePartialBlock" in handler) {
|
||||
const uiHelpers = createUIHelpers(config)
|
||||
const uiHelpers = createUIHelpers(config, block.call_id)
|
||||
const partialHandler = handler as IPartialBlockHandler
|
||||
await partialHandler.handlePartialBlock(block, uiHelpers)
|
||||
}
|
||||
@@ -600,6 +605,9 @@ export class ToolExecutor {
|
||||
toolWasExecuted = true
|
||||
this.pushToolResult(toolResult, block)
|
||||
|
||||
// Track the last executed tool for consecutive call detection (used by act_mode_respond)
|
||||
this.taskState.lastToolName = block.name
|
||||
|
||||
// Check abort before running PostToolUse hook (success path)
|
||||
if (this.taskState.abort) {
|
||||
return
|
||||
|
||||
+161
-51
@@ -60,7 +60,7 @@ import { HistoryItem } from "@shared/HistoryItem"
|
||||
import { DEFAULT_LANGUAGE_SETTINGS, getLanguageKey, LanguageDisplay } from "@shared/Languages"
|
||||
import { USER_CONTENT_TAGS } from "@shared/messages/constants"
|
||||
import { convertClineMessageToProto } from "@shared/proto-conversions/cline-message"
|
||||
import { ClineDefaultTool, READ_ONLY_TOOLS } from "@shared/tools"
|
||||
import { ClineDefaultTool, PARALLEL_SAFE_TOOLS, READ_ONLY_TOOLS } from "@shared/tools"
|
||||
import { ClineAskResponse } from "@shared/WebviewMessage"
|
||||
import { isClaude4PlusModelFamily, isGPT5ModelFamily, isLocalModel, isNextGenModelFamily } from "@utils/model-utils"
|
||||
import { arePathsEqual, getDesktopDir } from "@utils/path"
|
||||
@@ -586,6 +586,8 @@ export class Task {
|
||||
throw new Error("Cline instance aborted")
|
||||
}
|
||||
let askTs: number
|
||||
let askId: string | undefined
|
||||
|
||||
if (partial !== undefined) {
|
||||
const clineMessages = this.messageStateHandler.getClineMessages()
|
||||
const lastMessage = clineMessages.at(-1)
|
||||
@@ -600,18 +602,13 @@ export class Task {
|
||||
text,
|
||||
partial,
|
||||
})
|
||||
// todo be more efficient about saving and posting only new data or one whole message at a time so ignore partial for saves, and only post parts of partial message instead of whole array in new listener
|
||||
// await this.saveClineMessagesAndUpdateHistory()
|
||||
// await this.postStateToWebview()
|
||||
const protoMessage = convertClineMessageToProto(lastMessage)
|
||||
await sendPartialMessageEvent(protoMessage)
|
||||
throw new Error("Current ask promise was ignored 1")
|
||||
} else {
|
||||
// this is a new partial message, so add it with partial state
|
||||
// this.askResponse = undefined
|
||||
// this.askResponseText = undefined
|
||||
// this.askResponseImages = undefined
|
||||
askTs = Date.now()
|
||||
askId = this.taskState.pendingAskQueue.createPendingAsk(askTs)
|
||||
this.taskState.lastMessageTs = askTs
|
||||
await this.messageStateHandler.addToClineMessages({
|
||||
ts: askTs,
|
||||
@@ -627,11 +624,6 @@ export class Task {
|
||||
// partial=false means its a complete version of a previously partial message
|
||||
if (isUpdatingPreviousPartial) {
|
||||
// this is the complete version of a previously partial message, so replace the partial with the complete version
|
||||
this.taskState.askResponse = undefined
|
||||
this.taskState.askResponseText = undefined
|
||||
this.taskState.askResponseImages = undefined
|
||||
this.taskState.askResponseFiles = undefined
|
||||
|
||||
/*
|
||||
Bug for the history books:
|
||||
In the webview we use the ts as the chatrow key for the virtuoso list. Since we would update this ts right at the end of streaming, it would cause the view to flicker. The key prop has to be stable otherwise react has trouble reconciling items between renders, causing unmounting and remounting of components (flickering).
|
||||
@@ -639,22 +631,18 @@ export class Task {
|
||||
So in this case we must make sure that the message ts is never altered after first setting it.
|
||||
*/
|
||||
askTs = lastMessage.ts
|
||||
askId = this.taskState.pendingAskQueue.createPendingAsk(askTs)
|
||||
this.taskState.lastMessageTs = askTs
|
||||
// lastMessage.ts = askTs
|
||||
await this.messageStateHandler.updateClineMessage(lastMessageIndex, {
|
||||
text,
|
||||
partial: false,
|
||||
})
|
||||
// await this.postStateToWebview()
|
||||
const protoMessage = convertClineMessageToProto(lastMessage)
|
||||
await sendPartialMessageEvent(protoMessage)
|
||||
} else {
|
||||
// this is a new partial=false message, so add it like normal
|
||||
this.taskState.askResponse = undefined
|
||||
this.taskState.askResponseText = undefined
|
||||
this.taskState.askResponseImages = undefined
|
||||
this.taskState.askResponseFiles = undefined
|
||||
askTs = Date.now()
|
||||
askId = this.taskState.pendingAskQueue.createPendingAsk(askTs)
|
||||
this.taskState.lastMessageTs = askTs
|
||||
await this.messageStateHandler.addToClineMessages({
|
||||
ts: askTs,
|
||||
@@ -667,12 +655,8 @@ export class Task {
|
||||
}
|
||||
} else {
|
||||
// this is a new non-partial message, so add it like normal
|
||||
// const lastMessage = this.clineMessages.at(-1)
|
||||
this.taskState.askResponse = undefined
|
||||
this.taskState.askResponseText = undefined
|
||||
this.taskState.askResponseImages = undefined
|
||||
this.taskState.askResponseFiles = undefined
|
||||
askTs = Date.now()
|
||||
askId = this.taskState.pendingAskQueue.createPendingAsk(askTs)
|
||||
this.taskState.lastMessageTs = askTs
|
||||
await this.messageStateHandler.addToClineMessages({
|
||||
ts: askTs,
|
||||
@@ -683,30 +667,52 @@ export class Task {
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
|
||||
await pWaitFor(() => this.taskState.askResponse !== undefined || this.taskState.lastMessageTs !== askTs, {
|
||||
interval: 100,
|
||||
})
|
||||
if (this.taskState.lastMessageTs !== askTs) {
|
||||
// Wait for this specific ask to be resolved
|
||||
if (!askId) {
|
||||
throw new Error("Failed to create pending ask")
|
||||
}
|
||||
|
||||
await pWaitFor(
|
||||
() => {
|
||||
const pendingAsk = this.taskState.pendingAskQueue.getPendingAsk(askId!)
|
||||
return (
|
||||
pendingAsk?.resolved === true ||
|
||||
this.taskState.pendingAskQueue.wasAskInterrupted(askId!, this.taskState.lastMessageTs)
|
||||
)
|
||||
},
|
||||
{
|
||||
interval: 100,
|
||||
},
|
||||
)
|
||||
|
||||
const pendingAsk = this.taskState.pendingAskQueue.getPendingAsk(askId)
|
||||
if (!pendingAsk || this.taskState.pendingAskQueue.wasAskInterrupted(askId, this.taskState.lastMessageTs)) {
|
||||
throw new Error("Current ask promise was ignored") // could happen if we send multiple asks in a row i.e. with command_output. It's important that when we know an ask could fail, it is handled gracefully
|
||||
}
|
||||
|
||||
const result = {
|
||||
response: this.taskState.askResponse!,
|
||||
text: this.taskState.askResponseText,
|
||||
images: this.taskState.askResponseImages,
|
||||
files: this.taskState.askResponseFiles,
|
||||
response: pendingAsk.response!,
|
||||
text: pendingAsk.text,
|
||||
images: pendingAsk.images,
|
||||
files: pendingAsk.files,
|
||||
}
|
||||
this.taskState.askResponse = undefined
|
||||
this.taskState.askResponseText = undefined
|
||||
this.taskState.askResponseImages = undefined
|
||||
this.taskState.askResponseFiles = undefined
|
||||
|
||||
this.taskState.pendingAskQueue.removePendingAsk(askId)
|
||||
return result
|
||||
}
|
||||
|
||||
async handleWebviewAskResponse(askResponse: ClineAskResponse, text?: string, images?: string[], files?: string[]) {
|
||||
this.taskState.askResponse = askResponse
|
||||
this.taskState.askResponseText = text
|
||||
this.taskState.askResponseImages = images
|
||||
this.taskState.askResponseFiles = files
|
||||
// Get all pending asks and resolve them in the order they were created
|
||||
const pendingAsks = this.taskState.pendingAskQueue.getPendingAsks()
|
||||
|
||||
if (pendingAsks.length === 0) {
|
||||
console.warn("handleWebviewAskResponse: No pending asks to resolve")
|
||||
return
|
||||
}
|
||||
|
||||
// Resolve the first unresolved ask (FIFO order)
|
||||
const firstPendingAsk = pendingAsks[0]
|
||||
this.taskState.pendingAskQueue.resolvePendingAsk(firstPendingAsk.askId, askResponse, text, images, files)
|
||||
}
|
||||
|
||||
async say(
|
||||
@@ -715,6 +721,7 @@ export class Task {
|
||||
images?: string[],
|
||||
files?: string[],
|
||||
partial?: boolean,
|
||||
call_id?: string,
|
||||
): Promise<number | undefined> {
|
||||
// Allow hook messages even when aborted to enable proper cleanup
|
||||
if (this.taskState.abort && type !== "hook_status" && type !== "hook_output_stream") {
|
||||
@@ -729,9 +736,14 @@ export class Task {
|
||||
}
|
||||
|
||||
if (partial !== undefined) {
|
||||
const lastMessage = this.messageStateHandler.getClineMessages().at(-1)
|
||||
const clineMessages = this.messageStateHandler.getClineMessages()
|
||||
const lastMessage = clineMessages.at(-1)
|
||||
const isUpdatingPreviousPartial =
|
||||
lastMessage && lastMessage.partial && lastMessage.type === "say" && lastMessage.say === type
|
||||
lastMessage &&
|
||||
lastMessage.partial &&
|
||||
lastMessage.type === "say" &&
|
||||
lastMessage.say === type &&
|
||||
lastMessage.call_id === call_id
|
||||
if (partial) {
|
||||
if (isUpdatingPreviousPartial) {
|
||||
// existing partial message, so update it
|
||||
@@ -755,6 +767,7 @@ export class Task {
|
||||
files,
|
||||
partial,
|
||||
modelInfo,
|
||||
call_id,
|
||||
})
|
||||
await this.postStateToWebview()
|
||||
return sayTs
|
||||
@@ -788,6 +801,7 @@ export class Task {
|
||||
images,
|
||||
files,
|
||||
modelInfo,
|
||||
call_id,
|
||||
})
|
||||
await this.postStateToWebview()
|
||||
return sayTs
|
||||
@@ -805,6 +819,7 @@ export class Task {
|
||||
images,
|
||||
files,
|
||||
modelInfo,
|
||||
call_id,
|
||||
})
|
||||
await this.postStateToWebview()
|
||||
return sayTs
|
||||
@@ -821,12 +836,31 @@ export class Task {
|
||||
return formatResponse.toolError(formatResponse.missingToolParameterError(paramName))
|
||||
}
|
||||
|
||||
async removeLastPartialMessageIfExistsWithType(type: "ask" | "say", askOrSay: ClineAsk | ClineSay) {
|
||||
async removeLastPartialMessageIfExistsWithType(type: "ask" | "say", askOrSay: ClineAsk | ClineSay, call_id?: string) {
|
||||
const clineMessages = this.messageStateHandler.getClineMessages()
|
||||
const lastMessage = clineMessages.at(-1)
|
||||
if (lastMessage?.partial && lastMessage.type === type && (lastMessage.ask === askOrSay || lastMessage.say === askOrSay)) {
|
||||
this.messageStateHandler.setClineMessages(clineMessages.slice(0, -1))
|
||||
await this.messageStateHandler.saveClineMessagesAndUpdateHistory()
|
||||
|
||||
// If call_id is provided, find the message with matching call_id
|
||||
if (call_id) {
|
||||
const messageIndex = clineMessages.findIndex(
|
||||
(msg) =>
|
||||
msg.partial && msg.type === type && (msg.ask === askOrSay || msg.say === askOrSay) && msg.call_id === call_id,
|
||||
)
|
||||
if (messageIndex >= 0) {
|
||||
clineMessages.splice(messageIndex, 1)
|
||||
this.messageStateHandler.setClineMessages(clineMessages)
|
||||
await this.messageStateHandler.saveClineMessagesAndUpdateHistory()
|
||||
}
|
||||
} else {
|
||||
// Old behavior: remove the last message of this type (for sequential execution)
|
||||
const lastMessage = clineMessages.at(-1)
|
||||
if (
|
||||
lastMessage?.partial &&
|
||||
lastMessage.type === type &&
|
||||
(lastMessage.ask === askOrSay || lastMessage.say === askOrSay)
|
||||
) {
|
||||
this.messageStateHandler.setClineMessages(clineMessages.slice(0, -1))
|
||||
await this.messageStateHandler.saveClineMessagesAndUpdateHistory()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1933,8 +1967,25 @@ export class Task {
|
||||
attempt: this.taskState.autoRetryAttempts,
|
||||
maxAttempts: 3,
|
||||
delaySeconds: delay / 1000,
|
||||
errorMessage: streamingFailedMessage,
|
||||
}),
|
||||
)
|
||||
|
||||
// Clear streamingFailedMessage now that error_retry contains it
|
||||
// This prevents showing the error in both ErrorRow and error_retry
|
||||
const autoRetryApiReqIndex = findLastIndex(
|
||||
this.messageStateHandler.getClineMessages(),
|
||||
(m) => m.say === "api_req_started",
|
||||
)
|
||||
if (autoRetryApiReqIndex !== -1) {
|
||||
const clineMessages = this.messageStateHandler.getClineMessages()
|
||||
const currentApiReqInfo: ClineApiReqInfo = JSON.parse(clineMessages[autoRetryApiReqIndex].text || "{}")
|
||||
delete currentApiReqInfo.streamingFailedMessage
|
||||
await this.messageStateHandler.updateClineMessage(autoRetryApiReqIndex, {
|
||||
text: JSON.stringify(currentApiReqInfo),
|
||||
})
|
||||
}
|
||||
|
||||
await setTimeoutPromise(delay)
|
||||
} else {
|
||||
// Show error_retry with failed flag to indicate all retries exhausted (but not for insufficient credits)
|
||||
@@ -1946,6 +1997,7 @@ export class Task {
|
||||
maxAttempts: 3,
|
||||
delaySeconds: 0,
|
||||
failed: true, // Special flag to indicate retries exhausted
|
||||
errorMessage: streamingFailedMessage,
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -1991,6 +2043,34 @@ export class Task {
|
||||
yield* iterator
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects consecutive complete tool blocks that are safe to run in parallel.
|
||||
* Stops at the first non-parallel-safe tool or streaming tool.
|
||||
*/
|
||||
private collectConsecutiveParallelToolBlocks(): ToolUse[] {
|
||||
const toolBlocks: ToolUse[] = []
|
||||
let index = this.taskState.currentStreamingContentIndex
|
||||
|
||||
while (index < this.taskState.assistantMessageContent.length) {
|
||||
const block = this.taskState.assistantMessageContent[index]
|
||||
|
||||
// Only collect complete tool blocks
|
||||
if (block.type !== "tool_use" || block.partial) {
|
||||
break
|
||||
}
|
||||
|
||||
// Check if this tool can run in parallel
|
||||
if (!PARALLEL_SAFE_TOOLS.includes(block.name as any)) {
|
||||
break
|
||||
}
|
||||
|
||||
toolBlocks.push(block)
|
||||
index++
|
||||
}
|
||||
|
||||
return toolBlocks
|
||||
}
|
||||
|
||||
async presentAssistantMessage() {
|
||||
if (this.taskState.abort) {
|
||||
throw new Error("Cline instance aborted")
|
||||
@@ -2083,7 +2163,7 @@ export class Task {
|
||||
await this.say("text", content, undefined, undefined, block.partial)
|
||||
break
|
||||
}
|
||||
case "tool_use":
|
||||
case "tool_use": {
|
||||
// If we have a pending initial commit, we must block unsafe tools until it finishes.
|
||||
// Safe tools (read-only) can run in parallel.
|
||||
if (this.initialCheckpointCommitPromise) {
|
||||
@@ -2092,8 +2172,35 @@ export class Task {
|
||||
this.initialCheckpointCommitPromise = undefined
|
||||
}
|
||||
}
|
||||
|
||||
// Try to execute multiple parallel-safe tools concurrently
|
||||
if (this.isParallelToolCallingEnabled()) {
|
||||
const parallelTools = this.collectConsecutiveParallelToolBlocks()
|
||||
if (parallelTools.length > 1) {
|
||||
// Set flag to indicate we're in parallel execution
|
||||
// This allows CommandExecutor to use ConcurrentCommandOrchestrator
|
||||
this.taskState.isExecutingInParallel = true
|
||||
this.commandExecutor.setParallelExecution(true)
|
||||
|
||||
try {
|
||||
// Execute all parallel tools concurrently
|
||||
await Promise.all(parallelTools.map((toolBlock) => this.toolExecutor.executeTool(toolBlock)))
|
||||
} finally {
|
||||
// Clear the parallel execution flag
|
||||
this.taskState.isExecutingInParallel = false
|
||||
this.commandExecutor.setParallelExecution(false)
|
||||
}
|
||||
|
||||
// Advance past all executed tools
|
||||
this.taskState.currentStreamingContentIndex += parallelTools.length - 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to sequential execution for non-parallel tools
|
||||
await this.toolExecutor.executeTool(block)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -2672,6 +2779,7 @@ export class Task {
|
||||
attempt: this.taskState.autoRetryAttempts,
|
||||
maxAttempts: 3,
|
||||
delaySeconds: delay / 1000,
|
||||
errorMessage,
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -2693,6 +2801,7 @@ export class Task {
|
||||
maxAttempts: 3,
|
||||
delaySeconds: 0,
|
||||
failed: true, // Special flag to indicate retries exhausted
|
||||
errorMessage,
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -2914,6 +3023,8 @@ export class Task {
|
||||
|
||||
let response: ClineAskResponse
|
||||
|
||||
const noResponseErrorMessage = "No assistant message was received. Would you like to retry the request?"
|
||||
|
||||
if (this.taskState.autoRetryAttempts < 3) {
|
||||
// Auto-retry enabled with max 3 attempts: automatically approve the retry
|
||||
this.taskState.autoRetryAttempts++
|
||||
@@ -2927,6 +3038,7 @@ export class Task {
|
||||
attempt: this.taskState.autoRetryAttempts,
|
||||
maxAttempts: 3,
|
||||
delaySeconds: delay / 1000,
|
||||
errorMessage: noResponseErrorMessage,
|
||||
}),
|
||||
)
|
||||
await setTimeoutPromise(delay)
|
||||
@@ -2939,12 +3051,10 @@ export class Task {
|
||||
maxAttempts: 3,
|
||||
delaySeconds: 0,
|
||||
failed: true, // Special flag to indicate retries exhausted
|
||||
errorMessage: noResponseErrorMessage,
|
||||
}),
|
||||
)
|
||||
const askResult = await this.ask(
|
||||
"api_req_failed",
|
||||
"No assistant message was received. Would you like to retry the request?",
|
||||
)
|
||||
const askResult = await this.ask("api_req_failed", noResponseErrorMessage)
|
||||
response = askResult.response
|
||||
// Reset retry counter if user chooses to manually retry
|
||||
if (response === "yesButtonClicked") {
|
||||
|
||||
@@ -39,6 +39,18 @@ export class ActModeRespondHandler implements IToolHandler, IPartialBlockHandler
|
||||
)
|
||||
}
|
||||
|
||||
// Block consecutive act_mode_respond calls to prevent narration loops
|
||||
// Note: We intentionally do NOT increment consecutiveMistakeCount here to avoid
|
||||
// breaking the conversation flow - we just guide the model to use proper tools
|
||||
if (config.taskState.lastToolName === ClineDefaultTool.ACT_MODE) {
|
||||
return formatResponse.toolResult(
|
||||
`[BLOCKED] You cannot call act_mode_respond consecutively. ` +
|
||||
`Your next action MUST be a different tool that performs actual work: ` +
|
||||
`read_file, replace_in_file, write_to_file, execute_command, list_files, search_files, etc. ` +
|
||||
`Stop explaining and start doing.`,
|
||||
)
|
||||
}
|
||||
|
||||
// Validate required parameters
|
||||
if (!response) {
|
||||
config.taskState.consecutiveMistakeCount++
|
||||
@@ -56,8 +68,15 @@ export class ActModeRespondHandler implements IToolHandler, IPartialBlockHandler
|
||||
await config.callbacks.updateFCListFromToolResponse(taskProgress)
|
||||
}
|
||||
|
||||
// Note: lastToolName is tracked centrally by ToolExecutor after tool execution
|
||||
|
||||
// Return success immediately to allow LLM to continue execution
|
||||
// The key difference from plan_mode_respond: no blocking for user input
|
||||
return formatResponse.toolResult(`[Message displayed to user. You may now proceed with the next steps.]`)
|
||||
// NOTE: We explicitly tell the model to use a different tool next to prevent narration loops
|
||||
return formatResponse.toolResult(
|
||||
`[Message displayed. Now proceed with your next tool call - ` +
|
||||
`it must be a different tool (read_file, replace_in_file, execute_command, etc.), ` +
|
||||
`not act_mode_respond again.]`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ToolUse } from "@core/assistant-message"
|
||||
import { discoverSkills, getAvailableSkills, getSkillContent } from "@core/context/instructions/user-instructions/skills"
|
||||
import type { SkillMetadata } from "@shared/skills"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
import type { ToolResponse } from "../../index"
|
||||
import type { IPartialBlockHandler, IToolHandler } from "../ToolExecutorCoordinator"
|
||||
@@ -48,6 +49,13 @@ export class UseSkillToolHandler implements IToolHandler, IPartialBlockHandler {
|
||||
return `Error: No skills are available. Skills may be disabled or not configured.`
|
||||
}
|
||||
|
||||
const globalCount = availableSkills.filter((skill) => skill.source === "global").length
|
||||
const projectCount = availableSkills.filter((skill) => skill.source === "project").length
|
||||
|
||||
const apiConfig = config.services.stateManager.getApiConfiguration()
|
||||
const currentMode = config.services.stateManager.getGlobalSettingsKey("mode")
|
||||
const provider = currentMode === "plan" ? apiConfig.planModeApiProvider : apiConfig.actModeApiProvider
|
||||
|
||||
// Show tool message
|
||||
const message = JSON.stringify({ tool: "useSkill", path: skillName })
|
||||
await config.callbacks.say("tool", message, undefined, undefined, false)
|
||||
@@ -62,6 +70,20 @@ export class UseSkillToolHandler implements IToolHandler, IPartialBlockHandler {
|
||||
return `Error: Skill "${skillName}" not found. Available skills: ${availableNames || "none"}`
|
||||
}
|
||||
|
||||
telemetryService.safeCapture(
|
||||
() =>
|
||||
telemetryService.captureSkillUsed({
|
||||
ulid: config.ulid,
|
||||
skillName,
|
||||
skillSource: skillContent.source === "global" ? "global" : "project",
|
||||
skillsAvailableGlobal: globalCount,
|
||||
skillsAvailableProject: projectCount,
|
||||
provider,
|
||||
modelId: config.api.getModel().id,
|
||||
}),
|
||||
"UseSkillToolHandler.execute",
|
||||
)
|
||||
|
||||
return `# Skill "${skillContent.name}" is now active
|
||||
|
||||
${skillContent.instructions}
|
||||
|
||||
@@ -373,7 +373,6 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
|
||||
block,
|
||||
config.taskState.userMessageContent,
|
||||
ToolDisplayUtils.getToolDescription,
|
||||
config.api,
|
||||
config.coordinator,
|
||||
config.taskState.toolUseIdMap,
|
||||
)
|
||||
@@ -449,7 +448,6 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
|
||||
block,
|
||||
config.taskState.userMessageContent,
|
||||
ToolDisplayUtils.getToolDescription,
|
||||
config.api,
|
||||
config.coordinator,
|
||||
config.taskState.toolUseIdMap,
|
||||
)
|
||||
|
||||
@@ -84,7 +84,14 @@ export interface TaskServices {
|
||||
* All callback functions available to tool handlers
|
||||
*/
|
||||
export interface TaskCallbacks {
|
||||
say: (type: ClineSay, text?: string, images?: string[], files?: string[], partial?: boolean) => Promise<number | undefined>
|
||||
say: (
|
||||
type: ClineSay,
|
||||
text?: string,
|
||||
images?: string[],
|
||||
files?: string[],
|
||||
partial?: boolean,
|
||||
call_id?: string,
|
||||
) => Promise<number | undefined>
|
||||
|
||||
ask: (
|
||||
type: ClineAsk,
|
||||
@@ -101,7 +108,11 @@ export interface TaskCallbacks {
|
||||
|
||||
sayAndCreateMissingParamError: (toolName: ClineDefaultTool, paramName: string, relPath?: string) => Promise<any>
|
||||
|
||||
removeLastPartialMessageIfExistsWithType: (type: "ask" | "say", askOrSay: ClineAsk | ClineSay) => Promise<void>
|
||||
removeLastPartialMessageIfExistsWithType: (
|
||||
type: "ask" | "say",
|
||||
askOrSay: ClineAsk | ClineSay,
|
||||
call_id?: string,
|
||||
) => Promise<void>
|
||||
|
||||
executeCommandTool: (command: string, timeoutSeconds: number | undefined) => Promise<[boolean, any]>
|
||||
|
||||
|
||||
@@ -12,7 +12,14 @@ import type { TaskConfig } from "./TaskConfig"
|
||||
*/
|
||||
export interface StronglyTypedUIHelpers {
|
||||
// Core UI methods
|
||||
say: (type: ClineSay, text?: string, images?: string[], files?: string[], partial?: boolean) => Promise<number | undefined>
|
||||
say: (
|
||||
type: ClineSay,
|
||||
text?: string,
|
||||
images?: string[],
|
||||
files?: string[],
|
||||
partial?: boolean,
|
||||
call_id?: string,
|
||||
) => Promise<number | undefined>
|
||||
|
||||
ask: (
|
||||
type: ClineAsk,
|
||||
@@ -27,7 +34,11 @@ export interface StronglyTypedUIHelpers {
|
||||
|
||||
// Utility methods
|
||||
removeClosingTag: (block: ToolUse, tag: ToolParamName, text?: string) => string
|
||||
removeLastPartialMessageIfExistsWithType: (type: "ask" | "say", askOrSay: ClineAsk | ClineSay) => Promise<void>
|
||||
removeLastPartialMessageIfExistsWithType: (
|
||||
type: "ask" | "say",
|
||||
askOrSay: ClineAsk | ClineSay,
|
||||
call_id?: string,
|
||||
) => Promise<void>
|
||||
|
||||
// Approval methods
|
||||
shouldAutoApproveTool: (toolName: ClineDefaultTool) => boolean | [boolean, boolean]
|
||||
@@ -45,12 +56,14 @@ export interface StronglyTypedUIHelpers {
|
||||
/**
|
||||
* Creates strongly-typed UI helpers from a TaskConfig
|
||||
*/
|
||||
export function createUIHelpers(config: TaskConfig): StronglyTypedUIHelpers {
|
||||
export function createUIHelpers(config: TaskConfig, callId?: string): StronglyTypedUIHelpers {
|
||||
return {
|
||||
say: config.callbacks.say,
|
||||
say: (type: ClineSay, text?: string, images?: string[], files?: string[], partial?: boolean, call_id?: string) =>
|
||||
config.callbacks.say(type, text, images, files, partial, call_id ?? callId),
|
||||
ask: config.callbacks.ask,
|
||||
removeClosingTag: (block: ToolUse, tag: ToolParamName, text?: string) => removeClosingTag(block, tag, text),
|
||||
removeLastPartialMessageIfExistsWithType: config.callbacks.removeLastPartialMessageIfExistsWithType,
|
||||
removeLastPartialMessageIfExistsWithType: (type: "ask" | "say", askOrSay: ClineAsk | ClineSay, call_id?: string) =>
|
||||
config.callbacks.removeLastPartialMessageIfExistsWithType(type, askOrSay, call_id ?? callId),
|
||||
shouldAutoApproveTool: (toolName: ClineDefaultTool) => config.autoApprover.shouldAutoApproveTool(toolName),
|
||||
shouldAutoApproveToolWithPath: config.callbacks.shouldAutoApproveToolWithPath,
|
||||
askApproval: async (messageType: ClineAsk, message: string): Promise<boolean> => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ApiHandler } from "@core/api"
|
||||
import { ToolUse } from "@core/assistant-message"
|
||||
import { formatResponse } from "@core/prompts/responses"
|
||||
import { ToolResponse } from "@core/task"
|
||||
@@ -20,7 +19,6 @@ export class ToolResultUtils {
|
||||
block: ToolUse,
|
||||
userMessageContent: any[],
|
||||
toolDescription: (block: ToolUse) => string,
|
||||
_api: ApiHandler,
|
||||
coordinator?: ToolExecutorCoordinator,
|
||||
toolUseIdMap?: Map<string, string>,
|
||||
): void {
|
||||
|
||||
@@ -21,6 +21,7 @@ import { telemetryService } from "@services/telemetry"
|
||||
import { findLastIndex } from "@shared/array"
|
||||
import { ClineToolResponseContent } from "@shared/messages"
|
||||
import { orchestrateCommandExecution } from "./CommandOrchestrator"
|
||||
import { orchestrateConcurrentCommandExecution } from "./ConcurrentCommandOrchestrator"
|
||||
import { StandaloneTerminalManager } from "./standalone/StandaloneTerminalManager"
|
||||
import type {
|
||||
CommandExecutorCallbacks,
|
||||
@@ -51,6 +52,9 @@ export class CommandExecutor {
|
||||
// Flag to track if the current command was cancelled externally
|
||||
private wasCancelledExternally = false
|
||||
|
||||
// Track if we're currently in parallel execution mode
|
||||
private isParallelExecution = false
|
||||
|
||||
// Track shell integration warnings to determine when to show background terminal suggestion
|
||||
private shellIntegrationWarningTracker: ShellIntegrationWarningTracker = {
|
||||
timestamps: [],
|
||||
@@ -89,6 +93,14 @@ export class CommandExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether we're in parallel execution mode.
|
||||
* In parallel mode, commands don't use ask() to wait for user input on each output chunk.
|
||||
*/
|
||||
setParallelExecution(isParallel: boolean): void {
|
||||
this.isParallelExecution = isParallel
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a command in the terminal.
|
||||
*
|
||||
@@ -97,6 +109,9 @@ export class CommandExecutor {
|
||||
* This ensures subagents run in hidden terminals, not cluttering the user's VSCode terminal
|
||||
* 2. Regular commands → Use the configured terminal manager based on terminalExecutionMode
|
||||
*
|
||||
* In parallel execution mode, uses ConcurrentCommandOrchestrator which streams output
|
||||
* via say() instead of ask(), avoiding conflicts with multiple concurrent commands.
|
||||
*
|
||||
* @param command The command to execute
|
||||
* @param timeoutSeconds Optional timeout in seconds
|
||||
* @returns [userRejected, result] tuple
|
||||
@@ -136,20 +151,26 @@ export class CommandExecutor {
|
||||
process.once("completed", clearCurrentProcess)
|
||||
process.once("error", clearCurrentProcess)
|
||||
|
||||
// Choose orchestrator based on execution mode
|
||||
// In parallel mode, use ConcurrentCommandOrchestrator to avoid ask() conflicts
|
||||
const orchestrator = this.isParallelExecution ? orchestrateConcurrentCommandExecution : orchestrateCommandExecution
|
||||
|
||||
// Use shared orchestration logic
|
||||
// The StandaloneTerminalManager handles background command tracking internally
|
||||
const result = await orchestrateCommandExecution(process, manager, this.callbacks, {
|
||||
const result = await orchestrator(process, manager, this.callbacks, {
|
||||
command,
|
||||
timeoutSeconds,
|
||||
// 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,
|
||||
// (Not used in concurrent mode, but kept for compatibility)
|
||||
onProceedWhileRunning:
|
||||
useStandalone && !this.isParallelExecution
|
||||
? (existingOutput: string[]) => {
|
||||
const backgroundCmd = this.standaloneManager.trackBackgroundCommand(process, command, existingOutput)
|
||||
return { logFilePath: backgroundCmd.logFilePath }
|
||||
}
|
||||
: undefined,
|
||||
showShellIntegrationSuggestion: this.shouldShowBackgroundTerminalSuggestion(),
|
||||
terminalType: useStandalone ? "standalone" : "vscode",
|
||||
})
|
||||
|
||||
@@ -198,8 +198,16 @@ export async function orchestrateCommandExecution(
|
||||
await flushBuffer()
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Logger.error("Error while asking for command output")
|
||||
} catch (error) {
|
||||
// Handle ask being interrupted by concurrent commands
|
||||
// When multiple commands run in parallel, some asks may be ignored
|
||||
// This is expected behavior - just proceed with execution
|
||||
if (error instanceof Error && error.message.includes("ask promise was ignored")) {
|
||||
// Silently proceed - this command's ask was superseded by another concurrent command
|
||||
didContinue = true
|
||||
} else {
|
||||
Logger.error("Error while asking for command output", error)
|
||||
}
|
||||
} finally {
|
||||
// Clear the stuck timer
|
||||
if (bufferStuckTimer) {
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* ConcurrentCommandOrchestrator - Orchestration for parallel command execution.
|
||||
*
|
||||
* Unlike CommandOrchestrator which is designed for single commands,
|
||||
* this orchestrator handles multiple commands running in parallel without
|
||||
* calling ask() for each one (which would cause conflicts when parallel commands
|
||||
* try to ask at the same time).
|
||||
*
|
||||
* Key differences from CommandOrchestrator:
|
||||
* - Does NOT call ask() - output is delivered via say() directly
|
||||
* - Buffers output and streams it after command completion
|
||||
* - No "Proceed While Running" button (not needed for parallel execution)
|
||||
* - Handles concurrent output from multiple commands safely
|
||||
*/
|
||||
|
||||
import { Logger } from "@services/logging/Logger"
|
||||
import * as fs from "fs"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import {
|
||||
CHUNK_BYTE_SIZE,
|
||||
CHUNK_DEBOUNCE_MS,
|
||||
CHUNK_LINE_COUNT,
|
||||
COMPLETION_TIMEOUT_MS,
|
||||
MAX_BYTES_BEFORE_FILE,
|
||||
MAX_LINES_BEFORE_FILE,
|
||||
} from "./constants"
|
||||
import type {
|
||||
CommandExecutorCallbacks,
|
||||
ITerminalManager,
|
||||
OrchestrationOptions,
|
||||
OrchestrationResult,
|
||||
TerminalProcessResultPromise,
|
||||
} from "./types"
|
||||
|
||||
/**
|
||||
* Orchestrate concurrent command execution without interactive asks.
|
||||
* Multiple commands can run in parallel without trying to ask for each one's output.
|
||||
*/
|
||||
export async function orchestrateConcurrentCommandExecution(
|
||||
process: TerminalProcessResultPromise,
|
||||
terminalManager: ITerminalManager,
|
||||
callbacks: CommandExecutorCallbacks,
|
||||
options: OrchestrationOptions,
|
||||
): Promise<OrchestrationResult> {
|
||||
const { timeoutSeconds, onOutputLine, terminalType = "vscode" } = options
|
||||
|
||||
// Track command execution state
|
||||
callbacks.updateBackgroundCommandState(true)
|
||||
|
||||
const clearCommandState = async () => {
|
||||
callbacks.updateBackgroundCommandState(false)
|
||||
|
||||
// Mark the command message as completed
|
||||
const clineMessages = callbacks.getClineMessages()
|
||||
const findLastIndex = (arr: any[], predicate: (item: any) => boolean) => {
|
||||
for (let i = arr.length - 1; i >= 0; i--) {
|
||||
if (predicate(arr[i])) return i
|
||||
}
|
||||
return -1
|
||||
}
|
||||
const lastCommandIndex = findLastIndex(clineMessages, (m) => m.ask === "command" || m.say === "command")
|
||||
if (lastCommandIndex !== -1) {
|
||||
await callbacks.updateClineMessage(lastCommandIndex, {
|
||||
commandCompleted: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
process.once("completed", clearCommandState)
|
||||
process.once("error", clearCommandState)
|
||||
process.catch(() => {
|
||||
clearCommandState()
|
||||
})
|
||||
|
||||
// Accumulated output lines
|
||||
const outputLines: string[] = []
|
||||
let outputBuffer: string[] = []
|
||||
let outputBufferSize: number = 0
|
||||
let chunkTimer: NodeJS.Timeout | null = null
|
||||
let completionTimer: NodeJS.Timeout | null = null
|
||||
|
||||
// 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
|
||||
|
||||
const scheduleFlush = () => {
|
||||
if (chunkTimer) {
|
||||
clearTimeout(chunkTimer)
|
||||
}
|
||||
chunkTimer = setTimeout(async () => await flushBuffer(), CHUNK_DEBOUNCE_MS)
|
||||
}
|
||||
|
||||
const flushBuffer = async (force = false) => {
|
||||
if (outputBuffer.length === 0 && !force) {
|
||||
return
|
||||
}
|
||||
|
||||
const chunk = outputBuffer.join("\n")
|
||||
outputBuffer = []
|
||||
outputBufferSize = 0
|
||||
|
||||
if (chunk) {
|
||||
// In concurrent mode, we use say() directly without ask()
|
||||
// This avoids conflicts when multiple commands output simultaneously
|
||||
await callbacks.say("command_output", chunk)
|
||||
}
|
||||
}
|
||||
|
||||
const switchToFileBased = async () => {
|
||||
if (isWritingToFile) return
|
||||
|
||||
isWritingToFile = true
|
||||
|
||||
// Flush any pending buffer to UI
|
||||
if (outputBuffer.length > 0) {
|
||||
const chunk = outputBuffer.join("\n")
|
||||
outputBuffer = []
|
||||
outputBufferSize = 0
|
||||
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
|
||||
if (outputLines.length > 0) {
|
||||
largeOutputLogStream.write(outputLines.join("\n") + "\n")
|
||||
}
|
||||
|
||||
// Notify user
|
||||
await callbacks.say(
|
||||
"command_output",
|
||||
`\n📋 Output is large (${outputLines.length} lines, ${Math.round(totalOutputBytes / 1024)}KB). Writing to: ${largeOutputLogPath}`,
|
||||
)
|
||||
}
|
||||
|
||||
const processLine = async (line: string) => {
|
||||
outputLines.push(line)
|
||||
totalLineCount++
|
||||
totalOutputBytes += line.length + 1
|
||||
|
||||
// Check if we need to switch to file-based logging
|
||||
if (totalLineCount > MAX_LINES_BEFORE_FILE || totalOutputBytes > MAX_BYTES_BEFORE_FILE) {
|
||||
await switchToFileBased()
|
||||
}
|
||||
|
||||
// If file-based logging is enabled, write to file
|
||||
if (isWritingToFile && largeOutputLogStream) {
|
||||
largeOutputLogStream.write(line + "\n")
|
||||
} else {
|
||||
// Otherwise buffer for UI delivery
|
||||
outputBuffer.push(line)
|
||||
outputBufferSize += line.length + 1
|
||||
|
||||
// Flush when buffer is full
|
||||
if (outputBuffer.length >= CHUNK_LINE_COUNT || outputBufferSize >= CHUNK_BYTE_SIZE) {
|
||||
if (chunkTimer) {
|
||||
clearTimeout(chunkTimer)
|
||||
chunkTimer = null
|
||||
}
|
||||
await flushBuffer()
|
||||
} else {
|
||||
scheduleFlush()
|
||||
}
|
||||
}
|
||||
|
||||
// Call the line output handler if provided
|
||||
if (onOutputLine) {
|
||||
onOutputLine(line)
|
||||
}
|
||||
}
|
||||
|
||||
const completionHandler = async () => {
|
||||
// Clear timers
|
||||
if (chunkTimer) {
|
||||
clearTimeout(chunkTimer)
|
||||
chunkTimer = null
|
||||
}
|
||||
if (completionTimer) {
|
||||
clearTimeout(completionTimer)
|
||||
completionTimer = null
|
||||
}
|
||||
|
||||
// Final flush
|
||||
await flushBuffer(true)
|
||||
|
||||
// Close file stream if open
|
||||
if (largeOutputLogStream) {
|
||||
largeOutputLogStream.end()
|
||||
largeOutputLogStream = null
|
||||
}
|
||||
}
|
||||
|
||||
// Set up completion timeout
|
||||
completionTimer = setTimeout(async () => {
|
||||
await completionHandler()
|
||||
}, COMPLETION_TIMEOUT_MS)
|
||||
|
||||
try {
|
||||
// Listen for output lines
|
||||
process.on("line", async (line: string) => {
|
||||
await processLine(line)
|
||||
})
|
||||
|
||||
// Wait for process to complete
|
||||
const result = await process
|
||||
|
||||
await completionHandler()
|
||||
|
||||
// Process final output
|
||||
const terminalOutput = terminalManager.processOutput(outputLines)
|
||||
|
||||
return {
|
||||
userRejected: false,
|
||||
result: terminalOutput,
|
||||
completed: true,
|
||||
outputLines,
|
||||
}
|
||||
} catch (error) {
|
||||
await completionHandler()
|
||||
|
||||
if (error instanceof Error) {
|
||||
Logger.error(`Concurrent command execution error: ${error.message}`)
|
||||
if (largeOutputLogPath) {
|
||||
return {
|
||||
userRejected: false,
|
||||
result: `Error: ${error.message}\n\nOutput was logged to: ${largeOutputLogPath}`,
|
||||
completed: true,
|
||||
outputLines,
|
||||
}
|
||||
}
|
||||
return {
|
||||
userRejected: false,
|
||||
result: `Error: ${error.message}`,
|
||||
completed: true,
|
||||
outputLines,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
userRejected: false,
|
||||
result: "Unknown error occurred",
|
||||
completed: true,
|
||||
outputLines,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ export const ClineHeaders = {
|
||||
} as const
|
||||
export type ClineHeaderName = (typeof ClineHeaders)[keyof typeof ClineHeaders]
|
||||
|
||||
export async function buildClineExtraHeaders(): Promise<Record<string, string>> {
|
||||
export async function buildBasicClineHeaders(): Promise<Record<string, string>> {
|
||||
const headers: Record<string, string> = {}
|
||||
try {
|
||||
const host = await HostProvider.env.getHostVersion(EmptyRequest.create({}))
|
||||
@@ -31,6 +31,12 @@ export async function buildClineExtraHeaders(): Promise<Record<string, string>>
|
||||
}
|
||||
headers[ClineHeaders.CORE_VERSION] = ExtensionRegistryInfo.version
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
export async function buildClineExtraHeaders(): Promise<Record<string, string>> {
|
||||
const headers = await buildBasicClineHeaders()
|
||||
|
||||
try {
|
||||
const isMultiRoot = await isMultiRootWorkspace()
|
||||
headers[ClineHeaders.IS_MULTIROOT] = isMultiRoot ? "true" : "false"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user