mirror of
https://github.com/cline/cline.git
synced 2026-09-15 04:14:34 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b145f1331a | ||
|
|
5a31d02da4 | ||
|
|
7634f22104 | ||
|
|
fbf784f78b | ||
|
|
62bf50a659 | ||
|
|
d850fbc0ad | ||
|
|
4dd6c6dcc7 | ||
|
|
8813f8252c | ||
|
|
3210c4bc4b | ||
|
|
9f3daa4151 | ||
|
|
ac2db41815 | ||
|
|
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 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add claude 4.5 haiku
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix workflow slash command search to be case-insensitive
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix context menu not closing when pressing Escape key by calling setShowContextMenu(false).
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Log Persistence errors to PostHog
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fixed crash when OpenAI-compatible APIs send usage chunks with empty or null choices arrays at end of streaming
|
||||
@@ -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
|
||||
---
|
||||
|
||||
Update Claude 3.5 Haiku model to support image processing as per Anthropic API release notes (Feb 24, 2025).
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: add chat output on skill use
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Remove invalid pop-up message about storage failure
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: removes retry message from UI after retry succeeds
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
+36
-2
@@ -1,5 +1,40 @@
|
||||
# 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
|
||||
@@ -7,7 +42,6 @@
|
||||
- Improved image display in MCP responses
|
||||
- Auto-sync remote MCP servers from remote config to local settings
|
||||
|
||||
|
||||
## [3.48.0]
|
||||
|
||||
### Added
|
||||
@@ -1703,4 +1737,4 @@ Add Opus 4.1 through Claude Code
|
||||
|
||||
## [0.0.6]
|
||||
|
||||
- Initial release
|
||||
- Initial release
|
||||
@@ -48,6 +48,23 @@ The extension and webview communicate via gRPC-like protocol over VS Code messag
|
||||
- `src/core/controller/task/explainChanges.ts` - Handler implementation
|
||||
- `webview-ui/src/components/chat/ChatRow.tsx` - UI rendering
|
||||
|
||||
## Adding a New API Provider
|
||||
When adding a new provider (e.g., "openai-codex"), you must update the proto conversion layer in THREE places or the provider will silently reset to Anthropic:
|
||||
|
||||
1. `proto/cline/models.proto` - Add to the `ApiProvider` enum (e.g., `OPENAI_CODEX = 40;`)
|
||||
2. `convertApiProviderToProto()` in `src/shared/proto-conversions/models/api-configuration-conversion.ts` - Add case mapping string to proto enum
|
||||
3. `convertProtoToApiProvider()` in the same file - Add case mapping proto enum back to string
|
||||
|
||||
**Why this matters:** Without these, the provider string hits the `default` case and returns `ANTHROPIC`. The webview, provider list, and handler all work fine, but the state silently resets when it round-trips through proto serialization. No error is thrown.
|
||||
|
||||
**Other files to update when adding a provider:**
|
||||
- `src/shared/api.ts` - Add to `ApiProvider` union type, define models
|
||||
- `src/shared/providers/providers.json` - Add to provider list for dropdown
|
||||
- `src/core/api/index.ts` - Register handler in `createHandlerForProvider()`
|
||||
- `webview-ui/src/components/settings/utils/providerUtils.ts` - Add cases in `getModelsForProvider()` and `normalizeApiConfiguration()`
|
||||
- `webview-ui/src/utils/validate.ts` - Add validation case
|
||||
- `webview-ui/src/components/settings/ApiOptions.tsx` - Render provider component
|
||||
|
||||
## Adding Tools to System Prompt
|
||||
This is tricky—multiple prompt variants and configs. **Always search for existing similar tools first and follow their pattern.** Look at the full chain from prompt definition → variant configs → handler → UI before implementing.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -48,7 +48,7 @@ These labels match what you see in the Auto Approve menu.
|
||||
| Execute all commands | Run commands marked as requiring approval | Requires “Execute safe commands” |
|
||||
| Use the browser | Allows use of the browser tool for web fetching and searching | Proxy issues can apply |
|
||||
| Use MCP servers | Use MCP tools and access MCP resources | Some servers also have per-tool auto-approve |
|
||||
| Enable notifications | Notifies you about long-running auto-approved commands | Helpful for terminal work |
|
||||
| Enable notifications | Notifies you about long-running auto-approved commands | Accessible directly in the Auto Approve menu |
|
||||
|
||||
<Warning>
|
||||
“Read all files” and “Edit all files” only matter if their base toggle is enabled. They extend access outside your workspace.
|
||||
@@ -92,6 +92,9 @@ These are examples, not guarantees.
|
||||
|
||||
Auto-approved actions can run for a while, especially long terminal commands. If you enable notifications, Cline can notify you when an auto-approved command has been running for a while and may need attention.
|
||||
|
||||
The **Enable notifications** toggle is located at the bottom of the Auto Approve menu, below a separator line. This puts the notification setting right where you manage your auto-approval permissions, making it easy to discover and adjust.
|
||||
|
||||
|
||||
## Recommendations
|
||||
|
||||
A good default setup is:
|
||||
|
||||
@@ -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
+95
-84
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.49.0",
|
||||
"version": "3.51.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.49.0",
|
||||
"version": "3.51.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
@@ -84,6 +84,7 @@
|
||||
"p-timeout": "^6.1.4",
|
||||
"p-wait-for": "^5.0.2",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"picomatch": "^4.0.3",
|
||||
"posthog-node": "^5.8.0",
|
||||
"puppeteer-chromium-resolver": "^23.0.0",
|
||||
"puppeteer-core": "^23.4.0",
|
||||
@@ -1182,6 +1183,7 @@
|
||||
"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",
|
||||
@@ -2644,6 +2646,7 @@
|
||||
"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"
|
||||
@@ -3227,6 +3230,7 @@
|
||||
"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",
|
||||
@@ -3295,6 +3299,7 @@
|
||||
"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"
|
||||
}
|
||||
@@ -4910,8 +4915,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4924,8 +4928,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4938,8 +4941,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4952,8 +4954,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4966,8 +4967,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4980,8 +4980,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.52.4",
|
||||
@@ -4994,8 +4993,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.52.4",
|
||||
@@ -5008,8 +5006,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5022,8 +5019,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.52.4",
|
||||
@@ -5036,8 +5032,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5050,8 +5045,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5064,8 +5058,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5078,8 +5071,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.52.4",
|
||||
@@ -5092,8 +5084,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5106,8 +5097,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5120,8 +5110,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.52.4",
|
||||
@@ -5134,8 +5123,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -5148,8 +5136,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.52.4",
|
||||
@@ -5162,8 +5149,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.52.4",
|
||||
@@ -5176,8 +5162,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5190,8 +5175,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.52.4",
|
||||
@@ -5204,8 +5188,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@sap-ai-sdk/ai-api": {
|
||||
"version": "2.1.0",
|
||||
@@ -6768,8 +6751,7 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/get-folder-size": {
|
||||
"version": "3.0.4",
|
||||
@@ -6801,6 +6783,7 @@
|
||||
"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"
|
||||
}
|
||||
@@ -7006,6 +6989,19 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/test-cli/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/test-cli/node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"dev": true,
|
||||
@@ -7504,6 +7500,7 @@
|
||||
"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"
|
||||
},
|
||||
@@ -7651,6 +7648,19 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/append-transform": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz",
|
||||
@@ -8270,6 +8280,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.3",
|
||||
"caniuse-lite": "^1.0.30001741",
|
||||
@@ -9483,7 +9494,8 @@
|
||||
},
|
||||
"node_modules/devtools-protocol": {
|
||||
"version": "0.0.1342118",
|
||||
"license": "BSD-3-Clause"
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "5.2.0",
|
||||
@@ -12459,6 +12471,7 @@
|
||||
"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"
|
||||
}
|
||||
@@ -12692,6 +12705,7 @@
|
||||
"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"
|
||||
},
|
||||
@@ -13568,6 +13582,18 @@
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"dev": true,
|
||||
@@ -13760,6 +13786,19 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/mocha/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/mocha/node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"dev": true,
|
||||
@@ -15389,10 +15428,13 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
@@ -15572,7 +15614,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -15593,7 +15634,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
@@ -16261,7 +16301,6 @@
|
||||
"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"
|
||||
},
|
||||
@@ -17687,7 +17726,6 @@
|
||||
"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"
|
||||
@@ -17704,7 +17742,6 @@
|
||||
"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"
|
||||
},
|
||||
@@ -17717,19 +17754,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
|
||||
@@ -18068,6 +18092,7 @@
|
||||
"version": "5.5.3",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -18327,7 +18352,6 @@
|
||||
"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",
|
||||
@@ -18402,7 +18426,6 @@
|
||||
"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"
|
||||
},
|
||||
@@ -18415,19 +18438,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/voca": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/voca/-/voca-1.4.1.tgz",
|
||||
@@ -19098,6 +19108,7 @@
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
+6
-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.49.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"
|
||||
]
|
||||
@@ -528,6 +532,7 @@
|
||||
"p-timeout": "^6.1.4",
|
||||
"p-wait-for": "^5.0.2",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"picomatch": "^4.0.3",
|
||||
"posthog-node": "^5.8.0",
|
||||
"puppeteer-chromium-resolver": "^23.0.0",
|
||||
"puppeteer-core": "^23.4.0",
|
||||
|
||||
@@ -44,28 +44,6 @@ service AccountService {
|
||||
|
||||
// Returns a link the webview can use to redirect back to the user's IDE.
|
||||
rpc getRedirectUrl(EmptyRequest) returns (String);
|
||||
|
||||
// OpenAI Codex OAuth - Sign in with ChatGPT account
|
||||
rpc codexSignIn(EmptyRequest) returns (CodexAuthResult);
|
||||
|
||||
// OpenAI Codex OAuth - Sign out
|
||||
rpc codexSignOut(EmptyRequest) returns (Empty);
|
||||
|
||||
// Subscribe to Codex auth state changes
|
||||
rpc subscribeToCodexAuthState(EmptyRequest) returns (stream CodexAuthState);
|
||||
}
|
||||
|
||||
// Codex OAuth authentication result
|
||||
message CodexAuthResult {
|
||||
bool success = 1;
|
||||
optional string error = 2;
|
||||
optional string email = 3;
|
||||
}
|
||||
|
||||
// Codex authentication state
|
||||
message CodexAuthState {
|
||||
bool authenticated = 1;
|
||||
optional string email = 2;
|
||||
}
|
||||
|
||||
message AuthStateChangedRequest {
|
||||
|
||||
@@ -183,9 +183,6 @@ message ModelsApiSecrets {
|
||||
optional string oca_refresh_token = 37;
|
||||
optional string minimax_api_key = 38;
|
||||
optional string aihubmix_api_key = 39;
|
||||
optional string openai_codex_access_token = 40;
|
||||
optional string openai_codex_refresh_token = 41;
|
||||
optional string openai_codex_account_id = 42;
|
||||
}
|
||||
|
||||
// API configuration options (non-secret settings)
|
||||
@@ -442,7 +439,6 @@ enum ApiProvider {
|
||||
HICAP = 37;
|
||||
AIHUBMIX = 38;
|
||||
NOUSRESEARCH = 39;
|
||||
OPENAI_CODEX = 40;
|
||||
}
|
||||
|
||||
enum ApiFormat {
|
||||
@@ -579,10 +575,6 @@ message ModelsApiConfiguration {
|
||||
optional string aihubmix_app_code = 84;
|
||||
optional string nous_research_api_key = 85;
|
||||
optional bool azure_identity = 86;
|
||||
optional string openai_codex_access_token = 87;
|
||||
optional string openai_codex_refresh_token = 88;
|
||||
optional string openai_codex_account_id = 89;
|
||||
optional int64 openai_codex_token_expiry = 90;
|
||||
|
||||
// Plan mode configurations
|
||||
optional ApiProvider plan_mode_api_provider = 100;
|
||||
|
||||
+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)
|
||||
})
|
||||
@@ -22,19 +22,34 @@ echo ""
|
||||
# Always rebuild CLI to ensure latest changes
|
||||
echo -e "${CYAN}→${NC} ${DIM}Rebuilding CLI binaries...${NC}"
|
||||
cd "$PROJECT_ROOT"
|
||||
if npm run compile-cli 2>&1 | grep -E "(built|error|Error)" || true; then
|
||||
rm -rf "$PROJECT_ROOT/cli/bin"
|
||||
if command -v go >/dev/null 2>&1; then
|
||||
GO_BIN_DIR="$(go env GOPATH 2>/dev/null)/bin"
|
||||
if [ -d "$GO_BIN_DIR" ]; then
|
||||
export PATH="$GO_BIN_DIR:$PATH"
|
||||
fi
|
||||
fi
|
||||
if npm run compile-cli; then
|
||||
echo -e "${GREEN}✓${NC} CLI binaries rebuilt"
|
||||
else
|
||||
echo -e "${YELLOW}⚠${NC} CLI build may have issues - check output above"
|
||||
echo -e "${YELLOW}⚠${NC} CLI build failed - aborting install"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Always rebuild standalone to ensure latest cline-core.js
|
||||
echo -e "${CYAN}→${NC} ${DIM}Rebuilding standalone package (this may take ~30 seconds)...${NC}"
|
||||
if npm run compile-standalone 2>&1 | tail -5; then
|
||||
rm -rf "$PROJECT_ROOT/dist-standalone"
|
||||
if npm run compile-standalone; then
|
||||
echo -e "${GREEN}✓${NC} Standalone package rebuilt"
|
||||
else
|
||||
echo -e "${YELLOW}⚠${NC} Standalone build may have issues - check output above"
|
||||
echo -e "${YELLOW}⚠${NC} Standalone build failed - aborting install"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ensure extension package.json is present for cline-core startup
|
||||
mkdir -p "$PROJECT_ROOT/dist-standalone/extension"
|
||||
cp "$PROJECT_ROOT/package.json" "$PROJECT_ROOT/dist-standalone/extension/package.json"
|
||||
|
||||
echo ""
|
||||
|
||||
echo -e "${CYAN}→${NC} ${DIM}Installing to $INSTALL_DIR${NC}"
|
||||
@@ -57,6 +72,7 @@ rsync -a --exclude='bin' "$PROJECT_ROOT/dist-standalone/" "$INSTALL_DIR/"
|
||||
echo -e "${CYAN}→${NC} ${DIM}Installing runtime dependencies...${NC}"
|
||||
cd "$PROJECT_ROOT/standalone/runtime-files"
|
||||
npm install --silent 2>/dev/null || npm install
|
||||
rm -rf "$INSTALL_DIR/node_modules"
|
||||
cp -r node_modules "$INSTALL_DIR/"
|
||||
cp -r vscode "$INSTALL_DIR/node_modules/"
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ import { NousResearchHandler } from "./providers/nousresearch"
|
||||
import { OcaHandler } from "./providers/oca"
|
||||
import { OllamaHandler } from "./providers/ollama"
|
||||
import { OpenAiHandler } from "./providers/openai"
|
||||
import { OpenAiCodexHandler } from "./providers/openai-codex"
|
||||
import { OpenAiNativeHandler } from "./providers/openai-native"
|
||||
import { OpenRouterHandler } from "./providers/openrouter"
|
||||
import { QwenHandler } from "./providers/qwen"
|
||||
@@ -185,16 +184,6 @@ function createHandlerForProvider(
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
})
|
||||
case "openai-codex":
|
||||
return new OpenAiCodexHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
openAiCodexAccessToken: options.openAiCodexAccessToken,
|
||||
openAiCodexRefreshToken: options.openAiCodexRefreshToken,
|
||||
openAiCodexAccountId: options.openAiCodexAccountId,
|
||||
openAiCodexTokenExpiry: options.openAiCodexTokenExpiry,
|
||||
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "deepseek":
|
||||
return new DeepSeekHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
|
||||
@@ -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"].includes(this.getModel().id)) {
|
||||
if (["x-ai/grok-code-fast-1", "kwaipilot/kat-coder-pro"].includes(this.getModel().id)) {
|
||||
totalCost = 0
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,332 +0,0 @@
|
||||
import {
|
||||
ModelInfo,
|
||||
OpenAiCodexModelId,
|
||||
OpenAiCompatibleModelInfo,
|
||||
openAiCodexDefaultModelId,
|
||||
openAiCodexModels,
|
||||
} from "@shared/api"
|
||||
import type { ChatCompletionTool } from "openai/resources/chat/completions"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { CodexOAuthTokens, getCodexAuthProvider } from "@/services/auth/providers/CodexAuthProvider"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAIResponsesInput } from "../transform/openai-response-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
// Codex API endpoint - uses the ChatGPT backend Responses API
|
||||
const CODEX_API_BASE_URL = "https://chatgpt.com/backend-api"
|
||||
|
||||
interface OpenAiCodexHandlerOptions extends CommonApiHandlerOptions {
|
||||
openAiCodexAccessToken?: string
|
||||
openAiCodexRefreshToken?: string
|
||||
openAiCodexAccountId?: string
|
||||
openAiCodexTokenExpiry?: number
|
||||
reasoningEffort?: string
|
||||
apiModelId?: string
|
||||
// Callback to update stored tokens after refresh
|
||||
onTokenRefresh?: (tokens: CodexOAuthTokens) => void
|
||||
}
|
||||
|
||||
export class OpenAiCodexHandler implements ApiHandler {
|
||||
private options: OpenAiCodexHandlerOptions
|
||||
|
||||
constructor(options: OpenAiCodexHandlerOptions) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we have valid OAuth tokens
|
||||
*/
|
||||
isAuthenticated(): boolean {
|
||||
return !!(this.options.openAiCodexAccessToken && this.options.openAiCodexRefreshToken && this.options.openAiCodexAccountId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure we have a valid access token, refreshing if necessary
|
||||
*/
|
||||
private async ensureValidToken(): Promise<string> {
|
||||
if (!this.options.openAiCodexAccessToken || !this.options.openAiCodexRefreshToken) {
|
||||
throw new Error("OpenAI Codex: Not authenticated. Please sign in with your ChatGPT account.")
|
||||
}
|
||||
|
||||
const expiry = this.options.openAiCodexTokenExpiry || 0
|
||||
const authProvider = getCodexAuthProvider()
|
||||
|
||||
if (authProvider.shouldRefreshToken(expiry)) {
|
||||
Logger.debug("Codex: Refreshing access token...")
|
||||
try {
|
||||
const newTokens = await authProvider.refreshAccessToken(this.options.openAiCodexRefreshToken)
|
||||
|
||||
// Update options with new tokens
|
||||
this.options.openAiCodexAccessToken = newTokens.accessToken
|
||||
this.options.openAiCodexRefreshToken = newTokens.refreshToken
|
||||
this.options.openAiCodexAccountId = newTokens.accountId
|
||||
this.options.openAiCodexTokenExpiry = newTokens.expiresAt
|
||||
|
||||
// Notify caller to persist the updated tokens
|
||||
if (this.options.onTokenRefresh) {
|
||||
this.options.onTokenRefresh(newTokens)
|
||||
}
|
||||
|
||||
Logger.debug("Codex: Token refreshed successfully")
|
||||
} catch (error) {
|
||||
Logger.error("Codex: Token refresh failed:", error)
|
||||
throw new Error("Failed to refresh Codex token. Please sign in again.")
|
||||
}
|
||||
}
|
||||
|
||||
return this.options.openAiCodexAccessToken
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: ChatCompletionTool[]): ApiStream {
|
||||
// Codex backend ONLY supports the Responses API format
|
||||
if (!tools?.length) {
|
||||
throw new Error("Native Tool Call must be enabled in your settings for OpenAI Codex")
|
||||
}
|
||||
yield* this.createCodexResponseStream(systemPrompt, messages, tools)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a streaming response using the Codex Responses API
|
||||
* Note: Codex backend ONLY supports Responses API, not chat completions
|
||||
*/
|
||||
private async *createCodexResponseStream(
|
||||
systemPrompt: string,
|
||||
messages: ClineStorageMessage[],
|
||||
tools: ChatCompletionTool[],
|
||||
): ApiStream {
|
||||
const accessToken = await this.ensureValidToken()
|
||||
const model = this.getModel()
|
||||
|
||||
// Convert messages to Responses API input format
|
||||
const input = convertToOpenAIResponsesInput(messages)
|
||||
|
||||
// Convert ChatCompletion tools to Responses API format
|
||||
const responseTools = tools
|
||||
?.filter((tool) => tool.type === "function")
|
||||
.map((tool: any) => ({
|
||||
type: "function" as const,
|
||||
name: tool.function.name,
|
||||
description: tool.function.description,
|
||||
parameters: tool.function.parameters,
|
||||
strict: tool.function.strict ?? true,
|
||||
}))
|
||||
|
||||
Logger.debug("Codex Responses Input: " + JSON.stringify(input))
|
||||
|
||||
// Use direct fetch for Codex Responses API
|
||||
const response = await fetch(`${CODEX_API_BASE_URL}/codex/responses`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"ChatGPT-Account-Id": this.options.openAiCodexAccountId!,
|
||||
"OpenAI-Beta": "responses=experimental",
|
||||
originator: "cline",
|
||||
accept: "text/event-stream",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: model.id,
|
||||
instructions: systemPrompt,
|
||||
input,
|
||||
stream: true,
|
||||
tools: responseTools,
|
||||
store: false, // Required for Codex backend
|
||||
reasoning: { effort: this.options.reasoningEffort || "medium", summary: "auto" },
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => "")
|
||||
throw new Error(`Codex API error ${response.status}: ${errorText}`)
|
||||
}
|
||||
|
||||
// Process SSE stream
|
||||
const reader = response.body?.getReader()
|
||||
if (!reader) {
|
||||
throw new Error("No response body")
|
||||
}
|
||||
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ""
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
const lines = buffer.split("\n")
|
||||
buffer = lines.pop() || ""
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith("data: ")) continue
|
||||
const data = line.slice(6).trim()
|
||||
if (data === "[DONE]") continue
|
||||
|
||||
try {
|
||||
const chunk = JSON.parse(data)
|
||||
Logger.debug("Codex Responses Chunk: " + JSON.stringify(chunk))
|
||||
|
||||
// Handle different event types from Responses API
|
||||
if (chunk.type === "response.output_item.added") {
|
||||
const item = chunk.item
|
||||
if (item.type === "function_call" && item.id) {
|
||||
yield {
|
||||
type: "tool_calls",
|
||||
id: item.id,
|
||||
tool_call: {
|
||||
call_id: item.call_id,
|
||||
function: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
arguments: item.arguments,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
if (item.type === "reasoning" && item.encrypted_content && item.id) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
id: item.id,
|
||||
reasoning: "",
|
||||
redacted_data: item.encrypted_content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.type === "response.output_item.done") {
|
||||
const item = chunk.item
|
||||
if (item.type === "function_call") {
|
||||
yield {
|
||||
type: "tool_calls",
|
||||
id: item.id || item.call_id,
|
||||
tool_call: {
|
||||
call_id: item.call_id,
|
||||
function: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
arguments: item.arguments,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
if (item.type === "reasoning") {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
id: item.id,
|
||||
details: item.summary,
|
||||
reasoning: "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.type === "response.reasoning_summary_part.added") {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
id: chunk.item_id,
|
||||
reasoning: chunk.part.text,
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.type === "response.reasoning_summary_text.delta") {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
id: chunk.item_id,
|
||||
reasoning: chunk.delta,
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.type === "response.reasoning_summary_part.done") {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
id: chunk.item_id,
|
||||
details: chunk.part,
|
||||
reasoning: "",
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.type === "response.output_text.delta") {
|
||||
if (chunk.delta) {
|
||||
yield {
|
||||
id: chunk.item_id,
|
||||
type: "text",
|
||||
text: chunk.delta,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.type === "response.reasoning_text.delta") {
|
||||
if (chunk.delta) {
|
||||
yield {
|
||||
id: chunk.item_id,
|
||||
type: "reasoning",
|
||||
reasoning: chunk.delta,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.type === "response.function_call_arguments.delta") {
|
||||
yield {
|
||||
type: "tool_calls",
|
||||
tool_call: {
|
||||
function: {
|
||||
id: chunk.item_id,
|
||||
name: chunk.item_id,
|
||||
arguments: chunk.delta,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.type === "response.function_call_arguments.done") {
|
||||
if (chunk.item_id && chunk.name && chunk.arguments) {
|
||||
yield {
|
||||
type: "tool_calls",
|
||||
tool_call: {
|
||||
function: {
|
||||
id: chunk.item_id,
|
||||
name: chunk.name,
|
||||
arguments: chunk.arguments,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.type === "response.completed" && chunk.response?.usage) {
|
||||
const usage = chunk.response.usage
|
||||
const inputTokens = usage.input_tokens || 0
|
||||
const outputTokens = usage.output_tokens || 0
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
totalCost: 0, // Included in subscription
|
||||
id: chunk.response.id,
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.debug("Failed to parse SSE chunk:", data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getModel(): { id: OpenAiCodexModelId; info: OpenAiCompatibleModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
if (modelId && modelId in openAiCodexModels) {
|
||||
const id = modelId as OpenAiCodexModelId
|
||||
const info = openAiCodexModels[id]
|
||||
return { id, info: { ...info } }
|
||||
}
|
||||
return {
|
||||
id: openAiCodexDefaultModelId,
|
||||
info: { ...openAiCodexModels[openAiCodexDefaultModelId] },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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 ?? {}),
|
||||
})
|
||||
|
||||
@@ -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,54 @@
|
||||
import { expect } from "chai"
|
||||
import { evaluateRuleConditionals, extractPathLikeStrings } from "../rule-conditionals"
|
||||
|
||||
describe("rule-conditionals", () => {
|
||||
describe("evaluateRuleConditionals(paths)", () => {
|
||||
it("treats missing paths as universal", () => {
|
||||
const res = evaluateRuleConditionals({}, { paths: [] })
|
||||
expect(res.passed).to.equal(true)
|
||||
})
|
||||
|
||||
it("treats empty paths list in frontmatter as match-nothing (fail-closed)", () => {
|
||||
const res = evaluateRuleConditionals({ paths: [] }, { paths: ["src/index.ts"] })
|
||||
expect(res.passed).to.equal(false)
|
||||
})
|
||||
|
||||
it("does not activate path-scoped rules with empty context", () => {
|
||||
const res = evaluateRuleConditionals({ paths: ["src/**"] }, { paths: [] })
|
||||
expect(res.passed).to.equal(false)
|
||||
})
|
||||
|
||||
it("matches when any candidate path matches any glob", () => {
|
||||
const res = evaluateRuleConditionals({ paths: ["src/**", "apps/**"] }, { paths: ["src/index.ts"] })
|
||||
expect(res.passed).to.equal(true)
|
||||
expect(res.matchedConditions.paths).to.deep.equal(["src/**"])
|
||||
})
|
||||
|
||||
it("ignores invalid paths type (fail-open)", () => {
|
||||
const res = evaluateRuleConditionals({ paths: "src/**" as any }, { paths: [] })
|
||||
expect(res.passed).to.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("extractPathLikeStrings", () => {
|
||||
it("extracts basic relative paths", () => {
|
||||
const res = extractPathLikeStrings("edit apps/web/src/App.tsx and packages/foo/src")
|
||||
expect(res).to.deep.equal(["apps/web/src/App.tsx", "packages/foo/src"])
|
||||
})
|
||||
|
||||
it("extracts simple filenames with extensions (no slashes)", () => {
|
||||
const res = extractPathLikeStrings("Does foo.md exist? If not, create foo.md")
|
||||
expect(res).to.deep.equal(["foo.md"])
|
||||
})
|
||||
|
||||
it("does not extract bare words without an extension", () => {
|
||||
const res = extractPathLikeStrings("Please create foo and then update bar")
|
||||
expect(res).to.deep.equal([])
|
||||
})
|
||||
|
||||
it("ignores URLs", () => {
|
||||
const res = extractPathLikeStrings("see https://example.com/a/b and edit src/index.ts")
|
||||
expect(res).to.deep.equal(["src/index.ts"])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,117 @@
|
||||
import { expect } from "chai"
|
||||
import fs from "fs/promises"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import { getRuleFilesTotalContentWithMetadata } from "../rule-helpers"
|
||||
|
||||
describe("rule loading with paths frontmatter", () => {
|
||||
it("filters rules by evaluationContext.paths", async () => {
|
||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "cline-rules-test-"))
|
||||
try {
|
||||
const rulesDir = path.join(tmp, ".clinerules")
|
||||
await fs.mkdir(rulesDir, { recursive: true })
|
||||
await fs.writeFile(path.join(rulesDir, "universal.md"), "Always on")
|
||||
await fs.writeFile(path.join(rulesDir, "scoped.md"), `---\npaths:\n - "src/**"\n---\n\nOnly for src`)
|
||||
|
||||
const files = ["universal.md", "scoped.md"]
|
||||
const toggles: Record<string, boolean> = {
|
||||
[path.join(rulesDir, "universal.md")]: true,
|
||||
[path.join(rulesDir, "scoped.md")]: true,
|
||||
}
|
||||
|
||||
const res1 = await getRuleFilesTotalContentWithMetadata(files, rulesDir, toggles, {
|
||||
evaluationContext: { paths: ["src/index.ts"] },
|
||||
})
|
||||
expect(res1.content).to.contain("universal.md")
|
||||
expect(res1.content).to.contain("scoped.md")
|
||||
expect(res1.content).to.not.contain("paths:")
|
||||
expect(res1.activatedConditionalRules.map((r) => r.name)).to.include("scoped.md")
|
||||
|
||||
const res2 = await getRuleFilesTotalContentWithMetadata(files, rulesDir, toggles, {
|
||||
evaluationContext: { paths: ["docs/readme.md"] },
|
||||
})
|
||||
expect(res2.content).to.contain("universal.md")
|
||||
expect(res2.content).to.not.contain("scoped.md")
|
||||
} finally {
|
||||
await fs.rm(tmp, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it("treats invalid YAML frontmatter as fail-open and preserves the raw frontmatter for the LLM", async () => {
|
||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "cline-rules-test-"))
|
||||
try {
|
||||
const rulesDir = path.join(tmp, ".clinerules")
|
||||
await fs.mkdir(rulesDir, { recursive: true })
|
||||
// Intentionally invalid YAML (unquoted '*' is a YAML alias indicator)
|
||||
await fs.writeFile(
|
||||
path.join(rulesDir, "invalid.md"),
|
||||
`---\npaths: *\n---\n\nInvalid YAML, but should still be included`,
|
||||
)
|
||||
|
||||
const files = ["invalid.md"]
|
||||
const toggles: Record<string, boolean> = {
|
||||
[path.join(rulesDir, "invalid.md")]: true,
|
||||
}
|
||||
|
||||
const res = await getRuleFilesTotalContentWithMetadata(files, rulesDir, toggles, {
|
||||
evaluationContext: { paths: ["src/index.ts"] },
|
||||
})
|
||||
|
||||
// Fail-open: included even though frontmatter cannot be parsed.
|
||||
expect(res.content).to.contain("invalid.md")
|
||||
// Preserve raw frontmatter fence/content for the LLM.
|
||||
expect(res.content).to.contain("---")
|
||||
expect(res.content).to.contain("paths:")
|
||||
} finally {
|
||||
await fs.rm(tmp, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it("treats paths: [] as match-nothing (fail-closed)", async () => {
|
||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "cline-rules-test-"))
|
||||
try {
|
||||
const rulesDir = path.join(tmp, ".clinerules")
|
||||
await fs.mkdir(rulesDir, { recursive: true })
|
||||
await fs.writeFile(path.join(rulesDir, "scoped-empty.md"), `---\npaths: []\n---\n\nShould never activate`)
|
||||
|
||||
const files = ["scoped-empty.md"]
|
||||
const toggles: Record<string, boolean> = {
|
||||
[path.join(rulesDir, "scoped-empty.md")]: true,
|
||||
}
|
||||
|
||||
const res = await getRuleFilesTotalContentWithMetadata(files, rulesDir, toggles, {
|
||||
evaluationContext: { paths: ["src/index.ts"] },
|
||||
})
|
||||
|
||||
expect(res.content).to.not.contain("scoped-empty.md")
|
||||
} finally {
|
||||
await fs.rm(tmp, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it("keeps activatedConditionalRules order stable (matches input file order)", async () => {
|
||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "cline-rules-test-"))
|
||||
try {
|
||||
const rulesDir = path.join(tmp, ".clinerules")
|
||||
await fs.mkdir(rulesDir, { recursive: true })
|
||||
await fs.writeFile(path.join(rulesDir, "a.md"), `---\npaths:\n - "src/**"\n---\n\nA`)
|
||||
await fs.writeFile(path.join(rulesDir, "b.md"), `---\npaths:\n - "src/**"\n---\n\nB`)
|
||||
await fs.writeFile(path.join(rulesDir, "c.md"), `---\npaths:\n - "src/**"\n---\n\nC`)
|
||||
|
||||
const files = ["a.md", "b.md", "c.md"]
|
||||
const toggles: Record<string, boolean> = {
|
||||
[path.join(rulesDir, "a.md")]: true,
|
||||
[path.join(rulesDir, "b.md")]: true,
|
||||
[path.join(rulesDir, "c.md")]: true,
|
||||
}
|
||||
|
||||
const res = await getRuleFilesTotalContentWithMetadata(files, rulesDir, toggles, {
|
||||
evaluationContext: { paths: ["src/index.ts"] },
|
||||
})
|
||||
|
||||
expect(res.activatedConditionalRules.map((r) => r.name)).to.deep.equal(files)
|
||||
} finally {
|
||||
await fs.rm(tmp, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Rule frontmatter conditional evaluation.
|
||||
*
|
||||
* This module implements a small conditional "DSL" for Cline Rules YAML frontmatter.
|
||||
* It is used to decide whether a rule should be activated for a given request context.
|
||||
*
|
||||
* Notes:
|
||||
* - Unknown conditional keys are ignored for forward compatibility.
|
||||
* - The `paths` conditional matches if any candidate path matches any glob pattern.
|
||||
* - Candidate paths are expected to be workspace-root-relative POSIX paths.
|
||||
*/
|
||||
import * as path from "path"
|
||||
import picomatch from "picomatch"
|
||||
|
||||
export type RuleEvaluationContext = {
|
||||
/**
|
||||
* Candidate workspace-relative paths that represent the current request context.
|
||||
* These should be POSIX-style paths, relative to their workspace root.
|
||||
*/
|
||||
paths?: string[]
|
||||
}
|
||||
|
||||
export type ConditionalEvaluator = (frontmatterValue: unknown, context: RuleEvaluationContext) => boolean
|
||||
|
||||
type MatchedConditions = Record<string, string[]>
|
||||
|
||||
type ConditionalEvaluatorResult = {
|
||||
passed: boolean
|
||||
matched?: string[]
|
||||
}
|
||||
|
||||
type ConditionalEvaluatorWithMatch = (frontmatterValue: unknown, context: RuleEvaluationContext) => ConditionalEvaluatorResult
|
||||
|
||||
function toPosix(p: string): string {
|
||||
return p.replace(/\\/g, "/")
|
||||
}
|
||||
|
||||
function isNonEmptyStringArray(value: unknown): value is string[] {
|
||||
return Array.isArray(value) && value.every((v) => typeof v === "string" && v.length > 0)
|
||||
}
|
||||
|
||||
const evaluatePathsConditional: ConditionalEvaluatorWithMatch = (frontmatterValue: unknown, context: RuleEvaluationContext) => {
|
||||
// Invalid type -> ignore conditional (fail-open)
|
||||
if (!isNonEmptyStringArray(frontmatterValue)) {
|
||||
return { passed: true }
|
||||
}
|
||||
|
||||
const patterns = frontmatterValue.map((p) => p.trim()).filter(Boolean)
|
||||
// Policy:
|
||||
// - `paths` omitted => universal (because this evaluator is never invoked)
|
||||
// - `paths: []` (or `paths` that trims to no usable patterns) => match nothing (fail-closed)
|
||||
// This gives users an explicit way to disable a rule via frontmatter, while omission
|
||||
// remains the mechanism for "always on" rules.
|
||||
if (patterns.length === 0) {
|
||||
return { passed: false }
|
||||
}
|
||||
|
||||
const candidatePaths = (context.paths || []).map((p) => toPosix(p)).filter(Boolean)
|
||||
// Conservative: no evidence => do not activate path-scoped rules
|
||||
if (candidatePaths.length === 0) {
|
||||
return { passed: false }
|
||||
}
|
||||
|
||||
const matchedPatterns: string[] = []
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const matcher = picomatch(pattern, { dot: true })
|
||||
if (candidatePaths.some((candidate) => matcher(candidate))) {
|
||||
matchedPatterns.push(pattern)
|
||||
}
|
||||
}
|
||||
|
||||
return { passed: matchedPatterns.length > 0, matched: matchedPatterns.length > 0 ? matchedPatterns : undefined }
|
||||
}
|
||||
|
||||
const conditionalEvaluators: Record<string, ConditionalEvaluatorWithMatch> = {
|
||||
paths: evaluatePathsConditional,
|
||||
}
|
||||
|
||||
export function evaluateRuleConditionals(
|
||||
frontmatter: Record<string, unknown>,
|
||||
context: RuleEvaluationContext,
|
||||
): {
|
||||
passed: boolean
|
||||
matchedConditions: MatchedConditions
|
||||
} {
|
||||
const matchedConditions: MatchedConditions = {}
|
||||
|
||||
for (const [key, value] of Object.entries(frontmatter)) {
|
||||
const evaluator = conditionalEvaluators[key]
|
||||
if (!evaluator) {
|
||||
continue // unknown conditional: ignore
|
||||
}
|
||||
|
||||
const result = evaluator(value, context)
|
||||
if (!result.passed) {
|
||||
return { passed: false, matchedConditions: {} }
|
||||
}
|
||||
if (result.matched && result.matched.length > 0) {
|
||||
matchedConditions[key] = result.matched
|
||||
}
|
||||
}
|
||||
|
||||
return { passed: true, matchedConditions }
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts path-like strings from user text to help enable first-turn activation.
|
||||
* This is intentionally heuristic and conservative.
|
||||
*/
|
||||
export function extractPathLikeStrings(text: string): string[] {
|
||||
if (!text) return []
|
||||
|
||||
// 1) Remove URLs to avoid false positives.
|
||||
const withoutUrls = text.replace(/\b\w+:\/\/[^\s]+/g, " ")
|
||||
|
||||
// 2) Match tokens that look like paths.
|
||||
// - Either contain at least one slash (e.g. src/index.ts)
|
||||
// - Or look like a simple filename with an extension (e.g. foo.md)
|
||||
// (no slashes; conservative to reduce false positives).
|
||||
const tokenRegex =
|
||||
/(?:^|[\s([{"'`])((?:[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)+\/?|[A-Za-z0-9_.-]+\.[A-Za-z0-9]{1,10}))(?=$|[\s)\]}"'`,.;:!?])/g
|
||||
const matches: string[] = []
|
||||
let match: RegExpExecArray | null
|
||||
while ((match = tokenRegex.exec(withoutUrls))) {
|
||||
const candidate = match[1]
|
||||
if (!candidate) continue
|
||||
// Normalize away leading ./
|
||||
const normalized = candidate.startsWith("./") ? candidate.slice(2) : candidate
|
||||
// Avoid absurdly long tokens
|
||||
if (normalized.length > 300) continue
|
||||
matches.push(normalized)
|
||||
}
|
||||
|
||||
// De-dupe while preserving order
|
||||
const seen = new Set<string>()
|
||||
const result: string[] = []
|
||||
for (const m of matches) {
|
||||
const posix = m.replace(/\\/g, "/")
|
||||
if (posix === "/" || posix.startsWith("/") || posix.includes("..")) {
|
||||
// We only want repo/workspace-relative hints here.
|
||||
continue
|
||||
}
|
||||
if (!seen.has(posix)) {
|
||||
seen.add(posix)
|
||||
result.push(posix)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize an absolute filesystem path to a workspace-root-relative POSIX path.
|
||||
* Returns undefined if the absolute path is not within the given root.
|
||||
*/
|
||||
export function toWorkspaceRelativePosixPath(absPath: string, workspaceRoot: string): string | undefined {
|
||||
const rel = path.relative(workspaceRoot, absPath)
|
||||
// Outside the root
|
||||
if (rel.startsWith("..") || path.isAbsolute(rel)) return undefined
|
||||
return toPosix(rel)
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import { fileExistsAtPath, isDirectory, readDirectory } from "@utils/fs"
|
||||
import fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import { Controller } from "@/core/controller"
|
||||
import { parseYamlFrontmatter } from "./frontmatter"
|
||||
import { evaluateRuleConditionals, RuleEvaluationContext } from "./rule-conditionals"
|
||||
|
||||
/**
|
||||
* Recursively traverses directory and finds all files, including checking for optional whitelisted file extension
|
||||
@@ -143,19 +145,114 @@ export function combineRuleToggles(toggles1: ClineRulesToggles, toggles2: ClineR
|
||||
* Read the content of rules files
|
||||
*/
|
||||
export const getRuleFilesTotalContent = async (rulesFilePaths: string[], basePath: string, toggles: ClineRulesToggles) => {
|
||||
const ruleFilesTotalContent = await Promise.all(
|
||||
return (await getRuleFilesTotalContentWithMetadata(rulesFilePaths, basePath, toggles)).content
|
||||
}
|
||||
|
||||
export type ActivatedConditionalRule = {
|
||||
name: string
|
||||
matchedConditions: Record<string, string[]>
|
||||
}
|
||||
|
||||
export type RuleLoadResult = {
|
||||
content: string
|
||||
activatedConditionalRules: ActivatedConditionalRule[]
|
||||
}
|
||||
|
||||
export const getRuleFilesTotalContentWithMetadata = async (
|
||||
rulesFilePaths: string[],
|
||||
basePath: string,
|
||||
toggles: ClineRulesToggles,
|
||||
opts?: { evaluationContext?: RuleEvaluationContext },
|
||||
): Promise<RuleLoadResult> => {
|
||||
const evaluationContext = opts?.evaluationContext ?? {}
|
||||
|
||||
type RuleLoadPart = {
|
||||
contentPart: string | null
|
||||
activatedRule: ActivatedConditionalRule | null
|
||||
}
|
||||
|
||||
const parts: RuleLoadPart[] = await Promise.all(
|
||||
rulesFilePaths.map(async (filePath) => {
|
||||
const ruleFilePath = path.resolve(basePath, filePath)
|
||||
const ruleFilePathRelative = path.relative(basePath, ruleFilePath)
|
||||
|
||||
if (ruleFilePath in toggles && toggles[ruleFilePath] === false) {
|
||||
return null
|
||||
return { contentPart: null, activatedRule: null }
|
||||
}
|
||||
|
||||
return `${ruleFilePathRelative}\n` + (await fs.readFile(ruleFilePath, "utf8")).trim()
|
||||
const raw = (await fs.readFile(ruleFilePath, "utf8")).trim()
|
||||
if (!raw) {
|
||||
return { contentPart: null, activatedRule: null }
|
||||
}
|
||||
const { data, body, hadFrontmatter, parseError } = parseYamlFrontmatter(raw)
|
||||
// YAML parse errors are treated as fail-open.
|
||||
// NOTE: We intentionally preserve the raw frontmatter fence/content here so the LLM can still
|
||||
// see the author's intended scoping (e.g., `paths:`) and reason about it, even if it cannot be
|
||||
// evaluated reliably due to invalid YAML.
|
||||
if (hadFrontmatter && parseError) {
|
||||
return { contentPart: `${ruleFilePathRelative}\n${raw}`, activatedRule: null }
|
||||
}
|
||||
|
||||
const { passed, matchedConditions } = evaluateRuleConditionals(data, evaluationContext)
|
||||
if (!passed) {
|
||||
return { contentPart: null, activatedRule: null }
|
||||
}
|
||||
const activatedRule =
|
||||
hadFrontmatter && Object.keys(matchedConditions).length > 0
|
||||
? { name: ruleFilePathRelative, matchedConditions }
|
||||
: null
|
||||
|
||||
return { contentPart: `${ruleFilePathRelative}\n${body.trim()}`, activatedRule }
|
||||
}),
|
||||
).then((contents) => contents.filter(Boolean).join("\n\n"))
|
||||
return ruleFilesTotalContent
|
||||
)
|
||||
|
||||
return {
|
||||
content: parts
|
||||
.map((p) => p.contentPart)
|
||||
.filter(Boolean)
|
||||
.join("\n\n"),
|
||||
activatedConditionalRules: parts
|
||||
.map((p) => p.activatedRule)
|
||||
.filter((rule): rule is ActivatedConditionalRule => rule !== null),
|
||||
}
|
||||
}
|
||||
|
||||
export function getRemoteRulesTotalContentWithMetadata(
|
||||
remoteRules: GlobalInstructionsFile[],
|
||||
remoteToggles: ClineRulesToggles,
|
||||
opts?: { evaluationContext?: RuleEvaluationContext },
|
||||
): RuleLoadResult {
|
||||
const activatedConditionalRules: ActivatedConditionalRule[] = []
|
||||
const evaluationContext = opts?.evaluationContext ?? {}
|
||||
let combinedContent = ""
|
||||
|
||||
for (const rule of remoteRules) {
|
||||
const isEnabled = rule.alwaysEnabled || remoteToggles[rule.name] !== false
|
||||
if (!isEnabled) continue
|
||||
|
||||
const raw = (rule.contents || "").trim()
|
||||
if (!raw) continue
|
||||
|
||||
const { data, body, hadFrontmatter, parseError } = parseYamlFrontmatter(raw)
|
||||
if (hadFrontmatter && parseError) {
|
||||
// Fail open: include entire raw contents
|
||||
if (combinedContent) combinedContent += "\n\n"
|
||||
combinedContent += `${rule.name}\n${raw}`
|
||||
continue
|
||||
}
|
||||
|
||||
const { passed, matchedConditions } = evaluateRuleConditionals(data, evaluationContext)
|
||||
if (!passed) continue
|
||||
|
||||
if (hadFrontmatter && Object.keys(matchedConditions).length > 0) {
|
||||
activatedConditionalRules.push({ name: rule.name, matchedConditions })
|
||||
}
|
||||
|
||||
if (combinedContent) combinedContent += "\n\n"
|
||||
combinedContent += `${rule.name}\n${body.trim()}`
|
||||
}
|
||||
|
||||
return { content: combinedContent, activatedConditionalRules }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { CodexAuthResult } from "@shared/proto/cline/account"
|
||||
import { getCodexAuthProvider } from "@/services/auth/providers/CodexAuthProvider"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
* Initiates OpenAI Codex OAuth sign-in flow
|
||||
*/
|
||||
export async function codexSignIn(controller: Controller, _: EmptyRequest): Promise<CodexAuthResult> {
|
||||
try {
|
||||
const authProvider = getCodexAuthProvider()
|
||||
const tokens = await authProvider.signIn()
|
||||
|
||||
// Store the tokens in StateManager secrets
|
||||
controller.stateManager.setSecret("openAiCodexAccessToken", tokens.accessToken)
|
||||
controller.stateManager.setSecret("openAiCodexRefreshToken", tokens.refreshToken)
|
||||
controller.stateManager.setSecret("openAiCodexAccountId", tokens.accountId)
|
||||
|
||||
// Store token expiry in global state
|
||||
controller.stateManager.setGlobalState("openAiCodexTokenExpiry", tokens.expiresAt)
|
||||
|
||||
// Post updated state to webview
|
||||
controller.postStateToWebview()
|
||||
|
||||
Logger.info("Codex: Successfully authenticated", { email: tokens.email })
|
||||
|
||||
return {
|
||||
success: true,
|
||||
email: tokens.email,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
||||
Logger.error("Codex: Authentication failed", error)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Empty, EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
* Signs out from OpenAI Codex OAuth
|
||||
*/
|
||||
export async function codexSignOut(controller: Controller, _: EmptyRequest): Promise<Empty> {
|
||||
try {
|
||||
// Clear the tokens from StateManager secrets
|
||||
controller.stateManager.setSecret("openAiCodexAccessToken", undefined)
|
||||
controller.stateManager.setSecret("openAiCodexRefreshToken", undefined)
|
||||
controller.stateManager.setSecret("openAiCodexAccountId", undefined)
|
||||
|
||||
// Clear token expiry from global state
|
||||
controller.stateManager.setGlobalState("openAiCodexTokenExpiry", undefined)
|
||||
|
||||
// Post updated state to webview
|
||||
controller.postStateToWebview()
|
||||
|
||||
Logger.info("Codex: Successfully signed out")
|
||||
} catch (error) {
|
||||
Logger.error("Codex: Sign out failed", error)
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
@@ -33,7 +33,9 @@ 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"
|
||||
@@ -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,20 +122,12 @@ 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()
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.WARNING,
|
||||
message: "Saving settings to storage failed.",
|
||||
})
|
||||
} catch (recoveryError) {
|
||||
console.error("[Controller] Cache recovery failed:", recoveryError)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to save settings. Please restart the extension.",
|
||||
})
|
||||
Logger.error("[Controller] Cache persistence failed to recover:", recoveryError)
|
||||
}
|
||||
},
|
||||
onSyncExternalChange: async () => {
|
||||
@@ -186,6 +181,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 +531,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 +875,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 +960,8 @@ export class Controller {
|
||||
enableParallelToolCalling: this.stateManager.getGlobalSettingsKey("enableParallelToolCalling"),
|
||||
backgroundEditEnabled: this.stateManager.getGlobalSettingsKey("backgroundEditEnabled"),
|
||||
skillsEnabled,
|
||||
optOutOfRemoteConfig: this.stateManager.getGlobalSettingsKey("optOutOfRemoteConfig"),
|
||||
banners,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1003,32 +1004,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 BannerService.get().getActiveBanners()
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
return []
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, undefined, controller.mcpHub)
|
||||
if (isRemoteConfigEnabled(organizationId)) {
|
||||
await applyRemoteConfig(config, undefined, controller.mcpHub)
|
||||
} else {
|
||||
clearRemoteConfig()
|
||||
}
|
||||
controller.postStateToWebview()
|
||||
|
||||
return config
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
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"
|
||||
@@ -90,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
|
||||
@@ -208,6 +211,23 @@ async function applyRemoteOTELConfig(transformed: Partial<RemoteConfigFields>, t
|
||||
}
|
||||
}
|
||||
|
||||
export function clearRemoteConfig() {
|
||||
try {
|
||||
const stateManager = StateManager.get()
|
||||
|
||||
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
|
||||
@@ -224,11 +244,7 @@ export async function applyRemoteConfig(
|
||||
|
||||
// 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", {})
|
||||
clearRemoteConfig()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -279,3 +295,55 @@ export async function applyRemoteConfig(
|
||||
}
|
||||
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,771 +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,
|
||||
openAiCodexAccessToken,
|
||||
openAiCodexRefreshToken,
|
||||
openAiCodexAccountId,
|
||||
] = 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"]>,
|
||||
context.secrets.get("openAiCodexAccessToken") as Promise<Secrets["openAiCodexAccessToken"]>,
|
||||
context.secrets.get("openAiCodexRefreshToken") as Promise<Secrets["openAiCodexRefreshToken"]>,
|
||||
context.secrets.get("openAiCodexAccountId") as Promise<Secrets["openAiCodexAccountId"]>,
|
||||
])
|
||||
|
||||
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,
|
||||
openAiCodexAccessToken,
|
||||
openAiCodexRefreshToken,
|
||||
openAiCodexAccountId,
|
||||
}
|
||||
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")
|
||||
const openAiCodexTokenExpiry =
|
||||
context.globalState.get<GlobalStateAndSettings["openAiCodexTokenExpiry"]>("openAiCodexTokenExpiry")
|
||||
|
||||
// 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,
|
||||
openAiCodexTokenExpiry,
|
||||
// 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()
|
||||
}
|
||||
@@ -774,52 +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",
|
||||
"openAiCodexAccessToken",
|
||||
"openAiCodexRefreshToken",
|
||||
"openAiCodexAccountId",
|
||||
]
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ export class TaskState {
|
||||
didRejectTool = false
|
||||
didAlreadyUseTool = false
|
||||
didEditFile: boolean = false
|
||||
lastToolName: string = "" // Track last tool used for consecutive call detection
|
||||
|
||||
// Error tracking
|
||||
consecutiveMistakeCount: number = 0
|
||||
|
||||
@@ -599,6 +599,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
|
||||
|
||||
+26
-5
@@ -454,7 +454,7 @@ export class Task {
|
||||
const currentProvider = mode === "plan" ? apiConfiguration.planModeApiProvider : apiConfiguration.actModeApiProvider
|
||||
|
||||
const openaiReasoningEffort = this.stateManager.getGlobalSettingsKey("openaiReasoningEffort")
|
||||
if (currentProvider === "openai" || currentProvider === "openai-native" || currentProvider === "openai-codex" || currentProvider === "sapaicore") {
|
||||
if (currentProvider === "openai" || currentProvider === "openai-native" || currentProvider === "sapaicore") {
|
||||
if (mode === "plan") {
|
||||
effectiveApiConfiguration.planModeReasoningEffort = openaiReasoningEffort
|
||||
} else {
|
||||
@@ -1933,8 +1933,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 +1963,7 @@ export class Task {
|
||||
maxAttempts: 3,
|
||||
delaySeconds: 0,
|
||||
failed: true, // Special flag to indicate retries exhausted
|
||||
errorMessage: streamingFailedMessage,
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -2672,6 +2690,7 @@ export class Task {
|
||||
attempt: this.taskState.autoRetryAttempts,
|
||||
maxAttempts: 3,
|
||||
delaySeconds: delay / 1000,
|
||||
errorMessage,
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -2693,6 +2712,7 @@ export class Task {
|
||||
maxAttempts: 3,
|
||||
delaySeconds: 0,
|
||||
failed: true, // Special flag to indicate retries exhausted
|
||||
errorMessage,
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -2914,6 +2934,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 +2949,7 @@ export class Task {
|
||||
attempt: this.taskState.autoRetryAttempts,
|
||||
maxAttempts: 3,
|
||||
delaySeconds: delay / 1000,
|
||||
errorMessage: noResponseErrorMessage,
|
||||
}),
|
||||
)
|
||||
await setTimeoutPromise(delay)
|
||||
@@ -2939,12 +2962,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.]`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ export const PatchClineSayMap = {
|
||||
|
||||
export class ApplyPatchHandler implements IFullyManagedTool {
|
||||
readonly name = ClineDefaultTool.APPLY_PATCH
|
||||
private appliedCommit?: Commit
|
||||
private config?: TaskConfig
|
||||
private pathResolver?: PathResolver
|
||||
private providerOps?: FileProviderOperations
|
||||
@@ -96,12 +95,14 @@ export class ApplyPatchHandler implements IFullyManagedTool {
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
if (line.startsWith(PATCH_MARKERS.ADD)) {
|
||||
provider.editType = "modify"
|
||||
targetPath = line.substring(PATCH_MARKERS.ADD.length).trim()
|
||||
actionType = PatchActionType.ADD
|
||||
contentStartIndex = i + 1
|
||||
break
|
||||
}
|
||||
if (line.startsWith(PATCH_MARKERS.UPDATE)) {
|
||||
provider.editType = "modify"
|
||||
targetPath = line.substring(PATCH_MARKERS.UPDATE.length).trim()
|
||||
actionType = PatchActionType.UPDATE
|
||||
contentStartIndex = i + 1
|
||||
@@ -233,10 +234,8 @@ export class ApplyPatchHandler implements IFullyManagedTool {
|
||||
const { patch, fuzz } = parser.parse()
|
||||
|
||||
// Convert to commit
|
||||
const commit = this.patchToCommit(patch, currentFiles)
|
||||
const commit = await this.patchToCommit(patch, currentFiles)
|
||||
|
||||
// Store for potential revert
|
||||
this.appliedCommit = commit
|
||||
this.config = config
|
||||
|
||||
// Run PreToolUse hook before applying changes
|
||||
@@ -252,32 +251,88 @@ export class ApplyPatchHandler implements IFullyManagedTool {
|
||||
throw error
|
||||
}
|
||||
|
||||
// Apply the commit
|
||||
const applyResults = await this.applyCommit(commit)
|
||||
|
||||
// Generate summary
|
||||
const changedFiles = Object.keys(commit.changes)
|
||||
const messages = await this.generateChangeSummary(commit.changes)
|
||||
|
||||
const finalResponses = []
|
||||
const applyResults: Record<string, FileOpsResult> = {}
|
||||
|
||||
// Create a mapping from message path to original commit change key
|
||||
// (needed because for move operations, message.path is the new path, but commit.changes key is the old path)
|
||||
const pathToChangeKey = new Map<string, string>()
|
||||
for (const [originalPath, change] of Object.entries(commit.changes)) {
|
||||
if (change.type === PatchActionType.UPDATE && change.movePath) {
|
||||
pathToChangeKey.set(change.movePath, originalPath)
|
||||
} else {
|
||||
pathToChangeKey.set(originalPath, originalPath)
|
||||
}
|
||||
}
|
||||
|
||||
// For each file: prepare, get approval, then save
|
||||
for (const message of messages) {
|
||||
const messagePath = message.path
|
||||
if (!messagePath) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the original change key (for move operations, this is the old path)
|
||||
const originalPath = pathToChangeKey.get(messagePath)
|
||||
if (!originalPath) {
|
||||
continue
|
||||
}
|
||||
|
||||
const change = commit.changes[originalPath]
|
||||
if (!change) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Determine the actual file path to use for operations
|
||||
// For move operations, we prepare the new file, but the change is keyed by the old path
|
||||
const operationPath = change.type === PatchActionType.UPDATE && change.movePath ? change.movePath : originalPath
|
||||
|
||||
// Prepare the change for this file (open and update, but don't save)
|
||||
await this.prepareFileChange(change, operationPath)
|
||||
|
||||
// Get approval
|
||||
const approved = await this.handleApproval(config, block, message, rawInput)
|
||||
if (!approved) {
|
||||
await this.revertChanges()
|
||||
this.config = undefined
|
||||
config.taskState.didRejectTool = true
|
||||
await provider.revertChanges()
|
||||
await provider.reset()
|
||||
return "The user denied this patch operation."
|
||||
}
|
||||
|
||||
for (const filePath of changedFiles) {
|
||||
config.services.fileContextTracker.markFileAsEditedByCline(filePath)
|
||||
await config.services.fileContextTracker.trackFileContext(filePath, "cline_edited")
|
||||
// Save the changes for this file after approval
|
||||
const fileResult = await this.saveFileChange(change, operationPath)
|
||||
if (fileResult) {
|
||||
// For move operations, we need to handle both old and new paths
|
||||
if (change.type === PatchActionType.UPDATE && change.movePath) {
|
||||
applyResults[change.movePath] = fileResult
|
||||
// Delete the old file after saving the new one
|
||||
await this.providerOps!.deleteFile(originalPath)
|
||||
applyResults[originalPath] = { deleted: true }
|
||||
} else {
|
||||
applyResults[originalPath] = fileResult
|
||||
}
|
||||
}
|
||||
|
||||
config.taskState.didEditFile = true
|
||||
finalResponses.push(message.path)
|
||||
// Reset provider state to ensure clean state for the next file operation
|
||||
await provider.reset()
|
||||
|
||||
finalResponses.push(messagePath)
|
||||
}
|
||||
|
||||
// Track all changed files once after all operations are complete
|
||||
for (const changedFilePath of changedFiles) {
|
||||
const change = commit.changes[changedFilePath]
|
||||
// For move operations, track the new path instead
|
||||
const pathToTrack = change.type === PatchActionType.UPDATE && change.movePath ? change.movePath : changedFilePath
|
||||
config.services.fileContextTracker.markFileAsEditedByCline(pathToTrack)
|
||||
await config.services.fileContextTracker.trackFileContext(pathToTrack, "cline_edited")
|
||||
}
|
||||
|
||||
this.appliedCommit = undefined
|
||||
this.config = undefined
|
||||
|
||||
// Build response with file contents and diagnostics
|
||||
@@ -285,6 +340,7 @@ export class ApplyPatchHandler implements IFullyManagedTool {
|
||||
|
||||
for (const [path, result] of Object.entries(applyResults)) {
|
||||
if (result.deleted) {
|
||||
config.taskState.didEditFile = true
|
||||
responseLines.push(`\n${path}: [deleted]`)
|
||||
} else {
|
||||
// Format response similar to WriteToFileToolHandler
|
||||
@@ -321,9 +377,9 @@ export class ApplyPatchHandler implements IFullyManagedTool {
|
||||
return responseLines.join("\n")
|
||||
} catch (error) {
|
||||
await provider.revertChanges()
|
||||
await provider.reset()
|
||||
console.error("Reverted changes due to error in ApplyPatchHandler.", error)
|
||||
throw error
|
||||
} finally {
|
||||
await provider.reset()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,10 +506,15 @@ export class ApplyPatchHandler implements IFullyManagedTool {
|
||||
return files
|
||||
}
|
||||
|
||||
private patchToCommit(patch: Patch, originalFiles: Record<string, string>): Commit {
|
||||
private async patchToCommit(patch: Patch, originalFiles: Record<string, string>): Promise<Commit> {
|
||||
const changes: Record<string, FileChange> = {}
|
||||
|
||||
for (const [path, action] of Object.entries(patch.actions)) {
|
||||
const targetResolution = await this.pathResolver!.resolveAndValidate(path, "ApplyPatchHandler.previewPatch")
|
||||
if (!targetResolution) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case PatchActionType.DELETE:
|
||||
changes[path] = { type: PatchActionType.DELETE, oldContent: originalFiles[path] }
|
||||
@@ -531,93 +592,60 @@ export class ApplyPatchHandler implements IFullyManagedTool {
|
||||
return result.join("\n")
|
||||
}
|
||||
|
||||
private async applyCommit(commit: Commit): Promise<Record<string, FileOpsResult>> {
|
||||
/**
|
||||
* Prepares a single file change (opens file and updates content) without saving.
|
||||
* Call saveFileChange() after approval.
|
||||
*/
|
||||
private async prepareFileChange(change: FileChange, path: string): Promise<void> {
|
||||
const ops = this.providerOps!
|
||||
const results: Record<string, FileOpsResult> = {}
|
||||
|
||||
for (const [path, change] of Object.entries(commit.changes)) {
|
||||
switch (change.type) {
|
||||
case PatchActionType.DELETE:
|
||||
await ops.deleteFile(path)
|
||||
results[path] = { deleted: true }
|
||||
break
|
||||
case PatchActionType.ADD:
|
||||
if (!change.newContent) {
|
||||
throw new DiffError(`Cannot create ${path} with no content`)
|
||||
}
|
||||
const addResult = await ops.createFile(path, change.newContent)
|
||||
results[path] = {
|
||||
finalContent: addResult.finalContent,
|
||||
newProblemsMessage: addResult.newProblemsMessage,
|
||||
userEdits: addResult.userEdits,
|
||||
autoFormattingEdits: addResult.autoFormattingEdits,
|
||||
}
|
||||
break
|
||||
case PatchActionType.UPDATE:
|
||||
if (!change.newContent) {
|
||||
throw new DiffError(`UPDATE change for ${path} has no new content`)
|
||||
}
|
||||
if (change.movePath) {
|
||||
const moveResult = await ops.moveFile(path, change.movePath, change.newContent)
|
||||
results[change.movePath] = {
|
||||
finalContent: moveResult.finalContent,
|
||||
newProblemsMessage: moveResult.newProblemsMessage,
|
||||
userEdits: moveResult.userEdits,
|
||||
autoFormattingEdits: moveResult.autoFormattingEdits,
|
||||
}
|
||||
results[path] = { deleted: true }
|
||||
} else {
|
||||
const updateResult = await ops.modifyFile(path, change.newContent)
|
||||
results[path] = {
|
||||
finalContent: updateResult.finalContent,
|
||||
newProblemsMessage: updateResult.newProblemsMessage,
|
||||
userEdits: updateResult.userEdits,
|
||||
autoFormattingEdits: updateResult.autoFormattingEdits,
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
switch (change.type) {
|
||||
case PatchActionType.DELETE:
|
||||
await ops.deleteFile(path, false)
|
||||
break
|
||||
case PatchActionType.ADD:
|
||||
if (!change.newContent) {
|
||||
throw new DiffError(`Cannot create ${path} with no content`)
|
||||
}
|
||||
await ops.createFile(path, change.newContent, false)
|
||||
break
|
||||
case PatchActionType.UPDATE:
|
||||
if (!change.newContent) {
|
||||
throw new DiffError(`UPDATE change for ${path} has no new content`)
|
||||
}
|
||||
if (change.movePath) {
|
||||
// For move operations, prepare the new file (the old file will be handled separately)
|
||||
await ops.createFile(change.movePath, change.newContent, false)
|
||||
} else {
|
||||
await ops.modifyFile(path, change.newContent, false)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
private async revertChanges(): Promise<void> {
|
||||
if (!this.appliedCommit || !this.providerOps) {
|
||||
return
|
||||
}
|
||||
/**
|
||||
* Saves the changes for a single file after approval.
|
||||
*/
|
||||
private async saveFileChange(change: FileChange, path: string): Promise<FileOpsResult | undefined> {
|
||||
const ops = this.providerOps!
|
||||
|
||||
const ops = this.providerOps
|
||||
|
||||
for (const [path, change] of Object.entries(this.appliedCommit.changes)) {
|
||||
try {
|
||||
switch (change.type) {
|
||||
case PatchActionType.DELETE:
|
||||
if (change.oldContent !== undefined) {
|
||||
await ops.createFile(path, change.oldContent)
|
||||
}
|
||||
break
|
||||
case PatchActionType.ADD:
|
||||
await ops.deleteFile(path)
|
||||
break
|
||||
case PatchActionType.UPDATE:
|
||||
if (change.movePath) {
|
||||
await ops.deleteFile(change.movePath)
|
||||
if (change.oldContent !== undefined) {
|
||||
await ops.createFile(path, change.oldContent)
|
||||
}
|
||||
} else if (change.oldContent !== undefined) {
|
||||
await ops.modifyFile(path, change.oldContent)
|
||||
}
|
||||
break
|
||||
switch (change.type) {
|
||||
case PatchActionType.DELETE:
|
||||
// For delete operations, actually delete the file now (after approval)
|
||||
await ops.deleteFile(path)
|
||||
return { deleted: true }
|
||||
case PatchActionType.ADD:
|
||||
if (!change.newContent) {
|
||||
throw new DiffError(`Cannot create ${path} with no content`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to revert ${path}:`, error)
|
||||
}
|
||||
return await ops.saveChanges()
|
||||
case PatchActionType.UPDATE:
|
||||
if (!change.newContent) {
|
||||
throw new DiffError(`UPDATE change for ${path} has no new content`)
|
||||
}
|
||||
// For move operations, we're saving the new file (the old file deletion is handled in the calling code)
|
||||
return await ops.saveChanges()
|
||||
}
|
||||
|
||||
this.appliedCommit = undefined
|
||||
this.config = undefined
|
||||
}
|
||||
|
||||
private async generateChangeSummary(changes: Record<string, FileChange>): Promise<ClineSayTool[]> {
|
||||
@@ -703,6 +731,7 @@ export class ApplyPatchHandler implements IFullyManagedTool {
|
||||
undefined,
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
|
||||
return approved
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -14,34 +14,86 @@ export interface FileOpsResult {
|
||||
export class FileProviderOperations {
|
||||
constructor(private provider: DiffViewProvider) {}
|
||||
|
||||
async createFile(path: string, content: string): Promise<FileOpsResult> {
|
||||
async openFile(path: string): Promise<void> {
|
||||
await this.provider.open(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the current changes and returns the result.
|
||||
*/
|
||||
async saveChanges(): Promise<FileOpsResult> {
|
||||
const result = await this.provider.saveChanges()
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a file. If isFinal is false, prepares the creation without saving.
|
||||
* Call saveChanges() after approval when isFinal is false.
|
||||
*/
|
||||
async createFile(path: string, content: string, isFinal: boolean = true): Promise<FileOpsResult | undefined> {
|
||||
this.provider.editType = "create"
|
||||
await this.provider.open(path)
|
||||
await this.provider.update(content, true)
|
||||
const result = await this.provider.saveChanges()
|
||||
await this.provider.reset()
|
||||
return result
|
||||
await this.openFile(path)
|
||||
await this.provider.update(content, isFinal)
|
||||
|
||||
if (isFinal) {
|
||||
return await this.saveChanges()
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
async modifyFile(path: string, content: string): Promise<FileOpsResult> {
|
||||
/**
|
||||
* Modifies a file. If isFinal is false, prepares the modification without saving.
|
||||
* Call saveChanges() after approval when isFinal is false.
|
||||
*/
|
||||
async modifyFile(path: string, content: string, isFinal: boolean = true): Promise<FileOpsResult | undefined> {
|
||||
this.provider.editType = "modify"
|
||||
await this.provider.open(path)
|
||||
await this.provider.update(content, true)
|
||||
const result = await this.provider.saveChanges()
|
||||
await this.provider.reset()
|
||||
return result
|
||||
await this.openFile(path)
|
||||
await this.provider.update(content, isFinal)
|
||||
|
||||
if (isFinal) {
|
||||
return await this.saveChanges()
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
async deleteFile(path: string): Promise<void> {
|
||||
/**
|
||||
* Deletes a file. If isFinal is false, prepares the deletion without actually deleting.
|
||||
* Opens the file in the diff view to show it will be deleted.
|
||||
* Call deleteFile() with isFinal=true after approval when isFinal is false.
|
||||
*/
|
||||
async deleteFile(path: string, isFinal: boolean = true): Promise<FileOpsResult | undefined> {
|
||||
this.provider.editType = "delete"
|
||||
await this.provider.open(path)
|
||||
await this.provider.deleteFile(path)
|
||||
await this.openFile(path)
|
||||
|
||||
if (isFinal) {
|
||||
await this.provider.deleteFile(path)
|
||||
return undefined
|
||||
} else {
|
||||
// Update with empty content to show the file will be deleted
|
||||
await this.provider.update("", isFinal)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
async moveFile(oldPath: string, newPath: string, content: string): Promise<FileOpsResult> {
|
||||
const result = await this.createFile(newPath, content)
|
||||
await this.deleteFile(oldPath)
|
||||
return result
|
||||
/**
|
||||
* Moves a file from oldPath to newPath. If isFinal is false, prepares the move without saving.
|
||||
* Call saveChanges() after approval when isFinal is false.
|
||||
*/
|
||||
async moveFile(
|
||||
oldPath: string,
|
||||
newPath: string,
|
||||
content: string,
|
||||
isFinal: boolean = true,
|
||||
): Promise<FileOpsResult | undefined> {
|
||||
if (isFinal) {
|
||||
const result = await this.createFile(newPath, content, isFinal)
|
||||
await this.deleteFile(oldPath, isFinal)
|
||||
return result
|
||||
} else {
|
||||
await this.createFile(newPath, content, isFinal)
|
||||
await this.deleteFile(oldPath, isFinal)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
async getFileContent(): Promise<string | undefined> {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ClineEnv } from "@/config"
|
||||
import { CLINE_API_ENDPOINT } from "@/shared/cline/api"
|
||||
import { getAxiosSettings } from "@/shared/net"
|
||||
import { AuthService } from "../auth/AuthService"
|
||||
import { buildBasicClineHeaders } from "../EnvUtils"
|
||||
|
||||
export class ClineAccountService {
|
||||
private static instance: ClineAccountService
|
||||
@@ -58,6 +59,7 @@ export class ClineAccountService {
|
||||
headers: {
|
||||
Authorization: `Bearer ${clineAccountAuthToken}`,
|
||||
"Content-Type": "application/json",
|
||||
...(await buildBasicClineHeaders()),
|
||||
...config.headers,
|
||||
},
|
||||
...getAxiosSettings(),
|
||||
@@ -236,12 +238,15 @@ export class ClineAccountService {
|
||||
organizationId: organizationId || null, // Pass organization if provided
|
||||
},
|
||||
})
|
||||
const activeOrgId = this._authService.getActiveOrganizationId()
|
||||
if (activeOrgId !== organizationId) {
|
||||
// After user switches account, we will force a refresh of the id token by calling this function that restores the refresh token and retrieves new auth info
|
||||
await this._authService.restoreRefreshTokenAndRetrieveAuthInfo()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error switching account:", error)
|
||||
throw error
|
||||
} finally {
|
||||
// After user switches account, we will force a refresh of the id token by calling this function that restores the refresh token and retrieves new auth info
|
||||
await this._authService.restoreRefreshTokenAndRetrieveAuthInfo()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -115,7 +115,18 @@ export class AuthService {
|
||||
* Refreshing it if necessary.
|
||||
*/
|
||||
async getAuthToken(): Promise<string | null> {
|
||||
return this.internalGetAuthToken(this._provider)
|
||||
const token = await this.internalGetAuthToken(this._provider)
|
||||
if (!token) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (this._provider.timeUntilExpiry(token) <= 0) {
|
||||
// internalGetAuthToken may return stale data on network errors
|
||||
// Verify the token is not expired after refresh - We have a pending larger refactor to prevent this
|
||||
// This prevents 401 errors from using expired tokens
|
||||
return null
|
||||
}
|
||||
return `workos:${token}`
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,7 +163,7 @@ export class AuthService {
|
||||
if (this._refreshPromise) {
|
||||
Logger.info("Token refresh already in progress, waiting for completion")
|
||||
const updatedToken = await this._refreshPromise
|
||||
return updatedToken ? `workos:${updatedToken}` : null
|
||||
return updatedToken || null
|
||||
}
|
||||
|
||||
// Start a new refresh operation
|
||||
@@ -162,15 +173,6 @@ export class AuthService {
|
||||
try {
|
||||
const updatedAuthInfo = await provider.retrieveClineAuthInfo(this._controller)
|
||||
if (updatedAuthInfo) {
|
||||
// retrieveClineAuthInfo may return stale data on network errors
|
||||
// Verify the token is not expired after refresh
|
||||
// This prevents 401 errors from using expired tokens
|
||||
const nowInSeconds = Date.now() / 1000
|
||||
if ((updatedAuthInfo.expiresAt ?? nowInSeconds) < nowInSeconds) {
|
||||
clineAccountAuthToken = undefined
|
||||
return undefined
|
||||
}
|
||||
|
||||
this._clineAuthInfo = updatedAuthInfo
|
||||
this._authenticated = true
|
||||
clineAccountAuthToken = updatedAuthInfo.idToken
|
||||
@@ -206,10 +208,10 @@ export class AuthService {
|
||||
return clineAccountAuthToken
|
||||
})()
|
||||
|
||||
await this._refreshPromise
|
||||
clineAccountAuthToken = await this._refreshPromise
|
||||
}
|
||||
|
||||
return clineAccountAuthToken ? `workos:${clineAccountAuthToken}` : null
|
||||
return clineAccountAuthToken || null
|
||||
} catch (error) {
|
||||
Logger.error("Error getting auth token:", error)
|
||||
return null
|
||||
|
||||
@@ -5,6 +5,7 @@ import { setWelcomeViewCompleted } from "@/core/controller/state/setWelcomeViewC
|
||||
import { WebviewProvider } from "@/core/webview"
|
||||
import { CLINE_API_ENDPOINT } from "@/shared/cline/api"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { buildBasicClineHeaders } from "../EnvUtils"
|
||||
import { AuthService } from "./AuthService"
|
||||
|
||||
export class AuthServiceMock extends AuthService {
|
||||
@@ -62,6 +63,7 @@ export class AuthServiceMock extends AuthService {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(await buildBasicClineHeaders()),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
code: testCode,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { type JwtPayload } from "jwt-decode"
|
||||
import { ClineEnv, EnvironmentConfig } from "@/config"
|
||||
import { Controller } from "@/core/controller"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { buildBasicClineHeaders } from "@/services/EnvUtils"
|
||||
import { AuthInvalidTokenError, AuthNetworkError } from "@/services/error/ClineError"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
@@ -95,9 +96,14 @@ export class ClineAuthProvider {
|
||||
/**
|
||||
* Returns the time in seconds until token expiry
|
||||
*/
|
||||
private timeUntilExpiry(_refreshToken: string, expiresAt?: number): number {
|
||||
timeUntilExpiry(jwt: string): number {
|
||||
const data = this.extractTokenData(jwt)
|
||||
if (!data.exp) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const currentTime = Date.now() / 1000
|
||||
const expirationTime = expiresAt || 0
|
||||
const expirationTime = data.exp
|
||||
|
||||
return expirationTime - currentTime
|
||||
}
|
||||
@@ -108,7 +114,7 @@ export class ClineAuthProvider {
|
||||
const startedAt = storedAuthData?.startedAt
|
||||
const timeSinceStarted = Date.now() - (startedAt || 0)
|
||||
|
||||
const tokenData = this.extractTokenData(storedAuthData)
|
||||
const tokenData = this.extractTokenData(storedAuthData?.idToken)
|
||||
telemetryService.capture({
|
||||
event: "extension_logging_user_out",
|
||||
properties: {
|
||||
@@ -129,7 +135,7 @@ export class ClineAuthProvider {
|
||||
const startedAt = storedAuthData?.startedAt
|
||||
const timeSinceStarted = Date.now() - (startedAt || 0)
|
||||
|
||||
const tokenData = this.extractTokenData(storedAuthData)
|
||||
const tokenData = this.extractTokenData(storedAuthData?.idToken)
|
||||
telemetryService.capture({
|
||||
event: "extension_refresh_attempt_failed",
|
||||
properties: {
|
||||
@@ -142,14 +148,12 @@ export class ClineAuthProvider {
|
||||
})
|
||||
}
|
||||
|
||||
private extractTokenData(authInfo?: ClineAuthInfo): Partial<TokenData> {
|
||||
if (!authInfo || !authInfo.idToken) {
|
||||
private extractTokenData(token: string | undefined): Partial<TokenData> {
|
||||
if (!token) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const idToken = authInfo.idToken
|
||||
|
||||
return parseJwtPayload<TokenData>(idToken) || {}
|
||||
return parseJwtPayload<TokenData>(token) || {}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,10 +192,7 @@ export class ClineAuthProvider {
|
||||
// and it failed the first refresh attempt
|
||||
// with something other than invalid token
|
||||
// continue with the request
|
||||
if (
|
||||
this.refreshRetryCount > 0 &&
|
||||
this.timeUntilExpiry(storedAuthData.refreshToken, storedAuthData.expiresAt) > 30
|
||||
) {
|
||||
if (this.refreshRetryCount > 0 && this.timeUntilExpiry(storedAuthData.idToken) > 30) {
|
||||
this.refreshRetryCount = 0
|
||||
this.lastRefreshAttempt = 0
|
||||
return storedAuthData
|
||||
@@ -294,7 +295,7 @@ export class ClineAuthProvider {
|
||||
const endpoint = new URL(CLINE_API_ENDPOINT.REFRESH_TOKEN, this.config.apiBaseUrl)
|
||||
const response = await fetch(endpoint.toString(), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: await this.headers(),
|
||||
body: JSON.stringify({
|
||||
refreshToken: storedData.refreshToken,
|
||||
grantType: "refresh_token",
|
||||
@@ -357,10 +358,7 @@ export class ClineAuthProvider {
|
||||
method: "GET",
|
||||
redirect: "manual",
|
||||
credentials: "include", // Important for cookies if needed
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
headers: await this.headers(),
|
||||
})
|
||||
|
||||
// If we get a redirect status (3xx), get the Location header
|
||||
@@ -397,10 +395,7 @@ export class ClineAuthProvider {
|
||||
|
||||
const response = await fetch(tokenUrl.toString(), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
headers: await this.headers(),
|
||||
body: JSON.stringify({
|
||||
grant_type: "authorization_code",
|
||||
code: authorizationCode,
|
||||
@@ -449,6 +444,7 @@ export class ClineAuthProvider {
|
||||
const userResponse = await axios.get(`${ClineEnv.config().apiBaseUrl}/api/v1/users/me`, {
|
||||
headers: {
|
||||
Authorization: `Bearer workos:${tokenData.accessToken}`,
|
||||
...(await this.headers()),
|
||||
},
|
||||
...getAxiosSettings(),
|
||||
})
|
||||
@@ -467,4 +463,12 @@ export class ClineAuthProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async headers() {
|
||||
return {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
...(await buildBasicClineHeaders()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,483 +0,0 @@
|
||||
import * as crypto from "crypto"
|
||||
import * as http from "http"
|
||||
import * as vscode from "vscode"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { fetch } from "@/shared/net"
|
||||
|
||||
// OAuth constants (same as OpenAI Codex CLI)
|
||||
export const CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
|
||||
export const CODEX_AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize"
|
||||
export const CODEX_TOKEN_URL = "https://auth.openai.com/oauth/token"
|
||||
export const CODEX_OAUTH_PORT = 1455
|
||||
export const CODEX_REDIRECT_URI = `http://localhost:${CODEX_OAUTH_PORT}/auth/callback`
|
||||
export const CODEX_SCOPES = "openid profile email offline_access"
|
||||
|
||||
// JWT claim path for ChatGPT account ID
|
||||
const JWT_CLAIM_PATH = "https://api.openai.com/auth"
|
||||
|
||||
export interface CodexOAuthTokens {
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
expiresAt: number // Unix timestamp in milliseconds
|
||||
accountId: string
|
||||
email?: string
|
||||
}
|
||||
|
||||
interface TokenResponse {
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
expires_in: number
|
||||
token_type: string
|
||||
}
|
||||
|
||||
interface PKCEPair {
|
||||
verifier: string
|
||||
challenge: string
|
||||
}
|
||||
|
||||
interface AuthorizationFlow {
|
||||
pkce: PKCEPair
|
||||
state: string
|
||||
url: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a cryptographically secure random string for PKCE
|
||||
*/
|
||||
function generateCodeVerifier(): string {
|
||||
return crypto.randomBytes(32).toString("base64url")
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate SHA256 hash for PKCE code challenge
|
||||
*/
|
||||
function generateCodeChallenge(verifier: string): string {
|
||||
return crypto.createHash("sha256").update(verifier).digest("base64url")
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate random state for CSRF protection
|
||||
*/
|
||||
function generateState(): string {
|
||||
return crypto.randomBytes(16).toString("hex")
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a JWT token and extract the payload
|
||||
*/
|
||||
function decodeJWT(token: string): Record<string, any> | null {
|
||||
try {
|
||||
const parts = token.split(".")
|
||||
if (parts.length !== 3) {
|
||||
return null
|
||||
}
|
||||
const payload = Buffer.from(parts[1], "base64url").toString("utf-8")
|
||||
return JSON.parse(payload)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract ChatGPT account ID from access token JWT
|
||||
*/
|
||||
function extractAccountId(accessToken: string): string | null {
|
||||
const decoded = decodeJWT(accessToken)
|
||||
if (!decoded) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Try the official claim path
|
||||
const claims = decoded[JWT_CLAIM_PATH]
|
||||
if (claims?.chatgpt_account_id) {
|
||||
return claims.chatgpt_account_id
|
||||
}
|
||||
|
||||
// Fallback: try direct property
|
||||
if (decoded.chatgpt_account_id) {
|
||||
return decoded.chatgpt_account_id
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract email from access token JWT
|
||||
*/
|
||||
function extractEmail(accessToken: string): string | undefined {
|
||||
const decoded = decodeJWT(accessToken)
|
||||
return decoded?.email
|
||||
}
|
||||
|
||||
export class CodexAuthProvider {
|
||||
readonly name = "codex"
|
||||
|
||||
/**
|
||||
* Create the authorization URL with PKCE parameters
|
||||
*/
|
||||
createAuthorizationFlow(): AuthorizationFlow {
|
||||
const verifier = generateCodeVerifier()
|
||||
const challenge = generateCodeChallenge(verifier)
|
||||
const state = generateState()
|
||||
|
||||
const url = new URL(CODEX_AUTHORIZE_URL)
|
||||
url.searchParams.set("response_type", "code")
|
||||
url.searchParams.set("client_id", CODEX_CLIENT_ID)
|
||||
url.searchParams.set("redirect_uri", CODEX_REDIRECT_URI)
|
||||
url.searchParams.set("scope", CODEX_SCOPES)
|
||||
url.searchParams.set("code_challenge", challenge)
|
||||
url.searchParams.set("code_challenge_method", "S256")
|
||||
url.searchParams.set("state", state)
|
||||
url.searchParams.set("id_token_add_organizations", "true")
|
||||
url.searchParams.set("codex_cli_simplified_flow", "true")
|
||||
url.searchParams.set("originator", "cline")
|
||||
|
||||
return {
|
||||
pkce: { verifier, challenge },
|
||||
state,
|
||||
url: url.toString(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start local OAuth callback server
|
||||
*/
|
||||
startLocalOAuthServer(
|
||||
expectedState: string,
|
||||
): Promise<{ server: http.Server; getAuthCode: () => Promise<string | null>; close: () => void }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let authCode: string | null = null
|
||||
let codeReceived = false
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
try {
|
||||
const url = new URL(req.url || "", `http://localhost:${CODEX_OAUTH_PORT}`)
|
||||
|
||||
if (url.pathname !== "/auth/callback") {
|
||||
res.statusCode = 404
|
||||
res.end("Not found")
|
||||
return
|
||||
}
|
||||
|
||||
const state = url.searchParams.get("state")
|
||||
if (state !== expectedState) {
|
||||
res.statusCode = 400
|
||||
res.setHeader("Content-Type", "text/html; charset=utf-8")
|
||||
res.end(this.getErrorHtml("State mismatch - possible CSRF attack"))
|
||||
return
|
||||
}
|
||||
|
||||
const error = url.searchParams.get("error")
|
||||
if (error) {
|
||||
const errorDescription = url.searchParams.get("error_description") || error
|
||||
res.statusCode = 400
|
||||
res.setHeader("Content-Type", "text/html; charset=utf-8")
|
||||
res.end(this.getErrorHtml(errorDescription))
|
||||
return
|
||||
}
|
||||
|
||||
const code = url.searchParams.get("code")
|
||||
if (!code) {
|
||||
res.statusCode = 400
|
||||
res.setHeader("Content-Type", "text/html; charset=utf-8")
|
||||
res.end(this.getErrorHtml("Missing authorization code"))
|
||||
return
|
||||
}
|
||||
|
||||
authCode = code
|
||||
codeReceived = true
|
||||
|
||||
res.statusCode = 200
|
||||
res.setHeader("Content-Type", "text/html; charset=utf-8")
|
||||
res.end(this.getSuccessHtml())
|
||||
} catch (err) {
|
||||
Logger.error("Error handling OAuth callback:", err)
|
||||
res.statusCode = 500
|
||||
res.end("Internal server error")
|
||||
}
|
||||
})
|
||||
|
||||
server.on("error", (err: NodeJS.ErrnoException) => {
|
||||
if (err.code === "EADDRINUSE") {
|
||||
reject(new Error(`Port ${CODEX_OAUTH_PORT} is already in use. Please close any other Codex sessions.`))
|
||||
} else {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
|
||||
server.listen(CODEX_OAUTH_PORT, "127.0.0.1", () => {
|
||||
resolve({
|
||||
server,
|
||||
getAuthCode: async () => {
|
||||
// Poll for auth code with timeout
|
||||
const timeout = 120000 // 2 minutes
|
||||
const pollInterval = 100
|
||||
const startTime = Date.now()
|
||||
|
||||
while (!codeReceived && Date.now() - startTime < timeout) {
|
||||
await new Promise((r) => setTimeout(r, pollInterval))
|
||||
}
|
||||
|
||||
return authCode
|
||||
},
|
||||
close: () => {
|
||||
server.close()
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchange authorization code for tokens
|
||||
*/
|
||||
async exchangeAuthorizationCode(code: string, codeVerifier: string): Promise<CodexOAuthTokens> {
|
||||
const response = await fetch(CODEX_TOKEN_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
client_id: CODEX_CLIENT_ID,
|
||||
code,
|
||||
code_verifier: codeVerifier,
|
||||
redirect_uri: CODEX_REDIRECT_URI,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => "")
|
||||
Logger.error(`Codex token exchange failed: ${response.status}`, text)
|
||||
throw new Error(`Failed to exchange authorization code: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = (await response.json()) as TokenResponse
|
||||
|
||||
if (!data.access_token || !data.refresh_token || typeof data.expires_in !== "number") {
|
||||
throw new Error("Invalid token response from OpenAI")
|
||||
}
|
||||
|
||||
const accountId = extractAccountId(data.access_token)
|
||||
if (!accountId) {
|
||||
throw new Error("Could not extract ChatGPT account ID from token")
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
expiresAt: Date.now() + data.expires_in * 1000,
|
||||
accountId,
|
||||
email: extractEmail(data.access_token),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh access token using refresh token
|
||||
*/
|
||||
async refreshAccessToken(refreshToken: string): Promise<CodexOAuthTokens> {
|
||||
const response = await fetch(CODEX_TOKEN_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: refreshToken,
|
||||
client_id: CODEX_CLIENT_ID,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => "")
|
||||
Logger.error(`Codex token refresh failed: ${response.status}`, text)
|
||||
throw new Error(`Failed to refresh token: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = (await response.json()) as TokenResponse
|
||||
|
||||
if (!data.access_token || !data.refresh_token || typeof data.expires_in !== "number") {
|
||||
throw new Error("Invalid token response from OpenAI")
|
||||
}
|
||||
|
||||
const accountId = extractAccountId(data.access_token)
|
||||
if (!accountId) {
|
||||
throw new Error("Could not extract ChatGPT account ID from token")
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
expiresAt: Date.now() + data.expires_in * 1000,
|
||||
accountId,
|
||||
email: extractEmail(data.access_token),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the token needs to be refreshed
|
||||
*/
|
||||
shouldRefreshToken(expiresAt: number): boolean {
|
||||
const bufferMs = 30 * 1000 // 30 seconds buffer
|
||||
return Date.now() >= expiresAt - bufferMs
|
||||
}
|
||||
|
||||
/**
|
||||
* Open browser to authorization URL
|
||||
*/
|
||||
async openBrowser(url: string): Promise<boolean> {
|
||||
try {
|
||||
await vscode.env.openExternal(vscode.Uri.parse(url))
|
||||
return true
|
||||
} catch (err) {
|
||||
Logger.error("Failed to open browser:", err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Full sign-in flow
|
||||
*/
|
||||
async signIn(): Promise<CodexOAuthTokens> {
|
||||
// Create authorization URL with PKCE
|
||||
const flow = this.createAuthorizationFlow()
|
||||
|
||||
// Start local server to receive callback
|
||||
const { getAuthCode, close } = await this.startLocalOAuthServer(flow.state)
|
||||
|
||||
try {
|
||||
// Open browser for user to authenticate
|
||||
const opened = await this.openBrowser(flow.url)
|
||||
if (!opened) {
|
||||
throw new Error("Failed to open browser for authentication")
|
||||
}
|
||||
|
||||
// Wait for authorization code
|
||||
const code = await getAuthCode()
|
||||
if (!code) {
|
||||
throw new Error("Authentication timed out or was cancelled")
|
||||
}
|
||||
|
||||
// Exchange code for tokens
|
||||
return await this.exchangeAuthorizationCode(code, flow.pkce.verifier)
|
||||
} finally {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
private getSuccessHtml(): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Authentication Successful</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
background: white;
|
||||
padding: 40px 60px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||||
}
|
||||
.checkmark {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 20px;
|
||||
border-radius: 50%;
|
||||
background: #10b981;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.checkmark svg {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
fill: white;
|
||||
}
|
||||
h1 { color: #1f2937; margin: 0 0 10px; }
|
||||
p { color: #6b7280; margin: 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="checkmark">
|
||||
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
|
||||
</div>
|
||||
<h1>Authentication Successful</h1>
|
||||
<p>You can close this window and return to Cline.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
}
|
||||
|
||||
private getErrorHtml(message: string): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Authentication Failed</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
background: white;
|
||||
padding: 40px 60px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||||
}
|
||||
.error-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 20px;
|
||||
border-radius: 50%;
|
||||
background: #ef4444;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.error-icon svg {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
fill: white;
|
||||
}
|
||||
h1 { color: #1f2937; margin: 0 0 10px; }
|
||||
p { color: #6b7280; margin: 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="error-icon">
|
||||
<svg viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
|
||||
</div>
|
||||
<h1>Authentication Failed</h1>
|
||||
<p>${message}</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
let codexAuthProvider: CodexAuthProvider | null = null
|
||||
|
||||
export function getCodexAuthProvider(): CodexAuthProvider {
|
||||
if (!codexAuthProvider) {
|
||||
codexAuthProvider = new CodexAuthProvider()
|
||||
}
|
||||
return codexAuthProvider
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
/**
|
||||
* Tests for BannerService
|
||||
* Tests API fetching, caching, and client-side provider filtering
|
||||
*
|
||||
* NOTE: Tests temporarily disabled while banner API fetching is disabled
|
||||
* to prevent blocking the extension. Tests will be re-enabled when API is stable.
|
||||
*/
|
||||
|
||||
import type { BannerRules } from "@shared/ClineBanner"
|
||||
@@ -12,7 +15,7 @@ import type { Controller } from "@/core/controller"
|
||||
import { Logger } from "../logging/Logger"
|
||||
import { BannerService } from "./BannerService"
|
||||
|
||||
describe("BannerService", () => {
|
||||
describe.skip("BannerService (TEMPORARILY DISABLED - Banner API fetch disabled)", () => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
let bannerService: BannerService
|
||||
let axiosGetStub: sinon.SinonStub
|
||||
@@ -66,16 +69,18 @@ describe("BannerService", () => {
|
||||
}
|
||||
|
||||
axiosGetStub.resolves(mockResponse)
|
||||
const banners = await bannerService.fetchActiveBanners()
|
||||
const banners = await bannerService.getActiveBanners()
|
||||
|
||||
expect(axiosGetStub.calledOnce).to.be.true
|
||||
expect(banners).to.have.lengthOf(1)
|
||||
expect(banners[0].id).to.equal("bnr_test1")
|
||||
expect(banners[0].title).to.equal("Test Banner")
|
||||
expect(banners[0].description).to.equal("This is a test")
|
||||
})
|
||||
|
||||
it("should handle API errors gracefully", async () => {
|
||||
axiosGetStub.rejects(new Error("Network error"))
|
||||
const banners = await bannerService.fetchActiveBanners()
|
||||
const banners = await bannerService.getActiveBanners()
|
||||
expect(banners).to.have.lengthOf(0)
|
||||
})
|
||||
|
||||
@@ -102,25 +107,25 @@ describe("BannerService", () => {
|
||||
axiosGetStub.resolves(mockResponse)
|
||||
|
||||
// First call fetches from API
|
||||
await bannerService.fetchActiveBanners()
|
||||
await bannerService.getActiveBanners()
|
||||
expect(axiosGetStub.callCount).to.equal(1)
|
||||
|
||||
// Second call within cache window uses cache (no new API call)
|
||||
await bannerService.fetchActiveBanners()
|
||||
await bannerService.getActiveBanners()
|
||||
expect(axiosGetStub.callCount).to.equal(1)
|
||||
|
||||
// After 4 minutes, still uses cache
|
||||
clock.tick(4 * 60 * 1000)
|
||||
await bannerService.fetchActiveBanners()
|
||||
await bannerService.getActiveBanners()
|
||||
expect(axiosGetStub.callCount).to.equal(1)
|
||||
|
||||
// After 6 minutes total, cache expired, makes new API call
|
||||
clock.tick(2 * 60 * 1000)
|
||||
await bannerService.fetchActiveBanners()
|
||||
await bannerService.getActiveBanners()
|
||||
expect(axiosGetStub.callCount).to.equal(2)
|
||||
|
||||
// Force refresh always bypasses cache
|
||||
await bannerService.fetchActiveBanners(true)
|
||||
await bannerService.getActiveBanners(true)
|
||||
expect(axiosGetStub.callCount).to.equal(3)
|
||||
})
|
||||
})
|
||||
@@ -158,7 +163,7 @@ describe("BannerService", () => {
|
||||
}
|
||||
|
||||
axiosGetStub.resolves(mockResponse)
|
||||
const banners = await bannerService.fetchActiveBanners()
|
||||
const banners = await bannerService.getActiveBanners()
|
||||
|
||||
expect(banners).to.have.lengthOf(1)
|
||||
expect(banners[0].id).to.equal("bnr_openai")
|
||||
@@ -196,7 +201,7 @@ describe("BannerService", () => {
|
||||
}
|
||||
|
||||
axiosGetStub.resolves(mockResponse)
|
||||
const banners = await bannerService.fetchActiveBanners()
|
||||
const banners = await bannerService.getActiveBanners()
|
||||
|
||||
expect(banners).to.have.lengthOf(1)
|
||||
expect(banners[0].id).to.equal("bnr_anthropic")
|
||||
@@ -234,7 +239,7 @@ describe("BannerService", () => {
|
||||
}
|
||||
|
||||
axiosGetStub.resolves(mockResponse)
|
||||
const banners = await bannerService.fetchActiveBanners()
|
||||
const banners = await bannerService.getActiveBanners()
|
||||
|
||||
expect(banners).to.have.lengthOf(0)
|
||||
})
|
||||
@@ -271,7 +276,7 @@ describe("BannerService", () => {
|
||||
}
|
||||
|
||||
axiosGetStub.resolves(mockResponse)
|
||||
const banners = await bannerService.fetchActiveBanners()
|
||||
const banners = await bannerService.getActiveBanners()
|
||||
|
||||
expect(banners).to.have.lengthOf(1)
|
||||
expect(banners[0].id).to.equal("bnr_multi")
|
||||
@@ -307,7 +312,7 @@ describe("BannerService", () => {
|
||||
}
|
||||
|
||||
axiosGetStub.resolves(mockResponse)
|
||||
const banners = await bannerService.fetchActiveBanners()
|
||||
const banners = await bannerService.getActiveBanners()
|
||||
|
||||
expect(banners).to.have.lengthOf(0)
|
||||
})
|
||||
@@ -333,7 +338,7 @@ describe("BannerService", () => {
|
||||
}
|
||||
|
||||
axiosGetStub.resolves(mockResponse)
|
||||
const banners = await bannerService.fetchActiveBanners()
|
||||
const banners = await bannerService.getActiveBanners()
|
||||
|
||||
expect(banners).to.have.lengthOf(1)
|
||||
})
|
||||
@@ -357,7 +362,7 @@ describe("BannerService", () => {
|
||||
}
|
||||
|
||||
axiosGetStub.resolves(mockResponse)
|
||||
const banners = await bannerService.fetchActiveBanners()
|
||||
const banners = await bannerService.getActiveBanners()
|
||||
|
||||
expect(banners).to.have.lengthOf(1)
|
||||
expect(banners[0].id).to.equal("bnr_norules")
|
||||
@@ -385,12 +390,12 @@ describe("BannerService", () => {
|
||||
|
||||
axiosGetStub.resolves(mockResponse)
|
||||
|
||||
await bannerService.fetchActiveBanners()
|
||||
await bannerService.getActiveBanners()
|
||||
expect(axiosGetStub.calledOnce).to.be.true
|
||||
|
||||
bannerService.clearCache()
|
||||
|
||||
await bannerService.fetchActiveBanners()
|
||||
await bannerService.getActiveBanners()
|
||||
expect(axiosGetStub.calledTwice).to.be.true
|
||||
})
|
||||
})
|
||||
@@ -415,7 +420,7 @@ describe("BannerService", () => {
|
||||
}
|
||||
|
||||
axiosGetStub.resolves(mockResponse)
|
||||
await bannerService.fetchActiveBanners()
|
||||
await bannerService.getActiveBanners()
|
||||
|
||||
expect(axiosGetStub.calledOnce).to.be.true
|
||||
const call = axiosGetStub.getCall(0)
|
||||
@@ -450,7 +455,7 @@ describe("BannerService", () => {
|
||||
}
|
||||
|
||||
axiosGetStub.resolves(mockResponse)
|
||||
const banners = await bannerService.fetchActiveBanners()
|
||||
const banners = await bannerService.getActiveBanners()
|
||||
|
||||
Object.defineProperty(process, "platform", {
|
||||
value: originalPlatform,
|
||||
@@ -501,7 +506,7 @@ describe("BannerService", () => {
|
||||
// Clear cache to ensure fresh API call for each platform test
|
||||
bannerService.clearCache()
|
||||
|
||||
await bannerService.fetchActiveBanners()
|
||||
await bannerService.getActiveBanners()
|
||||
|
||||
expect(axiosGetStub.called).to.be.true
|
||||
const call = axiosGetStub.lastCall
|
||||
@@ -518,4 +523,233 @@ describe("BannerService", () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("Banner to BannerCardData Conversion", () => {
|
||||
it("should convert banner with valid action types", async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
id: "bnr_valid_actions",
|
||||
titleMd: "Valid Actions Banner",
|
||||
bodyMd: "Has valid actions",
|
||||
icon: "lightbulb",
|
||||
severity: "info" as const,
|
||||
placement: "top" as const,
|
||||
rulesJson: "{}",
|
||||
actions: [
|
||||
{ title: "Link", action: "link", arg: "https://example.com" },
|
||||
{ title: "Settings", action: "show-api-settings" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
axiosGetStub.resolves(mockResponse)
|
||||
const banners = await bannerService.getActiveBanners()
|
||||
|
||||
expect(banners).to.have.lengthOf(1)
|
||||
expect(banners[0].id).to.equal("bnr_valid_actions")
|
||||
expect(banners[0].title).to.equal("Valid Actions Banner")
|
||||
expect(banners[0].description).to.equal("Has valid actions")
|
||||
expect(banners[0].icon).to.equal("lightbulb")
|
||||
expect(banners[0].actions).to.have.lengthOf(2)
|
||||
expect(banners[0].actions![0].title).to.equal("Link")
|
||||
expect(banners[0].actions![0].action).to.equal("link")
|
||||
expect(banners[0].actions![0].arg).to.equal("https://example.com")
|
||||
expect(banners[0].actions![1].title).to.equal("Settings")
|
||||
expect(banners[0].actions![1].action).to.equal("show-api-settings")
|
||||
})
|
||||
|
||||
it("should drop banner with invalid action type and log error", async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
id: "bnr_invalid_action",
|
||||
titleMd: "Invalid Action Banner",
|
||||
bodyMd: "Has invalid action type",
|
||||
severity: "info" as const,
|
||||
placement: "top" as const,
|
||||
rulesJson: "{}",
|
||||
actions: [{ title: "Invalid", action: "unknown-action-type", arg: "test" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
axiosGetStub.resolves(mockResponse)
|
||||
const banners = await bannerService.getActiveBanners()
|
||||
|
||||
expect(banners).to.have.lengthOf(0)
|
||||
})
|
||||
|
||||
it("should keep valid banners and drop only invalid ones", async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
id: "bnr_valid",
|
||||
titleMd: "Valid Banner",
|
||||
bodyMd: "This one is valid",
|
||||
severity: "info" as const,
|
||||
placement: "top" as const,
|
||||
rulesJson: "{}",
|
||||
actions: [{ title: "Link", action: "link", arg: "https://example.com" }],
|
||||
},
|
||||
{
|
||||
id: "bnr_invalid",
|
||||
titleMd: "Invalid Banner",
|
||||
bodyMd: "This one has invalid action",
|
||||
severity: "info" as const,
|
||||
placement: "top" as const,
|
||||
rulesJson: "{}",
|
||||
actions: [{ title: "Bad", action: "not-a-real-action" }],
|
||||
},
|
||||
{
|
||||
id: "bnr_also_valid",
|
||||
titleMd: "Also Valid Banner",
|
||||
bodyMd: "This one is also valid",
|
||||
severity: "info" as const,
|
||||
placement: "top" as const,
|
||||
rulesJson: "{}",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
axiosGetStub.resolves(mockResponse)
|
||||
const banners = await bannerService.getActiveBanners()
|
||||
|
||||
expect(banners).to.have.lengthOf(2)
|
||||
expect(banners[0].id).to.equal("bnr_valid")
|
||||
expect(banners[1].id).to.equal("bnr_also_valid")
|
||||
})
|
||||
|
||||
it("should convert banner with no actions", async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
id: "bnr_no_actions",
|
||||
titleMd: "No Actions Banner",
|
||||
bodyMd: "Has no actions",
|
||||
severity: "info" as const,
|
||||
placement: "top" as const,
|
||||
rulesJson: "{}",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
axiosGetStub.resolves(mockResponse)
|
||||
const banners = await bannerService.getActiveBanners()
|
||||
|
||||
expect(banners).to.have.lengthOf(1)
|
||||
expect(banners[0].id).to.equal("bnr_no_actions")
|
||||
expect(banners[0].actions).to.have.lengthOf(0)
|
||||
})
|
||||
|
||||
it("should convert banner with empty actions array", async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
id: "bnr_empty_actions",
|
||||
titleMd: "Empty Actions Banner",
|
||||
bodyMd: "Has empty actions array",
|
||||
severity: "info" as const,
|
||||
placement: "top" as const,
|
||||
rulesJson: "{}",
|
||||
actions: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
axiosGetStub.resolves(mockResponse)
|
||||
const banners = await bannerService.getActiveBanners()
|
||||
|
||||
expect(banners).to.have.lengthOf(1)
|
||||
expect(banners[0].id).to.equal("bnr_empty_actions")
|
||||
expect(banners[0].actions).to.have.lengthOf(0)
|
||||
})
|
||||
|
||||
it("should drop banner when action has undefined action type", async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
id: "bnr_undefined_action",
|
||||
titleMd: "Undefined Action Type",
|
||||
bodyMd: "Action has no type defined",
|
||||
severity: "info" as const,
|
||||
placement: "top" as const,
|
||||
rulesJson: "{}",
|
||||
actions: [{ title: "Just a label" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
axiosGetStub.resolves(mockResponse)
|
||||
const banners = await bannerService.getActiveBanners()
|
||||
|
||||
expect(banners).to.have.lengthOf(0)
|
||||
})
|
||||
|
||||
it("should accept all valid BannerActionType values", async () => {
|
||||
const validActionTypes = [
|
||||
"link",
|
||||
"show-api-settings",
|
||||
"show-feature-settings",
|
||||
"show-account",
|
||||
"set-model",
|
||||
"install-cli",
|
||||
]
|
||||
|
||||
const mockResponse = {
|
||||
data: {
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
id: "bnr_all_valid_types",
|
||||
titleMd: "All Valid Types",
|
||||
bodyMd: "Has all valid action types",
|
||||
severity: "info" as const,
|
||||
placement: "top" as const,
|
||||
rulesJson: "{}",
|
||||
actions: validActionTypes.map((type, index) => ({
|
||||
title: `Action ${index}`,
|
||||
action: type,
|
||||
})),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
axiosGetStub.resolves(mockResponse)
|
||||
const banners = await bannerService.getActiveBanners()
|
||||
|
||||
expect(banners).to.have.lengthOf(1)
|
||||
expect(banners[0].actions).to.have.lengthOf(validActionTypes.length)
|
||||
banners[0].actions!.forEach((action, index) => {
|
||||
expect(action.action).to.equal(validActionTypes[index])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { Banner, BannerRules, BannersResponse } from "@shared/ClineBanner"
|
||||
import { BannerActionType, type BannerCardData } from "@shared/cline/banner"
|
||||
import axios from "axios"
|
||||
import { ClineEnv } from "@/config"
|
||||
import type { Controller } from "@/core/controller"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { getAxiosSettings } from "@/shared/net"
|
||||
import { AuthService } from "../auth/AuthService"
|
||||
import { buildBasicClineHeaders } from "../EnvUtils"
|
||||
import { getDistinctId } from "../logging/distinctId"
|
||||
import { Logger } from "../logging/Logger"
|
||||
|
||||
@@ -19,9 +21,12 @@ export class BannerService {
|
||||
private readonly CACHE_DURATION_MS = 5 * 60 * 1000 // 5 minutes
|
||||
private _controller: Controller
|
||||
private _authService?: AuthService
|
||||
private actionTypes: Set<string>
|
||||
private _fetchPromise: Promise<Banner[]> | null = null
|
||||
|
||||
private constructor(controller: Controller) {
|
||||
this._controller = controller
|
||||
this.actionTypes = new Set<string>(Object.values(BannerActionType))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,7 +83,7 @@ export class BannerService {
|
||||
* @param forceRefresh If true, bypasses cache and fetches fresh data
|
||||
* @returns Array of banners that match current environment
|
||||
*/
|
||||
public async fetchActiveBanners(forceRefresh = false): Promise<Banner[]> {
|
||||
private async internalGetActiveBanners(forceRefresh = false): Promise<Banner[]> {
|
||||
try {
|
||||
// Return cached banners if still valid
|
||||
const now = Date.now()
|
||||
@@ -87,6 +92,22 @@ export class BannerService {
|
||||
return this._cachedBanners
|
||||
}
|
||||
|
||||
if (this._fetchPromise && !forceRefresh) {
|
||||
return this._fetchPromise
|
||||
}
|
||||
|
||||
this._fetchPromise = this.fetchActiveBanners()
|
||||
return this._fetchPromise
|
||||
} catch (error) {
|
||||
// Log error but don't throw - banner fetching shouldn't break the extension
|
||||
Logger.error("BannerService: Error getting internal banners", error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchActiveBanners(): Promise<Banner[]> {
|
||||
try {
|
||||
const now = Date.now()
|
||||
const ideType = await this.getIdeType()
|
||||
const extensionVersion = await this.getExtensionVersion()
|
||||
const osType = await this.getOSType()
|
||||
@@ -102,13 +123,11 @@ export class BannerService {
|
||||
Logger.log(`BannerService: Fetching banners from ${url}`)
|
||||
|
||||
const authService = this.getAuthServiceInstance()
|
||||
let token: string | null = null
|
||||
if (authService) {
|
||||
token = await authService.getAuthToken()
|
||||
}
|
||||
const token: string | null = (await authService?.getAuthToken()) || null
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
...(await buildBasicClineHeaders()),
|
||||
}
|
||||
if (token) {
|
||||
headers["Authorization"] = `Bearer ${token}`
|
||||
@@ -136,11 +155,16 @@ export class BannerService {
|
||||
this._cachedBanners = matchingBanners
|
||||
this._lastFetchTime = now
|
||||
|
||||
if (matchingBanners.length > 0) {
|
||||
Logger.log(`BannerService: ${matchingBanners.length} active banner(s) fetched.`)
|
||||
}
|
||||
return matchingBanners
|
||||
} catch (error) {
|
||||
// Log error but don't throw - banner fetching shouldn't break the extension
|
||||
Logger.error("BannerService: Error fetching banners", error)
|
||||
return []
|
||||
} finally {
|
||||
this._fetchPromise = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,6 +349,7 @@ export class BannerService {
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(await buildBasicClineHeaders()),
|
||||
},
|
||||
...getAxiosSettings(),
|
||||
})
|
||||
@@ -379,14 +404,49 @@ export class BannerService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Banner (API response format) to BannerCardData (UI format)
|
||||
* @param banner The banner from the API
|
||||
* @returns BannerCardData suitable for the carousel, or null if banner is invalid.
|
||||
*/
|
||||
private convertToBannerCardData(banner: Banner): BannerCardData | null {
|
||||
// Validate all action types before conversion
|
||||
// Each action must have a valid action type - undefined is not allowed
|
||||
for (const action of banner.actions || []) {
|
||||
if (!action.action || !this.actionTypes.has(action.action)) {
|
||||
Logger.error(`BannerService: ${banner.id} has invalid or missing action type '${action.action ?? "undefined"}'.`)
|
||||
return null
|
||||
}
|
||||
if (!action.title) {
|
||||
Logger.error(`BannerService: ${banner.id} is missing an action title: ${JSON.stringify(action)}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const actions = (banner.actions || []).map((action) => ({
|
||||
title: action.title || "",
|
||||
action: action.action as BannerActionType,
|
||||
arg: action.arg,
|
||||
}))
|
||||
return {
|
||||
id: banner.id,
|
||||
title: banner.titleMd,
|
||||
description: banner.bodyMd,
|
||||
icon: banner.icon,
|
||||
actions,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets banners that haven't been dismissed by the user
|
||||
* @param forceRefresh If true, bypasses cache and fetches fresh data
|
||||
* @returns Array of non-dismissed banners
|
||||
* @returns Array of non-dismissed banners converted to BannerCardData format
|
||||
*
|
||||
* TEMPORARILY DISABLED: Returning empty array to prevent API calls
|
||||
*/
|
||||
public async getNonDismissedBanners(forceRefresh = false): Promise<Banner[]> {
|
||||
const allBanners = await this.fetchActiveBanners(forceRefresh)
|
||||
return allBanners.filter((banner) => !this.isBannerDismissed(banner.id))
|
||||
public async getActiveBanners(forceRefresh = false): Promise<BannerCardData[]> {
|
||||
// Disable all banner fetching to prevent blocking the extension
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -87,10 +87,6 @@ export class FeatureFlagsService {
|
||||
return this.cache.get(flagName) === true
|
||||
}
|
||||
|
||||
public getDoNothingFlag(): boolean {
|
||||
return this.getBooleanFlagEnabled(FeatureFlag.DO_NOTHING)
|
||||
}
|
||||
|
||||
public getHooksEnabled(): boolean {
|
||||
return this.getBooleanFlagEnabled(FeatureFlag.HOOKS)
|
||||
}
|
||||
|
||||
+19
-12
@@ -276,25 +276,29 @@ export class McpHub {
|
||||
const stateManager = StateManager.get()
|
||||
const remoteConfig = stateManager.getRemoteConfigSettings()
|
||||
|
||||
// If marketplace is explicitly disabled by enterprise config, block all local servers
|
||||
if (remoteConfig.mcpMarketplaceEnabled === false) {
|
||||
return
|
||||
}
|
||||
// Early exit for non-enterprise users: if no remote config is set, allow all local servers
|
||||
if (Object.keys(remoteConfig).length === 0) {
|
||||
// No remote config restrictions - proceed with connection (default behavior for non-enterprise users)
|
||||
// This ensures backwards compatibility and that regular users are not affected
|
||||
} else {
|
||||
// Enterprise restrictions apply
|
||||
|
||||
// Only apply allowlist restrictions if enterprise has configured an allowlist
|
||||
if (remoteConfig.allowedMCPServers && remoteConfig.allowedMCPServers.length > 0) {
|
||||
// Check if server is from GitHub marketplace
|
||||
if (name.startsWith("github.com/")) {
|
||||
const allowedIds = remoteConfig.allowedMCPServers.map((server: { id: string }) => server.id)
|
||||
// If marketplace is explicitly disabled by enterprise config, block all local servers
|
||||
if (remoteConfig.mcpMarketplaceEnabled === false) {
|
||||
return
|
||||
}
|
||||
|
||||
// If allowlist exists, only servers on the allowlist are allowed
|
||||
const hasAllowlist = remoteConfig.allowedMCPServers && remoteConfig.allowedMCPServers.length > 0
|
||||
if (hasAllowlist) {
|
||||
const allowedIds = remoteConfig.allowedMCPServers!.map((server: { id: string }) => server.id)
|
||||
if (!allowedIds.includes(name)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
// Non-GitHub local servers are allowed when there's an allowlist
|
||||
// (the allowlist only restricts marketplace servers)
|
||||
|
||||
// If marketplace is enabled with no allowlist, all local servers are allowed
|
||||
}
|
||||
// If no enterprise allowlist configured, allow all local servers (default behavior)
|
||||
}
|
||||
|
||||
if (config.disabled) {
|
||||
@@ -448,6 +452,9 @@ export class McpHub {
|
||||
break
|
||||
}
|
||||
case "streamableHttp": {
|
||||
// Use ReconnectingEventSource for auto-reconnection on connection drops
|
||||
global.EventSource = ReconnectingEventSource
|
||||
|
||||
// Custom fetch wrapper that treats 404 as 405 for GET requests.
|
||||
// The MCP SDK sends a GET request to check for SSE stream support.
|
||||
// Per MCP spec, servers should return 405 if they don't support SSE,
|
||||
|
||||
@@ -409,4 +409,39 @@ describe("Telemetry system is abstracted and can easily switch between providers
|
||||
await noOpProvider.dispose()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Skills Telemetry", () => {
|
||||
it("should capture skill used events correctly", async () => {
|
||||
const noOpProvider = new NoOpTelemetryProvider()
|
||||
const logSpy = sinon.spy(noOpProvider, "log")
|
||||
const telemetryService = new TelemetryService([noOpProvider], MOCK_METADATA)
|
||||
|
||||
logSpy.resetHistory()
|
||||
|
||||
telemetryService.captureSkillUsed({
|
||||
ulid: "task-123",
|
||||
skillName: "my-skill",
|
||||
skillSource: "global",
|
||||
skillsAvailableGlobal: 2,
|
||||
skillsAvailableProject: 3,
|
||||
provider: "cline",
|
||||
modelId: "anthropic/claude-sonnet-4.5",
|
||||
})
|
||||
|
||||
assert.ok(logSpy.calledOnce, "Log should be called once")
|
||||
const [eventName, properties] = logSpy.firstCall.args
|
||||
assert.strictEqual(eventName, "task.skill_used", "Event name should be task.skill_used")
|
||||
assert.ok(properties, "Properties should be defined")
|
||||
assert.strictEqual(properties.ulid, "task-123", "Properties should include task ULID")
|
||||
assert.strictEqual(properties.skillName, "my-skill", "Properties should include skillName")
|
||||
assert.strictEqual(properties.skillSource, "global", "Properties should include skillSource")
|
||||
assert.strictEqual(properties.skillsAvailableGlobal, 2, "Properties should include global skill count")
|
||||
assert.strictEqual(properties.skillsAvailableProject, 3, "Properties should include project skill count")
|
||||
assert.strictEqual(properties.provider, "cline", "Properties should include provider")
|
||||
assert.strictEqual(properties.modelId, "anthropic/claude-sonnet-4.5", "Properties should include modelId")
|
||||
|
||||
logSpy.restore()
|
||||
await noOpProvider.dispose()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -16,7 +16,7 @@ import { TelemetryProviderFactory } from "./TelemetryProviderFactory"
|
||||
* When adding a new category, add it both here and to the initial values in telemetryCategoryEnabled
|
||||
* Ensure `if (!this.isCategoryEnabled('<category_name>')` is added to the capture method
|
||||
*/
|
||||
type TelemetryCategory = "checkpoints" | "browser" | "focus_chain" | "dictation" | "subagents" | "hooks"
|
||||
type TelemetryCategory = "checkpoints" | "browser" | "focus_chain" | "dictation" | "subagents" | "skills" | "hooks"
|
||||
|
||||
/**
|
||||
* Terminal type for telemetry differentiation
|
||||
@@ -109,6 +109,7 @@ export class TelemetryService {
|
||||
["dictation", true], // Dictation telemetry enabled
|
||||
["focus_chain", true], // Focus Chain telemetry enabled
|
||||
["subagents", true], // CLI Subagents telemetry enabled
|
||||
["skills", true], // Skills telemetry enabled
|
||||
["hooks", true], // Hooks telemetry enabled
|
||||
])
|
||||
|
||||
@@ -284,6 +285,8 @@ export class TelemetryService {
|
||||
SUBAGENT_DISABLED: "task.subagent_disabled",
|
||||
SUBAGENT_STARTED: "task.subagent_started",
|
||||
SUBAGENT_COMPLETED: "task.subagent_completed",
|
||||
// Skills telemetry events
|
||||
SKILL_USED: "task.skill_used",
|
||||
},
|
||||
// UI interaction events for tracking user engagement
|
||||
UI: {
|
||||
@@ -1003,6 +1006,42 @@ export class TelemetryService {
|
||||
this.recordHistogram(TelemetryService.METRICS.TOOLS.CALLS_PER_TASK, toolCallCount, toolAttributes)
|
||||
}
|
||||
|
||||
public captureSkillUsed(args: {
|
||||
ulid: string
|
||||
skillName: string
|
||||
skillSource: "global" | "project"
|
||||
skillsAvailableGlobal: number
|
||||
skillsAvailableProject: number
|
||||
provider?: string
|
||||
modelId?: string
|
||||
}): void {
|
||||
if (!this.isCategoryEnabled("skills")) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!args.ulid || !args.skillName) {
|
||||
return
|
||||
}
|
||||
|
||||
const skillsAvailableGlobal = Math.max(0, args.skillsAvailableGlobal)
|
||||
const skillsAvailableProject = Math.max(0, args.skillsAvailableProject)
|
||||
|
||||
const properties = {
|
||||
ulid: args.ulid,
|
||||
skillName: args.skillName,
|
||||
skillSource: args.skillSource,
|
||||
skillsAvailableGlobal,
|
||||
skillsAvailableProject,
|
||||
provider: args.provider,
|
||||
modelId: args.modelId,
|
||||
}
|
||||
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.SKILL_USED,
|
||||
properties,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when an MCP tool is called.
|
||||
* This telemetry event is designed to monitor the usage and performance of MCP tools
|
||||
|
||||
@@ -9,11 +9,22 @@ export interface Banner {
|
||||
id: string
|
||||
titleMd: string
|
||||
bodyMd: string
|
||||
severity: BannerSeverity
|
||||
placement: BannerPlacement
|
||||
icon?: string
|
||||
actions?: BannerAction[]
|
||||
|
||||
rulesJson: string
|
||||
activeFrom?: string
|
||||
activeTo?: string
|
||||
|
||||
// Severity and placement are not used in the extension
|
||||
severity?: BannerSeverity
|
||||
placement?: BannerPlacement
|
||||
}
|
||||
|
||||
export interface BannerAction {
|
||||
action?: string
|
||||
arg?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
export interface BannersResponse {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AutoApprovalSettings } from "./AutoApprovalSettings"
|
||||
import { ApiConfiguration } from "./api"
|
||||
import { BrowserSettings } from "./BrowserSettings"
|
||||
import { ClineFeatureSetting } from "./ClineFeatureSetting"
|
||||
import { BannerCardData } from "./cline/banner"
|
||||
import { ClineRulesToggles } from "./cline-rules"
|
||||
import { DictationSettings } from "./DictationSettings"
|
||||
import { FocusChainSettings } from "./FocusChainSettings"
|
||||
@@ -110,6 +111,8 @@ export interface ExtensionState {
|
||||
nativeToolCallSetting?: boolean
|
||||
enableParallelToolCalling?: boolean
|
||||
backgroundEditEnabled?: boolean
|
||||
optOutOfRemoteConfig?: boolean
|
||||
banners?: BannerCardData[]
|
||||
}
|
||||
|
||||
export interface ClineMessage {
|
||||
@@ -199,6 +202,7 @@ export interface ClineSayTool {
|
||||
| "webFetch"
|
||||
| "webSearch"
|
||||
| "summarizeTask"
|
||||
| "useSkill"
|
||||
path?: string
|
||||
diff?: string
|
||||
content?: string
|
||||
|
||||
+33
-273
@@ -1,5 +1,5 @@
|
||||
import type { LanguageModelChatSelector } from "../core/api/providers/types"
|
||||
import { ApiFormat } from "./proto/cline/models"
|
||||
import { ApiHandlerSettings } from "./storage/state-keys"
|
||||
|
||||
export type ApiProvider =
|
||||
| "anthropic"
|
||||
@@ -42,198 +42,15 @@ export type ApiProvider =
|
||||
| "minimax"
|
||||
| "hicap"
|
||||
| "nousResearch"
|
||||
| "openai-codex"
|
||||
|
||||
export interface ApiHandlerSecrets {
|
||||
apiKey?: string // anthropic
|
||||
liteLlmApiKey?: string
|
||||
awsAccessKey?: string
|
||||
awsSecretKey?: string
|
||||
openRouterApiKey?: string
|
||||
aihubmixApiKey?: string
|
||||
aihubmixBaseUrl?: string
|
||||
aihubmixAppCode?: string
|
||||
export const DEFAULT_API_PROVIDER = "openrouter" as ApiProvider
|
||||
|
||||
clineAccountId?: string
|
||||
awsSessionToken?: string
|
||||
awsBedrockApiKey?: string
|
||||
openAiApiKey?: string
|
||||
geminiApiKey?: string
|
||||
openAiNativeApiKey?: string
|
||||
ollamaApiKey?: string
|
||||
deepSeekApiKey?: string
|
||||
requestyApiKey?: string
|
||||
togetherApiKey?: string
|
||||
fireworksApiKey?: string
|
||||
qwenApiKey?: string
|
||||
doubaoApiKey?: string
|
||||
mistralApiKey?: string
|
||||
authNonce?: string
|
||||
asksageApiKey?: string
|
||||
xaiApiKey?: string
|
||||
moonshotApiKey?: string
|
||||
zaiApiKey?: string
|
||||
huggingFaceApiKey?: string
|
||||
nebiusApiKey?: string
|
||||
sambanovaApiKey?: string
|
||||
cerebrasApiKey?: string
|
||||
sapAiCoreClientId?: string
|
||||
sapAiCoreClientSecret?: string
|
||||
groqApiKey?: string
|
||||
huaweiCloudMaasApiKey?: string
|
||||
basetenApiKey?: string
|
||||
vercelAiGatewayApiKey?: string
|
||||
difyApiKey?: string
|
||||
minimaxApiKey?: string
|
||||
hicapApiKey?: string
|
||||
nousResearchApiKey?: string
|
||||
openAiCodexAccessToken?: string
|
||||
openAiCodexRefreshToken?: string
|
||||
openAiCodexAccountId?: string
|
||||
openAiCodexTokenExpiry?: number
|
||||
}
|
||||
|
||||
export interface ApiHandlerOptions {
|
||||
// Global configuration (not mode-specific)
|
||||
export interface ApiHandlerOptions extends Partial<ApiHandlerSettings> {
|
||||
ulid?: string // Used to identify the task in API requests
|
||||
liteLlmBaseUrl?: string
|
||||
liteLlmUsePromptCache?: boolean
|
||||
openAiHeaders?: Record<string, string> // Custom headers for OpenAI requests
|
||||
anthropicBaseUrl?: string
|
||||
openRouterProviderSorting?: string
|
||||
awsRegion?: string
|
||||
awsUseCrossRegionInference?: boolean
|
||||
awsUseGlobalInference?: boolean
|
||||
awsBedrockUsePromptCache?: boolean
|
||||
awsAuthentication?: string
|
||||
awsUseProfile?: boolean
|
||||
awsProfile?: string
|
||||
awsBedrockEndpoint?: string
|
||||
claudeCodePath?: string
|
||||
vertexProjectId?: string
|
||||
vertexRegion?: string
|
||||
openAiBaseUrl?: string
|
||||
ollamaBaseUrl?: string
|
||||
ollamaApiOptionsCtxNum?: string
|
||||
lmStudioBaseUrl?: string
|
||||
lmStudioModelId?: string
|
||||
lmStudioMaxTokens?: string
|
||||
geminiBaseUrl?: string
|
||||
requestyBaseUrl?: string
|
||||
fireworksModelMaxCompletionTokens?: number
|
||||
fireworksModelMaxTokens?: number
|
||||
qwenCodeOauthPath?: string
|
||||
azureApiVersion?: string
|
||||
azureIdentity?: boolean
|
||||
qwenApiLine?: string
|
||||
moonshotApiLine?: string
|
||||
asksageApiUrl?: string
|
||||
requestTimeoutMs?: number
|
||||
sapAiResourceGroup?: string
|
||||
sapAiCoreTokenUrl?: string
|
||||
sapAiCoreBaseUrl?: string
|
||||
sapAiCoreUseOrchestrationMode?: boolean
|
||||
difyBaseUrl?: string
|
||||
zaiApiLine?: string
|
||||
hicapApiKey?: string
|
||||
hicapModelId?: string
|
||||
onRetryAttempt?: (attempt: number, maxRetries: number, delay: number, error: any) => void
|
||||
ocaBaseUrl?: string
|
||||
minimaxApiLine?: string
|
||||
ocaMode?: string
|
||||
aihubmixBaseUrl?: string
|
||||
aihubmixAppCode?: string
|
||||
|
||||
// Plan mode configurations
|
||||
planModeApiModelId?: string
|
||||
planModeThinkingBudgetTokens?: number
|
||||
geminiPlanModeThinkingLevel?: string
|
||||
planModeReasoningEffort?: string
|
||||
planModeVerbosity?: string
|
||||
planModeVsCodeLmModelSelector?: LanguageModelChatSelector
|
||||
planModeAwsBedrockCustomSelected?: boolean
|
||||
planModeAwsBedrockCustomModelBaseId?: string
|
||||
planModeOpenRouterModelId?: string
|
||||
planModeOpenRouterModelInfo?: ModelInfo
|
||||
planModeOpenAiModelId?: string
|
||||
planModeOpenAiModelInfo?: OpenAiCompatibleModelInfo
|
||||
planModeOllamaModelId?: string
|
||||
planModeLmStudioModelId?: string
|
||||
planModeLiteLlmModelId?: string
|
||||
planModeLiteLlmModelInfo?: LiteLLMModelInfo
|
||||
planModeRequestyModelId?: string
|
||||
planModeRequestyModelInfo?: ModelInfo
|
||||
planModeTogetherModelId?: string
|
||||
planModeFireworksModelId?: string
|
||||
planModeSapAiCoreModelId?: string
|
||||
planModeSapAiCoreDeploymentId?: string
|
||||
planModeGroqModelId?: string
|
||||
planModeGroqModelInfo?: ModelInfo
|
||||
planModeBasetenModelId?: string
|
||||
planModeBasetenModelInfo?: ModelInfo
|
||||
planModeHuggingFaceModelId?: string
|
||||
planModeHuggingFaceModelInfo?: ModelInfo
|
||||
planModeHuaweiCloudMaasModelId?: string
|
||||
planModeHuaweiCloudMaasModelInfo?: ModelInfo
|
||||
planModeOcaModelId?: string
|
||||
planModeOcaModelInfo?: OcaModelInfo
|
||||
planModeOcaReasoningEffort?: string
|
||||
planModeAihubmixModelId?: string
|
||||
planModeAihubmixModelInfo?: OpenAiCompatibleModelInfo
|
||||
planModeHicapModelId?: string
|
||||
planModeHicapModelInfo?: ModelInfo
|
||||
planModeNousResearchModelId?: string
|
||||
planModeVercelAiGatewayModelId?: string
|
||||
planModeVercelAiGatewayModelInfo?: ModelInfo
|
||||
|
||||
// Act mode configurations
|
||||
actModeApiModelId?: string
|
||||
actModeThinkingBudgetTokens?: number
|
||||
geminiActModeThinkingLevel?: string
|
||||
actModeReasoningEffort?: string
|
||||
actModeVerbosity?: string
|
||||
actModeVsCodeLmModelSelector?: LanguageModelChatSelector
|
||||
actModeAwsBedrockCustomSelected?: boolean
|
||||
actModeAwsBedrockCustomModelBaseId?: string
|
||||
actModeOpenRouterModelId?: string
|
||||
actModeOpenRouterModelInfo?: ModelInfo
|
||||
actModeOpenAiModelId?: string
|
||||
actModeOpenAiModelInfo?: OpenAiCompatibleModelInfo
|
||||
actModeOllamaModelId?: string
|
||||
actModeLmStudioModelId?: string
|
||||
actModeLiteLlmModelId?: string
|
||||
actModeLiteLlmModelInfo?: LiteLLMModelInfo
|
||||
actModeRequestyModelId?: string
|
||||
actModeRequestyModelInfo?: ModelInfo
|
||||
actModeTogetherModelId?: string
|
||||
actModeFireworksModelId?: string
|
||||
actModeSapAiCoreModelId?: string
|
||||
actModeSapAiCoreDeploymentId?: string
|
||||
actModeGroqModelId?: string
|
||||
actModeGroqModelInfo?: ModelInfo
|
||||
actModeBasetenModelId?: string
|
||||
actModeBasetenModelInfo?: ModelInfo
|
||||
actModeHuggingFaceModelId?: string
|
||||
actModeHuggingFaceModelInfo?: ModelInfo
|
||||
actModeHuaweiCloudMaasModelId?: string
|
||||
actModeHuaweiCloudMaasModelInfo?: ModelInfo
|
||||
actModeOcaModelId?: string
|
||||
actModeOcaModelInfo?: OcaModelInfo
|
||||
actModeOcaReasoningEffort?: string
|
||||
actModeAihubmixModelId?: string
|
||||
actModeAihubmixModelInfo?: OpenAiCompatibleModelInfo
|
||||
actModeHicapModelId?: string
|
||||
actModeHicapModelInfo?: ModelInfo
|
||||
actModeNousResearchModelId?: string
|
||||
actModeVercelAiGatewayModelId?: string
|
||||
actModeVercelAiGatewayModelInfo?: ModelInfo
|
||||
onRetryAttempt?: (attempt: number, maxRetries: number, delay: number, error: any) => void // Callback function
|
||||
}
|
||||
|
||||
export type ApiConfiguration = ApiHandlerOptions &
|
||||
ApiHandlerSecrets & {
|
||||
planModeApiProvider?: ApiProvider
|
||||
actModeApiProvider?: ApiProvider
|
||||
}
|
||||
export type ApiConfiguration = ApiHandlerOptions
|
||||
|
||||
// Models
|
||||
|
||||
@@ -1566,6 +1383,20 @@ export const openAiNativeModels = {
|
||||
supportsReasoning: true,
|
||||
supportsReasoningEffort: true,
|
||||
},
|
||||
"gpt-5.2-codex": {
|
||||
maxTokens: 8_192, // 128000 breaks context window truncation
|
||||
contextWindow: 400000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 1.75,
|
||||
outputPrice: 14.0,
|
||||
cacheReadsPrice: 0.175,
|
||||
apiFormat: ApiFormat.OPENAI_RESPONSES,
|
||||
temperature: 1,
|
||||
systemRole: "developer",
|
||||
supportsReasoning: true,
|
||||
supportsReasoningEffort: true,
|
||||
},
|
||||
"gpt-5.1-2025-11-13": {
|
||||
maxTokens: 8_192,
|
||||
contextWindow: 272000,
|
||||
@@ -1816,91 +1647,6 @@ export const openAiNativeModels = {
|
||||
},
|
||||
} as const satisfies Record<string, OpenAiCompatibleModelInfo>
|
||||
|
||||
// OpenAI Codex (uses ChatGPT subscription via OAuth)
|
||||
// https://help.openai.com/en/articles/11369540-using-codex-with-your-chatgpt-plan
|
||||
export type OpenAiCodexModelId = keyof typeof openAiCodexModels
|
||||
export const openAiCodexDefaultModelId: OpenAiCodexModelId = "gpt-5.1-codex"
|
||||
export const openAiCodexModels = {
|
||||
"gpt-5.2-codex": {
|
||||
maxTokens: 8_192,
|
||||
contextWindow: 400000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0, // Included in ChatGPT subscription
|
||||
outputPrice: 0,
|
||||
apiFormat: ApiFormat.OPENAI_RESPONSES,
|
||||
temperature: 1,
|
||||
systemRole: "developer",
|
||||
supportsReasoning: true,
|
||||
supportsReasoningEffort: true,
|
||||
},
|
||||
"gpt-5.2": {
|
||||
maxTokens: 8_192,
|
||||
contextWindow: 272000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
apiFormat: ApiFormat.OPENAI_RESPONSES,
|
||||
temperature: 1,
|
||||
systemRole: "developer",
|
||||
supportsReasoning: true,
|
||||
supportsReasoningEffort: true,
|
||||
},
|
||||
"gpt-5.1-codex-max": {
|
||||
maxTokens: 8_192,
|
||||
contextWindow: 400000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
apiFormat: ApiFormat.OPENAI_RESPONSES,
|
||||
temperature: 1,
|
||||
systemRole: "developer",
|
||||
supportsReasoning: true,
|
||||
supportsReasoningEffort: true,
|
||||
},
|
||||
"gpt-5.1-codex": {
|
||||
maxTokens: 8_192,
|
||||
contextWindow: 400000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
apiFormat: ApiFormat.OPENAI_RESPONSES,
|
||||
temperature: 1,
|
||||
systemRole: "developer",
|
||||
supportsReasoning: true,
|
||||
supportsReasoningEffort: true,
|
||||
},
|
||||
"gpt-5.1-codex-mini": {
|
||||
maxTokens: 8_192,
|
||||
contextWindow: 400000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
apiFormat: ApiFormat.OPENAI_RESPONSES,
|
||||
temperature: 1,
|
||||
systemRole: "developer",
|
||||
supportsReasoning: true,
|
||||
supportsReasoningEffort: true,
|
||||
},
|
||||
"gpt-5.1": {
|
||||
maxTokens: 8_192,
|
||||
contextWindow: 272000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
apiFormat: ApiFormat.OPENAI_RESPONSES,
|
||||
temperature: 1,
|
||||
systemRole: "developer",
|
||||
supportsReasoning: true,
|
||||
supportsReasoningEffort: true,
|
||||
},
|
||||
} as const satisfies Record<string, OpenAiCompatibleModelInfo>
|
||||
|
||||
// Azure OpenAI
|
||||
// https://learn.microsoft.com/en-us/azure/ai-services/openai/api-version-deprecation
|
||||
// https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#api-specs
|
||||
@@ -3728,6 +3474,13 @@ export const sapAiCoreDefaultModelId: SapAiCoreModelId = "anthropic--claude-3.5-
|
||||
// Pricing is calculated using Capacity Units, not directly in USD
|
||||
const sapAiCoreModelDescription = "Pricing is calculated using SAP's Capacity Units rather than direct USD pricing."
|
||||
export const sapAiCoreModels = {
|
||||
"anthropic--claude-4.5-haiku": {
|
||||
maxTokens: 64000,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
description: sapAiCoreModelDescription,
|
||||
},
|
||||
"anthropic--claude-4.5-sonnet": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
@@ -3742,6 +3495,13 @@ export const sapAiCoreModels = {
|
||||
supportsPromptCache: true,
|
||||
description: sapAiCoreModelDescription,
|
||||
},
|
||||
"anthropic--claude-4.5-opus": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
description: sapAiCoreModelDescription,
|
||||
},
|
||||
"anthropic--claude-4-opus": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
|
||||
@@ -17,16 +17,6 @@ export enum BannerActionType {
|
||||
InstallCli = "install-cli",
|
||||
}
|
||||
|
||||
/**
|
||||
* Backend banner format returned from server API
|
||||
*/
|
||||
export interface BackendBanner {
|
||||
id: string
|
||||
titleMd: string
|
||||
bodyMd: string
|
||||
rulesJson: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Banner data structure for backend-to-frontend communication.
|
||||
* Backend constructs this JSON, frontend renders it via BannerCarousel.
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { ClineMessage } from "./ExtensionMessage"
|
||||
|
||||
/**
|
||||
* Consolidates error_retry messages in a retry sequence, keeping only the latest one.
|
||||
* Consolidates error_retry messages in a retry sequence, keeping only the latest one,
|
||||
* and removes successful retry messages entirely.
|
||||
*
|
||||
* When an API request fails and auto-retry is enabled, multiple error_retry messages are created
|
||||
* (e.g., "Attempt 1 of 3", "Attempt 2 of 3", "Attempt 3 of 3"), interleaved with api_req_retried
|
||||
* messages. This function filters out earlier retry messages, showing only the most recent one.
|
||||
* messages. This function:
|
||||
* 1. Filters out earlier retry messages, showing only the most recent one
|
||||
* 2. Removes error_retry messages entirely when followed by a successful api_req_started
|
||||
* (indicating the retry succeeded)
|
||||
*
|
||||
* @param messages - An array of ClineMessage objects to process.
|
||||
* @returns A new array of ClineMessage objects with error_retry sequences consolidated.
|
||||
*
|
||||
* @example
|
||||
* // During retry sequence - shows only latest attempt:
|
||||
* const messages: ClineMessage[] = [
|
||||
* { type: 'say', say: 'error_retry', text: '{"attempt":1,"maxAttempts":3}', ts: 1000 },
|
||||
* { type: 'say', say: 'api_req_retried', ts: 1001 },
|
||||
@@ -20,6 +25,16 @@ import { ClineMessage } from "./ExtensionMessage"
|
||||
* ];
|
||||
* const result = combineErrorRetryMessages(messages);
|
||||
* // Result: [{ type: 'say', say: 'error_retry', text: '{"attempt":3,"maxAttempts":3}', ts: 1004 }]
|
||||
*
|
||||
* @example
|
||||
* // After successful retry - removes error_retry entirely:
|
||||
* const messages: ClineMessage[] = [
|
||||
* { type: 'say', say: 'error_retry', text: '{"attempt":1,"maxAttempts":3}', ts: 1000 },
|
||||
* { type: 'say', say: 'api_req_retried', ts: 1001 },
|
||||
* { type: 'say', say: 'api_req_started', text: '{}', ts: 1002 },
|
||||
* ];
|
||||
* const result = combineErrorRetryMessages(messages);
|
||||
* // Result: [{ type: 'say', say: 'api_req_started', text: '{}', ts: 1002 }]
|
||||
*/
|
||||
export function combineErrorRetryMessages(messages: ClineMessage[]): ClineMessage[] {
|
||||
const result: ClineMessage[] = []
|
||||
@@ -28,15 +43,41 @@ export function combineErrorRetryMessages(messages: ClineMessage[]): ClineMessag
|
||||
const message = messages[i]
|
||||
|
||||
if (message.say === "error_retry") {
|
||||
// Look ahead to see if the next non-api_req_retried message is also an error_retry
|
||||
let nextMessage = messages[i + 1]
|
||||
if (nextMessage?.say === "api_req_retried") {
|
||||
nextMessage = messages[i + 2]
|
||||
// Look ahead to find if there's another error_retry before the next api_req_started
|
||||
let hasLaterErrorRetry = false
|
||||
let hasApiReqStartedBefore = false
|
||||
|
||||
for (let j = i + 1; j < messages.length; j++) {
|
||||
const laterMessage = messages[j]
|
||||
if (laterMessage.say === "api_req_started") {
|
||||
hasApiReqStartedBefore = true
|
||||
break
|
||||
}
|
||||
if (laterMessage.say === "error_retry") {
|
||||
hasLaterErrorRetry = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (nextMessage?.say === "error_retry") {
|
||||
// Skip this message, we'll show the next one (or a later one in the sequence)
|
||||
|
||||
// Case 1: Another error_retry follows before api_req_started - skip this one
|
||||
if (hasLaterErrorRetry) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Case 2: api_req_started follows (no later error_retry) - retry succeeded
|
||||
// Don't show the error_retry unless it has failed: true
|
||||
if (hasApiReqStartedBefore) {
|
||||
try {
|
||||
const retryInfo = JSON.parse(message.text || "{}")
|
||||
// Only skip if this wasn't a final failure message
|
||||
if (!retryInfo.failed) {
|
||||
continue
|
||||
}
|
||||
} catch {
|
||||
// If we can't parse, still skip to be safe
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.push(message)
|
||||
|
||||
@@ -259,8 +259,6 @@ function convertApiProviderToProto(provider: string | undefined): ProtoApiProvid
|
||||
return ProtoApiProvider.GEMINI
|
||||
case "openai-native":
|
||||
return ProtoApiProvider.OPENAI_NATIVE
|
||||
case "openai-codex":
|
||||
return ProtoApiProvider.OPENAI_CODEX
|
||||
case "requesty":
|
||||
return ProtoApiProvider.REQUESTY
|
||||
case "together":
|
||||
@@ -349,8 +347,6 @@ export function convertProtoToApiProvider(provider: ProtoApiProvider): ApiProvid
|
||||
return "gemini"
|
||||
case ProtoApiProvider.OPENAI_NATIVE:
|
||||
return "openai-native"
|
||||
case ProtoApiProvider.OPENAI_CODEX:
|
||||
return "openai-codex"
|
||||
case ProtoApiProvider.REQUESTY:
|
||||
return "requesty"
|
||||
case ProtoApiProvider.TOGETHER:
|
||||
@@ -499,10 +495,6 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
|
||||
minimaxApiKey: config.minimaxApiKey,
|
||||
minimaxApiLine: config.minimaxApiLine,
|
||||
nousResearchApiKey: config.nousResearchApiKey,
|
||||
openaiCodexAccessToken: config.openAiCodexAccessToken,
|
||||
openaiCodexRefreshToken: config.openAiCodexRefreshToken,
|
||||
openaiCodexAccountId: config.openAiCodexAccountId,
|
||||
openaiCodexTokenExpiry: config.openAiCodexTokenExpiry,
|
||||
ocaMode: config.ocaMode,
|
||||
aihubmixApiKey: config.aihubmixApiKey,
|
||||
aihubmixBaseUrl: config.aihubmixBaseUrl,
|
||||
@@ -683,10 +675,6 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
|
||||
hicapApiKey: protoConfig.hicapApiKey,
|
||||
hicapModelId: protoConfig.hicapModelId,
|
||||
nousResearchApiKey: protoConfig.nousResearchApiKey,
|
||||
openAiCodexAccessToken: protoConfig.openaiCodexAccessToken,
|
||||
openAiCodexRefreshToken: protoConfig.openaiCodexRefreshToken,
|
||||
openAiCodexAccountId: protoConfig.openaiCodexAccountId,
|
||||
openAiCodexTokenExpiry: protoConfig.openaiCodexTokenExpiry,
|
||||
|
||||
// Plan mode configurations
|
||||
planModeApiProvider:
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
},
|
||||
{
|
||||
"value": "vscode-lm",
|
||||
"label": "VS Code LM API"
|
||||
"label": "GitHub Copilot"
|
||||
},
|
||||
{
|
||||
"value": "deepseek",
|
||||
@@ -36,10 +36,6 @@
|
||||
"value": "openai-native",
|
||||
"label": "OpenAI"
|
||||
},
|
||||
{
|
||||
"value": "openai-codex",
|
||||
"label": "OpenAI Codex"
|
||||
},
|
||||
{
|
||||
"value": "ollama",
|
||||
"label": "Ollama"
|
||||
|
||||
@@ -4,7 +4,6 @@ export enum FeatureFlag {
|
||||
CUSTOM_INSTRUCTIONS = "custom-instructions",
|
||||
DICTATION = "dictation",
|
||||
FOCUS_CHAIN_CHECKLIST = "focus_chain_checklist",
|
||||
DO_NOTHING = "do_nothing",
|
||||
HOOKS = "hooks",
|
||||
WEBTOOLS = "webtools",
|
||||
// Feature flag for showing the new onboarding flow or old welcome view.
|
||||
@@ -12,7 +11,6 @@ export enum FeatureFlag {
|
||||
}
|
||||
|
||||
export const FeatureFlagDefaultValue: Partial<Record<FeatureFlag, FeatureFlagPayload>> = {
|
||||
[FeatureFlag.DO_NOTHING]: false,
|
||||
[FeatureFlag.HOOKS]: false,
|
||||
[FeatureFlag.WEBTOOLS]: false,
|
||||
[FeatureFlag.ONBOARDING_MODELS]: process.env.E2E_TEST === "true" ? { models: {} } : undefined,
|
||||
|
||||
@@ -0,0 +1,470 @@
|
||||
/**
|
||||
* State Keys Type Safety Tests
|
||||
*
|
||||
* This test suite validates the type safety guarantees of the state-keys module,
|
||||
* which uses a single-source-of-truth pattern where types are auto-generated from
|
||||
* field definition objects.
|
||||
*
|
||||
* ## Type Safety Model
|
||||
*
|
||||
* The state-keys module generates TypeScript types from runtime objects using:
|
||||
* 1. Field definition objects with `default` values (e.g., `{ default: true as boolean }`)
|
||||
* 2. `satisfies FieldDefinitions` constraint to enforce structure
|
||||
* 3. `BuildInterface<T>` mapped type to extract types from `default` values
|
||||
*
|
||||
* ## Known Limitations (What These Tests Catch)
|
||||
*
|
||||
* The `as` type assertions on default values are TRUSTED by TypeScript. This means:
|
||||
* - `{ default: "foo" as number }` would compile but be wrong at runtime
|
||||
* - `{ default: undefined as string }` compiles but `string` doesn't include `undefined`
|
||||
*
|
||||
* These tests provide runtime validation to catch such mismatches that TypeScript cannot.
|
||||
*
|
||||
* ## What These Tests Validate
|
||||
*
|
||||
* 1. **Type-Value Consistency**: Default values match their declared types at runtime
|
||||
* 2. **Key Synchronization**: Generated key arrays match the source objects
|
||||
* 3. **Type Guard Correctness**: `isGlobalStateKey`, `isSettingsKey`, etc. work correctly
|
||||
* 4. **Default Value Retrieval**: `getDefaultValue` returns correct values
|
||||
* 5. **Transform Functions**: Transforms produce values of the correct type
|
||||
*
|
||||
* ## Running Tests
|
||||
*
|
||||
* ```bash
|
||||
* npm run test:unit -- --grep "State Keys"
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { expect } from "chai"
|
||||
import { describe, it } from "mocha"
|
||||
|
||||
import {
|
||||
applyTransform,
|
||||
GLOBAL_STATE_DEFAULTS,
|
||||
type GlobalState,
|
||||
GlobalStateAndSettingKeys,
|
||||
type GlobalStateAndSettings,
|
||||
type GlobalStateAndSettingsKey,
|
||||
type GlobalStateKey,
|
||||
getDefaultValue,
|
||||
hasTransform,
|
||||
isAsyncProperty,
|
||||
isComputedProperty,
|
||||
isGlobalStateKey,
|
||||
isLocalStateKey,
|
||||
isSecretKey,
|
||||
isSettingsKey,
|
||||
type LocalState,
|
||||
type LocalStateKey,
|
||||
LocalStateKeys,
|
||||
SETTINGS_DEFAULTS,
|
||||
SETTINGS_TRANSFORMS,
|
||||
type SecretKey,
|
||||
SecretKeys,
|
||||
type Settings,
|
||||
type SettingsKey,
|
||||
SettingsKeys,
|
||||
} from "../state-keys"
|
||||
|
||||
// ============================================================================
|
||||
// Test Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Validates that a value matches the expected TypeScript type at runtime.
|
||||
* This catches cases where `as` assertions mask type mismatches.
|
||||
*/
|
||||
function assertTypeMatch(value: unknown, expectedType: string, key: string): void {
|
||||
const actualType = value === null ? "null" : Array.isArray(value) ? "array" : typeof value
|
||||
|
||||
if (expectedType === "array") {
|
||||
expect(Array.isArray(value), `${key}: expected array, got ${actualType}`).to.be.true
|
||||
} else if (expectedType === "object") {
|
||||
expect(actualType, `${key}: expected object, got ${actualType}`).to.equal("object")
|
||||
expect(value, `${key}: expected object, got null`).to.not.be.null
|
||||
expect(Array.isArray(value), `${key}: expected object, got array`).to.be.false
|
||||
} else if (expectedType === "undefined") {
|
||||
expect(value, `${key}: expected undefined, got ${actualType}`).to.be.undefined
|
||||
} else {
|
||||
expect(actualType, `${key}: expected ${expectedType}, got ${actualType}`).to.equal(expectedType)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Infers the expected runtime type from a default value.
|
||||
* Used to validate that default values are consistent with their purpose.
|
||||
*/
|
||||
function inferExpectedType(value: unknown): string {
|
||||
if (value === undefined) {
|
||||
return "undefined"
|
||||
}
|
||||
if (value === null) {
|
||||
return "null"
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return "array"
|
||||
}
|
||||
return typeof value
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
describe("State Keys Type Safety", () => {
|
||||
describe("Type-Value Consistency", () => {
|
||||
/**
|
||||
* These tests validate that default values match their declared types.
|
||||
* This catches mistakes like `{ default: "string" as number }` which
|
||||
* TypeScript would accept but would cause runtime issues.
|
||||
*/
|
||||
|
||||
it("should have GlobalState defaults with correct runtime types", () => {
|
||||
const defaults = GLOBAL_STATE_DEFAULTS as Record<string, unknown>
|
||||
|
||||
// Validate each default value has a sensible runtime type
|
||||
for (const [key, value] of Object.entries(defaults)) {
|
||||
const type = inferExpectedType(value)
|
||||
// Re-validate to ensure consistency
|
||||
assertTypeMatch(value, type, `GLOBAL_STATE_DEFAULTS.${key}`)
|
||||
}
|
||||
})
|
||||
|
||||
it("should have Settings defaults with correct runtime types", () => {
|
||||
const defaults = SETTINGS_DEFAULTS as Record<string, unknown>
|
||||
|
||||
for (const [key, value] of Object.entries(defaults)) {
|
||||
const type = inferExpectedType(value)
|
||||
assertTypeMatch(value, type, `SETTINGS_DEFAULTS.${key}`)
|
||||
}
|
||||
})
|
||||
|
||||
it("should not have undefined defaults masquerading as non-optional types", () => {
|
||||
// This test catches the pattern: { default: undefined as SomeType }
|
||||
// where SomeType doesn't include undefined
|
||||
const allDefaults = { ...GLOBAL_STATE_DEFAULTS, ...SETTINGS_DEFAULTS } as Record<string, unknown>
|
||||
|
||||
const undefinedKeys = Object.entries(allDefaults)
|
||||
.filter(([_, value]) => value === undefined)
|
||||
.map(([key]) => key)
|
||||
|
||||
// These keys have undefined defaults, which is valid only if their
|
||||
// declared type includes `| undefined`. This test documents which
|
||||
// keys are expected to have undefined defaults.
|
||||
expect(undefinedKeys).to.be.an("array")
|
||||
// If a key unexpectedly becomes undefined, this test will catch it
|
||||
})
|
||||
|
||||
it("should have array defaults that are actually arrays", () => {
|
||||
const allDefaults = { ...GLOBAL_STATE_DEFAULTS, ...SETTINGS_DEFAULTS } as Record<string, unknown>
|
||||
|
||||
for (const [key, value] of Object.entries(allDefaults)) {
|
||||
if (Array.isArray(value)) {
|
||||
// Verify it's a proper array, not an array-like object
|
||||
expect(value, `${key} should be a true array`).to.be.instanceOf(Array)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it("should have object defaults that are plain objects", () => {
|
||||
const allDefaults = { ...GLOBAL_STATE_DEFAULTS, ...SETTINGS_DEFAULTS } as Record<string, unknown>
|
||||
|
||||
for (const [key, value] of Object.entries(allDefaults)) {
|
||||
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
||||
// Verify it's a plain object
|
||||
expect(Object.getPrototypeOf(value), `${key} should be a plain object`).to.equal(Object.prototype)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("Key Array Synchronization", () => {
|
||||
/**
|
||||
* These tests ensure the generated key arrays stay in sync with
|
||||
* their source objects. A mismatch could cause runtime errors.
|
||||
*/
|
||||
|
||||
it("should have SettingsKeys match SETTINGS_DEFAULTS keys", () => {
|
||||
const defaultKeys = Object.keys(SETTINGS_DEFAULTS)
|
||||
const exportedKeys = new Set<string>(SettingsKeys)
|
||||
|
||||
// Every key in defaults should be in the exported array
|
||||
for (const key of defaultKeys) {
|
||||
expect(exportedKeys.has(key), `SettingsKeys missing key: ${key}`).to.be.true
|
||||
}
|
||||
})
|
||||
|
||||
it("should have GlobalStateAndSettingKeys be a superset of SettingsKeys", () => {
|
||||
const combinedSet = new Set<string>(GlobalStateAndSettingKeys)
|
||||
|
||||
for (const key of SettingsKeys) {
|
||||
expect(combinedSet.has(key), `GlobalStateAndSettingKeys missing settings key: ${key}`).to.be.true
|
||||
}
|
||||
})
|
||||
|
||||
it("should have no duplicate keys in exported arrays", () => {
|
||||
expect(new Set(SettingsKeys).size, "SettingsKeys has duplicates").to.equal(SettingsKeys.length)
|
||||
expect(new Set(SecretKeys).size, "SecretKeys has duplicates").to.equal(SecretKeys.length)
|
||||
expect(new Set(LocalStateKeys).size, "LocalStateKeys has duplicates").to.equal(LocalStateKeys.length)
|
||||
expect(new Set(GlobalStateAndSettingKeys).size, "GlobalStateAndSettingKeys has duplicates").to.equal(
|
||||
GlobalStateAndSettingKeys.length,
|
||||
)
|
||||
})
|
||||
|
||||
it("should have SecretKeys as strings", () => {
|
||||
for (const key of SecretKeys) {
|
||||
expect(typeof key, `SecretKey ${key} should be string`).to.equal("string")
|
||||
expect(key.length, `SecretKey should not be empty`).to.be.greaterThan(0)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("Type Guard Functions", () => {
|
||||
/**
|
||||
* Type guards narrow `string` to specific key types.
|
||||
* These tests ensure they correctly identify valid keys.
|
||||
*/
|
||||
|
||||
it("should correctly identify GlobalState keys", () => {
|
||||
// Known GlobalState keys from the defaults
|
||||
const knownGlobalStateKeys = Object.keys(GLOBAL_STATE_DEFAULTS)
|
||||
|
||||
for (const key of knownGlobalStateKeys) {
|
||||
expect(isGlobalStateKey(key), `${key} should be a GlobalStateKey`).to.be.true
|
||||
}
|
||||
|
||||
// Non-existent keys should return false
|
||||
expect(isGlobalStateKey("nonExistentKey123")).to.be.false
|
||||
expect(isGlobalStateKey("")).to.be.false
|
||||
})
|
||||
|
||||
it("should correctly identify Settings keys", () => {
|
||||
for (const key of SettingsKeys) {
|
||||
expect(isSettingsKey(key), `${key} should be a SettingsKey`).to.be.true
|
||||
}
|
||||
|
||||
expect(isSettingsKey("nonExistentKey123")).to.be.false
|
||||
})
|
||||
|
||||
it("should correctly identify Secret keys", () => {
|
||||
// Sample known secret keys
|
||||
const knownSecretKeys = ["apiKey", "openRouterApiKey", "awsAccessKey"]
|
||||
|
||||
for (const key of knownSecretKeys) {
|
||||
expect(isSecretKey(key), `${key} should be a SecretKey`).to.be.true
|
||||
}
|
||||
|
||||
expect(isSecretKey("notASecretKey")).to.be.false
|
||||
})
|
||||
|
||||
it("should correctly identify LocalState keys", () => {
|
||||
for (const key of LocalStateKeys) {
|
||||
expect(isLocalStateKey(key), `${key} should be a LocalStateKey`).to.be.true
|
||||
}
|
||||
|
||||
expect(isLocalStateKey("notALocalStateKey")).to.be.false
|
||||
})
|
||||
|
||||
it("should have mutually exclusive key categories where expected", () => {
|
||||
// Secret keys should not overlap with settings keys
|
||||
for (const secretKey of SecretKeys) {
|
||||
// Most secret keys should not be in settings (they're stored separately)
|
||||
// This is a sanity check, not a strict requirement
|
||||
if (isSettingsKey(secretKey)) {
|
||||
// If there is overlap, document it
|
||||
console.log(`Note: ${secretKey} is both a SecretKey and SettingsKey`)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("Default Value Retrieval", () => {
|
||||
/**
|
||||
* Tests for the getDefaultValue utility function.
|
||||
*/
|
||||
|
||||
it("should return correct default values for known keys", () => {
|
||||
// Test a few known defaults
|
||||
const testCases: Array<{ key: GlobalStateAndSettingsKey; expectedType: string }> = [
|
||||
{ key: "autoApprovalSettings", expectedType: "object" },
|
||||
{ key: "browserSettings", expectedType: "object" },
|
||||
{ key: "shellIntegrationTimeout", expectedType: "number" },
|
||||
{ key: "preferredLanguage", expectedType: "string" },
|
||||
{ key: "yoloModeToggled", expectedType: "boolean" },
|
||||
]
|
||||
|
||||
for (const { key, expectedType } of testCases) {
|
||||
const value = getDefaultValue(key)
|
||||
if (value !== undefined) {
|
||||
assertTypeMatch(value, expectedType, `getDefaultValue(${key})`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it("should return undefined for keys without defaults", () => {
|
||||
// Keys with `undefined` as default should return undefined
|
||||
const keysWithUndefinedDefaults = GlobalStateAndSettingKeys.filter((key) => {
|
||||
const value = getDefaultValue(key)
|
||||
return value === undefined
|
||||
})
|
||||
|
||||
// This is expected behavior - document which keys have undefined defaults
|
||||
expect(keysWithUndefinedDefaults).to.be.an("array")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Transform Functions", () => {
|
||||
/**
|
||||
* Tests for transform functions that modify values before storage.
|
||||
*/
|
||||
|
||||
it("should have transforms return the same type as input", () => {
|
||||
// Get keys that have transforms
|
||||
const keysWithTransforms = Object.keys(SETTINGS_TRANSFORMS)
|
||||
|
||||
expect(keysWithTransforms.length, "Should have at least one transform").to.be.greaterThan(0)
|
||||
|
||||
for (const key of keysWithTransforms) {
|
||||
expect(hasTransform(key), `hasTransform(${key}) should be true`).to.be.true
|
||||
}
|
||||
})
|
||||
|
||||
it("should correctly identify keys without transforms", () => {
|
||||
expect(hasTransform("nonExistentKey")).to.be.false
|
||||
expect(hasTransform("")).to.be.false
|
||||
})
|
||||
|
||||
it("should apply transforms without throwing", () => {
|
||||
const keysWithTransforms = Object.keys(SETTINGS_TRANSFORMS)
|
||||
|
||||
for (const key of keysWithTransforms) {
|
||||
// Transform should handle various inputs gracefully
|
||||
expect(() => applyTransform(key, {})).to.not.throw()
|
||||
expect(() => applyTransform(key, undefined)).to.not.throw()
|
||||
expect(() => applyTransform(key, null)).to.not.throw()
|
||||
}
|
||||
})
|
||||
|
||||
it("should pass through values for keys without transforms", () => {
|
||||
const testValue = { test: "value" }
|
||||
const result = applyTransform("keyWithoutTransform", testValue)
|
||||
|
||||
expect(result).to.equal(testValue)
|
||||
})
|
||||
|
||||
it("should merge defaults in browserSettings transform", () => {
|
||||
if (hasTransform("browserSettings")) {
|
||||
const partial = { viewport: { width: 800, height: 600 } }
|
||||
const result = applyTransform("browserSettings", partial)
|
||||
|
||||
expect(result).to.be.an("object")
|
||||
expect(result.viewport).to.deep.equal({ width: 800, height: 600 })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("Metadata Properties", () => {
|
||||
/**
|
||||
* Tests for isAsync and isComputed metadata flags.
|
||||
*/
|
||||
|
||||
it("should correctly identify async properties", () => {
|
||||
// taskHistory is known to be async
|
||||
expect(isAsyncProperty("taskHistory")).to.be.true
|
||||
expect(isAsyncProperty("preferredLanguage")).to.be.false
|
||||
expect(isAsyncProperty("nonExistent")).to.be.false
|
||||
})
|
||||
|
||||
it("should correctly identify computed properties", () => {
|
||||
// planActSeparateModelsSetting is known to be computed
|
||||
expect(isComputedProperty("planActSeparateModelsSetting")).to.be.true
|
||||
expect(isComputedProperty("preferredLanguage")).to.be.false
|
||||
expect(isComputedProperty("nonExistent")).to.be.false
|
||||
})
|
||||
})
|
||||
|
||||
describe("Type Exports", () => {
|
||||
/**
|
||||
* Compile-time tests that verify type exports work correctly.
|
||||
* If these fail to compile, the types are broken.
|
||||
*/
|
||||
|
||||
it("should export usable GlobalState type", () => {
|
||||
// This is a compile-time check - if GlobalState is broken, this won't compile
|
||||
const partialState: Partial<GlobalState> = {
|
||||
isNewUser: true,
|
||||
favoritedModelIds: [],
|
||||
}
|
||||
expect(partialState.isNewUser).to.equal(true)
|
||||
})
|
||||
|
||||
it("should export usable Settings type", () => {
|
||||
const partialSettings: Partial<Settings> = {
|
||||
preferredLanguage: "English",
|
||||
shellIntegrationTimeout: 5000,
|
||||
}
|
||||
expect(partialSettings.preferredLanguage).to.equal("English")
|
||||
})
|
||||
|
||||
it("should export usable key types", () => {
|
||||
// These assignments verify the key types are correctly narrowed
|
||||
const globalKey: GlobalStateKey = "isNewUser"
|
||||
const settingsKey: SettingsKey = "preferredLanguage"
|
||||
const secretKey: SecretKey = "apiKey"
|
||||
const localKey: LocalStateKey = "localClineRulesToggles"
|
||||
|
||||
expect(globalKey).to.be.a("string")
|
||||
expect(settingsKey).to.be.a("string")
|
||||
expect(secretKey).to.be.a("string")
|
||||
expect(localKey).to.be.a("string")
|
||||
})
|
||||
|
||||
it("should have GlobalStateAndSettings include both GlobalState and Settings", () => {
|
||||
const combined: Partial<GlobalStateAndSettings> = {
|
||||
// From GlobalState
|
||||
isNewUser: true,
|
||||
// From Settings
|
||||
preferredLanguage: "English",
|
||||
}
|
||||
expect(combined.isNewUser).to.equal(true)
|
||||
expect(combined.preferredLanguage).to.equal("English")
|
||||
})
|
||||
|
||||
it("should have LocalState keys map to ClineRulesToggles", () => {
|
||||
const localState: Partial<LocalState> = {
|
||||
localClineRulesToggles: {},
|
||||
localCursorRulesToggles: { "some-rule": true },
|
||||
}
|
||||
expect(localState.localClineRulesToggles).to.deep.equal({})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Edge Cases", () => {
|
||||
/**
|
||||
* Tests for edge cases and boundary conditions.
|
||||
*/
|
||||
|
||||
it("should handle empty string keys gracefully", () => {
|
||||
expect(isGlobalStateKey("")).to.be.false
|
||||
expect(isSettingsKey("")).to.be.false
|
||||
expect(isSecretKey("")).to.be.false
|
||||
expect(isLocalStateKey("")).to.be.false
|
||||
})
|
||||
|
||||
it("should handle keys with special characters", () => {
|
||||
// The cline:clineAccountId key has a colon
|
||||
expect(SecretKeys).to.include("cline:clineAccountId")
|
||||
expect(isSecretKey("cline:clineAccountId")).to.be.true
|
||||
})
|
||||
|
||||
it("should not have keys that could cause prototype pollution", () => {
|
||||
const dangerousKeys = ["__proto__", "constructor", "prototype"]
|
||||
|
||||
for (const key of dangerousKeys) {
|
||||
expect(isGlobalStateKey(key), `${key} should not be a GlobalStateKey`).to.be.false
|
||||
expect(isSettingsKey(key), `${key} should not be a SettingsKey`).to.be.false
|
||||
expect(isSecretKey(key), `${key} should not be a SecretKey`).to.be.false
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
+425
-276
@@ -1,299 +1,448 @@
|
||||
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
|
||||
import { ApiProvider, ModelInfo, type OcaModelInfo } from "@shared/api"
|
||||
import { BrowserSettings } from "@shared/BrowserSettings"
|
||||
import { AutoApprovalSettings, DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
|
||||
import {
|
||||
ANTHROPIC_MIN_THINKING_BUDGET,
|
||||
ApiProvider,
|
||||
DEFAULT_API_PROVIDER,
|
||||
LiteLLMModelInfo,
|
||||
ModelInfo,
|
||||
type OcaModelInfo,
|
||||
OpenAiCompatibleModelInfo,
|
||||
} from "@shared/api"
|
||||
import { BrowserSettings, DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import { DictationSettings } from "@shared/DictationSettings"
|
||||
import { FocusChainSettings } from "@shared/FocusChainSettings"
|
||||
import { DEFAULT_DICTATION_SETTINGS, DictationSettings } from "@shared/DictationSettings"
|
||||
import { DEFAULT_FOCUS_CHAIN_SETTINGS, FocusChainSettings } from "@shared/FocusChainSettings"
|
||||
import { HistoryItem } from "@shared/HistoryItem"
|
||||
import { McpDisplayMode } from "@shared/McpDisplayMode"
|
||||
import { DEFAULT_MCP_DISPLAY_MODE, McpDisplayMode } from "@shared/McpDisplayMode"
|
||||
import { WorkspaceRoot } from "@shared/multi-root/types"
|
||||
import { GlobalInstructionsFile } from "@shared/remote-config/schema"
|
||||
import { Mode, OpenaiReasoningEffort } from "@shared/storage/types"
|
||||
import { TelemetrySetting } from "@shared/TelemetrySetting"
|
||||
import { UserInfo } from "@shared/UserInfo"
|
||||
import { LanguageModelChatSelector } from "vscode"
|
||||
export type SecretKey = keyof Secrets
|
||||
|
||||
export type GlobalStateKey = keyof GlobalState
|
||||
// ============================================================================
|
||||
// SINGLE SOURCE OF TRUTH FOR STORAGE KEYS
|
||||
//
|
||||
// Property definitions with types, default values, and metadata
|
||||
// NOTE: When adding a new field, the scripts/generate-state-proto.mjs will be
|
||||
// executed automatically to regenerate the proto/cline/state.proto file with the
|
||||
// new fields once the file is staged and committed.
|
||||
// ============================================================================
|
||||
|
||||
export type LocalStateKey = keyof LocalState
|
||||
|
||||
export type SettingsKey = keyof Settings
|
||||
|
||||
export type GlobalStateAndSettingsKey = keyof (GlobalState & Settings)
|
||||
|
||||
export type GlobalStateAndSettings = GlobalState & Settings
|
||||
|
||||
export interface RemoteConfigExtraFields {
|
||||
remoteConfiguredProviders: string[]
|
||||
allowedMCPServers: Array<{ id: string }>
|
||||
remoteMCPServers?: Array<{ name: string; url: string; alwaysEnabled?: boolean }>
|
||||
previousRemoteMCPServers?: Array<{ name: string; url: string }>
|
||||
remoteGlobalRules?: GlobalInstructionsFile[]
|
||||
remoteGlobalWorkflows?: GlobalInstructionsFile[]
|
||||
blockPersonalRemoteMCPServers?: boolean
|
||||
openTelemetryOtlpHeaders: Record<string, string> | undefined
|
||||
/**
|
||||
* Defines the shape of a field definition. Each field must have a `default` value,
|
||||
* and optionally can have `isAsync`, `isComputed`, or `transform` metadata.
|
||||
*
|
||||
* The type casting on `default` (e.g., `true as boolean`) is necessary because
|
||||
* TypeScript would otherwise infer the literal type (`true`) instead of the
|
||||
* wider type (`boolean`). This ensures the generated interfaces allow any
|
||||
* value of that type, not just the default literal.
|
||||
*/
|
||||
type FieldDefinition<T> = {
|
||||
default: T // The default value for the field with proper type casting using as (e.g., `true as boolean | undefined`)
|
||||
isAsync?: boolean
|
||||
isComputed?: boolean
|
||||
transform?: (value: any) => T
|
||||
}
|
||||
|
||||
export type RemoteConfigFields = GlobalStateAndSettings & RemoteConfigExtraFields
|
||||
type FieldDefinitions = Record<string, FieldDefinition<any>>
|
||||
|
||||
export interface GlobalState {
|
||||
lastShownAnnouncementId: string | undefined
|
||||
taskHistory: HistoryItem[]
|
||||
userInfo: UserInfo | undefined
|
||||
favoritedModelIds: string[]
|
||||
mcpMarketplaceEnabled: boolean
|
||||
mcpResponsesCollapsed: boolean
|
||||
terminalReuseEnabled: boolean
|
||||
vscodeTerminalExecutionMode: "vscodeTerminal" | "backgroundExec"
|
||||
isNewUser: boolean
|
||||
welcomeViewCompleted: boolean | undefined
|
||||
mcpDisplayMode: McpDisplayMode
|
||||
// Multi-root workspace support
|
||||
workspaceRoots: WorkspaceRoot[] | undefined
|
||||
primaryRootIndex: number
|
||||
multiRootEnabled: boolean
|
||||
lastDismissedInfoBannerVersion: number
|
||||
lastDismissedModelBannerVersion: number
|
||||
lastDismissedCliBannerVersion: number
|
||||
nativeToolCallEnabled: boolean
|
||||
remoteRulesToggles: ClineRulesToggles
|
||||
remoteWorkflowToggles: ClineRulesToggles
|
||||
dismissedBanners: Array<{ bannerId: string; dismissedAt: number }>
|
||||
}
|
||||
const REMOTE_CONFIG_EXTRA_FIELDS = {
|
||||
remoteConfiguredProviders: { default: [] as ApiProvider[] },
|
||||
allowedMCPServers: { default: [] as Array<{ id: string }> },
|
||||
remoteMCPServers: { default: undefined as Array<{ name: string; url: string; alwaysEnabled?: boolean }> | undefined },
|
||||
previousRemoteMCPServers: { default: undefined as Array<{ name: string; url: string }> | undefined },
|
||||
remoteGlobalRules: { default: undefined as GlobalInstructionsFile[] | undefined },
|
||||
remoteGlobalWorkflows: { default: undefined as GlobalInstructionsFile[] | undefined },
|
||||
blockPersonalRemoteMCPServers: { default: false as boolean },
|
||||
openTelemetryOtlpHeaders: { default: undefined as Record<string, string> | undefined },
|
||||
} satisfies FieldDefinitions
|
||||
|
||||
export interface Settings {
|
||||
awsRegion: string | undefined
|
||||
awsUseCrossRegionInference: boolean | undefined
|
||||
awsUseGlobalInference: boolean | undefined
|
||||
awsBedrockUsePromptCache: boolean | undefined
|
||||
awsBedrockEndpoint: string | undefined
|
||||
awsProfile: string | undefined
|
||||
awsAuthentication: string | undefined
|
||||
awsUseProfile: boolean | undefined
|
||||
vertexProjectId: string | undefined
|
||||
vertexRegion: string | undefined
|
||||
requestyBaseUrl: string | undefined
|
||||
openAiBaseUrl: string | undefined
|
||||
openAiHeaders: Record<string, string>
|
||||
ollamaBaseUrl: string | undefined
|
||||
ollamaApiOptionsCtxNum: string | undefined
|
||||
lmStudioBaseUrl: string | undefined
|
||||
lmStudioMaxTokens: string | undefined
|
||||
anthropicBaseUrl: string | undefined
|
||||
geminiBaseUrl: string | undefined
|
||||
azureApiVersion: string | undefined
|
||||
azureIdentity: boolean | undefined
|
||||
openRouterProviderSorting: string | undefined
|
||||
autoApprovalSettings: AutoApprovalSettings
|
||||
globalClineRulesToggles: ClineRulesToggles
|
||||
globalWorkflowToggles: ClineRulesToggles
|
||||
browserSettings: BrowserSettings
|
||||
liteLlmBaseUrl: string | undefined
|
||||
liteLlmUsePromptCache: boolean | undefined
|
||||
fireworksModelMaxCompletionTokens: number | undefined
|
||||
fireworksModelMaxTokens: number | undefined
|
||||
qwenApiLine: string | undefined
|
||||
moonshotApiLine: string | undefined
|
||||
zaiApiLine: string | undefined
|
||||
telemetrySetting: TelemetrySetting
|
||||
asksageApiUrl: string | undefined
|
||||
planActSeparateModelsSetting: boolean
|
||||
enableCheckpointsSetting: boolean
|
||||
requestTimeoutMs: number | undefined
|
||||
shellIntegrationTimeout: number
|
||||
defaultTerminalProfile: string
|
||||
terminalOutputLineLimit: number
|
||||
maxConsecutiveMistakes: number
|
||||
subagentTerminalOutputLineLimit: number
|
||||
sapAiCoreTokenUrl: string | undefined
|
||||
sapAiCoreBaseUrl: string | undefined
|
||||
sapAiResourceGroup: string | undefined
|
||||
sapAiCoreUseOrchestrationMode: boolean | undefined
|
||||
claudeCodePath: string | undefined
|
||||
qwenCodeOauthPath: string | undefined
|
||||
strictPlanModeEnabled: boolean
|
||||
yoloModeToggled: boolean
|
||||
useAutoCondense: boolean
|
||||
clineWebToolsEnabled: boolean
|
||||
preferredLanguage: string
|
||||
openaiReasoningEffort: OpenaiReasoningEffort
|
||||
mode: Mode
|
||||
dictationSettings: DictationSettings
|
||||
focusChainSettings: FocusChainSettings
|
||||
customPrompt: "compact" | undefined
|
||||
difyBaseUrl: string | undefined
|
||||
autoCondenseThreshold: number | undefined // number from 0 to 1
|
||||
ocaBaseUrl: string | undefined
|
||||
minimaxApiLine: string | undefined
|
||||
ocaMode: string | undefined
|
||||
aihubmixBaseUrl: string | undefined
|
||||
aihubmixAppCode: string | undefined
|
||||
openAiCodexTokenExpiry: number | undefined
|
||||
hooksEnabled: boolean
|
||||
subagentsEnabled: boolean
|
||||
skillsEnabled: boolean
|
||||
globalSkillsToggles: Record<string, boolean>
|
||||
enableParallelToolCalling: boolean
|
||||
backgroundEditEnabled: boolean
|
||||
const GLOBAL_STATE_FIELDS = {
|
||||
lastShownAnnouncementId: { default: undefined as string | undefined },
|
||||
taskHistory: { default: [] as HistoryItem[], isAsync: true },
|
||||
userInfo: { default: undefined as UserInfo | undefined },
|
||||
favoritedModelIds: { default: [] as string[] },
|
||||
mcpMarketplaceEnabled: { default: true as boolean },
|
||||
mcpResponsesCollapsed: { default: false as boolean },
|
||||
terminalReuseEnabled: { default: true as boolean },
|
||||
vscodeTerminalExecutionMode: {
|
||||
default: "vscodeTerminal" as "vscodeTerminal" | "backgroundExec",
|
||||
},
|
||||
isNewUser: { default: true as boolean },
|
||||
welcomeViewCompleted: { default: undefined as boolean | undefined },
|
||||
mcpDisplayMode: { default: DEFAULT_MCP_DISPLAY_MODE as McpDisplayMode },
|
||||
workspaceRoots: { default: undefined as WorkspaceRoot[] | undefined },
|
||||
primaryRootIndex: { default: 0 as number },
|
||||
multiRootEnabled: { default: false as boolean },
|
||||
lastDismissedInfoBannerVersion: { default: 0 as number },
|
||||
lastDismissedModelBannerVersion: { default: 0 as number },
|
||||
lastDismissedCliBannerVersion: { default: 0 as number },
|
||||
nativeToolCallEnabled: { default: true as boolean },
|
||||
remoteRulesToggles: { default: {} as ClineRulesToggles },
|
||||
remoteWorkflowToggles: { default: {} as ClineRulesToggles },
|
||||
dismissedBanners: { default: [] as Array<{ bannerId: string; dismissedAt: number }> },
|
||||
} satisfies FieldDefinitions
|
||||
|
||||
// Fields that map directly to ApiHandlerOptions in @shared/api.ts
|
||||
const API_HANDLER_SETTINGS_FIELDS = {
|
||||
// Global configuration (not mode-specific)
|
||||
liteLlmBaseUrl: { default: undefined as string | undefined },
|
||||
liteLlmUsePromptCache: { default: undefined as boolean | undefined },
|
||||
openAiHeaders: { default: {} as Record<string, string> },
|
||||
anthropicBaseUrl: { default: undefined as string | undefined },
|
||||
openRouterProviderSorting: { default: undefined as string | undefined },
|
||||
awsRegion: { default: undefined as string | undefined },
|
||||
awsUseCrossRegionInference: { default: undefined as boolean | undefined },
|
||||
awsUseGlobalInference: { default: undefined as boolean | undefined },
|
||||
awsBedrockUsePromptCache: { default: undefined as boolean | undefined },
|
||||
awsAuthentication: { default: undefined as string | undefined },
|
||||
awsUseProfile: { default: undefined as boolean | undefined },
|
||||
awsProfile: { default: undefined as string | undefined },
|
||||
awsBedrockEndpoint: { default: undefined as string | undefined },
|
||||
claudeCodePath: { default: undefined as string | undefined },
|
||||
vertexProjectId: { default: undefined as string | undefined },
|
||||
vertexRegion: { default: undefined as string | undefined },
|
||||
openAiBaseUrl: { default: undefined as string | undefined },
|
||||
ollamaBaseUrl: { default: undefined as string | undefined },
|
||||
ollamaApiOptionsCtxNum: { default: undefined as string | undefined },
|
||||
lmStudioBaseUrl: { default: undefined as string | undefined },
|
||||
lmStudioMaxTokens: { default: undefined as string | undefined },
|
||||
geminiBaseUrl: { default: undefined as string | undefined },
|
||||
requestyBaseUrl: { default: undefined as string | undefined },
|
||||
fireworksModelMaxCompletionTokens: { default: undefined as number | undefined },
|
||||
fireworksModelMaxTokens: { default: undefined as number | undefined },
|
||||
qwenCodeOauthPath: { default: undefined as string | undefined },
|
||||
azureApiVersion: { default: undefined as string | undefined },
|
||||
azureIdentity: { default: undefined as boolean | undefined },
|
||||
qwenApiLine: { default: undefined as string | undefined },
|
||||
moonshotApiLine: { default: undefined as string | undefined },
|
||||
asksageApiUrl: { default: undefined as string | undefined },
|
||||
requestTimeoutMs: { default: undefined as number | undefined },
|
||||
sapAiResourceGroup: { default: undefined as string | undefined },
|
||||
sapAiCoreTokenUrl: { default: undefined as string | undefined },
|
||||
sapAiCoreBaseUrl: { default: undefined as string | undefined },
|
||||
sapAiCoreUseOrchestrationMode: { default: true as boolean },
|
||||
difyBaseUrl: { default: undefined as string | undefined },
|
||||
zaiApiLine: { default: undefined as string | undefined },
|
||||
ocaBaseUrl: { default: undefined as string | undefined },
|
||||
minimaxApiLine: { default: undefined as string | undefined },
|
||||
ocaMode: { default: "internal" as string },
|
||||
aihubmixBaseUrl: { default: undefined as string | undefined },
|
||||
aihubmixAppCode: { default: undefined as string | undefined },
|
||||
|
||||
// Plan mode configurations
|
||||
planModeApiModelId: { default: undefined as string | undefined },
|
||||
planModeThinkingBudgetTokens: { default: ANTHROPIC_MIN_THINKING_BUDGET as number | undefined },
|
||||
geminiPlanModeThinkingLevel: { default: undefined as string | undefined },
|
||||
planModeReasoningEffort: { default: undefined as string | undefined },
|
||||
planModeVerbosity: { default: undefined as string | undefined },
|
||||
planModeVsCodeLmModelSelector: { default: undefined as LanguageModelChatSelector | undefined },
|
||||
planModeAwsBedrockCustomSelected: { default: undefined as boolean | undefined },
|
||||
planModeAwsBedrockCustomModelBaseId: { default: undefined as string | undefined },
|
||||
planModeOpenRouterModelId: { default: undefined as string | undefined },
|
||||
planModeOpenRouterModelInfo: { default: undefined as ModelInfo | undefined },
|
||||
planModeOpenAiModelId: { default: undefined as string | undefined },
|
||||
planModeOpenAiModelInfo: { default: undefined as OpenAiCompatibleModelInfo | undefined },
|
||||
planModeOllamaModelId: { default: undefined as string | undefined },
|
||||
planModeLmStudioModelId: { default: undefined as string | undefined },
|
||||
planModeLiteLlmModelId: { default: undefined as string | undefined },
|
||||
planModeLiteLlmModelInfo: { default: undefined as LiteLLMModelInfo | undefined },
|
||||
planModeRequestyModelId: { default: undefined as string | undefined },
|
||||
planModeRequestyModelInfo: { default: undefined as ModelInfo | undefined },
|
||||
planModeTogetherModelId: { default: undefined as string | undefined },
|
||||
planModeFireworksModelId: { default: undefined as string | undefined },
|
||||
planModeSapAiCoreModelId: { default: undefined as string | undefined },
|
||||
planModeSapAiCoreDeploymentId: { default: undefined as string | undefined },
|
||||
planModeGroqModelId: { default: undefined as string | undefined },
|
||||
planModeGroqModelInfo: { default: undefined as ModelInfo | undefined },
|
||||
planModeBasetenModelId: { default: undefined as string | undefined },
|
||||
planModeBasetenModelInfo: { default: undefined as ModelInfo | undefined },
|
||||
planModeHuggingFaceModelId: { default: undefined as string | undefined },
|
||||
planModeHuggingFaceModelInfo: { default: undefined as ModelInfo | undefined },
|
||||
planModeHuaweiCloudMaasModelId: { default: undefined as string | undefined },
|
||||
planModeHuaweiCloudMaasModelInfo: { default: undefined as ModelInfo | undefined },
|
||||
planModeOcaModelId: { default: undefined as string | undefined },
|
||||
planModeOcaModelInfo: { default: undefined as OcaModelInfo | undefined },
|
||||
planModeOcaReasoningEffort: { default: undefined as string | undefined },
|
||||
planModeAihubmixModelId: { default: undefined as string | undefined },
|
||||
planModeAihubmixModelInfo: { default: undefined as OpenAiCompatibleModelInfo | undefined },
|
||||
planModeHicapModelId: { default: undefined as string | undefined },
|
||||
planModeHicapModelInfo: { default: undefined as ModelInfo | undefined },
|
||||
planModeNousResearchModelId: { default: undefined as string | undefined },
|
||||
planModeVercelAiGatewayModelId: { default: undefined as string | undefined },
|
||||
planModeVercelAiGatewayModelInfo: { default: undefined as ModelInfo | undefined },
|
||||
|
||||
// Act mode configurations
|
||||
actModeApiModelId: { default: undefined as string | undefined },
|
||||
actModeThinkingBudgetTokens: { default: ANTHROPIC_MIN_THINKING_BUDGET as number | undefined },
|
||||
geminiActModeThinkingLevel: { default: undefined as string | undefined },
|
||||
actModeReasoningEffort: { default: undefined as string | undefined },
|
||||
actModeVerbosity: { default: undefined as string | undefined },
|
||||
actModeVsCodeLmModelSelector: { default: undefined as LanguageModelChatSelector | undefined },
|
||||
actModeAwsBedrockCustomSelected: { default: undefined as boolean | undefined },
|
||||
actModeAwsBedrockCustomModelBaseId: { default: undefined as string | undefined },
|
||||
actModeOpenRouterModelId: { default: undefined as string | undefined },
|
||||
actModeOpenRouterModelInfo: { default: undefined as ModelInfo | undefined },
|
||||
actModeOpenAiModelId: { default: undefined as string | undefined },
|
||||
actModeOpenAiModelInfo: { default: undefined as OpenAiCompatibleModelInfo | undefined },
|
||||
actModeOllamaModelId: { default: undefined as string | undefined },
|
||||
actModeLmStudioModelId: { default: undefined as string | undefined },
|
||||
actModeLiteLlmModelId: { default: undefined as string | undefined },
|
||||
actModeLiteLlmModelInfo: { default: undefined as LiteLLMModelInfo | undefined },
|
||||
actModeRequestyModelId: { default: undefined as string | undefined },
|
||||
actModeRequestyModelInfo: { default: undefined as ModelInfo | undefined },
|
||||
actModeTogetherModelId: { default: undefined as string | undefined },
|
||||
actModeFireworksModelId: { default: undefined as string | undefined },
|
||||
actModeSapAiCoreModelId: { default: undefined as string | undefined },
|
||||
actModeSapAiCoreDeploymentId: { default: undefined as string | undefined },
|
||||
actModeGroqModelId: { default: undefined as string | undefined },
|
||||
actModeGroqModelInfo: { default: undefined as ModelInfo | undefined },
|
||||
actModeBasetenModelId: { default: undefined as string | undefined },
|
||||
actModeBasetenModelInfo: { default: undefined as ModelInfo | undefined },
|
||||
actModeHuggingFaceModelId: { default: undefined as string | undefined },
|
||||
actModeHuggingFaceModelInfo: { default: undefined as ModelInfo | undefined },
|
||||
actModeHuaweiCloudMaasModelId: { default: undefined as string | undefined },
|
||||
actModeHuaweiCloudMaasModelInfo: { default: undefined as ModelInfo | undefined },
|
||||
actModeOcaModelId: { default: undefined as string | undefined },
|
||||
actModeOcaModelInfo: { default: undefined as OcaModelInfo | undefined },
|
||||
actModeOcaReasoningEffort: { default: undefined as string | undefined },
|
||||
actModeAihubmixModelId: { default: undefined as string | undefined },
|
||||
actModeAihubmixModelInfo: { default: undefined as OpenAiCompatibleModelInfo | undefined },
|
||||
actModeHicapModelId: { default: undefined as string | undefined },
|
||||
actModeHicapModelInfo: { default: undefined as ModelInfo | undefined },
|
||||
actModeNousResearchModelId: { default: undefined as string | undefined },
|
||||
actModeVercelAiGatewayModelId: { default: undefined as string | undefined },
|
||||
actModeVercelAiGatewayModelInfo: { default: undefined as ModelInfo | undefined },
|
||||
|
||||
// Model-specific settings
|
||||
hicapModelId: string | undefined
|
||||
// Plan mode configurations
|
||||
planModeApiProvider: ApiProvider
|
||||
planModeApiModelId: string | undefined
|
||||
planModeThinkingBudgetTokens: number | undefined
|
||||
geminiPlanModeThinkingLevel: string | undefined
|
||||
planModeReasoningEffort: string | undefined
|
||||
planModeVsCodeLmModelSelector: LanguageModelChatSelector | undefined
|
||||
planModeAwsBedrockCustomSelected: boolean | undefined
|
||||
planModeAwsBedrockCustomModelBaseId: string | undefined
|
||||
planModeOpenRouterModelId: string | undefined
|
||||
planModeOpenRouterModelInfo: ModelInfo | undefined
|
||||
planModeOpenAiModelId: string | undefined
|
||||
planModeOpenAiModelInfo: ModelInfo | undefined
|
||||
planModeOllamaModelId: string | undefined
|
||||
planModeLmStudioModelId: string | undefined
|
||||
planModeLiteLlmModelId: string | undefined
|
||||
planModeLiteLlmModelInfo: ModelInfo | undefined
|
||||
planModeRequestyModelId: string | undefined
|
||||
planModeRequestyModelInfo: ModelInfo | undefined
|
||||
planModeTogetherModelId: string | undefined
|
||||
planModeFireworksModelId: string | undefined
|
||||
planModeSapAiCoreModelId: string | undefined
|
||||
planModeSapAiCoreDeploymentId: string | undefined
|
||||
planModeGroqModelId: string | undefined
|
||||
planModeGroqModelInfo: ModelInfo | undefined
|
||||
planModeBasetenModelId: string | undefined
|
||||
planModeBasetenModelInfo: ModelInfo | undefined
|
||||
planModeHuggingFaceModelId: string | undefined
|
||||
planModeHuggingFaceModelInfo: ModelInfo | undefined
|
||||
planModeHuaweiCloudMaasModelId: string | undefined
|
||||
planModeHuaweiCloudMaasModelInfo: ModelInfo | undefined
|
||||
planModeOcaModelId: string | undefined
|
||||
planModeOcaModelInfo: OcaModelInfo | undefined
|
||||
planModeOcaReasoningEffort: string | undefined
|
||||
planModeHicapModelId: string | undefined
|
||||
planModeHicapModelInfo: ModelInfo | undefined
|
||||
planModeAihubmixModelId: string | undefined
|
||||
planModeAihubmixModelInfo: ModelInfo | undefined
|
||||
planModeNousResearchModelId: string | undefined
|
||||
planModeVercelAiGatewayModelId: string | undefined
|
||||
planModeVercelAiGatewayModelInfo: ModelInfo | undefined
|
||||
// Act mode configurations
|
||||
actModeApiProvider: ApiProvider
|
||||
actModeApiModelId: string | undefined
|
||||
actModeThinkingBudgetTokens: number | undefined
|
||||
geminiActModeThinkingLevel: string | undefined
|
||||
actModeReasoningEffort: string | undefined
|
||||
actModeVsCodeLmModelSelector: LanguageModelChatSelector | undefined
|
||||
actModeAwsBedrockCustomSelected: boolean | undefined
|
||||
actModeAwsBedrockCustomModelBaseId: string | undefined
|
||||
actModeOpenRouterModelId: string | undefined
|
||||
actModeOpenRouterModelInfo: ModelInfo | undefined
|
||||
actModeOpenAiModelId: string | undefined
|
||||
actModeOpenAiModelInfo: ModelInfo | undefined
|
||||
actModeOllamaModelId: string | undefined
|
||||
actModeLmStudioModelId: string | undefined
|
||||
actModeLiteLlmModelId: string | undefined
|
||||
actModeLiteLlmModelInfo: ModelInfo | undefined
|
||||
actModeRequestyModelId: string | undefined
|
||||
actModeRequestyModelInfo: ModelInfo | undefined
|
||||
actModeTogetherModelId: string | undefined
|
||||
actModeFireworksModelId: string | undefined
|
||||
actModeSapAiCoreModelId: string | undefined
|
||||
actModeSapAiCoreDeploymentId: string | undefined
|
||||
actModeGroqModelId: string | undefined
|
||||
actModeGroqModelInfo: ModelInfo | undefined
|
||||
actModeBasetenModelId: string | undefined
|
||||
actModeBasetenModelInfo: ModelInfo | undefined
|
||||
actModeHuggingFaceModelId: string | undefined
|
||||
actModeHuggingFaceModelInfo: ModelInfo | undefined
|
||||
actModeHuaweiCloudMaasModelId: string | undefined
|
||||
actModeHuaweiCloudMaasModelInfo: ModelInfo | undefined
|
||||
actModeOcaModelId: string | undefined
|
||||
actModeOcaModelInfo: OcaModelInfo | undefined
|
||||
actModeOcaReasoningEffort: string | undefined
|
||||
actModeHicapModelId: string | undefined
|
||||
actModeHicapModelInfo: ModelInfo | undefined
|
||||
actModeAihubmixModelId: string | undefined
|
||||
actModeAihubmixModelInfo: ModelInfo | undefined
|
||||
actModeNousResearchModelId: string | undefined
|
||||
actModeVercelAiGatewayModelId: string | undefined
|
||||
actModeVercelAiGatewayModelInfo: ModelInfo | undefined
|
||||
planModeApiProvider: { default: DEFAULT_API_PROVIDER as ApiProvider },
|
||||
actModeApiProvider: { default: DEFAULT_API_PROVIDER as ApiProvider },
|
||||
|
||||
// Deprecated model settings
|
||||
hicapModelId: { default: undefined as string | undefined },
|
||||
lmStudioModelId: { default: undefined as string | undefined },
|
||||
} satisfies FieldDefinitions
|
||||
|
||||
const USER_SETTINGS_FIELDS = {
|
||||
// Settings that are NOT part of ApiHandlerOptions
|
||||
autoApprovalSettings: {
|
||||
default: DEFAULT_AUTO_APPROVAL_SETTINGS as AutoApprovalSettings,
|
||||
},
|
||||
globalClineRulesToggles: { default: {} as ClineRulesToggles },
|
||||
globalWorkflowToggles: { default: {} as ClineRulesToggles },
|
||||
globalSkillsToggles: { default: {} as Record<string, boolean> },
|
||||
browserSettings: {
|
||||
default: DEFAULT_BROWSER_SETTINGS as BrowserSettings,
|
||||
transform: (v: any) => ({ ...DEFAULT_BROWSER_SETTINGS, ...v }),
|
||||
},
|
||||
telemetrySetting: { default: "unset" as TelemetrySetting },
|
||||
planActSeparateModelsSetting: { default: false as boolean, isComputed: true },
|
||||
enableCheckpointsSetting: { default: true as boolean },
|
||||
shellIntegrationTimeout: { default: 4000 as number },
|
||||
defaultTerminalProfile: { default: "default" as string },
|
||||
terminalOutputLineLimit: { default: 500 as number },
|
||||
maxConsecutiveMistakes: { default: 3 as number },
|
||||
subagentTerminalOutputLineLimit: { default: 2000 as number },
|
||||
strictPlanModeEnabled: { default: true as boolean },
|
||||
yoloModeToggled: { default: false as boolean },
|
||||
useAutoCondense: { default: false as boolean },
|
||||
clineWebToolsEnabled: { default: true as boolean },
|
||||
preferredLanguage: { default: "English" as string },
|
||||
openaiReasoningEffort: { default: "medium" as OpenaiReasoningEffort },
|
||||
mode: { default: "act" as Mode },
|
||||
dictationSettings: {
|
||||
default: DEFAULT_DICTATION_SETTINGS as DictationSettings,
|
||||
transform: (v: any) => ({ ...DEFAULT_DICTATION_SETTINGS, ...v }),
|
||||
},
|
||||
focusChainSettings: { default: DEFAULT_FOCUS_CHAIN_SETTINGS as FocusChainSettings },
|
||||
customPrompt: { default: undefined as "compact" | undefined },
|
||||
autoCondenseThreshold: { default: 0.75 as number }, // number from 0 to 1
|
||||
hooksEnabled: { default: false as boolean },
|
||||
subagentsEnabled: { default: false as boolean },
|
||||
enableParallelToolCalling: { default: false as boolean },
|
||||
backgroundEditEnabled: { default: false as boolean },
|
||||
skillsEnabled: { default: false as boolean },
|
||||
optOutOfRemoteConfig: { default: false as boolean },
|
||||
|
||||
// OpenTelemetry configuration
|
||||
openTelemetryEnabled: boolean
|
||||
openTelemetryMetricsExporter: string | undefined
|
||||
openTelemetryLogsExporter: string | undefined
|
||||
openTelemetryOtlpProtocol: string
|
||||
openTelemetryOtlpEndpoint: string
|
||||
openTelemetryOtlpMetricsProtocol: string | undefined
|
||||
openTelemetryOtlpMetricsEndpoint: string | undefined
|
||||
openTelemetryOtlpLogsProtocol: string | undefined
|
||||
openTelemetryOtlpLogsEndpoint: string | undefined
|
||||
openTelemetryMetricExportInterval: number
|
||||
openTelemetryOtlpInsecure: boolean
|
||||
openTelemetryLogBatchSize: number
|
||||
openTelemetryLogBatchTimeout: number
|
||||
openTelemetryLogMaxQueueSize: number
|
||||
openTelemetryEnabled: { default: true as boolean },
|
||||
openTelemetryMetricsExporter: { default: undefined as string | undefined },
|
||||
openTelemetryLogsExporter: { default: undefined as string | undefined },
|
||||
openTelemetryOtlpProtocol: { default: "http/json" as string | undefined },
|
||||
openTelemetryOtlpEndpoint: { default: "http://localhost:4318" as string | undefined },
|
||||
openTelemetryOtlpMetricsProtocol: { default: undefined as string | undefined },
|
||||
openTelemetryOtlpMetricsEndpoint: { default: undefined as string | undefined },
|
||||
openTelemetryOtlpLogsProtocol: { default: undefined as string | undefined },
|
||||
openTelemetryOtlpLogsEndpoint: { default: undefined as string | undefined },
|
||||
openTelemetryMetricExportInterval: { default: 60000 as number | undefined },
|
||||
openTelemetryOtlpInsecure: { default: false as boolean | undefined },
|
||||
openTelemetryLogBatchSize: { default: 512 as number | undefined },
|
||||
openTelemetryLogBatchTimeout: { default: 5000 as number | undefined },
|
||||
openTelemetryLogMaxQueueSize: { default: 2048 as number | undefined },
|
||||
} satisfies FieldDefinitions
|
||||
|
||||
const SETTINGS_FIELDS = { ...API_HANDLER_SETTINGS_FIELDS, ...USER_SETTINGS_FIELDS }
|
||||
const GLOBAL_STATE_AND_SETTINGS_FIELDS = { ...GLOBAL_STATE_FIELDS, ...SETTINGS_FIELDS }
|
||||
|
||||
// ============================================================================
|
||||
// SECRET KEYS AND LOCAL STATE - Static definitions
|
||||
// ============================================================================
|
||||
|
||||
// Secret keys used in Api Configuration
|
||||
const SECRETS_KEYS = [
|
||||
"apiKey",
|
||||
"clineAccountId", // Cline Account ID for Firebase
|
||||
"cline:clineAccountId",
|
||||
"openRouterApiKey",
|
||||
"awsAccessKey",
|
||||
"awsSecretKey",
|
||||
"awsSessionToken",
|
||||
"awsBedrockApiKey",
|
||||
"openAiApiKey",
|
||||
"geminiApiKey",
|
||||
"openAiNativeApiKey",
|
||||
"ollamaApiKey",
|
||||
"deepSeekApiKey",
|
||||
"requestyApiKey",
|
||||
"togetherApiKey",
|
||||
"fireworksApiKey",
|
||||
"qwenApiKey",
|
||||
"doubaoApiKey",
|
||||
"mistralApiKey",
|
||||
"liteLlmApiKey",
|
||||
"authNonce",
|
||||
"asksageApiKey",
|
||||
"xaiApiKey",
|
||||
"moonshotApiKey",
|
||||
"zaiApiKey",
|
||||
"huggingFaceApiKey",
|
||||
"nebiusApiKey",
|
||||
"sambanovaApiKey",
|
||||
"cerebrasApiKey",
|
||||
"sapAiCoreClientId",
|
||||
"sapAiCoreClientSecret",
|
||||
"groqApiKey",
|
||||
"huaweiCloudMaasApiKey",
|
||||
"basetenApiKey",
|
||||
"vercelAiGatewayApiKey",
|
||||
"difyApiKey",
|
||||
"minimaxApiKey",
|
||||
"hicapApiKey",
|
||||
"aihubmixApiKey",
|
||||
"nousResearchApiKey",
|
||||
"remoteLiteLlmApiKey",
|
||||
"ocaApiKey",
|
||||
"ocaRefreshToken",
|
||||
"mcpOAuthSecrets",
|
||||
] as const
|
||||
|
||||
export const LocalStateKeys = [
|
||||
"localClineRulesToggles",
|
||||
"localCursorRulesToggles",
|
||||
"localWindsurfRulesToggles",
|
||||
"localAgentsRulesToggles",
|
||||
"localSkillsToggles",
|
||||
"workflowToggles",
|
||||
] as const
|
||||
|
||||
// ============================================================================
|
||||
// GENERATED TYPES - Auto-generated from property definitions
|
||||
// ============================================================================
|
||||
|
||||
type ExtractDefault<T> = T extends { default: infer U } ? U : never
|
||||
type BuildInterface<T extends Record<string, { default: any }>> = { [K in keyof T]: ExtractDefault<T[K]> }
|
||||
|
||||
export type GlobalState = BuildInterface<typeof GLOBAL_STATE_FIELDS>
|
||||
export type Settings = BuildInterface<typeof SETTINGS_FIELDS>
|
||||
type RemoteConfigExtra = BuildInterface<typeof REMOTE_CONFIG_EXTRA_FIELDS>
|
||||
export type ApiHandlerOptionSettings = BuildInterface<typeof API_HANDLER_SETTINGS_FIELDS>
|
||||
export type ApiHandlerSettings = ApiHandlerOptionSettings & Secrets
|
||||
export type GlobalStateAndSettings = GlobalState & Settings
|
||||
export type RemoteConfigFields = GlobalStateAndSettings & RemoteConfigExtra
|
||||
|
||||
// ============================================================================
|
||||
// TYPE ALIASES
|
||||
// ============================================================================
|
||||
|
||||
export type Secrets = { [K in (typeof SecretKeys)[number]]: string | undefined }
|
||||
export type LocalState = { [K in (typeof LocalStateKeys)[number]]: ClineRulesToggles }
|
||||
export type SecretKey = (typeof SecretKeys)[number]
|
||||
export type GlobalStateKey = keyof GlobalState
|
||||
export type LocalStateKey = keyof LocalState
|
||||
export type SettingsKey = keyof Settings
|
||||
export type GlobalStateAndSettingsKey = keyof GlobalStateAndSettings
|
||||
|
||||
// ============================================================================
|
||||
// GENERATED KEYS AND LOOKUP SETS - Auto-generated from property definitions
|
||||
// ============================================================================
|
||||
|
||||
const GlobalStateKeys = new Set(Object.keys(GLOBAL_STATE_FIELDS))
|
||||
const SettingsKeysSet = new Set(Object.keys(SETTINGS_FIELDS))
|
||||
const GlobalStateAndSettingsKeySet = new Set(Object.keys(GLOBAL_STATE_AND_SETTINGS_FIELDS))
|
||||
const ApiHandlerSettingsKeysSet = new Set(Object.keys(API_HANDLER_SETTINGS_FIELDS))
|
||||
|
||||
export const SecretKeys = Array.from(SECRETS_KEYS)
|
||||
export const SettingsKeys = Array.from(SettingsKeysSet) as (keyof Settings)[]
|
||||
export const ApiHandlerSettingsKeys = Array.from(ApiHandlerSettingsKeysSet) as (keyof ApiHandlerOptionSettings)[]
|
||||
export const GlobalStateAndSettingKeys = Array.from(GlobalStateAndSettingsKeySet) as GlobalStateAndSettingsKey[]
|
||||
|
||||
// GENERATED DEFAULTS - Auto-generated from property definitions
|
||||
// ============================================================================
|
||||
|
||||
export const GLOBAL_STATE_DEFAULTS = extractDefaults(GLOBAL_STATE_FIELDS)
|
||||
export const SETTINGS_DEFAULTS = extractDefaults(SETTINGS_FIELDS)
|
||||
export const SETTINGS_TRANSFORMS = extractTransforms(SETTINGS_FIELDS)
|
||||
export const ASYNC_PROPERTIES = extractMetadata({ ...GLOBAL_STATE_FIELDS, ...SETTINGS_FIELDS }, "isAsync")
|
||||
export const COMPUTED_PROPERTIES = extractMetadata({ ...GLOBAL_STATE_FIELDS, ...SETTINGS_FIELDS }, "isComputed")
|
||||
|
||||
// ============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
export const isGlobalStateKey = (key: string): key is GlobalStateKey => GlobalStateKeys.has(key)
|
||||
export const isSettingsKey = (key: string): key is SettingsKey => SettingsKeysSet.has(key)
|
||||
export const isSecretKey = (key: string): key is SecretKey => new Set(SECRETS_KEYS).has(key as SecretKey)
|
||||
export const isLocalStateKey = (key: string): key is LocalStateKey => new Set(LocalStateKeys).has(key as LocalStateKey)
|
||||
|
||||
// ============================================================================
|
||||
// UTILITY FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
export const isAsyncProperty = (key: string): boolean => ASYNC_PROPERTIES.has(key)
|
||||
export const isComputedProperty = (key: string): boolean => COMPUTED_PROPERTIES.has(key)
|
||||
|
||||
export const getDefaultValue = <K extends GlobalStateAndSettingsKey>(key: K): GlobalStateAndSettings[K] | undefined => {
|
||||
return ((GLOBAL_STATE_DEFAULTS as any)[key] ?? (SETTINGS_DEFAULTS as any)[key]) as GlobalStateAndSettings[K] | undefined
|
||||
}
|
||||
|
||||
export interface Secrets {
|
||||
apiKey: string | undefined
|
||||
clineAccountId: string | undefined
|
||||
"cline:clineAccountId": string | undefined // Auth_Provider:AccountId
|
||||
openRouterApiKey: string | undefined
|
||||
awsAccessKey: string | undefined
|
||||
awsSecretKey: string | undefined
|
||||
awsSessionToken: string | undefined
|
||||
awsBedrockApiKey: string | undefined
|
||||
openAiApiKey: string | undefined
|
||||
geminiApiKey: string | undefined
|
||||
openAiNativeApiKey: string | undefined
|
||||
ollamaApiKey: string | undefined
|
||||
deepSeekApiKey: string | undefined
|
||||
requestyApiKey: string | undefined
|
||||
togetherApiKey: string | undefined
|
||||
fireworksApiKey: string | undefined
|
||||
qwenApiKey: string | undefined
|
||||
doubaoApiKey: string | undefined
|
||||
mistralApiKey: string | undefined
|
||||
liteLlmApiKey: string | undefined
|
||||
remoteLiteLlmApiKey: string | undefined
|
||||
authNonce: string | undefined
|
||||
asksageApiKey: string | undefined
|
||||
xaiApiKey: string | undefined
|
||||
moonshotApiKey: string | undefined
|
||||
zaiApiKey: string | undefined
|
||||
huggingFaceApiKey: string | undefined
|
||||
nebiusApiKey: string | undefined
|
||||
sambanovaApiKey: string | undefined
|
||||
cerebrasApiKey: string | undefined
|
||||
sapAiCoreClientId: string | undefined
|
||||
sapAiCoreClientSecret: string | undefined
|
||||
groqApiKey: string | undefined
|
||||
huaweiCloudMaasApiKey: string | undefined
|
||||
basetenApiKey: string | undefined
|
||||
vercelAiGatewayApiKey: string | undefined
|
||||
difyApiKey: string | undefined
|
||||
ocaApiKey: string | undefined
|
||||
ocaRefreshToken: string | undefined
|
||||
minimaxApiKey: string | undefined
|
||||
hicapApiKey: string | undefined
|
||||
aihubmixApiKey: string | undefined
|
||||
mcpOAuthSecrets: string | undefined
|
||||
nousResearchApiKey: string | undefined
|
||||
openAiCodexAccessToken: string | undefined
|
||||
openAiCodexRefreshToken: string | undefined
|
||||
openAiCodexAccountId: string | undefined
|
||||
export const hasTransform = (key: string): boolean => key in SETTINGS_TRANSFORMS
|
||||
export const applyTransform = <T>(key: string, value: T): T => {
|
||||
const transform = SETTINGS_TRANSFORMS[key]
|
||||
return transform ? transform(value) : value
|
||||
}
|
||||
|
||||
export interface LocalState {
|
||||
localClineRulesToggles: ClineRulesToggles
|
||||
localCursorRulesToggles: ClineRulesToggles
|
||||
localWindsurfRulesToggles: ClineRulesToggles
|
||||
localAgentsRulesToggles: ClineRulesToggles
|
||||
workflowToggles: ClineRulesToggles
|
||||
localSkillsToggles: ClineRulesToggles
|
||||
function extractDefaults<T extends Record<string, any>>(props: T): Partial<BuildInterface<T>> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(props)
|
||||
.map(([key, prop]) => [key, prop.default])
|
||||
.filter(([_, value]) => value !== undefined),
|
||||
) as Partial<BuildInterface<T>>
|
||||
}
|
||||
|
||||
function extractTransforms<T extends Record<string, any>>(props: T): Record<string, (value: any) => any> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(props)
|
||||
.filter(([_, prop]) => "transform" in prop && prop.transform !== undefined)
|
||||
.map(([key, prop]) => [key, prop.transform]),
|
||||
)
|
||||
}
|
||||
|
||||
function extractMetadata<T extends Record<string, any>>(props: T, field: string): Set<string> {
|
||||
return new Set(
|
||||
Object.entries(props)
|
||||
.filter(([_, prop]) => field in prop && prop[field] === true)
|
||||
.map(([key]) => key),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,9 +16,6 @@ e2e.describe("Diff Editor", () => {
|
||||
await sidebar.getByTestId("send-button").click()
|
||||
await expect(inputbox).toHaveValue("")
|
||||
|
||||
// Loading State initially
|
||||
await expect(sidebar.getByText("API Request...")).toBeVisible({ timeout: 10000 })
|
||||
|
||||
// Back to home page with history
|
||||
await sidebar.getByRole("button", { name: "Start New Task" }).click()
|
||||
await expect(sidebar.getByText("Recent Tasks")).toBeVisible()
|
||||
|
||||
@@ -97,7 +97,8 @@ export class E2ETestHelper {
|
||||
return null
|
||||
}
|
||||
|
||||
await E2ETestHelper.waitUntil(async () => (await findSidebarFrame()) !== null)
|
||||
// Use longer timeout (30s) for sidebar - macOS CI runners can be slow
|
||||
await E2ETestHelper.waitUntil(async () => (await findSidebarFrame()) !== null, 30000)
|
||||
return (await findSidebarFrame()) || page.mainFrame()
|
||||
}
|
||||
|
||||
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
declare module "picomatch" {
|
||||
type PicomatchOptions = {
|
||||
dot?: boolean
|
||||
nocase?: boolean
|
||||
ignore?: string | string[]
|
||||
posix?: boolean
|
||||
windows?: boolean
|
||||
}
|
||||
|
||||
type PicomatchMatcher = (input: string) => boolean
|
||||
|
||||
function picomatch(pattern: string | string[], options?: PicomatchOptions): PicomatchMatcher
|
||||
|
||||
export default picomatch
|
||||
}
|
||||
+1
@@ -21,6 +21,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.3.tgz",
|
||||
"integrity": "sha512-FTXHdOoPbZrBjlVLHuKbDZnsTxXv2BlHF57xw6LuThXacXvtkahEPED0CKMk6obZDf65Hv4k3z62eyPNpvinIg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@grpc/proto-loader": "^0.7.13",
|
||||
"@js-sdsl/ordered-map": "^4.4.2"
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
],
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
"./src/types",
|
||||
"./src/test/types"
|
||||
],
|
||||
"outDir": "out",
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node"
|
||||
"moduleResolution": "node",
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
"./src/types"
|
||||
]
|
||||
},
|
||||
"ts-node": {
|
||||
"require": [
|
||||
|
||||
@@ -511,7 +511,8 @@ const createErrorMessages = () => [
|
||||
"text",
|
||||
"I can see there are TypeScript errors in your code. Let me examine the files and fix these issues.",
|
||||
),
|
||||
createMessage(3.3, "say", "tool", JSON.stringify({ tool: "readFile", path: "src/components/UserProfile.tsx" })),
|
||||
createMessage(3.3, "say", "tool", JSON.stringify({ tool: "readFile", path: "src/components/UserProfile_1.tsx" })),
|
||||
createMessage(3.3, "say", "tool", JSON.stringify({ tool: "readFile", path: "src/components/UserProfile_2.tsx" })),
|
||||
createMessage(
|
||||
3,
|
||||
"say",
|
||||
@@ -569,7 +570,7 @@ const createPlanModeMessages = () => [
|
||||
createApiReqMessage(4.5, "Detailed planning request", { tokensIn: 20002, tokensOut: 12500, cost: 0.095 }),
|
||||
createAskMessage(
|
||||
"plan_mode_respond",
|
||||
"Here's my comprehensive plan for refactoring your React application with TypeScript migration and performance optimization phases.",
|
||||
"Here's my comprehensive plan for refactoring your React application with TypeScript migration and performance optimization phases.\n\n\n\n\nPhase 1: TypeScript Migration\n1. Set up TypeScript in the project\n2. Rename .js files to .tsx/.ts\n3. Add type definitions for components and props\n4. Fix type errors and ensure type safety\n\nPhase 2: Performance Optimization\n1. Analyze current performance bottlenecks\n2. Implement code-splitting and lazy loading\n3. Optimize rendering with React.memo and useCallback\n4. Minimize bundle size with tree-shaking and minification\n5. Test performance improvements using profiling tools",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -630,7 +631,7 @@ export const BrowserAutomation: Story = {
|
||||
const createToolApprovalMessages = () => [
|
||||
createMessage(5, "say", "task", "Help me read the configuration file"),
|
||||
createMessage(4.7, "say", "text", "I need to read a file to understand your configuration."),
|
||||
createAskMessage("tool", JSON.stringify({ tool: "read_file", path: "config.json" })),
|
||||
createAskMessage("tool", JSON.stringify({ tool: "readFile", path: "config.json" })),
|
||||
]
|
||||
|
||||
export const ToolApproval: Story = {
|
||||
@@ -676,6 +677,7 @@ const quickStory = (
|
||||
clineMessages: [
|
||||
...createLongMessages(),
|
||||
createMessage(6, "say", "task", `Help with ${name.toLowerCase()}`),
|
||||
createMessage(5, "say", "reasoning", `Thinking about helping user with ${name.toLowerCase()}`),
|
||||
createMessage(4.7, "say", "text", `I'll help you with ${name.toLowerCase()}.`),
|
||||
createAskMessage(askType, text, streamingFailedMessage),
|
||||
],
|
||||
@@ -725,7 +727,7 @@ export const MistakeLimitReached = quickStory(
|
||||
export const CompletionResult = quickStory(
|
||||
"Task Completion",
|
||||
"completion_result",
|
||||
"Task completed successfully! I've implemented all the requested features.",
|
||||
"Task completed successfully! I've implemented all the requested features.\n\nWould you like to start a new task?\n\n- View Changes\n- Start New Task\n- Resume Previous Task HAS_CHANGES",
|
||||
"Shows task completion state with Start New Task button.",
|
||||
)
|
||||
export const BrowserActionLaunch = quickStory(
|
||||
@@ -772,7 +774,7 @@ export const ApiRequestActive: Story = {
|
||||
export const PlanModeResponse = quickStory(
|
||||
"Plan Mode Response",
|
||||
"plan_mode_respond",
|
||||
"Here's my detailed plan for creating a comprehensive testing strategy.",
|
||||
"Here's my comprehensive plan for refactoring your React application with TypeScript migration and performance optimization phases.\n\n\n\n\nPhase 1: TypeScript Migration\n1. Set up TypeScript in the project\n2. Rename .js files to .tsx/.ts\n3. Add type definitions for components and props\n4. Fix type errors and ensure type safety\n\nPhase 2: Performance Optimization\n1. Analyze current performance bottlenecks\n2. Implement code-splitting and lazy loading\n3. Optimize rendering with React.memo and useCallback\n4. Minimize bundle size with tree-shaking and minification\n5. Test performance improvements using profiling tools",
|
||||
"Shows plan mode response where Cline presents a detailed plan for user approval.",
|
||||
)
|
||||
export const CondenseConversation = quickStory(
|
||||
@@ -784,7 +786,10 @@ export const CondenseConversation = quickStory(
|
||||
export const ReportBug = quickStory(
|
||||
"Report Bug",
|
||||
"report_bug",
|
||||
"Would you like to report this issue to help improve Cline?",
|
||||
JSON.stringify({
|
||||
steps_to_reproduce: "1. Open Cline\n2. Start a new task\n3. Observe the error",
|
||||
what_happened: "Cline crashes unexpectedly",
|
||||
}),
|
||||
"Shows utility action to report bugs to the GitHub repository.",
|
||||
)
|
||||
export const ResumeCompletedTask = quickStory(
|
||||
@@ -794,6 +799,231 @@ export const ResumeCompletedTask = quickStory(
|
||||
"Shows Start New Task option for resume completed task.",
|
||||
)
|
||||
|
||||
export const ShellIntegrationWarningWithSuggestion: Story = {
|
||||
decorators: [
|
||||
createStoryDecorator({
|
||||
clineMessages: [
|
||||
createMessage(5, "say", "task", "Run a command"),
|
||||
createMessage(4.7, "say", "text", "I'll run the command for you."),
|
||||
createMessage(4.5, "say", "shell_integration_warning_with_suggestion", ""),
|
||||
],
|
||||
vscodeTerminalExecutionMode: "integrated",
|
||||
}),
|
||||
],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Shows shell integration warning with suggestion to enable Background Terminal mode.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const ShellIntegrationWarningBackgroundEnabled: Story = {
|
||||
decorators: [
|
||||
createStoryDecorator({
|
||||
clineMessages: [
|
||||
createMessage(5, "say", "task", "Run a command"),
|
||||
createMessage(4.7, "say", "text", "I'll run the command for you."),
|
||||
createMessage(4.5, "say", "shell_integration_warning_with_suggestion", ""),
|
||||
],
|
||||
vscodeTerminalExecutionMode: "backgroundExec",
|
||||
}),
|
||||
],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Shows shell integration warning when Background Terminal mode is already enabled.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const ShellIntegrationWarning: Story = {
|
||||
decorators: [
|
||||
createStoryDecorator({
|
||||
clineMessages: [
|
||||
createMessage(5, "say", "task", "Run a command"),
|
||||
createMessage(4.7, "say", "text", "I'll run the command for you."),
|
||||
createMessage(4.5, "say", "shell_integration_warning", ""),
|
||||
],
|
||||
}),
|
||||
],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Shows shell integration unavailable warning with instructions to update VSCode and select a supported shell.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const ErrorRetryInProgress: Story = {
|
||||
decorators: [
|
||||
createStoryDecorator({
|
||||
clineMessages: [
|
||||
createMessage(5, "say", "task", "Process a request"),
|
||||
createMessage(4.7, "say", "text", "Attempting to process your request."),
|
||||
createMessage(
|
||||
4.5,
|
||||
"say",
|
||||
"error_retry",
|
||||
JSON.stringify({ attempt: 2, maxAttempts: 5, delaySeconds: 10, failed: false }),
|
||||
),
|
||||
],
|
||||
}),
|
||||
],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Shows auto-retry in progress with attempt count and delay.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const ErrorRetryFailed: Story = {
|
||||
decorators: [
|
||||
createStoryDecorator({
|
||||
clineMessages: [
|
||||
createMessage(5, "say", "task", "Process a request"),
|
||||
createMessage(4.7, "say", "text", "Attempting to process your request."),
|
||||
createMessage(
|
||||
4.5,
|
||||
"say",
|
||||
"error_retry",
|
||||
JSON.stringify({ attempt: 5, maxAttempts: 5, delaySeconds: 0, failed: true }),
|
||||
),
|
||||
],
|
||||
}),
|
||||
],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Shows auto-retry failed after max attempts with manual intervention required.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const GenerateExplanationInProgress: Story = {
|
||||
decorators: [
|
||||
createStoryDecorator({
|
||||
clineMessages: [
|
||||
createMessage(5, "say", "task", "Explain my recent changes"),
|
||||
createMessage(4.7, "say", "text", "I'll generate an explanation of your changes."),
|
||||
createMessage(
|
||||
4.5,
|
||||
"say",
|
||||
"generate_explanation",
|
||||
JSON.stringify({
|
||||
title: "Authentication refactor",
|
||||
fromRef: "abc123def",
|
||||
toRef: "working directory",
|
||||
status: "generating",
|
||||
}),
|
||||
),
|
||||
],
|
||||
}),
|
||||
],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Shows explanation generation in progress with spinner.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const GenerateExplanationComplete: Story = {
|
||||
decorators: [
|
||||
createStoryDecorator({
|
||||
clineMessages: [
|
||||
createMessage(5, "say", "task", "Explain my recent changes"),
|
||||
createMessage(4.7, "say", "text", "I'll generate an explanation of your changes."),
|
||||
createMessage(
|
||||
4.5,
|
||||
"say",
|
||||
"generate_explanation",
|
||||
JSON.stringify({
|
||||
title: "Authentication refactor",
|
||||
fromRef: "abc123def",
|
||||
toRef: "xyz789ghi",
|
||||
status: "complete",
|
||||
}),
|
||||
),
|
||||
],
|
||||
}),
|
||||
],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Shows successfully generated explanation with git refs.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const GenerateExplanationError: Story = {
|
||||
decorators: [
|
||||
createStoryDecorator({
|
||||
clineMessages: [
|
||||
createMessage(5, "say", "task", "Explain my recent changes"),
|
||||
createMessage(4.7, "say", "text", "I'll generate an explanation of your changes."),
|
||||
createMessage(
|
||||
4.5,
|
||||
"say",
|
||||
"generate_explanation",
|
||||
JSON.stringify({
|
||||
title: "Authentication refactor",
|
||||
fromRef: "abc123def",
|
||||
toRef: "",
|
||||
status: "error",
|
||||
error: "Failed to generate explanation: Git repository not found",
|
||||
}),
|
||||
),
|
||||
],
|
||||
}),
|
||||
],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Shows explanation generation error with error message.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const GenerateExplanationCancelled: Story = {
|
||||
decorators: [
|
||||
createStoryDecorator({
|
||||
clineMessages: [
|
||||
createMessage(5, "say", "task", "Explain my recent changes"),
|
||||
createMessage(4.7, "say", "text", "I'll generate an explanation of your changes."),
|
||||
createMessage(
|
||||
4.5,
|
||||
"say",
|
||||
"generate_explanation",
|
||||
JSON.stringify({
|
||||
title: "Authentication refactor",
|
||||
fromRef: "abc123def",
|
||||
toRef: "",
|
||||
status: "generating",
|
||||
}),
|
||||
),
|
||||
createMessage(4.3, "ask", undefined, "Task was cancelled", { ask: "resume_task" }),
|
||||
],
|
||||
}),
|
||||
],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Shows explanation generation cancelled state (detected via resume_task message).",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Diff Edit Stories - New Format
|
||||
const createNewFormatMultiFileMessages = () => [
|
||||
createMessage(5, "say", "task", "Help me refactor the authentication module"),
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export const ClineCompactIcon = () => (
|
||||
<svg height="16" viewBox="0 0 92 96" width="16">
|
||||
<g fill="currentColor">
|
||||
<path d="M65.4492701,16.3 C76.3374701,16.3 85.1635558,25.16479 85.1635558,36.1 L85.1635558,42.7 L90.9027661,54.1647464 C91.4694141,55.2966923 91.4668177,56.6300535 90.8957658,57.7597839 L85.1635558,69.1 L85.1635558,75.7 C85.1635558,86.63554 76.3374701,95.5 65.4492701,95.5 L26.0206986,95.5 C15.1328272,95.5 6.30641291,86.63554 6.30641291,75.7 L6.30641291,69.1 L0.448507752,57.7954874 C-0.14693501,56.6464093 -0.149634367,55.2802504 0.441262896,54.1288283 L6.30641291,42.7 L6.30641291,36.1 C6.30641291,25.16479 15.1328272,16.3 26.0206986,16.3 L65.4492701,16.3 Z M62.9301895,22 L29.189529,22 C19.8723267,22 12.3191987,29.5552188 12.3191987,38.875 L12.3191987,44.5 L7.44288578,53.9634655 C6.84794449,55.1180686 6.85066096,56.4896598 7.45017099,57.6418974 L12.3191987,67 L12.3191987,72.625 C12.3191987,81.9450625 19.8723267,89.5 29.189529,89.5 L62.9301895,89.5 C72.2476729,89.5 79.8005198,81.9450625 79.8005198,72.625 L79.8005198,67 L84.5682187,57.6061395 C85.1432011,56.473244 85.1458141,55.1345713 84.5752587,53.9994398 L79.8005198,44.5 L79.8005198,38.875 C79.8005198,29.5552188 72.2476729,22 62.9301895,22 Z" />
|
||||
<ellipse cx="45.7349843" cy="11" rx="12" ry="14" />
|
||||
<ellipse cx="33.5" cy="55.5" rx="8" ry="9" />
|
||||
<ellipse cx="57.5" cy="55.5" rx="8" ry="9" />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user