mirror of
https://github.com/cline/cline.git
synced 2026-09-06 20:41:02 +08:00
Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 006a92976e | |||
| e91c8208a3 | |||
| 0fb40527c8 | |||
| 8a2e90084d | |||
| 4c3384988c | |||
| 8539bdce24 | |||
| cd7f3ef6a7 | |||
| 80fbcda03b | |||
| cef79e06db | |||
| 5dddbef65c | |||
| b6f6358d4a | |||
| e607d02ab2 | |||
| b5e2916bd6 | |||
| 371db77007 | |||
| 80f2e9f6ea | |||
| b46d396de2 | |||
| 8683980c90 | |||
| b916e495e6 | |||
| d45077c4c5 | |||
| 6ced4472d3 | |||
| dcb39a77f2 | |||
| 3301577934 | |||
| a36c11eb97 | |||
| 7d1f199883 | |||
| 6a1e0e518b | |||
| 004b313d20 | |||
| bf37bfa7a3 | |||
| e6dbde70a9 | |||
| cb9a339442 | |||
| 47b5df14d7 | |||
| 4ecbecb1e2 | |||
| fadaf00835 | |||
| 575cfd48cc | |||
| a0787e3d36 | |||
| c9b922009f | |||
| 2d6ff38e69 | |||
| 3069e27413 | |||
| 57c8b8120d | |||
| 5243f0b9b1 | |||
| c014060275 | |||
| d790ce86a0 | |||
| 5e2b199377 | |||
| 9234d0cdc4 | |||
| db1db8c95d | |||
| f53af72643 | |||
| 260e0d5f8e | |||
| 5b68ee5523 | |||
| 3e5abd5e72 | |||
| 1ba5873454 | |||
| 1bdaf8ef6f | |||
| 7f6038c74e | |||
| 2fd9635b97 | |||
| 568b834338 | |||
| 381e9b9d1f | |||
| d86861629d | |||
| 7fb10ba053 | |||
| b7ca95ed57 | |||
| 6bd8726dd6 | |||
| 347d4f48da | |||
| baa5aaa0a7 | |||
| 16f066dcbf | |||
| 59f42c7a81 | |||
| 3a86938a56 | |||
| 042bf359a9 | |||
| 17200740a8 | |||
| c38f443ec4 | |||
| 13f1f0d44b | |||
| 30a169e0c3 | |||
| 77ab7f8ef9 | |||
| 6d5ea98026 | |||
| 1879798b68 | |||
| 3ed47fba17 | |||
| 439e99d8d1 | |||
| 8de99c90a6 | |||
| 5b475fe88c | |||
| 190d3bd2dc |
@@ -0,0 +1,75 @@
|
||||
# Git Diff Analysis Workflow
|
||||
|
||||
## Objective
|
||||
Analyze the current branch's changes against main to provide informed insights and context for development decisions.
|
||||
|
||||
## Step 1: Gather Git Information
|
||||
<important>Do not return any text or conversation other than what is necessary to run these commands</important>
|
||||
|
||||
**First, check the expected output size:**
|
||||
```shell
|
||||
(git branch --show-current && echo "=== STATUS ===" && git status --porcelain | cat && echo "=== COMMIT MESSAGES ===" && git log main..HEAD --oneline | cat && echo "=== CHANGED FILES ===" && git diff main --name-only | cat && echo "=== FULL DIFF ===" && git diff main | cat) | wc -l
|
||||
```
|
||||
|
||||
**If the expected line count is greater than 500 lines, use the file-based approach:**
|
||||
```shell
|
||||
git branch --show-current > cline-git-analysis.temp && echo "=== STATUS ===" >> cline-git-analysis.temp && git status --porcelain >> cline-git-analysis.temp && echo "=== COMMIT MESSAGES ===" >> cline-git-analysis.temp && git log main..HEAD --oneline >> cline-git-analysis.temp && echo "=== CHANGED FILES ===" >> cline-git-analysis.temp && git diff main --name-only >> cline-git-analysis.temp && echo "=== FULL DIFF ===" >> cline-git-analysis.temp && git diff main >> cline-git-analysis.temp
|
||||
```
|
||||
|
||||
Then, read the file using the read_file tool. After you have read the file but before you proceed with subsequent steps, delete it:
|
||||
```shell
|
||||
rm cline-git-analysis.temp
|
||||
```
|
||||
|
||||
**If the expected line count is 500 lines or fewer, use the direct approach:**
|
||||
```shell
|
||||
git branch --show-current && echo "=== STATUS ===" && git status --porcelain | cat && echo "=== COMMIT MESSAGES ===" && git log main..HEAD --oneline | cat && echo "=== CHANGED FILES ===" && git diff main --name-only | cat && echo "=== FULL DIFF ===" && git diff main | cat
|
||||
```
|
||||
|
||||
<important>If using the direct approach, pipe outputs through `cat` to avoid interactive terminals. If the user's shell is not bash/zsh, adjust the command and chaining
|
||||
syntax accordingly.</important>
|
||||
|
||||
## Step 2: Silent, Structured Analysis Phase
|
||||
- Analyze all git output without providing commentary or narration
|
||||
- Read the full diff to understand the scope and nature of changes
|
||||
- Identify patterns, architectural modifications, or potential impacts
|
||||
- Use `read_file` to examine any related files providing additional context on the changes you have observed
|
||||
|
||||
## Step 3: Context Gathering
|
||||
- Analyze related code without providing commentary or narration
|
||||
- Read relevant related source files if needed for complete understanding
|
||||
- Check dependencies, imports, or cross-references spanning the changes
|
||||
- Understand the broader codebase context around modifications
|
||||
- This additional context gathering should include related backend code, as well as related ui/frontend code
|
||||
- You will typically need to analyze at least several files, potentially many, in order to fully complete this step
|
||||
- You should not continue reading additional context if you have exhausted more than 60% of your available context window
|
||||
- If you have exhausted less than 40% of your context window, you should continue reviewing additional context
|
||||
|
||||
## Step 4: Ready for User Interaction
|
||||
**Only after completing the full analysis:**
|
||||
- Engage with the user based on comprehensive understanding
|
||||
- Provide insights about specific modifications and their impacts
|
||||
- If you are certain they exist, note potential breaking changes or compatibility issues
|
||||
- Answer questions with informed context from the complete change set and context gathering
|
||||
- If the user has not provided a question, or the question is insufficient to provide a quality response, ask brief (one sentence) clarifying questions.
|
||||
- Only offer recommendations if they are applicable to the user's request and relevant to the changes that you have observed
|
||||
|
||||
## Key Rules
|
||||
- **No prose or conversation during git research phase**
|
||||
- **No prose or conversation during context gathering phase**
|
||||
- **Complete all analysis before any user interaction**
|
||||
- **Use gathered information for all subsequent questions and insights**
|
||||
- **Focus on understanding the complete picture before discussing**
|
||||
|
||||
## Optional: Additional Analysis Commands
|
||||
For deeper investigation when needed:
|
||||
|
||||
```shell
|
||||
# Detailed commit history with author info
|
||||
git log main..HEAD --format="%h %s (%an)" | cat
|
||||
|
||||
# Change statistics
|
||||
git diff main --stat | cat
|
||||
|
||||
# Specific file type changes
|
||||
git diff main --name-only | grep -E '\.(ts|js|tsx|jsx|py|md)$' | cat
|
||||
@@ -0,0 +1,88 @@
|
||||
name: E2E Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
matrix_prep:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- id: set-matrix
|
||||
run: |
|
||||
echo 'matrix=[{"runner":"ubuntu"},{"runner":"windows"},{"runner":"macos"}]' >> $GITHUB_OUTPUT
|
||||
|
||||
e2e:
|
||||
needs: matrix_prep
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include: ${{ fromJson(needs.matrix_prep.outputs.matrix) }}
|
||||
runs-on: ${{ matrix.runner }}-latest
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
# Cache root dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache root dependencies
|
||||
uses: actions/cache@v4
|
||||
id: root-cache
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
# Cache webview-ui dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache webview-ui dependencies
|
||||
uses: actions/cache@v4
|
||||
id: webview-cache
|
||||
with:
|
||||
path: webview-ui/node_modules
|
||||
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
|
||||
|
||||
- name: Install root dependencies
|
||||
if: steps.root-cache.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
if: steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
- name: Install xvfb on Linux
|
||||
if: matrix.runner == 'ubuntu'
|
||||
run: sudo apt-get update && sudo apt-get install -y xvfb
|
||||
|
||||
# Build the extension before running tests
|
||||
- name: Build Tests and Extension
|
||||
run: npm run pretest
|
||||
|
||||
- name: Run E2E tests - Linux
|
||||
if: matrix.runner == 'ubuntu'
|
||||
run: xvfb-run -a npm run test:e2e
|
||||
|
||||
- name: Run E2E tests - Non-Linux
|
||||
if: matrix.runner != 'ubuntu'
|
||||
run: npm run test:e2e
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ failure() }}
|
||||
with:
|
||||
name: playwright-recordings-${{ matrix.runner }}
|
||||
path: |
|
||||
test-results/playwright/
|
||||
@@ -68,6 +68,10 @@ jobs:
|
||||
if: steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
- name: Install xvfb on Linux
|
||||
if: runner.os == 'Linux'
|
||||
run: sudo apt-get update && sudo apt-get install -y xvfb
|
||||
|
||||
- name: Install local modules on windows
|
||||
if: runner.os == 'Windows' && steps.root-cache.outputs.cache-hit == 'true'
|
||||
run: |
|
||||
|
||||
+3
-3
@@ -34,8 +34,8 @@ src/shared/proto/host/*.ts
|
||||
# Webview
|
||||
webview-ui/src/services/grpc-client.ts
|
||||
# Host bridge
|
||||
src/hosts/vscode/*/methods.ts
|
||||
src/hosts/vscode/*/index.ts
|
||||
src/hosts/vscode/client/host-grpc-client.ts
|
||||
src/hosts/vscode/host-grpc-service-config.ts
|
||||
src/standalone/server-setup.ts
|
||||
|
||||
# E2E Tests
|
||||
test-results
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { defineConfig } from "@vscode/test-cli"
|
||||
import path from "path"
|
||||
|
||||
export default defineConfig({
|
||||
files: "{out/**/*.test.js,src/**/*.test.js}",
|
||||
files: "{out/**/*.test.js,src/**/*.test.js,!src/test/e2e/**/*.test.js,!out/src/test/e2e/**/*.test.js}",
|
||||
mocha: {
|
||||
ui: "bdd",
|
||||
timeout: 20000, // Maximum time (in ms) that a test can run before failing
|
||||
|
||||
+23
-5
@@ -1,27 +1,40 @@
|
||||
# Default
|
||||
.vscode/**
|
||||
.vscode-test/**
|
||||
out/**
|
||||
dist-standalone/**
|
||||
node_modules/**
|
||||
out/
|
||||
dist-standalone/
|
||||
node_modules/
|
||||
src/**
|
||||
standalone/**
|
||||
.gitignore
|
||||
.yarnrc
|
||||
esbuild.js
|
||||
vsc-extension-quickstart.md
|
||||
**/tsconfig.json
|
||||
tsconfig*.json
|
||||
**/.eslintrc.json
|
||||
**/*.map
|
||||
**/*.ts
|
||||
**/.vscode-test.*
|
||||
eslint-rules/**
|
||||
.github/**
|
||||
.husky/**
|
||||
|
||||
# Custom
|
||||
demo.gif
|
||||
**/demo.gif
|
||||
.nvmrc
|
||||
.gitattributes
|
||||
.prettierignore
|
||||
.husky/
|
||||
.github/
|
||||
eslint-rules/
|
||||
old_docs/
|
||||
evals/
|
||||
.changie.yaml
|
||||
.codespellrc
|
||||
.mocharc.json
|
||||
buf.yaml
|
||||
.changeset/
|
||||
.clinerules/
|
||||
|
||||
# Ignore all webview-ui files except the build directory (https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/frameworks/hello-world-react-cra/.vscodeignore)
|
||||
webview-ui/src/**
|
||||
@@ -46,3 +59,8 @@ old_docs/**
|
||||
|
||||
# Include icons
|
||||
!assets/icons/**
|
||||
|
||||
# Ignore E2E build files
|
||||
e2e-build.js
|
||||
e2e.vsix
|
||||
test-results/
|
||||
|
||||
+47
-1
@@ -1,5 +1,51 @@
|
||||
# Changelog
|
||||
|
||||
## [3.19.5]
|
||||
|
||||
- Add Groq as a new API provider with support for all Groq models including Kimi-K2
|
||||
- Add user role display in organization UI for Cline account users
|
||||
- Fix message dialogs not showing option buttons properly
|
||||
- Fix authentication issues when using multiple VSCode windows
|
||||
|
||||
## [3.19.4]
|
||||
|
||||
- Add ability to choose Chinese endpoint for Moonshot provider
|
||||
|
||||
## [3.19.3]
|
||||
|
||||
- Add Moonshot AI provider
|
||||
|
||||
## [3.19.2]
|
||||
|
||||
- Show request ID in error messages returned by Cline Accounts API to help debug user reported issues
|
||||
|
||||
## [3.19.1]
|
||||
|
||||
- Fix documentation
|
||||
|
||||
## [3.19.0]
|
||||
|
||||
- Add Kimi-K2 as a recommended model in the Cline Provider, and route to Together/Groq for 131k context window and high throughput
|
||||
- Added API Key support for Bedrock integration
|
||||
|
||||
## [3.18.14]
|
||||
|
||||
- Fix bug where Cline account users logged in with invalid token would not be shown as logged out in webview presentation layer
|
||||
|
||||
## [3.18.13]
|
||||
|
||||
- Fix authentication issue where Cline accounts users would keep getting logged out or seeing 'Unexpected API response' errors
|
||||
|
||||
## [3.18.12]
|
||||
|
||||
- Fix flaky organization switching behavior in Cline provider that caused UI inconsistencies and double loading
|
||||
- Fix insufficient credits error display to properly show error messages when account balance is too low
|
||||
- Improve credit balance validation and error handling for Cline provider requests
|
||||
|
||||
## [3.18.11]
|
||||
|
||||
- Fix authentication issues with Cline provider by ensuring the client always uses the latest auth token
|
||||
|
||||
## [3.18.10]
|
||||
|
||||
- Update recommended fast & cheap model to Grok 4 in OpenRouter model picker
|
||||
@@ -22,7 +68,7 @@
|
||||
## [3.18.6]
|
||||
|
||||
- Update request header to include `"ai-client-type": "Cline"` to SAP Api Provider
|
||||
- Add organization organization accounts
|
||||
- Add organization accounts
|
||||
|
||||
## [3.18.5]
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ Thanks to [Claude 3.7 Sonnet's agentic coding capabilities](https://www.anthrop
|
||||
|
||||
### Use any API and Model
|
||||
|
||||
Cline supports API providers like OpenRouter, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, GCP Vertex, and Cerebras. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using OpenRouter, the extension fetches their latest model list, allowing you to use the newest models as soon as they're available.
|
||||
Cline supports API providers like OpenRouter, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, GCP Vertex, Cerebras and Groq. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using OpenRouter, the extension fetches their latest model list, allowing you to use the newest models as soon as they're available.
|
||||
|
||||
The extension also keeps track of total tokens and API usage cost for the entire task loop and individual requests, keeping you informed of spend every step of the way.
|
||||
|
||||
|
||||
+1
-1
@@ -61,7 +61,6 @@
|
||||
"getting-started/installing-cline",
|
||||
"getting-started/installing-dev-essentials",
|
||||
"getting-started/model-selection-guide",
|
||||
"getting-started/our-favorite-tech-stack",
|
||||
"getting-started/task-management",
|
||||
"getting-started/understanding-context-management",
|
||||
"getting-started/what-is-cline"
|
||||
@@ -147,6 +146,7 @@
|
||||
"pages": [
|
||||
"provider-config/anthropic",
|
||||
"provider-config/claude-code",
|
||||
"provider-config/aws-bedrock-with-apikey-authentication",
|
||||
"provider-config/aws-bedrock-with-credentials-authentication",
|
||||
"provider-config/aws-bedrock-with-profile-authentication",
|
||||
"provider-config/gcp-vertex-ai",
|
||||
|
||||
@@ -14,6 +14,8 @@ Certain scenarios may warrant using local models, including handling highly sens
|
||||
|
||||
#### [IAM Security Best Practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html) (For administrators)
|
||||
|
||||
#### [AWS Bedrock setup for API Keys](/provider-config/aws-bedrock-with-apikey-authentication)
|
||||
|
||||
#### [AWS Bedrock setup for Legacy IAM (AWS Credentials)](/provider-config/aws-bedrock-with-credentials-authentication)
|
||||
|
||||
#### [AWS Bedrock setup for SSO token (AWS Profile)](/provider-config/aws-bedrock-with-profile-authentication)
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
---
|
||||
title: "Our Favorite Tech Stack"
|
||||
description: "A curated list of our recommended technologies and tools for building modern web applications with Cline."
|
||||
---
|
||||
|
||||
## Recommended Stack for New Cline Users (2025)
|
||||
|
||||
### Your Complete Development Environment
|
||||
|
||||
#### Development Tools
|
||||
|
||||
- **VS Code** - Your code editor, [download here](https://code.visualstudio.com/)
|
||||
- **GitHub** - Where your code lives, [sign up here](https://github.com)
|
||||
|
||||
#### Frontend
|
||||
|
||||
- **Next.js 14+** - React framework with App Router
|
||||
- **Tailwind CSS** - Beautiful styling without writing CSS
|
||||
- **TypeScript** - JavaScript, but safer and smarter
|
||||
|
||||
#### Backend
|
||||
|
||||
- **Supabase** - Your complete backend solution, [sign up with GitHub](https://supabase.com)
|
||||
- PostgreSQL database
|
||||
- Authentication
|
||||
- File storage
|
||||
- Real-time updates
|
||||
|
||||
#### Deployment
|
||||
|
||||
- **Vercel** - Where your app runs, [sign up with GitHub](https://vercel.com)
|
||||
- Automatic deployments from GitHub
|
||||
- Preview deployments for testing
|
||||
- Production-ready CDN
|
||||
|
||||
#### AI Development
|
||||
|
||||
Choose your AI assistant based on your needs:
|
||||
|
||||
| Model | Input Cost (per 1M tokens) | Output Cost (per 1M tokens) | Best For |
|
||||
| ----------------- | -------------------------- | --------------------------- | ------------------------------ |
|
||||
| Claude 3.5 Sonnet | $3.00 | $15.00 | Production apps, complex tasks |
|
||||
| DeepSeek R1 | $1.00 | $3.00 | Budget-conscious production |
|
||||
| DeepSeek V3 | $0.14 | $2.20 | Budget-conscious development |
|
||||
|
||||
#### Free Tier Benefits
|
||||
|
||||
**Vercel (Hobby)**
|
||||
|
||||
- 100 GB data transfer/month
|
||||
- 100k serverless function invocations
|
||||
- 100 MB deployment size
|
||||
- Automatic HTTPS & CI/CD
|
||||
|
||||
**Supabase (Free)**
|
||||
|
||||
- 500 MB database storage
|
||||
- 1 GB file storage
|
||||
- 50k monthly active users
|
||||
- 2M real-time messages/month
|
||||
|
||||
**GitHub (Free)**
|
||||
|
||||
- Unlimited public repositories
|
||||
- GitHub Actions CI/CD
|
||||
- Project management tools
|
||||
- Collaboration features
|
||||
|
||||
### Getting Started
|
||||
|
||||
1. Install the development essentials:
|
||||
- Follow our [Development Essentials Installation Guide](https://docs.cline.bot/getting-started/installing-dev-essentials)
|
||||
2. Set up Cline's Memory Bank:
|
||||
- Follow the [Memory Bank setup instructions](https://docs.cline.bot/prompting/cline-memory-bank)
|
||||
- Create an empty `cline_docs` folder in your project root
|
||||
- Create `projectBrief.md` in the `cline_docs` folder (see example below)
|
||||
- Tell Cline to "initialize memory bank"
|
||||
3. Add our recommended stack configuration:
|
||||
- Create `.clinerules` file (see template below)
|
||||
- Let Cline handle the rest!
|
||||
|
||||
#### Example Project Brief
|
||||
|
||||
```markdown
|
||||
# Project Brief
|
||||
|
||||
## Overview
|
||||
|
||||
Building a [type of application] that will [main purpose].
|
||||
|
||||
## Core Features
|
||||
|
||||
- Feature 1
|
||||
- Feature 2
|
||||
- Feature 3
|
||||
|
||||
## Target Users
|
||||
|
||||
[Describe who will use your application]
|
||||
|
||||
## Technical Preferences (optional)
|
||||
|
||||
- Any specific technologies you want to use
|
||||
- Any specific requirements or constraints
|
||||
```
|
||||
|
||||
### .clinerules Template
|
||||
|
||||
```markdown
|
||||
# Project Configuration
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- Next.js 14+ with App Router
|
||||
- Tailwind CSS for styling
|
||||
- Supabase for backend
|
||||
- Vercel for deployment
|
||||
- GitHub for version control
|
||||
|
||||
## Project Structure
|
||||
|
||||
/src
|
||||
/app # Next.js App Router pages
|
||||
/components # React components
|
||||
/lib # Utility functions
|
||||
/types # TypeScript types
|
||||
/supabase
|
||||
/migrations # SQL migration files
|
||||
/seed # Seed data files
|
||||
/public # Static assets
|
||||
|
||||
## Database Migrations
|
||||
|
||||
SQL files in /supabase/migrations should:
|
||||
|
||||
- Use sequential numbering: 001, 002, etc.
|
||||
- Include descriptive names
|
||||
- Be reviewed by Cline before execution
|
||||
Example: 001_create_users_table.sql
|
||||
|
||||
## Development Workflow
|
||||
|
||||
- Cline helps write and review code changes
|
||||
- Vercel automatically deploys from main branch
|
||||
- Database migrations reviewed by Cline before execution
|
||||
|
||||
## Security
|
||||
|
||||
DO NOT read or modify:
|
||||
|
||||
- .env files
|
||||
- \*_/config/secrets._
|
||||
- Any file containing API keys or credentials
|
||||
```
|
||||
|
||||
### Learning Resources (2025)
|
||||
|
||||
Want to learn more about the technologies we're using? Here are some great resources:
|
||||
|
||||
#### Next.js and React
|
||||
|
||||
- [Official Learn Next.js Course](https://nextjs.org/learn) - Interactive tutorial
|
||||
- [NextJS App Router: Modern Web Dev in 1 Hour](https://www.youtube.com/nextjs-modern) - Quick overview
|
||||
- [Building Real-World Apps with Next.js](https://www.youtube.com/nextjs-real-world) - Practical examples
|
||||
|
||||
#### Supabase
|
||||
|
||||
- [Supabase From Scratch](https://www.udemy.com/supabase-scratch) - Comprehensive course
|
||||
- [Official Quickstart Guides](https://supabase.com/docs/guides/getting-started)
|
||||
- [Real-Time Apps with Next.js and Supabase](https://www.newline.co/courses/supabase-nextjs)
|
||||
|
||||
#### Tailwind CSS
|
||||
|
||||
- [Tailwind CSS Tutorial for Beginners](https://www.youtube.com/tailwind-2025)
|
||||
- [Official Tailwind Documentation](https://tailwindcss.com/docs)
|
||||
- Interactive course at [Scrimba Tailwind CSS Course](https://scrimba.com/learn/tailwind)
|
||||
|
||||
### Other Things to Know
|
||||
|
||||
#### Working with Git & GitHub
|
||||
|
||||
Git helps you track changes in your code and collaborate with others. Here are the essential commands you'll use:
|
||||
|
||||
**Daily Development**
|
||||
|
||||
```bash
|
||||
# Save your changes (do this often!)
|
||||
git add . # Stage all changed files
|
||||
git commit -m "Add login page" # Save changes with a clear message
|
||||
|
||||
# Share your changes
|
||||
git push origin main # Upload to GitHub
|
||||
```
|
||||
|
||||
**Common Workflow**
|
||||
|
||||
1. **Start of day**: Get latest changes
|
||||
|
||||
```bash
|
||||
git pull origin main # Download latest code
|
||||
```
|
||||
|
||||
2. **During development**: Save work regularly
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "Clear message about changes"
|
||||
```
|
||||
|
||||
3. **End of day**: Share your progress
|
||||
|
||||
```bash
|
||||
git push origin main # Upload to GitHub
|
||||
```
|
||||
|
||||
**Best Practices**
|
||||
|
||||
- Commit often with clear messages
|
||||
- Pull before starting new work
|
||||
- Push completed work to share with others
|
||||
- Use `.gitignore` to avoid committing sensitive files
|
||||
|
||||
> **Tip**: Vercel automatically deploys when you push to main!
|
||||
|
||||
#### Environment Variables
|
||||
|
||||
- Store secrets in `.env.local` for development
|
||||
- Add them to Vercel project settings for production
|
||||
- Never commit `.env` files to Git
|
||||
|
||||
#### Getting Help
|
||||
|
||||
1. Use `/help` in Cline chat for immediate assistance
|
||||
2. Check [Cline Documentation](https://docs.cline.bot)
|
||||
3. Join our [Discord Community](https://discord.gg/cline)
|
||||
4. Search GitHub issues for common problems
|
||||
|
||||
Remember: Cline is here to help at every step. Just ask for guidance or clarification when needed!
|
||||
@@ -0,0 +1,135 @@
|
||||
---
|
||||
title: "AWS Bedrock"
|
||||
description: "Learn how to set up AWS Bedrock with Cline using credentials authentication. This guide covers AWS environment setup, regional access verification, and secure integration with the Cline VS Code extension."
|
||||
---
|
||||
|
||||
### Overview
|
||||
|
||||
- **AWS Bedrock:** A fully managed service that offers access to leading generative AI models (e.g., Anthropic Claude, Amazon Nova) through AWS.\
|
||||
[Learn more about AWS Bedrock](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html).
|
||||
- **Cline:** A VS Code extension that acts as a coding assistant by integrating with AI models—empowering developers to generate code, debug, and analyze data.
|
||||
- **Developer Focus:** This guide is tailored for individual developers that want to enable access to frontier models via AWS Bedrock with a simplified setup using API Keys.
|
||||
|
||||
---
|
||||
|
||||
### Step 1: Prepare Your AWS Environment
|
||||
|
||||
#### 1.1 Individual user setup - Create a Bedrock API Key
|
||||
|
||||
For more detailed instructions check the [documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys.html).
|
||||
|
||||
1. **Sign in to the AWS Management Console:**\
|
||||
[AWS Console](https://aws.amazon.com/console/)
|
||||
2. **Access Bedrock Console:**
|
||||
- [Bedrock Console](https://console.aws.amazon.com/bedrock)
|
||||
- Create a new Long Lived API Key. This API Key will have by default the `AmazonBedrockLimitedAccess` IAM policy
|
||||
[View AmazonBedrockLimitedAccess Policy Details](https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html)
|
||||
|
||||
#### 1.2 Create or Modify the Policy
|
||||
|
||||
To ensure Cline can interact with AWS Bedrock, your IAM user or role needs specific permissions. While the `AmazonBedrockLimitedAccess` managed policy provides comprehensive access, for a more restricted and secure setup adhering to the principle of least privilege, the following minimal permissions are sufficient for Cline's core model invocation functionality:
|
||||
|
||||
- `bedrock:InvokeModel`
|
||||
- `bedrock:InvokeModelWithResponseStream`
|
||||
- `bedrock:CallWithBearerToken`
|
||||
|
||||
You can create a custom IAM policy with these permissions and attach it to your IAM user or role.
|
||||
|
||||
1. In the AWS IAM console, create a new policy.
|
||||
2. Use the JSON editor to add the following policy document:
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream", "bedrock:CallWithBearerToken"],
|
||||
"Resource": "*" // For enhanced security, scope this to specific model ARNs if possible.
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
3. Name the policy (e.g., `ClineBedrockInvokeAccess`) and attach it to the IAM user associated with the key you created. The IAM user and the API key have the same prefix.
|
||||
|
||||
**Important Considerations:**
|
||||
|
||||
- **Model Listing in Cline:** The minimal permissions (`bedrock:InvokeModel`, `bedrock:InvokeModelWithResponseStream`) are sufficient for Cline to _use_ a model if you specify the model ID directly in Cline's settings. If you rely on Cline to dynamically list available Bedrock models, you might need additional permissions like `bedrock:ListFoundationModels`.
|
||||
- **AWS Marketplace Subscriptions:** For third-party models (e.g., Anthropic Claude), the **`AmazonBedrockLimitedAccess`** policy grants you the necessary permissions to subscribe via the AWS Marketplace. There is no explicit access to be enabled. For Anthropic models you are still required to submit a First Time Use (FTU) form via the Console. If you get the following message in the Cline chat `[ERROR] Failed to process response: Model use case details have not been submitted for this account. Fill out the Anthropic use case details form before using the model.` then open the [Playground in the AWS Bedrock Console](https://console.aws.amazon.com/bedrock/home?#/text-generation-playground), select any Anthropic model and fill in the form (you might need to send a prompt first)
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Verify Regional and Model Access
|
||||
|
||||
#### 2.1 Choose and Confirm a Region
|
||||
|
||||
1. **Select a Region:**\
|
||||
AWS Bedrock is available in multiple regions (e.g., US East, Europe, Asia Pacific). Choose the region that meets your latency and compliance needs.\
|
||||
[AWS Global Infrastructure](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/)
|
||||
2. **Verify Model Access:**
|
||||
- **Note:** Some models are only accessible via an [Inference Profile](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html). In such case check the box "Cross Region Inference".
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Configure the Cline VS Code Extension
|
||||
|
||||
#### 3.1 Install and Open Cline
|
||||
|
||||
1. **Install VS Code:**\
|
||||
Download from the [VS Code website](https://code.visualstudio.com/).
|
||||
2. **Install the Cline Extension:**
|
||||
- Open VS Code.
|
||||
- Go to the Extensions Marketplace (`Ctrl+Shift+X` or `Cmd+Shift+X`).
|
||||
- Search for **Cline** and install it.
|
||||
|
||||
#### 3.2 Configure Cline Settings
|
||||
|
||||
1. **Open Cline Settings:**
|
||||
- Click on the settings ⚙️ to select your API Provider.
|
||||
2. **Select AWS Bedrock as the API Provider:**
|
||||
- From the API Provider dropdown, choose **AWS Bedrock**.
|
||||
3. **Enter Your AWS API Key:**
|
||||
- Input your **API Key**
|
||||
- Specify the correct **AWS Region** (e.g., `us-east-1` or your enterprise-approved region).
|
||||
4. **Select a Model:**
|
||||
- Choose an on-demand model (e.g., **anthropic.claude-3-5-sonnet-20241022-v2:0**).
|
||||
5. **Save and Test:**
|
||||
- Click **Done/Save** to apply your settings.
|
||||
- Test the integration by sending a simple prompt (e.g., "Generate a Python function to check if a number is prime.").
|
||||
|
||||
---
|
||||
|
||||
### Step 4: Security, Monitoring, and Best Practices
|
||||
|
||||
1. **Secure Access:**
|
||||
- Prefer AWS SSO/federated roles over long-lived API Key when possible.
|
||||
- [AWS IAM Best Practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html)
|
||||
2. **Enhance Network Security:**
|
||||
- Consider setting up [AWS PrivateLink](https://docs.aws.amazon.com/vpc/latest/userguide/endpoint-services-overview.html) to securely connect to Bedrock.
|
||||
3. **Monitor and Log Activity:**
|
||||
- Enable AWS CloudTrail to log Bedrock API calls.
|
||||
- Use CloudWatch to monitor metrics like invocation count, latency, and token usage.
|
||||
- Set up alerts for abnormal activity.
|
||||
4. **Handle Errors and Manage Costs:**
|
||||
- Implement exponential backoff for throttling errors.
|
||||
- Use AWS Cost Explorer and set billing alerts to track usage.\
|
||||
[AWS Cost Management](https://docs.aws.amazon.com/cost-management/latest/userguide/what-is-aws-cost-management.html)
|
||||
5. **Regular Audits and Compliance:**
|
||||
- Periodically review IAM roles and CloudTrail logs.
|
||||
- Follow internal data privacy and governance policies.
|
||||
|
||||
---
|
||||
|
||||
### Conclusion
|
||||
|
||||
By following these steps, your enterprise team can securely integrate AWS Bedrock with the Cline VS Code extension to accelerate development:
|
||||
|
||||
1. **Prepare Your AWS Environment:** Create or use a secure IAM role/user, attach the `AmazonBedrockLimitedAccess` policy, and ensure necessary permissions.
|
||||
2. **Verify Region and Model Access:** Confirm that your selected region supports your required models.
|
||||
3. **Configure Cline in VS Code:** Install and set up Cline with your AWS credentials and choose an appropriate model.
|
||||
4. **Implement Security and Monitoring:** Use best practices for IAM, network security, monitoring, and cost management.
|
||||
|
||||
For further details, consult the [AWS Bedrock Documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html) and coordinate with your internal cloud team. Happy coding!
|
||||
|
||||
---
|
||||
|
||||
_This guide will be updated as AWS Bedrock and Cline evolve. Always refer to the latest documentation and internal policies for up-to-date practices._
|
||||
@@ -5,7 +5,7 @@ description: "Learn how to set up AWS Bedrock with Cline using credentials authe
|
||||
|
||||
### Overview
|
||||
|
||||
- **AWS Bedrock:** A fully managed service that offers access to leading generative AI models (e.g., Anthropic Claude, Amazon Titan) through AWS.\
|
||||
- **AWS Bedrock:** A fully managed service that offers access to leading generative AI models (e.g., Anthropic Claude, Amazon Nova) through AWS.\
|
||||
[Learn more about AWS Bedrock](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html).
|
||||
- **Cline:** A VS Code extension that acts as a coding assistant by integrating with AI models—empowering developers to generate code, debug, and analyze data.
|
||||
- **Enterprise Focus:** This guide is tailored for organizations with established AWS environments (using IAM roles, AWS SSO, AWS Organizations, etc.) to ensure secure and compliant usage.
|
||||
@@ -25,7 +25,7 @@ description: "Learn how to set up AWS Bedrock with Cline using credentials authe
|
||||
|
||||
#### 1.2 Attach the Required Policies
|
||||
|
||||
To ensure Cline can interact with AWS Bedrock, your IAM user or role needs specific permissions. While the `AmazonBedrockFullAccess` managed policy provides comprehensive access, for a more restricted and secure setup adhering to the principle of least privilege, the following minimal permissions are sufficient for Cline's core model invocation functionality:
|
||||
To ensure Cline can interact with AWS Bedrock, your IAM user or role needs specific permissions. While the `AmazonBedrockLimitedAccess` managed policy provides comprehensive access, for a more restricted and secure setup adhering to the principle of least privilege, the following minimal permissions are sufficient for Cline's core model invocation functionality:
|
||||
|
||||
- `bedrock:InvokeModel`
|
||||
- `bedrock:InvokeModelWithResponseStream`
|
||||
@@ -52,8 +52,8 @@ You can create a custom IAM policy with these permissions and attach it to your
|
||||
|
||||
**Option 2: Using a Managed Policy (Simpler Initial Setup)**
|
||||
|
||||
- Alternatively, you can attach the AWS managed policy **`AmazonBedrockFullAccess`**. This grants broader permissions, including the ability to list models, manage provisioning, and other Bedrock features. This might be simpler for initial setup or if you require these wider capabilities.
|
||||
[View AmazonBedrockFullAccess Policy Details](https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html)
|
||||
- Alternatively, you can attach the AWS managed policy **`AmazonBedrockLimitedAccess`**. This grants broader permissions, including the ability to list models, manage provisioning, and other Bedrock features. This might be simpler for initial setup or if you require these wider capabilities.
|
||||
[View AmazonBedrockLimitedAccess Policy Details](https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html)
|
||||
|
||||
**Important Considerations:**
|
||||
|
||||
@@ -71,8 +71,8 @@ You can create a custom IAM policy with these permissions and attach it to your
|
||||
AWS Bedrock is available in multiple regions (e.g., US East, Europe, Asia Pacific). Choose the region that meets your latency and compliance needs.\
|
||||
[AWS Global Infrastructure](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/)
|
||||
2. **Verify Model Access:**
|
||||
- In the AWS Bedrock console, confirm that the models your team requires (e.g., Anthropic Claude, Amazon Titan) are marked as "Access granted."
|
||||
- **Note:** Some advanced models might require an [Inference Profile](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-prereq.html) if not available on-demand.
|
||||
- In the AWS Bedrock console, confirm that the models your team requires (e.g., Anthropic Claude, Amazon Nova) are marked as "Access granted."
|
||||
- **Note:** Some advanced models might require an [Inference Profile](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html) if not available on-demand.
|
||||
|
||||
#### 2.2 Set Up AWS Marketplace Subscriptions (if needed)
|
||||
|
||||
@@ -138,7 +138,7 @@ You can create a custom IAM policy with these permissions and attach it to your
|
||||
|
||||
By following these steps, your enterprise team can securely integrate AWS Bedrock with the Cline VS Code extension to accelerate development:
|
||||
|
||||
1. **Prepare Your AWS Environment:** Create or use a secure IAM role/user, attach the `AmazonBedrockFullAccess` policy, and ensure necessary permissions.
|
||||
1. **Prepare Your AWS Environment:** Create or use a secure IAM role/user, attach the `AmazonBedrockLimitedAccess` policy, and ensure necessary permissions.
|
||||
2. **Verify Region and Model Access:** Confirm that your selected region supports your required models and subscribe via AWS Marketplace if needed.
|
||||
3. **Configure Cline in VS Code:** Install and set up Cline with your AWS credentials and choose an appropriate model.
|
||||
4. **Implement Security and Monitoring:** Use best practices for IAM, network security, monitoring, and cost management.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
title: "Claude Code"
|
||||
description: "Use your Claude Max subscription with Cline instead of paying per token. Learn how to set up and configure the Claude Code provider."
|
||||
description: "Use your Claude Max or Pro subscription with Cline instead of paying per token. Learn how to set up and configure the Claude Code provider."
|
||||
---
|
||||
|
||||
**Website:** [https://docs.anthropic.com/en/docs/claude-code/setup](https://docs.anthropic.com/en/docs/claude-code/setup)
|
||||
|
||||
The Claude Code provider lets you use your existing Claude subscription with Cline. If you have Claude Max, this means you can use Claude in Cline without paying extra API costs.
|
||||
The Claude Code provider lets you use your existing Claude subscription with Cline. If you have Claude Max or Pro, this means you can use Claude in Cline without paying extra API costs.
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
|
||||
+14
-3
@@ -5,6 +5,7 @@ const path = require("path")
|
||||
const production = process.argv.includes("--production")
|
||||
const watch = process.argv.includes("--watch")
|
||||
const standalone = process.argv.includes("--standalone")
|
||||
const e2eBuild = process.argv.includes("--e2e-build")
|
||||
const destDir = standalone ? "dist-standalone" : "dist"
|
||||
|
||||
/**
|
||||
@@ -153,15 +154,25 @@ const extensionConfig = {
|
||||
// Standalone-specific configuration
|
||||
const standaloneConfig = {
|
||||
...baseConfig,
|
||||
entryPoints: ["src/standalone/standalone.ts"],
|
||||
outfile: `${destDir}/standalone.js`,
|
||||
entryPoints: ["src/standalone/cline-core.ts"],
|
||||
outfile: `${destDir}/cline-core.js`,
|
||||
// These gRPC protos need to load files from the module directory at runtime,
|
||||
// so they cannot be bundled.
|
||||
external: ["vscode", "@grpc/reflection", "grpc-health-check"],
|
||||
}
|
||||
|
||||
// E2E build script configuration
|
||||
const e2eBuildConfig = {
|
||||
...baseConfig,
|
||||
entryPoints: ["src/test/e2e/utils/build.ts"],
|
||||
outfile: `${destDir}/e2e-build.js`,
|
||||
external: ["@vscode/test-electron", "execa"],
|
||||
sourcemap: false,
|
||||
plugins: [aliasResolverPlugin, esbuildProblemMatcherPlugin],
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const config = standalone ? standaloneConfig : extensionConfig
|
||||
const config = standalone ? standaloneConfig : e2eBuild ? e2eBuildConfig : extensionConfig
|
||||
const extensionCtx = await esbuild.context(config)
|
||||
if (watch) {
|
||||
await extensionCtx.watch()
|
||||
|
||||
@@ -36,7 +36,7 @@ It starts with our test cases. Each one is a JSON file in `./cases` that has the
|
||||
Then, for every test run, we set up a specific configuration. This includes which LLM we're testing, which system prompt it gets, which function we use to parse the model's raw output, and which function we use to actually apply the diff. Here's the command I've been using:
|
||||
|
||||
```bash
|
||||
npm run diff-eval -- --model-ids "anthropic/claude-3-5-sonnet-20241022,x-ai/grok-3-beta" --max-cases 4 --valid-attempts-per-case 2 --verbose --parallel
|
||||
npm run diff-eval -- --model-ids "anthropic/claude-3-5-sonnet,x-ai/grok-3-beta,anthropic/claude-3.7-sonnet,anthropic/claude-sonnet-4,google/gemini-2.5-pro-preview,google/gemini-2.5-flash" --max-cases 5 --valid-attempts-per-case 5 --parallel --diff-edit-function diff-06-26-25 --verbose
|
||||
```
|
||||
|
||||
This will build the eval script, run it, and then open the streamlit dashboard to show the results.
|
||||
|
||||
@@ -937,8 +937,18 @@ def main():
|
||||
|
||||
# Build current URL
|
||||
# Dynamically derive the base URL
|
||||
server_address = st.server.server_address if hasattr(st.server, 'server_address') else "localhost"
|
||||
server_port = st.server.server_port if hasattr(st.server, 'server_port') else "8501"
|
||||
try:
|
||||
# For older Streamlit versions
|
||||
server_address = st.server.server_address
|
||||
server_port = st.server.server_port
|
||||
except AttributeError:
|
||||
# Fallback for newer Streamlit versions where st.server is removed
|
||||
# We can't reliably get the server address/port from within the script anymore.
|
||||
# We'll default to localhost and the default port.
|
||||
# The user can see the correct network URL in the terminal.
|
||||
server_address = "localhost"
|
||||
server_port = 8501
|
||||
|
||||
base_url = f"http://{server_address}:{server_port}"
|
||||
current_url = f"{base_url}/?run_id={st.session_state.selected_run_id}"
|
||||
if st.session_state.drill_down_model:
|
||||
|
||||
Generated
+4657
-1395
File diff suppressed because it is too large
Load Diff
+12
-6
@@ -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.18.10",
|
||||
"version": "3.19.5",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -330,12 +330,12 @@
|
||||
"watch:esbuild": "node esbuild.js --watch",
|
||||
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
|
||||
"package": "npm run check-types && npm run build:webview && npm run lint && node esbuild.js --production",
|
||||
"protos": "node proto/build-proto.js && node scripts/generate-server-setup.mjs && node scripts/generate-host-bridge-client.mjs",
|
||||
"protos": "node scripts/build-proto.mjs && node scripts/generate-server-setup.mjs && node scripts/generate-host-bridge-client.mjs",
|
||||
"postprotos": "prettier src/shared/proto src/core/controller src/hosts/ webview-ui/src/services src/generated --write --log-level warn",
|
||||
"clean": "rimraf dist dist-standalone webview-ui/build src/generated out/",
|
||||
"compile-tests": "node ./scripts/build-tests.js",
|
||||
"watch-tests": "tsc -p . -w --outDir out",
|
||||
"check-types": "npm run protos && tsc --noEmit",
|
||||
"check-types": "npm run protos && npx tsc --noEmit && cd webview-ui && npx tsc -b --noEmit",
|
||||
"lint": "eslint src --ext ts && eslint webview-ui/src --ext ts && buf lint && cd webview-ui && npm run lint",
|
||||
"format": "prettier . --check",
|
||||
"format:fix": "prettier . --write",
|
||||
@@ -345,6 +345,8 @@
|
||||
"test:integration": "vscode-test",
|
||||
"test:unit": "TS_NODE_PROJECT='./tsconfig.unit-test.json' mocha",
|
||||
"test:coverage": "vscode-test --coverage",
|
||||
"e2e": "playwright test -c playwright.config.ts",
|
||||
"test:e2e": "playwright install && vsce package --no-dependencies --out dist/e2e.vsix && node src/test/e2e/utils/build.js && playwright test",
|
||||
"install:all": "npm install && cd webview-ui && npm install",
|
||||
"dev:webview": "cd webview-ui && npm run dev",
|
||||
"build:webview": "cd webview-ui && npm run build",
|
||||
@@ -383,7 +385,8 @@
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@typescript-eslint/utils": "^8.33.0",
|
||||
"@vscode/test-cli": "^0.0.10",
|
||||
"@vscode/test-electron": "^2.4.1",
|
||||
"@vscode/test-electron": "^2.5.2",
|
||||
"@vscode/vsce": "^3.6.0",
|
||||
"chai": "^4.3.10",
|
||||
"chalk": "^5.3.0",
|
||||
"esbuild": "^0.25.0",
|
||||
@@ -392,6 +395,7 @@
|
||||
"grpc-tools": "^1.13.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.1.0",
|
||||
"minimatch": "^3.0.3",
|
||||
"mintlify": "^4.0.515",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.3.3",
|
||||
@@ -408,8 +412,8 @@
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
"@anthropic-ai/vertex-sdk": "^0.6.4",
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.826.0",
|
||||
"@aws-sdk/credential-providers": "^3.826.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.840.0",
|
||||
"@aws-sdk/credential-providers": "^3.840.0",
|
||||
"@bufbuild/protobuf": "^2.2.5",
|
||||
"@cerebras/cerebras_cloud_sdk": "^1.35.0",
|
||||
"@google-cloud/vertexai": "^1.9.3",
|
||||
@@ -424,6 +428,7 @@
|
||||
"@opentelemetry/sdk-node": "^0.39.1",
|
||||
"@opentelemetry/sdk-trace-node": "^1.30.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.30.0",
|
||||
"@playwright/test": "^1.53.2",
|
||||
"@sentry/browser": "^9.12.0",
|
||||
"@streamparser/json": "^0.0.22",
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
@@ -448,6 +453,7 @@
|
||||
"image-size": "^2.0.2",
|
||||
"isbinaryfile": "^5.0.2",
|
||||
"jschardet": "^3.1.4",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"mammoth": "^1.8.0",
|
||||
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
||||
"nice-grpc": "^2.1.12",
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { defineConfig } from "@playwright/test"
|
||||
|
||||
const isGitHubAction = !!process.env.CI
|
||||
|
||||
export default defineConfig({
|
||||
workers: 1,
|
||||
retries: 1,
|
||||
testDir: "src/test/e2e",
|
||||
timeout: 20000,
|
||||
expect: {
|
||||
timeout: 20000,
|
||||
},
|
||||
fullyParallel: true,
|
||||
reporter: isGitHubAction ? [["github"], ["list"]] : [["list"]],
|
||||
globalSetup: require.resolve("./src/test/e2e/utils/setup"),
|
||||
globalTeardown: require.resolve("./src/test/e2e/utils/teardown"),
|
||||
})
|
||||
@@ -0,0 +1,39 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package host;
|
||||
option java_package = "bot.cline.host.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
// Provides methods for diff views.
|
||||
service DiffService {
|
||||
// Open the diff view/editor.
|
||||
rpc openDiff(OpenDiffRequest) returns (OpenDiffResponse);
|
||||
rpc replaceText(ReplaceTextRequest) returns (ReplaceTextResponse);
|
||||
}
|
||||
|
||||
message OpenDiffRequest {
|
||||
optional cline.Metadata metadata = 1;
|
||||
// The absolute path of the document being edited.
|
||||
optional string path = 2;
|
||||
// The new content for the file.
|
||||
optional string content = 3;
|
||||
}
|
||||
|
||||
message OpenDiffResponse {
|
||||
// A unique identifier for the diff view that was opened.
|
||||
optional string diff_id = 1;
|
||||
}
|
||||
|
||||
message ReplaceTextRequest {
|
||||
optional cline.Metadata metadata = 1;
|
||||
optional string diff_id = 2;
|
||||
optional string content = 3;
|
||||
optional int32 start_line = 4;
|
||||
optional int32 end_line = 5;
|
||||
}
|
||||
|
||||
message ReplaceTextResponse {
|
||||
// TBD
|
||||
}
|
||||
@@ -11,6 +11,7 @@ service WindowService {
|
||||
// Opens a text document in the editor and returns editor information.
|
||||
rpc showTextDocument(ShowTextDocumentRequest) returns (TextEditorInfo);
|
||||
rpc showOpenDialogue(ShowOpenDialogueRequest) returns (SelectedResources);
|
||||
rpc showMessage(ShowMessageRequest) returns (SelectedResponse);
|
||||
}
|
||||
|
||||
message ShowTextDocumentRequest {
|
||||
@@ -46,3 +47,27 @@ message ShowOpenDialogueFilterOption {
|
||||
message SelectedResources {
|
||||
repeated string paths = 1;
|
||||
}
|
||||
|
||||
enum ShowMessageType {
|
||||
ERROR = 0;
|
||||
INFORMATION = 1;
|
||||
WARNING = 2;
|
||||
}
|
||||
|
||||
message ShowMessageRequest {
|
||||
cline.Metadata metadata = 1;
|
||||
ShowMessageType type = 2;
|
||||
string message = 3;
|
||||
optional ShowMessageRequestOptions options = 4;
|
||||
}
|
||||
|
||||
message ShowMessageRequestOptions {
|
||||
repeated string items = 1;
|
||||
optional bool modal = 2;
|
||||
optional string detail = 3;
|
||||
|
||||
}
|
||||
|
||||
message SelectedResponse {
|
||||
optional string selected_option = 1;
|
||||
}
|
||||
+14
-3
@@ -23,6 +23,8 @@ service ModelsService {
|
||||
rpc subscribeToOpenRouterModels(EmptyRequest) returns (stream OpenRouterCompatibleModelInfo);
|
||||
// Updates API configuration
|
||||
rpc updateApiConfigurationProto(UpdateApiConfigurationRequest) returns (Empty);
|
||||
// Refreshes and returns Groq models
|
||||
rpc refreshGroqModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
}
|
||||
|
||||
// List of VS Code LM models
|
||||
@@ -120,8 +122,10 @@ enum ApiProvider {
|
||||
XAI = 21;
|
||||
SAMBANOVA = 22;
|
||||
CEREBRAS = 23;
|
||||
SAPAICORE = 24;
|
||||
CLAUDE_CODE = 25;
|
||||
GROQ = 24;
|
||||
SAPAICORE = 25;
|
||||
CLAUDE_CODE = 26;
|
||||
MOONSHOT = 27;
|
||||
}
|
||||
|
||||
// Model info for OpenAI-compatible models
|
||||
@@ -235,4 +239,11 @@ message ModelsApiConfiguration {
|
||||
optional string sap_ai_core_token_url = 71;
|
||||
optional string sap_ai_core_base_url = 72;
|
||||
optional string claude_code_path = 73;
|
||||
}
|
||||
optional string aws_authentication = 74;
|
||||
optional string aws_bedrock_api_key = 75;
|
||||
optional string moonshot_api_key = 76;
|
||||
optional string moonshot_api_line = 77;
|
||||
optional string groq_api_key = 78;
|
||||
optional string groq_model_id = 79;
|
||||
optional OpenRouterModelInfo groq_model_info = 80;
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
+9
-1
@@ -112,7 +112,7 @@ message UpdateSettingsRequest {
|
||||
optional int64 shell_integration_timeout = 8;
|
||||
optional bool terminal_reuse_enabled = 9;
|
||||
optional bool mcp_responses_collapsed = 10;
|
||||
optional bool mcp_rich_display_enabled = 11;
|
||||
optional string mcp_display_mode = 11;
|
||||
optional int64 terminal_output_line_limit = 12;
|
||||
}
|
||||
|
||||
@@ -232,4 +232,12 @@ message ApiConfiguration {
|
||||
|
||||
// Claude Code specific
|
||||
optional string claude_code_path = 77;
|
||||
|
||||
// Extension fields for Bedrock Api Keys
|
||||
optional string aws_authentication = 78;
|
||||
optional string aws_bedrock_api_key = 79;
|
||||
|
||||
// Moonshot
|
||||
optional string moonshot_api_key = 80;
|
||||
optional string moonshot_api_line = 81;
|
||||
}
|
||||
|
||||
@@ -27,5 +27,6 @@ export const hostServiceNameMap = {
|
||||
workspace: "host.WorkspaceService",
|
||||
env: "host.EnvService",
|
||||
window: "host.WindowService",
|
||||
diff: "host.DiffService",
|
||||
// Add new host services here
|
||||
}
|
||||
@@ -9,22 +9,22 @@ import chalk from "chalk"
|
||||
import os from "os"
|
||||
|
||||
import { createRequire } from "module"
|
||||
import { serviceNameMap, hostServiceNameMap } from "./build-proto-config.js"
|
||||
import { serviceNameMap, hostServiceNameMap } from "./build-proto-config.mjs"
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const PROTOC = path.join(require.resolve("grpc-tools"), "../bin/protoc")
|
||||
|
||||
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url))
|
||||
const ROOT_DIR = path.resolve(SCRIPT_DIR, "..")
|
||||
const SCRIPT_NAME = path.relative(process.cwd(), fileURLToPath(import.meta.url))
|
||||
|
||||
const TS_OUT_DIR = path.join(ROOT_DIR, "src", "shared", "proto")
|
||||
const GRPC_JS_OUT_DIR = path.join(ROOT_DIR, "src", "generated", "grpc-js")
|
||||
const NICE_JS_OUT_DIR = path.join(ROOT_DIR, "src", "generated", "nice-grpc")
|
||||
const DESCRIPTOR_OUT_DIR = path.join(ROOT_DIR, "dist-standalone", "proto")
|
||||
const PROTO_DIR = path.resolve("proto")
|
||||
const TS_OUT_DIR = path.resolve("src/shared/proto")
|
||||
const GRPC_JS_OUT_DIR = path.resolve("src/generated/grpc-js")
|
||||
const NICE_JS_OUT_DIR = path.resolve("src/generated/nice-grpc")
|
||||
const DESCRIPTOR_OUT_DIR = path.resolve("dist-standalone/proto")
|
||||
|
||||
const isWindows = process.platform === "win32"
|
||||
const TS_PROTO_PLUGIN = isWindows
|
||||
? path.join(ROOT_DIR, "node_modules", ".bin", "protoc-gen-ts_proto.cmd") // Use the .bin directory path for Windows
|
||||
? path.resolve("node_modules/.bin/protoc-gen-ts_proto.cmd") // Use the .bin directory path for Windows
|
||||
: require.resolve("ts-proto/protoc-gen-ts_proto")
|
||||
|
||||
const TS_PROTO_OPTIONS = [
|
||||
@@ -37,12 +37,7 @@ const TS_PROTO_OPTIONS = [
|
||||
]
|
||||
|
||||
// Service directories derived from imported serviceNameMap
|
||||
const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src", "core", "controller", serviceKey))
|
||||
|
||||
// Host service directories derived from imported hostServiceNameMap
|
||||
const hostServiceDirs = Object.keys(hostServiceNameMap).map((serviceKey) =>
|
||||
path.join(ROOT_DIR, "src", "hosts", "vscode", serviceKey),
|
||||
)
|
||||
const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join("src/core/controller", serviceKey))
|
||||
|
||||
async function main() {
|
||||
console.log(chalk.bold.blue("Starting Protocol Buffer code generation..."))
|
||||
@@ -61,8 +56,8 @@ async function main() {
|
||||
await ensureProtoFilesExist()
|
||||
|
||||
// Process all proto files
|
||||
const protoFiles = await globby("**/*.proto", { cwd: SCRIPT_DIR, realpath: true })
|
||||
console.log(chalk.cyan(`Processing ${protoFiles.length} proto files from`), SCRIPT_DIR)
|
||||
const protoFiles = await globby("**/*.proto", { cwd: PROTO_DIR, realpath: true })
|
||||
console.log(chalk.cyan(`Processing ${protoFiles.length} proto files from`), PROTO_DIR)
|
||||
|
||||
tsProtoc(TS_OUT_DIR, protoFiles, TS_PROTO_OPTIONS)
|
||||
// grpc-js is used to generate service impls for the ProtoBus service.
|
||||
@@ -73,7 +68,7 @@ async function main() {
|
||||
const descriptorFile = path.join(DESCRIPTOR_OUT_DIR, "descriptor_set.pb")
|
||||
const descriptorProtocCommand = [
|
||||
PROTOC,
|
||||
`--proto_path="${SCRIPT_DIR}"`,
|
||||
`--proto_path="${PROTO_DIR}"`,
|
||||
`--descriptor_set_out="${descriptorFile}"`,
|
||||
"--include_imports",
|
||||
...protoFiles,
|
||||
@@ -89,11 +84,12 @@ async function main() {
|
||||
log_verbose(chalk.green("Protocol Buffer code generation completed successfully."))
|
||||
log_verbose(chalk.green(`TypeScript files generated in: ${TS_OUT_DIR}`))
|
||||
|
||||
await generateMethodRegistrations()
|
||||
await generateHostMethodRegistrations()
|
||||
await generateServiceConfig()
|
||||
await generateHostServiceConfig()
|
||||
await generateGrpcClientConfig()
|
||||
await generateProtoBusServiceConfig()
|
||||
await generateProtoBusMethodRegistrations()
|
||||
await generateProtoBusGrpcClientConfig()
|
||||
|
||||
await generateHostBridgeServiceConfig()
|
||||
await generateHostBridgeMethodRegistrations()
|
||||
|
||||
console.log(chalk.bold.blue("Finished Protocol Buffer code generation."))
|
||||
}
|
||||
@@ -102,7 +98,7 @@ async function tsProtoc(outDir, protoFiles, protoOptions) {
|
||||
// Build the protoc command with proper path handling for cross-platform
|
||||
const command = [
|
||||
PROTOC,
|
||||
`--proto_path="${SCRIPT_DIR}"`,
|
||||
`--proto_path="${PROTO_DIR}"`,
|
||||
`--plugin=protoc-gen-ts_proto="${TS_PROTO_PLUGIN}"`,
|
||||
`--ts_proto_out="${outDir}"`,
|
||||
`--ts_proto_opt=${protoOptions.join(",")} `,
|
||||
@@ -122,7 +118,7 @@ async function tsProtoc(outDir, protoFiles, protoOptions) {
|
||||
* Generate a gRPC client configuration file for the webview
|
||||
* This eliminates the need for manual imports and client creation in grpc-client.ts
|
||||
*/
|
||||
async function generateGrpcClientConfig() {
|
||||
async function generateProtoBusGrpcClientConfig() {
|
||||
log_verbose(chalk.cyan("Generating gRPC client configuration..."))
|
||||
|
||||
const serviceImports = []
|
||||
@@ -147,7 +143,7 @@ async function generateGrpcClientConfig() {
|
||||
|
||||
// Generate the file content
|
||||
const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
// Generated by ${SCRIPT_NAME}
|
||||
|
||||
import { createGrpcClient } from "./grpc-client-base"
|
||||
${serviceImports.join("\n")}
|
||||
@@ -158,7 +154,7 @@ export {
|
||||
${serviceExports.join(",\n\t")}
|
||||
}`
|
||||
|
||||
const filePath = path.join(ROOT_DIR, "webview-ui", "src", "services", "grpc-client.ts")
|
||||
const filePath = path.resolve("webview-ui/src/services/grpc-client.ts")
|
||||
await writeFileWithMkdirs(filePath, content)
|
||||
log_verbose(chalk.green(`Generated gRPC client at ${filePath}`))
|
||||
}
|
||||
@@ -221,12 +217,12 @@ async function parseProtoForStreamingMethods(protoFiles, scriptDir) {
|
||||
return streamingMethodsMap
|
||||
}
|
||||
|
||||
async function generateMethodRegistrations() {
|
||||
async function generateProtoBusMethodRegistrations() {
|
||||
log_verbose(chalk.cyan("Generating method registration files..."))
|
||||
|
||||
// Parse proto files for streaming methods
|
||||
const protoFiles = await globby("*.proto", { cwd: SCRIPT_DIR })
|
||||
const streamingMethodsMap = await parseProtoForStreamingMethods(protoFiles, SCRIPT_DIR)
|
||||
const protoFiles = await globby("*.proto", { cwd: PROTO_DIR })
|
||||
const streamingMethodsMap = await parseProtoForStreamingMethods(protoFiles, PROTO_DIR)
|
||||
|
||||
for (const serviceDir of serviceDirs) {
|
||||
const serviceName = path.basename(serviceDir)
|
||||
@@ -243,7 +239,7 @@ async function generateMethodRegistrations() {
|
||||
|
||||
// Create the methods.ts file with header
|
||||
let methodsContent = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
// Generated by ${SCRIPT_NAME}
|
||||
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"\n`
|
||||
@@ -292,7 +288,7 @@ export function registerAllMethods(): void {
|
||||
// Generate index.ts file
|
||||
const capitalizedServiceName = serviceName.charAt(0).toUpperCase() + serviceName.slice(1)
|
||||
const indexContent = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
// Generated by ${SCRIPT_NAME}
|
||||
|
||||
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
|
||||
import { StreamingResponseHandler } from "../grpc-handler"
|
||||
@@ -327,7 +323,7 @@ registerAllMethods()`
|
||||
* Generate a service configuration file that maps service names to their handlers
|
||||
* This eliminates the need for manual switch/case statements in grpc-handler.ts
|
||||
*/
|
||||
async function generateServiceConfig() {
|
||||
async function generateProtoBusServiceConfig() {
|
||||
log_verbose(chalk.cyan("Generating service configuration file..."))
|
||||
|
||||
const serviceImports = []
|
||||
@@ -347,7 +343,7 @@ async function generateServiceConfig() {
|
||||
}
|
||||
|
||||
const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
// Generated by ${SCRIPT_NAME}
|
||||
|
||||
import { Controller } from "./index"
|
||||
import { StreamingResponseHandler } from "./grpc-handler"
|
||||
@@ -367,7 +363,7 @@ export interface ServiceHandlerConfig {
|
||||
export const serviceHandlers: Record<string, ServiceHandlerConfig> = {${serviceConfigs.join(",")}
|
||||
};`
|
||||
|
||||
const configPath = path.join(ROOT_DIR, "src", "core", "controller", "grpc-service-config.ts")
|
||||
const configPath = path.resolve("src/core/controller/grpc-service-config.ts")
|
||||
await writeFileWithMkdirs(configPath, content)
|
||||
log_verbose(chalk.green(`Generated service configuration at ${configPath}`))
|
||||
}
|
||||
@@ -380,7 +376,7 @@ async function ensureProtoFilesExist() {
|
||||
log_verbose(chalk.cyan("Checking for missing proto files..."))
|
||||
|
||||
// Get existing proto files
|
||||
const existingProtoFiles = await globby("*.proto", { cwd: SCRIPT_DIR })
|
||||
const existingProtoFiles = await globby("*.proto", { cwd: PROTO_DIR })
|
||||
const existingProtoServices = existingProtoFiles.map((file) => path.basename(file, ".proto"))
|
||||
|
||||
// Check each service in serviceNameMap
|
||||
@@ -417,7 +413,7 @@ service ${serviceClassName} {
|
||||
`
|
||||
|
||||
// Write the template proto file
|
||||
const protoFilePath = path.join(SCRIPT_DIR, `${serviceName}.proto`)
|
||||
const protoFilePath = path.join(PROTO_DIR, `${serviceName}.proto`)
|
||||
await fs.writeFile(protoFilePath, protoContent)
|
||||
log_verbose(chalk.green(`Created template proto file at ${protoFilePath}`))
|
||||
}
|
||||
@@ -427,17 +423,22 @@ service ${serviceClassName} {
|
||||
/**
|
||||
* Generate method registration files for host services
|
||||
*/
|
||||
async function generateHostMethodRegistrations() {
|
||||
async function generateHostBridgeMethodRegistrations() {
|
||||
log_verbose(chalk.cyan("Generating host method registration files..."))
|
||||
// Host service directories derived from imported hostServiceNameMap
|
||||
const hostServiceDirs = Object.keys(hostServiceNameMap).map((serviceKey) =>
|
||||
path.join("src/hosts/vscode/hostbridge", serviceKey),
|
||||
)
|
||||
|
||||
// Parse proto files for streaming methods
|
||||
const hostProtoFiles = await globby("*.proto", { cwd: path.join(SCRIPT_DIR, "host") })
|
||||
const streamingMethodsMap = await parseProtoForStreamingMethods(hostProtoFiles, path.join(SCRIPT_DIR, "host"))
|
||||
const hostProtoFiles = await globby("*.proto", { cwd: path.join(PROTO_DIR, "host") })
|
||||
const streamingMethodsMap = await parseProtoForStreamingMethods(hostProtoFiles, path.join(PROTO_DIR, "host"))
|
||||
|
||||
for (const serviceDir of hostServiceDirs) {
|
||||
const serviceName = path.basename(serviceDir)
|
||||
const fullServiceName = hostServiceNameMap[serviceName]
|
||||
const streamingMethods = streamingMethodsMap.get(fullServiceName) || []
|
||||
const outputDir = path.join("src/generated/hosts/vscode/hostbridge", serviceName)
|
||||
|
||||
log_verbose(chalk.cyan(`Generating method registrations for host ${serviceName}...`))
|
||||
|
||||
@@ -449,7 +450,7 @@ async function generateHostMethodRegistrations() {
|
||||
|
||||
// Create the methods.ts file with header
|
||||
let methodsContent = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
// Generated ${SCRIPT_NAME}
|
||||
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"\n`
|
||||
@@ -457,7 +458,7 @@ import { registerMethod } from "./index"\n`
|
||||
// Import implementations directly
|
||||
for (const file of implementationFiles) {
|
||||
const baseName = path.basename(file, ".ts")
|
||||
methodsContent += `import { ${baseName} } from "./${baseName}"\n`
|
||||
methodsContent += `import { ${baseName} } from "@hosts/vscode/hostbridge/${serviceName}/${baseName}"\n`
|
||||
}
|
||||
|
||||
// Add streaming methods information
|
||||
@@ -491,17 +492,17 @@ export function registerAllMethods(): void {
|
||||
methodsContent += `}`
|
||||
|
||||
// Write the methods.ts file
|
||||
const registryFile = path.join(serviceDir, "methods.ts")
|
||||
const registryFile = path.join(outputDir, "methods.ts")
|
||||
await writeFileWithMkdirs(registryFile, methodsContent)
|
||||
log_verbose(chalk.green(`Generated ${registryFile}`))
|
||||
|
||||
// Generate index.ts file
|
||||
const capitalizedServiceName = serviceName.charAt(0).toUpperCase() + serviceName.slice(1)
|
||||
const indexContent = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
// Generated by ${SCRIPT_NAME}
|
||||
|
||||
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../host-grpc-service"
|
||||
import { StreamingResponseHandler } from "../host-grpc-handler"
|
||||
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "@hosts/vscode/hostbridge-grpc-service"
|
||||
import { StreamingResponseHandler } from "@hosts/vscode/hostbridge-grpc-handler"
|
||||
import { registerAllMethods } from "./methods"
|
||||
|
||||
// Create ${serviceName} service registry
|
||||
@@ -521,7 +522,7 @@ export const isStreamingMethod = ${serviceName}Service.isStreamingMethod
|
||||
registerAllMethods()`
|
||||
|
||||
// Write the index.ts file
|
||||
const indexFile = path.join(serviceDir, "index.ts")
|
||||
const indexFile = path.join(outputDir, "index.ts")
|
||||
await writeFileWithMkdirs(indexFile, indexContent)
|
||||
log_verbose(chalk.green(`Generated ${indexFile}`))
|
||||
}
|
||||
@@ -532,7 +533,7 @@ registerAllMethods()`
|
||||
/**
|
||||
* Generate a service configuration file for host services
|
||||
*/
|
||||
async function generateHostServiceConfig() {
|
||||
async function generateHostBridgeServiceConfig() {
|
||||
log_verbose(chalk.cyan("Generating host service configuration file..."))
|
||||
|
||||
const serviceImports = []
|
||||
@@ -542,7 +543,7 @@ async function generateHostServiceConfig() {
|
||||
for (const [dirName, fullServiceName] of Object.entries(hostServiceNameMap)) {
|
||||
const capitalizedName = dirName.charAt(0).toUpperCase() + dirName.slice(1)
|
||||
serviceImports.push(
|
||||
`import { handle${capitalizedName}ServiceRequest, handle${capitalizedName}ServiceStreamingRequest } from "./${dirName}/index"`,
|
||||
`import { handle${capitalizedName}ServiceRequest, handle${capitalizedName}ServiceStreamingRequest } from "@generated/hosts/vscode/hostbridge/${dirName}/index"`,
|
||||
)
|
||||
serviceConfigs.push(`
|
||||
"${fullServiceName}": {
|
||||
@@ -552,9 +553,9 @@ async function generateHostServiceConfig() {
|
||||
}
|
||||
|
||||
const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
// Generated by ${SCRIPT_NAME}
|
||||
|
||||
import { StreamingResponseHandler } from "./host-grpc-handler"
|
||||
import { StreamingResponseHandler } from "@/hosts/vscode/hostbridge-grpc-handler"
|
||||
${serviceImports.join("\n")}
|
||||
|
||||
/**
|
||||
@@ -571,7 +572,7 @@ export interface HostServiceHandlerConfig {
|
||||
export const hostServiceHandlers: Record<string, HostServiceHandlerConfig> = {${serviceConfigs.join(",")}
|
||||
};`
|
||||
|
||||
const filePath = path.join(ROOT_DIR, "src/hosts/vscode/host-grpc-service-config.ts")
|
||||
const filePath = "src/generated/hosts/vscode/hostbridge-grpc-service-config.ts"
|
||||
await writeFileWithMkdirs(filePath, content)
|
||||
log_verbose(chalk.green(`Generated host service configuration at ${filePath}`))
|
||||
}
|
||||
@@ -583,15 +584,33 @@ async function cleanup() {
|
||||
for (const file of existingFiles) {
|
||||
await fs.unlink(path.join(TS_OUT_DIR, file))
|
||||
}
|
||||
await rmdir(path.join(ROOT_DIR, "src", "generated"))
|
||||
await rmdir("src/generated")
|
||||
|
||||
// Clean up generated files that were moved.
|
||||
await fs.rm(path.join(ROOT_DIR, "src", "standalone", "services", "host-grpc-client.ts"), { force: true })
|
||||
await rmdir(path.join(ROOT_DIR, "src", "standalone", "services"))
|
||||
await fs.rm(path.join(ROOT_DIR, "hosts", "vscode"), { force: true, recursive: true })
|
||||
await rmdir(path.join(ROOT_DIR, "hosts"))
|
||||
await fs.rm("src/standalone/services/host-grpc-client.ts", { force: true })
|
||||
await rmdir("src/standalone/services")
|
||||
await fs.rm("hosts/vscode", { force: true, recursive: true })
|
||||
await rmdir("hosts")
|
||||
|
||||
await fs.rm(path.join(ROOT_DIR, "src/standalone/server-setup.ts"), { force: true })
|
||||
await fs.rm("src/standalone/server-setup.ts", { force: true })
|
||||
await fs.rm("src/hosts/vscode/host-grpc-service-config.ts", { force: true })
|
||||
const oldhostbridgefiles = [
|
||||
"src/hosts/vscode/workspace/methods.ts",
|
||||
"src/hosts/vscode/workspace/index.ts",
|
||||
"src/hosts/vscode/diff/methods.ts",
|
||||
"src/hosts/vscode/diff/index.ts",
|
||||
"src/hosts/vscode/env/methods.ts",
|
||||
"src/hosts/vscode/env/index.ts",
|
||||
"src/hosts/vscode/window/methods.ts",
|
||||
"src/hosts/vscode/window/index.ts",
|
||||
"src/hosts/vscode/watch/methods.ts",
|
||||
"src/hosts/vscode/watch/index.ts",
|
||||
"src/hosts/vscode/uri/methods.ts",
|
||||
"src/hosts/vscode/uri/index.ts",
|
||||
]
|
||||
for (const file of oldhostbridgefiles) {
|
||||
await fs.rm(file, { force: true })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -5,17 +5,31 @@ DIR=${1:-src/}
|
||||
DEST_DIR=dist-standalone
|
||||
SDK_DEST=$DEST_DIR/vscode-sdk-uses.txt
|
||||
CSS_DEST=$DEST_DIR/vscode-css-uses.txt
|
||||
TMP=/tmp/vscode-sdk-uses.txt.tmp
|
||||
mkdir -p $DEST_DIR
|
||||
|
||||
{
|
||||
git grep -h 'vscode\.' $DIR |
|
||||
grep -Ev '//.*vscode' | # remove commented out code
|
||||
grep -v vscode.commands.executeCommand | # executeCommand is handled separately
|
||||
grep -Ev '"vscode' | # remove command strings that get included because they start with vscode
|
||||
sed 's|.*vscode\.|vscode.|'| # remove everything before vscode.
|
||||
sed 's/[^a-zA-Z0-9_.].*$//' | # remove everything after last identifier
|
||||
grep -E '\.[a-z][^.]+$' | # remove types (last part of identifier should be lowercase)
|
||||
sort | uniq -c | sort -n | # Count occurrences
|
||||
cat > $SDK_DEST
|
||||
cat > $TMP
|
||||
}
|
||||
{
|
||||
grep -rh vscode.commands.executeCommand $DIR |
|
||||
perl -ne 'print if /["\x27"]/' | # Remove occurrences where the command is not on the same line (line doesnt contain quote chars) :(
|
||||
sed -n 's|.*\(vscode.commands.executeCommand[^,]*\).*|\1|p'| # Remove all params after the first one
|
||||
sed 's|\(".*"\).*|\1)|'| # Close the parantheses
|
||||
cat >> $TMP
|
||||
}
|
||||
|
||||
# Count occurrences
|
||||
cat $TMP | sort | uniq -c | sort -n > $SDK_DEST
|
||||
rm $TMP
|
||||
|
||||
echo Wrote uses of the vscode SDK to $(realpath $SDK_DEST)
|
||||
|
||||
{
|
||||
|
||||
Regular → Executable
+91
-16
@@ -1,9 +1,11 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import archiver from "archiver"
|
||||
import { execSync } from "child_process"
|
||||
import fs from "fs"
|
||||
import { cp } from "fs/promises"
|
||||
import { glob } from "glob"
|
||||
import ignore from "ignore"
|
||||
import minimatch from "minimatch"
|
||||
import path from "path"
|
||||
const BUILD_DIR = "dist-standalone"
|
||||
const RUNTIME_DEPS_DIR = "standalone/runtime-files"
|
||||
@@ -45,8 +47,6 @@ async function zipDistribution() {
|
||||
const zipPath = path.join(BUILD_DIR, "standalone.zip")
|
||||
const output = fs.createWriteStream(zipPath)
|
||||
const archive = archiver("zip", { zlib: { level: 3 } })
|
||||
// Use the same ignore file that vscode uses when packaging the extension.
|
||||
const vscodeignore = ignore().add(fs.readFileSync(".vscodeignore", "utf8"))
|
||||
|
||||
output.on("close", () => {
|
||||
console.log(`Created ${zipPath} (${(archive.pointer() / 1024 / 1024).toFixed(1)} MB)`)
|
||||
@@ -65,20 +65,14 @@ async function zipDistribution() {
|
||||
ignore: ["standalone.zip"],
|
||||
})
|
||||
|
||||
// Add the whole cline directory under "extension"
|
||||
// Exclude the same files as the VCE vscode extension packager.
|
||||
// Also ignore the dist directory, the build directory for the extension.
|
||||
const isIgnored = createIsIgnored(["dist/**"])
|
||||
|
||||
// Add the whole cline directory under "extension", except the for the ignored files.
|
||||
archive.directory(process.cwd(), "extension", (entry) => {
|
||||
if (entry.name.startsWith(".git")) {
|
||||
return false
|
||||
}
|
||||
if (entry.name.endsWith(".DS_Store")) {
|
||||
return false
|
||||
}
|
||||
if (entry.name === "dist" || entry.name.startsWith("dist" + path.sep)) {
|
||||
// Don't include the vscode extension build dir.
|
||||
return false
|
||||
}
|
||||
if (vscodeignore.ignores(entry.name)) {
|
||||
// Exclude entries also ignored by the vscode packager.
|
||||
if (isIgnored(entry.name)) {
|
||||
log_verbose("Ignoring", entry.name)
|
||||
return false
|
||||
}
|
||||
return entry
|
||||
@@ -88,6 +82,81 @@ async function zipDistribution() {
|
||||
await archive.finalize()
|
||||
}
|
||||
|
||||
/**
|
||||
* This is based on https://github.com/microsoft/vscode-vsce/blob/fafad8a63e9cf31179f918eb7a4eeb376834c904/src/package.ts#L1695
|
||||
* because the .vscodeignore format is not compatible with the `ignore` npm module.
|
||||
*/
|
||||
function createIsIgnored(standaloneIgnores) {
|
||||
const MinimatchOptions = { dot: true }
|
||||
const defaultIgnore = [
|
||||
".vscodeignore",
|
||||
"package-lock.json",
|
||||
"npm-debug.log",
|
||||
"yarn.lock",
|
||||
"yarn-error.log",
|
||||
"npm-shrinkwrap.json",
|
||||
".editorconfig",
|
||||
".npmrc",
|
||||
".yarnrc",
|
||||
".gitattributes",
|
||||
"*.todo",
|
||||
"tslint.yaml",
|
||||
".eslintrc*",
|
||||
".babelrc*",
|
||||
".prettierrc*",
|
||||
".cz-config.js",
|
||||
".commitlintrc*",
|
||||
"webpack.config.js",
|
||||
"ISSUE_TEMPLATE.md",
|
||||
"CONTRIBUTING.md",
|
||||
"PULL_REQUEST_TEMPLATE.md",
|
||||
"CODE_OF_CONDUCT.md",
|
||||
".github",
|
||||
".travis.yml",
|
||||
"appveyor.yml",
|
||||
"**/.git",
|
||||
"**/.git/**",
|
||||
"**/*.vsix",
|
||||
"**/.DS_Store",
|
||||
"**/*.vsixmanifest",
|
||||
"**/.vscode-test/**",
|
||||
"**/.vscode-test-web/**",
|
||||
]
|
||||
|
||||
const rawIgnore = fs.readFileSync(".vscodeignore", "utf8")
|
||||
|
||||
// Parse raw ignore by splitting output into lines and filtering out empty lines and comments
|
||||
const parsedIgnore = rawIgnore
|
||||
.split(/[\n\r]/)
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => !!s)
|
||||
.filter((i) => !/^\s*#/.test(i))
|
||||
|
||||
// Add '/**' to possible folder names
|
||||
const expandedIgnore = [
|
||||
...parsedIgnore,
|
||||
...parsedIgnore.filter((i) => !/(^|\/)[^/]*\*[^/]*$/.test(i)).map((i) => (/\/$/.test(i) ? `${i}**` : `${i}/**`)),
|
||||
]
|
||||
|
||||
// Combine with default ignore list
|
||||
// Also ignore the dist directory- the build directory for the extension.
|
||||
const allIgnore = [...defaultIgnore, ...expandedIgnore, ...standaloneIgnores]
|
||||
|
||||
// Split into ignore and negate list
|
||||
const [ignore, negate] = allIgnore.reduce(
|
||||
(r, e) => (!/^\s*!/.test(e) ? [[...r[0], e], r[1]] : [r[0], [...r[1], e]]),
|
||||
[[], []],
|
||||
)
|
||||
|
||||
function isIgnored(f) {
|
||||
return (
|
||||
ignore.some((i) => minimatch(f, i, MinimatchOptions)) &&
|
||||
!negate.some((i) => minimatch(f, i.substr(1), MinimatchOptions))
|
||||
)
|
||||
}
|
||||
return isIgnored
|
||||
}
|
||||
|
||||
/* cp -r */
|
||||
async function cpr(source, dest) {
|
||||
await cp(source, dest, {
|
||||
@@ -97,4 +166,10 @@ async function cpr(source, dest) {
|
||||
})
|
||||
}
|
||||
|
||||
function log_verbose(...args) {
|
||||
if (process.argv.includes("-v") || process.argv.includes("--verbose")) {
|
||||
console.log(...args)
|
||||
}
|
||||
}
|
||||
|
||||
await main()
|
||||
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eu #x
|
||||
# This compiles the cline-core app, installs it to the user's home directory,
|
||||
# and runs the service.
|
||||
|
||||
CORE_DIR=~/.cline/core
|
||||
INSTALL_DIR=$CORE_DIR/0.0.1
|
||||
|
||||
# Build cline core
|
||||
npm run compile-standalone
|
||||
|
||||
# Remove old unpacked versions to force reinstall
|
||||
rm -rf $CORE_DIR/* || true
|
||||
|
||||
mkdir -p $INSTALL_DIR
|
||||
cp dist-standalone/standalone.zip $INSTALL_DIR
|
||||
cd $INSTALL_DIR
|
||||
unp standalone.zip > /dev/null
|
||||
|
||||
pkill -f cline-core.js || true
|
||||
NODE_PATH=./node_modules node cline-core.js
|
||||
@@ -27,6 +27,8 @@ import { SambanovaHandler } from "./providers/sambanova"
|
||||
import { CerebrasHandler } from "./providers/cerebras"
|
||||
import { SapAiCoreHandler } from "./providers/sapaicore"
|
||||
import { ClaudeCodeHandler } from "./providers/claude-code"
|
||||
import { MoonshotHandler } from "./providers/moonshot"
|
||||
import { GroqHandler } from "./providers/groq"
|
||||
|
||||
export interface ApiHandler {
|
||||
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
|
||||
@@ -63,6 +65,8 @@ function createHandlerForProvider(apiProvider: string | undefined, options: Omit
|
||||
awsSecretKey: options.awsSecretKey,
|
||||
awsSessionToken: options.awsSessionToken,
|
||||
awsRegion: options.awsRegion,
|
||||
awsAuthentication: options.awsAuthentication,
|
||||
awsBedrockApiKey: options.awsBedrockApiKey,
|
||||
awsUseCrossRegionInference: options.awsUseCrossRegionInference,
|
||||
awsBedrockUsePromptCache: options.awsBedrockUsePromptCache,
|
||||
awsUseProfile: options.awsUseProfile,
|
||||
@@ -185,6 +189,12 @@ function createHandlerForProvider(apiProvider: string | undefined, options: Omit
|
||||
liteLlmUsePromptCache: options.liteLlmUsePromptCache,
|
||||
taskId: options.taskId,
|
||||
})
|
||||
case "moonshot":
|
||||
return new MoonshotHandler({
|
||||
moonshotApiKey: options.moonshotApiKey,
|
||||
moonshotApiLine: options.moonshotApiLine,
|
||||
apiModelId: options.apiModelId,
|
||||
})
|
||||
case "nebius":
|
||||
return new NebiusHandler({
|
||||
nebiusApiKey: options.nebiusApiKey,
|
||||
@@ -212,6 +222,13 @@ function createHandlerForProvider(apiProvider: string | undefined, options: Omit
|
||||
cerebrasApiKey: options.cerebrasApiKey,
|
||||
apiModelId: options.apiModelId,
|
||||
})
|
||||
case "groq":
|
||||
return new GroqHandler({
|
||||
groqApiKey: options.groqApiKey,
|
||||
groqModelId: options.groqModelId,
|
||||
groqModelInfo: options.groqModelInfo,
|
||||
apiModelId: options.apiModelId,
|
||||
})
|
||||
case "sapaicore":
|
||||
return new SapAiCoreHandler({
|
||||
sapAiCoreClientId: options.sapAiCoreClientId,
|
||||
|
||||
@@ -182,6 +182,24 @@ describe("AwsBedrockHandler", () => {
|
||||
|
||||
process.env["AWS_PROFILE"]!.should.equal(preAWSProfile)
|
||||
})
|
||||
|
||||
it("should work with AWS_BEARER_TOKEN_BEDROCK", async () => {
|
||||
process.env["AWS_BEARER_TOKEN_BEDROCK"] = "test-key"
|
||||
|
||||
const preAWSProfile = process.env["AWS_BEARER_TOKEN_BEDROCK"]
|
||||
|
||||
await AwsBedrockHandler["withTempEnv"](
|
||||
() => {
|
||||
delete process.env["AWS_BEARER_TOKEN_BEDROCK"]
|
||||
},
|
||||
async () => {
|
||||
should.not.exist(process.env["AWS_BEARER_TOKEN_BEDROCK"])
|
||||
return "test"
|
||||
},
|
||||
)
|
||||
|
||||
process.env["AWS_BEARER_TOKEN_BEDROCK"]!.should.equal(preAWSProfile)
|
||||
})
|
||||
})
|
||||
|
||||
const mockOptions: ApiHandlerOptions = {
|
||||
@@ -192,6 +210,7 @@ describe("AwsBedrockHandler", () => {
|
||||
awsSessionToken: "",
|
||||
awsUseProfile: false,
|
||||
awsProfile: "",
|
||||
awsBedrockApiKey: "",
|
||||
awsBedrockUsePromptCache: false,
|
||||
awsUseCrossRegionInference: false,
|
||||
awsBedrockEndpoint: "",
|
||||
|
||||
@@ -22,6 +22,8 @@ interface AwsBedrockHandlerOptions {
|
||||
awsSecretKey?: string
|
||||
awsSessionToken?: string
|
||||
awsRegion?: string
|
||||
awsAuthentication?: string
|
||||
awsBedrockApiKey?: string
|
||||
awsUseCrossRegionInference?: boolean
|
||||
awsBedrockUsePromptCache?: boolean
|
||||
awsUseProfile?: boolean
|
||||
@@ -186,7 +188,10 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
}> {
|
||||
// Configure provider options
|
||||
const providerOptions: ProviderChainOptions = {}
|
||||
if (this.options.awsUseProfile) {
|
||||
const useProfile =
|
||||
(this.options.awsAuthentication === undefined && this.options.awsUseProfile) ||
|
||||
this.options.awsAuthentication === "profile"
|
||||
if (useProfile) {
|
||||
// For profile-based auth, always use ignoreCache to detect credential file changes
|
||||
// This solves the AWS Identity Manager issue where credential files change externally
|
||||
providerOptions.ignoreCache = true
|
||||
@@ -200,7 +205,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
return await AwsBedrockHandler.withTempEnv(
|
||||
() => {
|
||||
AwsBedrockHandler.setEnv("AWS_REGION", this.options.awsRegion)
|
||||
if (this.options.awsUseProfile) {
|
||||
if (useProfile) {
|
||||
AwsBedrockHandler.setEnv("AWS_PROFILE", this.options.awsProfile)
|
||||
} else {
|
||||
delete process.env["AWS_PROFILE"]
|
||||
@@ -224,15 +229,26 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
* Creates a BedrockRuntimeClient with the appropriate credentials
|
||||
*/
|
||||
private async getBedrockClient(): Promise<BedrockRuntimeClient> {
|
||||
const credentials = await this.getAwsCredentials()
|
||||
let auth: any
|
||||
|
||||
if (this.options.awsAuthentication === "apikey") {
|
||||
auth = {
|
||||
token: { token: this.options.awsBedrockApiKey },
|
||||
authSchemePreference: ["httpBearerAuth"],
|
||||
}
|
||||
} else {
|
||||
const credentials = await this.getAwsCredentials()
|
||||
auth = {
|
||||
credentials: {
|
||||
accessKeyId: credentials.accessKeyId,
|
||||
secretAccessKey: credentials.secretAccessKey,
|
||||
sessionToken: credentials.sessionToken,
|
||||
},
|
||||
}
|
||||
}
|
||||
return new BedrockRuntimeClient({
|
||||
region: this.getRegion(),
|
||||
credentials: {
|
||||
accessKeyId: credentials.accessKeyId,
|
||||
secretAccessKey: credentials.secretAccessKey,
|
||||
sessionToken: credentials.sessionToken,
|
||||
},
|
||||
...auth,
|
||||
...(this.options.awsBedrockEndpoint && { endpoint: this.options.awsBedrockEndpoint }),
|
||||
})
|
||||
}
|
||||
|
||||
+30
-25
@@ -4,11 +4,14 @@ import { ClineAccountService } from "@/services/account/ClineAccountService"
|
||||
import { ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "@shared/api"
|
||||
import { createOpenRouterStream } from "../transform/openrouter-stream"
|
||||
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
|
||||
import axios, { AxiosRequestConfig, AxiosResponse } from "axios"
|
||||
import axios from "axios"
|
||||
import { OpenRouterErrorResponse } from "./types"
|
||||
import { withRetry } from "../retry"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
import OpenAI from "openai"
|
||||
import { version as extensionVersion } from "../../../package.json"
|
||||
import { shouldSkipReasoningForModel } from "@utils/model-utils"
|
||||
import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "@/shared/ClineAccount"
|
||||
|
||||
interface ClineHandlerOptions {
|
||||
taskId?: string
|
||||
@@ -38,11 +41,11 @@ export class ClineHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
private async ensureClient(): Promise<OpenAI> {
|
||||
const clineAccountAuthToken = await this._authService.getAuthToken()
|
||||
if (!clineAccountAuthToken) {
|
||||
throw new Error(CLINE_ACCOUNT_AUTH_ERROR_MESSAGE)
|
||||
}
|
||||
if (!this.client) {
|
||||
const clineAccountAuthToken = await this._authService.getAuthToken()
|
||||
if (!clineAccountAuthToken) {
|
||||
throw new Error("Cline account authentication token is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL: `${this._baseUrl}/api/v1`,
|
||||
@@ -51,34 +54,27 @@ export class ClineHandler implements ApiHandler {
|
||||
"HTTP-Referer": "https://cline.bot",
|
||||
"X-Title": "Cline",
|
||||
"X-Task-ID": this.options.taskId || "",
|
||||
"X-Cline-Version": extensionVersion,
|
||||
},
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating Cline client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
// Ensure the client is always using the latest auth token
|
||||
this.client.apiKey = clineAccountAuthToken
|
||||
return this.client
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = await this.ensureClient()
|
||||
const clineAccountAuthToken = await this._authService.getAuthToken()
|
||||
if (!clineAccountAuthToken) {
|
||||
throw new Error("Unauthorized: Please sign in to Cline before trying again.")
|
||||
}
|
||||
|
||||
this.lastGenerationId = undefined
|
||||
|
||||
const me = await this.clineAccountService.fetchMe()
|
||||
console.log(
|
||||
"SwitchAuthToken: Active Organization",
|
||||
me?.organizations.filter((org) => org.active)[0]?.name || "No active organization",
|
||||
)
|
||||
|
||||
let didOutputUsage: boolean = false
|
||||
|
||||
try {
|
||||
const client = await this.ensureClient()
|
||||
|
||||
this.lastGenerationId = undefined
|
||||
|
||||
let didOutputUsage: boolean = false
|
||||
|
||||
const stream = await createOpenRouterStream(
|
||||
client,
|
||||
systemPrompt,
|
||||
@@ -127,7 +123,8 @@ export class ClineHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
// Reasoning tokens are returned separately from the content
|
||||
if ("reasoning" in delta && delta.reasoning) {
|
||||
// Skip reasoning content for Grok 4 models since it only displays "thinking" without providing useful information
|
||||
if ("reasoning" in delta && delta.reasoning && !shouldSkipReasoningForModel(this.options.openRouterModelId)) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
// @ts-ignore-next-line
|
||||
@@ -181,10 +178,18 @@ export class ClineHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code === "ERR_BAD_REQUEST" || error.status === 401) {
|
||||
throw new Error("Unauthorized: Please sign in to Cline before trying again.")
|
||||
}
|
||||
console.error("Cline API Error:", error)
|
||||
const requestId = error?.request_id ? `\n | Request ID: ${error.request_id}` : ""
|
||||
if (error.code === "ERR_BAD_REQUEST" || error.status === 401) {
|
||||
throw new Error(CLINE_ACCOUNT_AUTH_ERROR_MESSAGE + requestId)
|
||||
} else if (error.code === "insufficient_credits" || error.status === 402) {
|
||||
if (error.error) {
|
||||
throw new Error(JSON.stringify(error.error))
|
||||
}
|
||||
}
|
||||
const _error = error instanceof Error ? error : new Error(String(error))
|
||||
_error.message = _error.message + requestId
|
||||
throw _error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
-17
@@ -256,23 +256,18 @@ export class GeminiHandler implements ApiHandler {
|
||||
totalDurationSdkMs > 0 && outputTokens > 0 ? outputTokens / (totalDurationSdkMs / 1000) : undefined
|
||||
|
||||
if (this.options.taskId) {
|
||||
telemetryService.captureGeminiApiPerformance(
|
||||
this.options.taskId,
|
||||
modelId,
|
||||
{
|
||||
ttftSec: ttftSdkMs !== undefined ? ttftSdkMs / 1000 : undefined,
|
||||
totalDurationSec: totalDurationSdkMs / 1000,
|
||||
promptTokens,
|
||||
outputTokens,
|
||||
cacheReadTokens,
|
||||
cacheHit,
|
||||
cacheHitPercentage,
|
||||
apiSuccess,
|
||||
apiError,
|
||||
throughputTokensPerSec: throughputTokensPerSecSdk,
|
||||
},
|
||||
true,
|
||||
)
|
||||
telemetryService.captureGeminiApiPerformance(this.options.taskId, modelId, {
|
||||
ttftSec: ttftSdkMs !== undefined ? ttftSdkMs / 1000 : undefined,
|
||||
totalDurationSec: totalDurationSdkMs / 1000,
|
||||
promptTokens,
|
||||
outputTokens,
|
||||
cacheReadTokens,
|
||||
cacheHit,
|
||||
cacheHitPercentage,
|
||||
apiSuccess,
|
||||
apiError,
|
||||
throughputTokensPerSec: throughputTokensPerSecSdk,
|
||||
})
|
||||
} else {
|
||||
console.warn("GeminiHandler: taskId not available for telemetry in createMessage.")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiHandler } from "../"
|
||||
import { GroqModelId, ModelInfo, groqDefaultModelId, groqModels } from "@shared/api"
|
||||
import { calculateApiCostOpenAI } from "../../utils/cost"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface GroqHandlerOptions {
|
||||
groqApiKey?: string
|
||||
groqModelId?: string
|
||||
groqModelInfo?: ModelInfo
|
||||
apiModelId?: string // For backward compatibility
|
||||
}
|
||||
|
||||
// Model family definitions for enhanced behavior
|
||||
interface GroqModelFamily {
|
||||
name: string
|
||||
supportedFeatures: {
|
||||
streaming: boolean
|
||||
temperature: boolean
|
||||
vision: boolean
|
||||
tools: boolean
|
||||
}
|
||||
maxTokensOverride?: number
|
||||
specialParams?: Record<string, any>
|
||||
}
|
||||
|
||||
const MODEL_FAMILIES: Record<string, GroqModelFamily> = {
|
||||
// Moonshort 4 Family - Latest generation with vision support
|
||||
"kimi-k2": {
|
||||
name: "kimi-k2",
|
||||
supportedFeatures: { streaming: true, temperature: true, vision: true, tools: true },
|
||||
maxTokensOverride: 8192,
|
||||
},
|
||||
// Llama 4 Family - Latest generation with vision support
|
||||
llama4: {
|
||||
name: "Llama 4",
|
||||
supportedFeatures: { streaming: true, temperature: true, vision: true, tools: true },
|
||||
maxTokensOverride: 8192,
|
||||
},
|
||||
// Llama 3.3 Family - Balanced performance
|
||||
"llama3.3": {
|
||||
name: "Llama 3.3",
|
||||
supportedFeatures: { streaming: true, temperature: true, vision: false, tools: true },
|
||||
maxTokensOverride: 32768,
|
||||
},
|
||||
// Llama 3.1 Family - Fast inference
|
||||
"llama3.1": {
|
||||
name: "Llama 3.1",
|
||||
supportedFeatures: { streaming: true, temperature: true, vision: false, tools: true },
|
||||
maxTokensOverride: 131072,
|
||||
},
|
||||
// DeepSeek Family - Reasoning-optimized
|
||||
deepseek: {
|
||||
name: "DeepSeek",
|
||||
supportedFeatures: { streaming: true, temperature: true, vision: false, tools: true },
|
||||
maxTokensOverride: 8192,
|
||||
specialParams: {
|
||||
top_p: 0.95,
|
||||
reasoning_format: "parsed",
|
||||
},
|
||||
},
|
||||
// Qwen Family - Enhanced for Q&A
|
||||
qwen: {
|
||||
name: "Qwen",
|
||||
supportedFeatures: { streaming: true, temperature: true, vision: false, tools: true },
|
||||
maxTokensOverride: 32768,
|
||||
},
|
||||
// Compound Models - Hybrid architectures
|
||||
compound: {
|
||||
name: "Compound",
|
||||
supportedFeatures: { streaming: true, temperature: true, vision: false, tools: true },
|
||||
maxTokensOverride: 8192,
|
||||
},
|
||||
}
|
||||
|
||||
export class GroqHandler implements ApiHandler {
|
||||
private options: GroqHandlerOptions
|
||||
private client: OpenAI | undefined
|
||||
|
||||
constructor(options: GroqHandlerOptions) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.groqApiKey) {
|
||||
throw new Error("Groq API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.groq.com/openai/v1",
|
||||
apiKey: this.options.groqApiKey,
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Groq client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {
|
||||
const inputTokens = usage?.prompt_tokens || 0
|
||||
const outputTokens = usage?.completion_tokens || 0
|
||||
const totalCost = calculateApiCostOpenAI(info, inputTokens, outputTokens)
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
totalCost,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the model family based on the model ID
|
||||
*/
|
||||
private detectModelFamily(modelId: string): GroqModelFamily {
|
||||
if (modelId.includes("kimi-k2")) {
|
||||
return MODEL_FAMILIES["kimi-k2"]
|
||||
}
|
||||
// Llama 4 variants
|
||||
if (modelId.includes("llama-4") || modelId.includes("llama/llama-4")) {
|
||||
return MODEL_FAMILIES.llama4
|
||||
}
|
||||
// Llama 3.3 variants
|
||||
if (modelId.includes("llama-3.3")) {
|
||||
return MODEL_FAMILIES["llama3.3"]
|
||||
}
|
||||
// Llama 3.1 variants
|
||||
if (modelId.includes("llama-3.1")) {
|
||||
return MODEL_FAMILIES["llama3.1"]
|
||||
}
|
||||
// DeepSeek variants
|
||||
if (modelId.includes("deepseek")) {
|
||||
return MODEL_FAMILIES.deepseek
|
||||
}
|
||||
// Qwen variants
|
||||
if (modelId.includes("qwen")) {
|
||||
return MODEL_FAMILIES.qwen
|
||||
}
|
||||
// Compound variants
|
||||
if (modelId.includes("compound")) {
|
||||
return MODEL_FAMILIES.compound
|
||||
}
|
||||
|
||||
// Default fallback to Llama 3.3 behavior
|
||||
return MODEL_FAMILIES["kimi-k2"]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the optimal max_tokens based on model family and capabilities
|
||||
*/
|
||||
private getOptimalMaxTokens(model: { id: string; info: ModelInfo }, modelFamily: GroqModelFamily): number {
|
||||
// Use model-specific max tokens if available
|
||||
if (model.info.maxTokens && model.info.maxTokens > 0) {
|
||||
return model.info.maxTokens
|
||||
}
|
||||
|
||||
// Use family override if available
|
||||
if (modelFamily.maxTokensOverride) {
|
||||
return modelFamily.maxTokensOverride
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return 8192
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
const modelFamily = this.detectModelFamily(model.id)
|
||||
|
||||
// Optimize parameters based on model family
|
||||
const temperature = 0
|
||||
const maxTokens = this.getOptimalMaxTokens(model, modelFamily)
|
||||
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
// Build request parameters with model-specific optimizations
|
||||
const requestParams: OpenAI.Chat.ChatCompletionCreateParamsStreaming & {
|
||||
reasoning_format?: "parsed" | "raw" | "hidden"
|
||||
top_p?: number
|
||||
} = {
|
||||
model: model.id,
|
||||
max_tokens: maxTokens,
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
temperature,
|
||||
}
|
||||
|
||||
// Add any special parameters for specific model families
|
||||
if (modelFamily.specialParams) {
|
||||
Object.assign(requestParams, modelFamily.specialParams)
|
||||
}
|
||||
|
||||
const stream = await client.chat.completions.create(requestParams)
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
|
||||
// Handle reasoning field if present (for reasoning models with parsed output)
|
||||
if ((delta as any)?.reasoning) {
|
||||
const reasoningContent = (delta as any).reasoning as string
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: reasoningContent,
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle content field - trust the parsed output from Groq
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle usage information
|
||||
if (chunk.usage) {
|
||||
yield* this.yieldUsage(model.info, chunk.usage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current model supports vision/images
|
||||
*/
|
||||
supportsImages(): boolean {
|
||||
const model = this.getModel()
|
||||
return model.info.supportsImages === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current model supports tools
|
||||
*/
|
||||
supportsTools(): boolean {
|
||||
const model = this.getModel()
|
||||
const modelFamily = this.detectModelFamily(model.id)
|
||||
return modelFamily.supportedFeatures.tools
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets model information with enhanced family detection
|
||||
*/
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
// First priority: groqModelId and groqModelInfo (like Requesty does)
|
||||
const groqModelId = this.options.groqModelId
|
||||
const groqModelInfo = this.options.groqModelInfo
|
||||
if (groqModelId && groqModelInfo) {
|
||||
return { id: groqModelId, info: groqModelInfo }
|
||||
}
|
||||
|
||||
// Second priority: groqModelId with static model info
|
||||
if (groqModelId && groqModelId in groqModels) {
|
||||
const id = groqModelId as GroqModelId
|
||||
return { id, info: groqModels[id] }
|
||||
}
|
||||
|
||||
// Third priority: apiModelId (for backward compatibility)
|
||||
const apiModelId = this.options.apiModelId
|
||||
if (apiModelId && apiModelId in groqModels) {
|
||||
const id = apiModelId as GroqModelId
|
||||
return { id, info: groqModels[id] }
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return {
|
||||
id: groqDefaultModelId,
|
||||
info: groqModels[groqDefaultModelId],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets model family information for debugging/introspection
|
||||
*/
|
||||
getModelFamily(): GroqModelFamily {
|
||||
const model = this.getModel()
|
||||
return this.detectModelFamily(model.id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiHandler } from "../index"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { ModelInfo, MoonshotModelId, moonshotModels, moonshotDefaultModelId } from "@/shared/api"
|
||||
|
||||
interface MoonshotHandlerOptions {
|
||||
moonshotApiKey?: string
|
||||
moonshotApiLine?: string
|
||||
apiModelId?: string
|
||||
}
|
||||
|
||||
export class MoonshotHandler implements ApiHandler {
|
||||
private client: OpenAI | undefined
|
||||
|
||||
constructor(private readonly options: MoonshotHandlerOptions) {}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.moonshotApiKey) {
|
||||
throw new Error("Moonshot API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL:
|
||||
this.options.moonshotApiLine === "china" ? "https://api.moonshot.cn/v1" : "https://api.moonshot.ai/v1",
|
||||
apiKey: this.options.moonshotApiKey,
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Moonshot client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
messages: openAiMessages,
|
||||
temperature: 0,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
})
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
|
||||
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: (delta.reasoning_content as string | undefined) || "",
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getModel(): { id: MoonshotModelId; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
|
||||
if (modelId && modelId in moonshotModels) {
|
||||
const id = modelId as MoonshotModelId
|
||||
return { id, info: moonshotModels[id] }
|
||||
}
|
||||
return { id: moonshotDefaultModelId, info: moonshotModels[moonshotDefaultModelId] }
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { withRetry } from "../retry"
|
||||
import { createOpenRouterStream } from "../transform/openrouter-stream"
|
||||
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
|
||||
import { OpenRouterErrorResponse } from "./types"
|
||||
import { shouldSkipReasoningForModel } from "@utils/model-utils"
|
||||
|
||||
interface OpenRouterHandlerOptions {
|
||||
openRouterApiKey?: string
|
||||
@@ -112,7 +113,8 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
// Reasoning tokens are returned separately from the content
|
||||
if ("reasoning" in delta && delta.reasoning) {
|
||||
// Skip reasoning content for Grok 4 models since it only displays "thinking" without providing useful information
|
||||
if ("reasoning" in delta && delta.reasoning && !shouldSkipReasoningForModel(this.options.openRouterModelId)) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
// @ts-ignore-next-line
|
||||
|
||||
@@ -6,6 +6,7 @@ import { convertToOpenAiMessages } from "@api/transform/openai-format"
|
||||
import { ApiStream } from "@api/transform/stream"
|
||||
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
|
||||
import { withRetry } from "../retry"
|
||||
import { shouldSkipReasoningForModel } from "@utils/model-utils"
|
||||
|
||||
interface XAIHandlerOptions {
|
||||
xaiApiKey?: string
|
||||
@@ -70,10 +71,13 @@ export class XAIHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
// @ts-ignore-next-line
|
||||
reasoning: delta.reasoning_content,
|
||||
// Skip reasoning content for Grok 4 models since it only displays "thinking" without providing useful information
|
||||
if (!shouldSkipReasoningForModel(modelId)) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
// @ts-ignore-next-line
|
||||
reasoning: delta.reasoning_content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -139,6 +139,10 @@ export async function createOpenRouterStream(
|
||||
shouldApplyMiddleOutTransform = true
|
||||
}
|
||||
|
||||
// hardcoded provider sorting for kimi-k2
|
||||
const isKimiK2 = model.id.startsWith("moonshotai/kimi-k2")
|
||||
openRouterProviderSorting = isKimiK2 ? undefined : openRouterProviderSorting
|
||||
|
||||
// @ts-ignore-next-line
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
@@ -153,6 +157,8 @@ export async function createOpenRouterStream(
|
||||
...(model.id.startsWith("openai/o") ? { reasoning_effort: reasoningEffort || "medium" } : {}),
|
||||
...(reasoning ? { reasoning } : {}),
|
||||
...(openRouterProviderSorting ? { provider: { sort: openRouterProviderSorting } } : {}),
|
||||
// limit providers to only those that support the 131k context window
|
||||
...(isKimiK2 ? { provider: { order: ["groq", "together"], allow_fallbacks: false } } : {}),
|
||||
})
|
||||
|
||||
return stream
|
||||
|
||||
@@ -6,9 +6,9 @@ import * as path from "path"
|
||||
import { FileContextTracker } from "./FileContextTracker"
|
||||
import * as diskModule from "@core/storage/disk"
|
||||
import type { TaskMetadata, FileMetadataEntry } from "./ContextTrackerTypes"
|
||||
import type { WebviewProviderCreator } from "@/hosts/host-providers"
|
||||
import type { DiffViewProviderCreator, WebviewProviderCreator } from "@/hosts/host-providers"
|
||||
import * as hostProviders from "@hosts/host-providers"
|
||||
import { vscodeHostBridgeClient } from "@/hosts/vscode/client/host-grpc-client"
|
||||
import { vscodeHostBridgeClient } from "@/hosts/vscode/hostbridge/client/host-grpc-client"
|
||||
|
||||
describe("FileContextTracker", () => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
@@ -53,7 +53,11 @@ describe("FileContextTracker", () => {
|
||||
mockTaskMetadata = { files_in_context: [], model_usage: [] }
|
||||
getTaskMetadataStub = sandbox.stub(diskModule, "getTaskMetadata").resolves(mockTaskMetadata)
|
||||
saveTaskMetadataStub = sandbox.stub(diskModule, "saveTaskMetadata").resolves()
|
||||
hostProviders.initializeHostProviders(((_) => {}) as WebviewProviderCreator, vscodeHostBridgeClient)
|
||||
hostProviders.initializeHostProviders(
|
||||
((_) => {}) as WebviewProviderCreator,
|
||||
(() => {}) as DiffViewProviderCreator,
|
||||
vscodeHostBridgeClient,
|
||||
)
|
||||
|
||||
// Create tracker instance
|
||||
taskId = "test-task-id"
|
||||
|
||||
@@ -3,11 +3,12 @@ import { RuleFileRequest, RuleFile } from "@shared/proto/file"
|
||||
import { FileMethodHandler } from "./index"
|
||||
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
|
||||
import { createRuleFile as createRuleFileImpl } from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import { handleFileServiceRequest } from "./index"
|
||||
import { refreshWorkflowToggles } from "@/core/context/instructions/user-instructions/workflows"
|
||||
import { getCwd, getDesktopDir } from "@/utils/path"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
|
||||
/**
|
||||
* Creates a rule file in either global or workspace rules directory
|
||||
@@ -42,7 +43,13 @@ export const createRuleFile: FileMethodHandler = async (controller: Controller,
|
||||
const fileTypeName = request.type === "workflow" ? "workflow" : "rule"
|
||||
|
||||
if (fileExists) {
|
||||
vscode.window.showWarningMessage(`${fileTypeName} file "${request.filename}" already exists.`)
|
||||
const message = `${fileTypeName} file "${request.filename}" already exists.`
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.WARNING,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
// Still open it for editing
|
||||
await handleFileServiceRequest(controller, "openFile", { value: filePath })
|
||||
} else {
|
||||
@@ -55,8 +62,12 @@ export const createRuleFile: FileMethodHandler = async (controller: Controller,
|
||||
|
||||
await handleFileServiceRequest(controller, "openFile", { value: filePath })
|
||||
|
||||
vscode.window.showInformationMessage(
|
||||
`Created new ${request.isGlobal ? "global" : "workspace"} ${fileTypeName} file: ${request.filename}`,
|
||||
const message = `Created new ${request.isGlobal ? "global" : "workspace"} ${fileTypeName} file: ${request.filename}`
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { deleteRuleFile as deleteRuleFileImpl } from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
import { RuleFile, RuleFileRequest } from "@shared/proto/file"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { Controller } from ".."
|
||||
import { FileMethodHandler } from "./index"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
|
||||
/**
|
||||
* Deletes a rule file from either global or workspace rules directory
|
||||
@@ -44,7 +45,13 @@ export const deleteRuleFile: FileMethodHandler = async (controller: Controller,
|
||||
|
||||
const fileTypeName = request.type === "workflow" ? "workflow" : "rule"
|
||||
|
||||
vscode.window.showInformationMessage(`${fileTypeName} file "${fileName}" deleted successfully`)
|
||||
const message = `${fileTypeName} file "${fileName}" deleted successfully`
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
|
||||
return RuleFile.create({
|
||||
filePath: request.rulePath,
|
||||
|
||||
@@ -27,20 +27,15 @@ import pWaitFor from "p-wait-for"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { ensureMcpServersDirectoryExists, ensureSettingsDirectoryExists, GlobalFileNames } from "../storage/disk"
|
||||
import {
|
||||
getAllExtensionState,
|
||||
getGlobalState,
|
||||
getWorkspaceState,
|
||||
storeSecret,
|
||||
updateGlobalState,
|
||||
updateWorkspaceState,
|
||||
} from "../storage/state"
|
||||
import { getAllExtensionState, getGlobalState, getWorkspaceState, storeSecret, updateGlobalState } from "../storage/state"
|
||||
import { Task } from "../task"
|
||||
import { handleGrpcRequest, handleGrpcRequestCancel } from "./grpc-handler"
|
||||
import { sendStateUpdate } from "./state/subscribeToState"
|
||||
import { sendAddToInputEvent } from "./ui/subscribeToAddToInput"
|
||||
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
|
||||
/*
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
@@ -79,7 +74,7 @@ export class Controller {
|
||||
)
|
||||
this.accountService = ClineAccountService.getInstance()
|
||||
this.authService = AuthService.getInstance(context)
|
||||
this.authService.restoreAuthToken()
|
||||
this.authService.restoreRefreshTokenAndRetrieveAuthInfo()
|
||||
|
||||
// Clean up legacy checkpoints
|
||||
cleanupLegacyCheckpoints(this.context.globalStorageUri.fsPath, this.outputChannel).catch((error) => {
|
||||
@@ -118,9 +113,19 @@ export class Controller {
|
||||
await updateGlobalState(this.context, "userInfo", undefined)
|
||||
await updateGlobalState(this.context, "apiProvider", "openrouter")
|
||||
await this.postStateToWebview()
|
||||
vscode.window.showInformationMessage("Successfully logged out of Cline")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Successfully logged out of Cline",
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage("Logout failed")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Logout failed",
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,16 +461,6 @@ export class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
// Auth
|
||||
public async validateAuthState(state: string | null): Promise<boolean> {
|
||||
const storedNonce = this.authService.authNonce
|
||||
if (!state || state !== storedNonce) {
|
||||
return false
|
||||
}
|
||||
this.authService.resetAuthNonce() // Clear the nonce after validation
|
||||
return true
|
||||
}
|
||||
|
||||
async handleAuthCallback(customToken: string, provider: string | null = null) {
|
||||
try {
|
||||
await this.authService.handleAuthCallback(customToken, provider ? provider : "google")
|
||||
@@ -489,7 +484,12 @@ export class Controller {
|
||||
await this.postStateToWebview()
|
||||
} catch (error) {
|
||||
console.error("Failed to handle auth callback:", error)
|
||||
vscode.window.showErrorMessage("Failed to log in to Cline")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to log in to Cline",
|
||||
}),
|
||||
)
|
||||
// Even on login failure, we preserve any existing tokens
|
||||
// Only clear tokens on explicit logout
|
||||
}
|
||||
@@ -524,7 +524,12 @@ export class Controller {
|
||||
console.error("Failed to fetch MCP marketplace:", error)
|
||||
if (!silent) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Failed to fetch MCP marketplace"
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: errorMessage,
|
||||
}),
|
||||
)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
@@ -608,7 +613,12 @@ export class Controller {
|
||||
} catch (error) {
|
||||
console.error("Failed to handle cached MCP marketplace:", error)
|
||||
const errorMessage = error instanceof Error ? error.message : "Failed to handle cached MCP marketplace"
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: errorMessage,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -819,7 +829,7 @@ export class Controller {
|
||||
chatSettings: storedChatSettings,
|
||||
userInfo,
|
||||
mcpMarketplaceEnabled,
|
||||
mcpRichDisplayEnabled,
|
||||
mcpDisplayMode,
|
||||
telemetrySetting,
|
||||
planActSeparateModelsSetting,
|
||||
enableCheckpointsSetting,
|
||||
@@ -872,7 +882,7 @@ export class Controller {
|
||||
chatSettings,
|
||||
userInfo,
|
||||
mcpMarketplaceEnabled,
|
||||
mcpRichDisplayEnabled,
|
||||
mcpDisplayMode,
|
||||
telemetrySetting,
|
||||
planActSeparateModelsSetting,
|
||||
enableCheckpointsSetting: enableCheckpointsSetting ?? true,
|
||||
@@ -895,7 +905,6 @@ export class Controller {
|
||||
|
||||
async clearTask() {
|
||||
if (this.task) {
|
||||
await telemetryService.sendCollectedEvents(this.task.taskId)
|
||||
}
|
||||
await this.task?.abortTask()
|
||||
this.task = undefined // removes reference to it, so once promises end it will be garbage collected
|
||||
@@ -972,14 +981,24 @@ export class Controller {
|
||||
// Check if there's a workspace folder open
|
||||
const cwd = await getCwd()
|
||||
if (!cwd) {
|
||||
vscode.window.showErrorMessage("No workspace folder open")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "No workspace folder open",
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the git diff
|
||||
const gitDiff = await getWorkingState(cwd)
|
||||
if (gitDiff === "No changes in working directory") {
|
||||
vscode.window.showInformationMessage("No changes in workspace for commit message")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "No changes in workspace for commit message",
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1046,25 +1065,59 @@ Commit message:`
|
||||
if (api && api.repositories.length > 0) {
|
||||
const repo = api.repositories[0]
|
||||
repo.inputBox.value = commitMessage
|
||||
vscode.window.showInformationMessage("Commit message generated and applied")
|
||||
const message = "Commit message generated and applied"
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
vscode.window.showErrorMessage("No Git repositories found")
|
||||
const message = "No Git repositories found"
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
vscode.window.showErrorMessage("Git extension not found")
|
||||
const message = "Git extension not found"
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
vscode.window.showErrorMessage("Failed to generate commit message")
|
||||
const message = "Failed to generate commit message"
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
}
|
||||
} catch (innerError) {
|
||||
const innerErrorMessage = innerError instanceof Error ? innerError.message : String(innerError)
|
||||
vscode.window.showErrorMessage(`Failed to generate commit message: ${innerErrorMessage}`)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to generate commit message: ${innerErrorMessage}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
vscode.window.showErrorMessage(`Failed to generate commit message: ${errorMessage}`)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to generate commit message: ${errorMessage}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
import { Controller } from ".."
|
||||
import { EmptyRequest } from "../../../shared/proto/common"
|
||||
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "../../../shared/proto/models"
|
||||
import { getAllExtensionState } from "../../storage/state"
|
||||
import { groqModels } from "../../../shared/api"
|
||||
import axios from "axios"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { fileExistsAtPath } from "@utils/fs"
|
||||
import { GlobalFileNames } from "@core/storage/disk"
|
||||
|
||||
/**
|
||||
* Refreshes the Groq models and returns the updated model list
|
||||
* @param controller The controller instance
|
||||
* @param request Empty request object
|
||||
* @returns Response containing the Groq models
|
||||
*/
|
||||
export async function refreshGroqModels(controller: Controller, request: EmptyRequest): Promise<OpenRouterCompatibleModelInfo> {
|
||||
const groqModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.groqModels)
|
||||
|
||||
// Get the Groq API key from the controller's state
|
||||
const { apiConfiguration } = await getAllExtensionState(controller.context)
|
||||
const groqApiKey = apiConfiguration?.groqApiKey
|
||||
|
||||
let models: Record<string, Partial<OpenRouterModelInfo>> = {}
|
||||
try {
|
||||
if (!groqApiKey) {
|
||||
console.log("No Groq API key found, using static models as fallback")
|
||||
// Don't throw an error, just use static models
|
||||
for (const [modelId, modelInfo] of Object.entries(groqModels)) {
|
||||
models[modelId] = {
|
||||
maxTokens: modelInfo.maxTokens,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
supportsImages: modelInfo.supportsImages,
|
||||
supportsPromptCache: modelInfo.supportsPromptCache,
|
||||
inputPrice: modelInfo.inputPrice,
|
||||
outputPrice: modelInfo.outputPrice,
|
||||
cacheWritesPrice: (modelInfo as any).cacheWritesPrice || 0,
|
||||
cacheReadsPrice: (modelInfo as any).cacheReadsPrice || 0,
|
||||
description: modelInfo.description || `${modelId} model`,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Ensure the API key is properly formatted
|
||||
const cleanApiKey = groqApiKey.trim()
|
||||
if (!cleanApiKey.startsWith("gsk_")) {
|
||||
throw new Error("Invalid Groq API key format. Groq API keys should start with 'gsk_'")
|
||||
}
|
||||
|
||||
console.log("Fetching Groq models with API key:", cleanApiKey.substring(0, 10) + "...")
|
||||
|
||||
const response = await axios.get("https://api.groq.com/openai/v1/models", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${cleanApiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Cline-VSCode-Extension",
|
||||
},
|
||||
timeout: 10000, // 10 second timeout
|
||||
})
|
||||
|
||||
if (response.data?.data) {
|
||||
const rawModels = response.data.data
|
||||
|
||||
for (const rawModel of rawModels) {
|
||||
// Filter out non-chat models and validate model capabilities
|
||||
if (!isValidChatModel(rawModel)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if we have static pricing information for this model
|
||||
const staticModelInfo = groqModels[rawModel.id as keyof typeof groqModels]
|
||||
|
||||
const modelInfo: Partial<OpenRouterModelInfo> = {
|
||||
maxTokens: rawModel.max_completion_tokens || staticModelInfo?.maxTokens || 8192,
|
||||
contextWindow: rawModel.context_window || staticModelInfo?.contextWindow || 8192,
|
||||
supportsImages: detectImageSupport(rawModel, staticModelInfo),
|
||||
supportsPromptCache: staticModelInfo?.supportsPromptCache || false,
|
||||
inputPrice: staticModelInfo?.inputPrice || 0,
|
||||
outputPrice: staticModelInfo?.outputPrice || 0,
|
||||
cacheWritesPrice: (staticModelInfo as any)?.cacheWritesPrice || 0,
|
||||
cacheReadsPrice: (staticModelInfo as any).cacheReadsPrice || 0,
|
||||
description: generateModelDescription(rawModel, staticModelInfo),
|
||||
}
|
||||
|
||||
models[rawModel.id] = modelInfo
|
||||
}
|
||||
} else {
|
||||
console.error("Invalid response from Groq API")
|
||||
}
|
||||
await fs.writeFile(groqModelsFilePath, JSON.stringify(models))
|
||||
console.log("Groq models fetched and saved", models)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching Groq models:", error)
|
||||
|
||||
// Provide more specific error messages
|
||||
let errorMessage = "Unknown error occurred"
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.response?.status === 401) {
|
||||
errorMessage = "Invalid Groq API key. Please check your API key in settings."
|
||||
} else if (error.response?.status === 403) {
|
||||
errorMessage = "Access forbidden. Please verify your Groq API key has the correct permissions."
|
||||
} else if (error.response?.status === 429) {
|
||||
errorMessage = "Rate limit exceeded. Please try again later."
|
||||
} else if (error.code === "ECONNABORTED") {
|
||||
errorMessage = "Request timeout. Please check your internet connection."
|
||||
} else {
|
||||
errorMessage = `API request failed: ${error.response?.status || error.code || "Unknown error"}`
|
||||
}
|
||||
} else if (error instanceof Error) {
|
||||
errorMessage = error.message
|
||||
}
|
||||
|
||||
console.error("Groq API Error:", errorMessage)
|
||||
|
||||
// If we failed to fetch models, try to read cached models first
|
||||
const cachedModels = await readGroqModels(controller)
|
||||
if (cachedModels && Object.keys(cachedModels).length > 0) {
|
||||
console.log("Using cached Groq models")
|
||||
models = cachedModels
|
||||
} else {
|
||||
// Fall back to static models from shared/api.ts
|
||||
console.log("Using static Groq models as fallback")
|
||||
for (const [modelId, modelInfo] of Object.entries(groqModels)) {
|
||||
models[modelId] = {
|
||||
maxTokens: modelInfo.maxTokens,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
supportsImages: modelInfo.supportsImages,
|
||||
supportsPromptCache: modelInfo.supportsPromptCache,
|
||||
inputPrice: modelInfo.inputPrice,
|
||||
outputPrice: modelInfo.outputPrice,
|
||||
cacheWritesPrice: (modelInfo as any).cacheWritesPrice || 0,
|
||||
cacheReadsPrice: (modelInfo as any).cacheReadsPrice || 0,
|
||||
description: modelInfo.description || `${modelId} model`,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert the Record<string, Partial<OpenRouterModelInfo>> to Record<string, OpenRouterModelInfo>
|
||||
// by filling in any missing required fields with defaults
|
||||
const typedModels: Record<string, OpenRouterModelInfo> = {}
|
||||
for (const [key, model] of Object.entries(models)) {
|
||||
typedModels[key] = {
|
||||
maxTokens: model.maxTokens ?? 8192,
|
||||
contextWindow: model.contextWindow ?? 8192,
|
||||
supportsImages: model.supportsImages ?? false,
|
||||
supportsPromptCache: model.supportsPromptCache ?? false,
|
||||
inputPrice: model.inputPrice ?? 0,
|
||||
outputPrice: model.outputPrice ?? 0,
|
||||
cacheWritesPrice: model.cacheWritesPrice ?? 0,
|
||||
cacheReadsPrice: model.cacheReadsPrice ?? 0,
|
||||
description: model.description ?? "",
|
||||
tiers: model.tiers ?? [],
|
||||
}
|
||||
}
|
||||
|
||||
return OpenRouterCompatibleModelInfo.create({ models: typedModels })
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads cached Groq models from disk
|
||||
*/
|
||||
async function readGroqModels(controller: Controller): Promise<Record<string, Partial<OpenRouterModelInfo>> | undefined> {
|
||||
const groqModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.groqModels)
|
||||
const fileExists = await fileExistsAtPath(groqModelsFilePath)
|
||||
if (fileExists) {
|
||||
try {
|
||||
const fileContents = await fs.readFile(groqModelsFilePath, "utf8")
|
||||
return JSON.parse(fileContents)
|
||||
} catch (error) {
|
||||
console.error("Error reading cached Groq models:", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a model is suitable for chat completions
|
||||
*/
|
||||
function isValidChatModel(rawModel: any): boolean {
|
||||
// Check if model is active (if the property exists)
|
||||
if (rawModel.hasOwnProperty("active") && !rawModel.active) {
|
||||
return false
|
||||
}
|
||||
// Filter out non-chat models (whisper, TTS, guard models, etc.)
|
||||
if (
|
||||
rawModel.id.includes("whisper") ||
|
||||
rawModel.id.includes("tts") ||
|
||||
rawModel.id.includes("guard") ||
|
||||
rawModel.id.includes("embedding") ||
|
||||
rawModel.id.includes("moderation") ||
|
||||
rawModel.id.includes("allam")
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if model supports chat completions
|
||||
if (rawModel.object === "model" && rawModel.id) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if a model supports image input
|
||||
*/
|
||||
function detectImageSupport(rawModel: any, staticModelInfo?: any): boolean {
|
||||
// Use static info if available
|
||||
if (staticModelInfo?.supportsImages !== undefined) {
|
||||
return staticModelInfo.supportsImages
|
||||
}
|
||||
|
||||
// Detect based on model name patterns
|
||||
const modelId = rawModel.id.toLowerCase()
|
||||
if (modelId.includes("vision") || modelId.includes("maverick") || modelId.includes("scout")) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a descriptive name for the model
|
||||
*/
|
||||
function generateModelDescription(rawModel: any, staticModelInfo?: any): string {
|
||||
// Use static description if available
|
||||
if (staticModelInfo?.description) {
|
||||
return staticModelInfo.description
|
||||
}
|
||||
|
||||
// Generate description based on model characteristics
|
||||
const modelId = rawModel.id
|
||||
const contextWindow = rawModel.context_window || 8192
|
||||
const ownedBy = rawModel.owned_by || "Unknown"
|
||||
|
||||
// Special handling for new models
|
||||
if (modelId.includes("compound")) {
|
||||
return `${ownedBy}'s ${modelId} model with ${contextWindow.toLocaleString()} token context window - Advanced compound architecture`
|
||||
}
|
||||
|
||||
return `${ownedBy} model with ${contextWindow.toLocaleString()} token context window`
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the cache directory exists and returns its path
|
||||
*/
|
||||
async function ensureCacheDirectoryExists(controller: Controller): Promise<string> {
|
||||
const cacheDir = path.join(controller.context.globalStorageUri.fsPath, "cache")
|
||||
await fs.mkdir(cacheDir, { recursive: true })
|
||||
return cacheDir
|
||||
}
|
||||
@@ -104,6 +104,12 @@ export async function refreshOpenRouterModels(
|
||||
modelInfo.cacheWritesPrice = 0.75
|
||||
modelInfo.cacheReadsPrice = 0
|
||||
break
|
||||
case "moonshotai/kimi-k2":
|
||||
// forcing kimi-k2 to use the together provider for full context and best throughput
|
||||
modelInfo.inputPrice = 1
|
||||
modelInfo.outputPrice = 3
|
||||
modelInfo.contextWindow = 131_000
|
||||
break
|
||||
default:
|
||||
if (rawModel.id.startsWith("openai/")) {
|
||||
modelInfo.cacheReadsPrice = parsePrice(rawModel.pricing?.input_cache_read)
|
||||
|
||||
@@ -2,8 +2,9 @@ import { Controller } from ".."
|
||||
import { Empty } from "../../../shared/proto/common"
|
||||
import { ResetStateRequest } from "../../../shared/proto/state"
|
||||
import { resetGlobalState, resetWorkspaceState } from "../../../core/storage/state"
|
||||
import * as vscode from "vscode"
|
||||
import { sendChatButtonClickedEvent } from "../ui/subscribeToChatButtonClicked"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
|
||||
/**
|
||||
* Resets the extension state to its defaults
|
||||
@@ -14,10 +15,20 @@ import { sendChatButtonClickedEvent } from "../ui/subscribeToChatButtonClicked"
|
||||
export async function resetState(controller: Controller, request: ResetStateRequest): Promise<Empty> {
|
||||
try {
|
||||
if (request.global) {
|
||||
vscode.window.showInformationMessage("Resetting global state...")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Resetting global state...",
|
||||
}),
|
||||
)
|
||||
await resetGlobalState(controller.context)
|
||||
} else {
|
||||
vscode.window.showInformationMessage("Resetting workspace state...")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Resetting workspace state...",
|
||||
}),
|
||||
)
|
||||
await resetWorkspaceState(controller.context)
|
||||
}
|
||||
|
||||
@@ -26,7 +37,12 @@ export async function resetState(controller: Controller, request: ResetStateRequ
|
||||
controller.task = undefined
|
||||
}
|
||||
|
||||
vscode.window.showInformationMessage("State reset")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "State reset",
|
||||
}),
|
||||
)
|
||||
await controller.postStateToWebview()
|
||||
|
||||
await sendChatButtonClickedEvent(controller.id)
|
||||
@@ -34,7 +50,12 @@ export async function resetState(controller: Controller, request: ResetStateRequ
|
||||
return Empty.create()
|
||||
} catch (error) {
|
||||
console.error("Error resetting state:", error)
|
||||
vscode.window.showErrorMessage(`Failed to reset state: ${error instanceof Error ? error.message : String(error)}`)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to reset state: ${error instanceof Error ? error.message : String(error)}`,
|
||||
}),
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export async function subscribeToState(
|
||||
const initialState = await controller.getStateToPostToWebview()
|
||||
const initialStateJson = JSON.stringify(initialState)
|
||||
|
||||
console.log(`[DEBUG] set up state subscription for controller ${controllerId}`)
|
||||
//console.log(`[DEBUG] set up state subscription for controller ${controllerId}`)
|
||||
|
||||
await responseStream({
|
||||
stateJson: initialStateJson,
|
||||
@@ -37,7 +37,7 @@ export async function subscribeToState(
|
||||
// Register cleanup when the connection is closed
|
||||
const cleanup = () => {
|
||||
activeStateSubscriptions.delete(controllerId)
|
||||
console.log(`[DEBUG] Cleaned up state subscription for controller ${controllerId}`)
|
||||
//console.log(`[DEBUG] Cleaned up state subscription for controller ${controllerId}`)
|
||||
}
|
||||
|
||||
// Register the cleanup function with the request registry if we have a requestId
|
||||
@@ -68,7 +68,7 @@ export async function sendStateUpdate(controllerId: string, state: any): Promise
|
||||
},
|
||||
false, // Not the last message
|
||||
)
|
||||
console.log(`[DEBUG] sending followup state to controller ${controllerId}`, stateJson.length, "chars")
|
||||
//console.log(`[DEBUG] sending followup state to controller ${controllerId}`, stateJson.length, "chars")
|
||||
} catch (error) {
|
||||
console.error(`Error sending state update to controller ${controllerId}:`, error)
|
||||
// Remove the subscription if there was an error
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import * as vscode from "vscode"
|
||||
import { Controller } from "../index"
|
||||
import * as proto from "@/shared/proto"
|
||||
import { updateGlobalState } from "../../storage/state"
|
||||
import { TerminalInfo } from "@/integrations/terminal/TerminalRegistry"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
|
||||
export async function updateDefaultTerminalProfile(
|
||||
controller: Controller,
|
||||
@@ -25,16 +26,25 @@ export async function updateDefaultTerminalProfile(
|
||||
|
||||
// Show information message if terminals were closed
|
||||
if (closedCount > 0) {
|
||||
vscode.window.showInformationMessage(
|
||||
`Closed ${closedCount} ${closedCount === 1 ? "terminal" : "terminals"} with different profile.`,
|
||||
const message = `Closed ${closedCount} ${closedCount === 1 ? "terminal" : "terminals"} with different profile.`
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// Show warning if there are busy terminals that couldn't be closed
|
||||
if (busyTerminals.length > 0) {
|
||||
vscode.window.showWarningMessage(
|
||||
const message =
|
||||
`${busyTerminals.length} busy ${busyTerminals.length === 1 ? "terminal has" : "terminals have"} a different profile. ` +
|
||||
`Close ${busyTerminals.length === 1 ? "it" : "them"} to use the new profile for all commands.`,
|
||||
`Close ${busyTerminals.length === 1 ? "it" : "them"} to use the new profile for all commands.`
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.WARNING,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,9 +50,9 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
await controller.context.globalState.update("mcpResponsesCollapsed", request.mcpResponsesCollapsed)
|
||||
}
|
||||
|
||||
// Update MCP responses collapsed setting
|
||||
if (request.mcpRichDisplayEnabled !== undefined) {
|
||||
await controller.context.globalState.update("mcpRichDisplayEnabled", request.mcpRichDisplayEnabled)
|
||||
// Update MCP display mode setting
|
||||
if (request.mcpDisplayMode !== undefined) {
|
||||
await controller.context.globalState.update("mcpDisplayMode", request.mcpDisplayMode)
|
||||
}
|
||||
|
||||
// Update chat settings
|
||||
|
||||
@@ -4,7 +4,8 @@ import { Controller } from ".."
|
||||
import { DeleteAllTaskHistoryCount } from "../../../shared/proto/task"
|
||||
import { getGlobalState, updateGlobalState } from "../../storage/state"
|
||||
import { fileExistsAtPath } from "../../../utils/fs"
|
||||
import vscode from "vscode"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
|
||||
/**
|
||||
* Deletes all task history, with an option to preserve favorites
|
||||
@@ -21,12 +22,18 @@ export async function deleteAllTaskHistory(controller: Controller): Promise<Dele
|
||||
const taskHistory = ((await getGlobalState(controller.context, "taskHistory")) as any[]) || []
|
||||
const totalTasks = taskHistory.length
|
||||
|
||||
const userChoice = await vscode.window.showWarningMessage(
|
||||
"What would you like to delete?",
|
||||
{ modal: true },
|
||||
"Delete All Except Favorites",
|
||||
"Delete Everything",
|
||||
)
|
||||
const userChoice = (
|
||||
await getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.WARNING,
|
||||
message: "What would you like to delete?",
|
||||
options: {
|
||||
modal: true,
|
||||
items: ["Delete All Except Favorites", "Delete Everything"],
|
||||
},
|
||||
}),
|
||||
)
|
||||
)?.selectedOption
|
||||
|
||||
// Default VS Code Cancel button returns `undefined` - don't delete anything
|
||||
if (userChoice === undefined) {
|
||||
@@ -59,11 +66,18 @@ export async function deleteAllTaskHistory(controller: Controller): Promise<Dele
|
||||
})
|
||||
} else {
|
||||
// No favorited tasks found - show warning and ask user what to do
|
||||
const answer = await vscode.window.showWarningMessage(
|
||||
"No favorited tasks found. Would you like to delete all tasks anyway?",
|
||||
{ modal: true },
|
||||
"Delete All Tasks",
|
||||
)
|
||||
const answer = (
|
||||
await getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.WARNING,
|
||||
message: "No favorited tasks found. Would you like to delete all tasks anyway?",
|
||||
options: {
|
||||
modal: true,
|
||||
items: ["Delete All Tasks"],
|
||||
},
|
||||
}),
|
||||
)
|
||||
)?.selectedOption
|
||||
|
||||
// User cancelled - don't delete anything
|
||||
if (answer === undefined) {
|
||||
@@ -91,8 +105,11 @@ export async function deleteAllTaskHistory(controller: Controller): Promise<Dele
|
||||
await fs.rm(checkpointsDirPath, { recursive: true, force: true })
|
||||
}
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(
|
||||
`Encountered error while deleting task history, there may be some files left behind. Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Encountered error while deleting task history, there may be some files left behind. Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import vscode from "vscode"
|
||||
import { Controller } from ".."
|
||||
import { Empty, StringArrayRequest, BooleanRequest } from "../../../shared/proto/common"
|
||||
import { Empty, StringArrayRequest } from "../../../shared/proto/common"
|
||||
import { TaskMethodHandler } from "./index"
|
||||
import { fileExistsAtPath } from "../../../utils/fs"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
|
||||
/**
|
||||
* Deletes tasks with the specified IDs
|
||||
@@ -27,7 +28,13 @@ export const deleteTasksWithIds: TaskMethodHandler = async (
|
||||
? "Are you sure you want to delete this task? This action cannot be undone."
|
||||
: `Are you sure you want to delete these ${taskCount} tasks? This action cannot be undone.`
|
||||
|
||||
const userChoice = await vscode.window.showWarningMessage(message, { modal: true }, "Delete")
|
||||
const userChoice = await getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.WARNING,
|
||||
message,
|
||||
options: { modal: true, items: ["Delete"] },
|
||||
}),
|
||||
)
|
||||
|
||||
if (userChoice === undefined) {
|
||||
return Empty.create()
|
||||
|
||||
@@ -42,6 +42,17 @@ export async function initializeWebview(controller: Controller, request: EmptyRe
|
||||
}
|
||||
})
|
||||
|
||||
handleModelsServiceRequest(controller, "refreshGroqModels", EmptyRequest.create()).then(async (response) => {
|
||||
if (response && response.models) {
|
||||
// update model info in state for Groq
|
||||
const { apiConfiguration } = await getAllExtensionState(controller.context)
|
||||
if (apiConfiguration.groqModelId && response.models[apiConfiguration.groqModelId]) {
|
||||
await updateGlobalState(controller.context, "groqModelInfo", response.models[apiConfiguration.groqModelId])
|
||||
await controller.postStateToWebview()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// GUI relies on model info to be up-to-date to provide the most accurate pricing, so we need to fetch the latest details on launch.
|
||||
// We do this for all users since many users switch between api providers and if they were to switch back to openrouter it would be showing outdated model info if we hadn't retrieved the latest at this point
|
||||
// (see normalizeApiConfiguration > openrouter)
|
||||
|
||||
@@ -13,6 +13,8 @@ import { getWorkingState } from "@utils/git"
|
||||
import { FileContextTracker } from "../context/context-tracking/FileContextTracker"
|
||||
import { getCwd } from "@/utils/path"
|
||||
import { openExternal } from "@utils/env"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
|
||||
export async function openMention(mention?: string): Promise<void> {
|
||||
if (!mention) {
|
||||
@@ -76,7 +78,12 @@ export async function parseMentions(
|
||||
await urlContentFetcher.launchBrowser()
|
||||
} catch (error) {
|
||||
launchBrowserError = error
|
||||
vscode.window.showErrorMessage(`Error fetching content for ${urlMention}: ${error.message}`)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Error fetching content for ${urlMention}: ${error.message}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +100,12 @@ export async function parseMentions(
|
||||
const markdown = await urlContentFetcher.urlToMarkdown(mention)
|
||||
result = markdown
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`Error fetching content for ${mention}: ${error.message}`)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Error fetching content for ${mention}: ${error.message}`,
|
||||
}),
|
||||
)
|
||||
result = `Error fetching content: ${error.message}`
|
||||
}
|
||||
}
|
||||
@@ -120,7 +132,7 @@ export async function parseMentions(
|
||||
}
|
||||
} else if (mention === "problems") {
|
||||
try {
|
||||
const problems = getWorkspaceProblems(cwd)
|
||||
const problems = await getWorkspaceProblems()
|
||||
parsedText += `\n\n<workspace_diagnostics>\n${problems}\n</workspace_diagnostics>`
|
||||
} catch (error) {
|
||||
parsedText += `\n\n<workspace_diagnostics>\nError fetching diagnostics: ${error.message}\n</workspace_diagnostics>`
|
||||
@@ -216,13 +228,9 @@ async function getFileOrFolderContent(mentionPath: string, cwd: string): Promise
|
||||
}
|
||||
}
|
||||
|
||||
function getWorkspaceProblems(cwd: string): string {
|
||||
async function getWorkspaceProblems(): Promise<string> {
|
||||
const diagnostics = vscode.languages.getDiagnostics()
|
||||
const result = diagnosticsToProblemsString(
|
||||
diagnostics,
|
||||
[vscode.DiagnosticSeverity.Error, vscode.DiagnosticSeverity.Warning],
|
||||
cwd,
|
||||
)
|
||||
const result = diagnosticsToProblemsString(diagnostics, [vscode.DiagnosticSeverity.Error, vscode.DiagnosticSeverity.Warning])
|
||||
if (!result) {
|
||||
return "No errors or warnings detected."
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ export const GlobalFileNames = {
|
||||
contextHistory: "context_history.json",
|
||||
uiMessages: "ui_messages.json",
|
||||
openRouterModels: "openrouter_models.json",
|
||||
groqModels: "groq_models.json",
|
||||
mcpSettings: "cline_mcp_settings.json",
|
||||
clineRules: ".clinerules",
|
||||
workflows: ".clinerules/workflows",
|
||||
|
||||
@@ -5,6 +5,7 @@ export type SecretKey =
|
||||
| "awsAccessKey"
|
||||
| "awsSecretKey"
|
||||
| "awsSessionToken"
|
||||
| "awsBedrockApiKey"
|
||||
| "openAiApiKey"
|
||||
| "geminiApiKey"
|
||||
| "openAiNativeApiKey"
|
||||
@@ -19,11 +20,13 @@ export type SecretKey =
|
||||
| "authNonce"
|
||||
| "asksageApiKey"
|
||||
| "xaiApiKey"
|
||||
| "moonshotApiKey"
|
||||
| "nebiusApiKey"
|
||||
| "sambanovaApiKey"
|
||||
| "cerebrasApiKey"
|
||||
| "sapAiCoreClientId"
|
||||
| "sapAiCoreClientSecret"
|
||||
| "groqApiKey"
|
||||
|
||||
export type GlobalStateKey =
|
||||
| "awsRegion"
|
||||
@@ -31,6 +34,8 @@ export type GlobalStateKey =
|
||||
| "awsBedrockUsePromptCache"
|
||||
| "awsBedrockEndpoint"
|
||||
| "awsProfile"
|
||||
| "awsBedrockApiKey"
|
||||
| "awsAuthentication"
|
||||
| "awsUseProfile"
|
||||
| "vertexProjectId"
|
||||
| "vertexRegion"
|
||||
@@ -58,6 +63,7 @@ export type GlobalStateKey =
|
||||
| "fireworksModelMaxCompletionTokens"
|
||||
| "fireworksModelMaxTokens"
|
||||
| "qwenApiLine"
|
||||
| "moonshotApiLine"
|
||||
| "mcpMarketplaceCatalog"
|
||||
| "telemetrySetting"
|
||||
| "asksageApiUrl"
|
||||
@@ -73,7 +79,7 @@ export type GlobalStateKey =
|
||||
| "isNewUser"
|
||||
| "welcomeViewCompleted"
|
||||
| "terminalOutputLineLimit"
|
||||
| "mcpRichDisplayEnabled"
|
||||
| "mcpDisplayMode"
|
||||
| "sapAiCoreTokenUrl"
|
||||
| "sapAiCoreBaseUrl"
|
||||
| "sapAiResourceGroup"
|
||||
@@ -112,5 +118,7 @@ export type GlobalStateKey =
|
||||
| "previousModeAwsBedrockCustomSelected"
|
||||
| "previousModeAwsBedrockCustomModelBaseId"
|
||||
| "previousModeSapAiCoreModelId"
|
||||
| "groqModelId"
|
||||
| "groqModelInfo"
|
||||
|
||||
export type LocalStateKey = "localClineRulesToggles" | "localCursorRulesToggles" | "localWindsurfRulesToggles" | "workflowToggles"
|
||||
|
||||
@@ -11,6 +11,7 @@ import { StoredChatSettings } from "@shared/ChatSettings"
|
||||
import { TelemetrySetting } from "@shared/TelemetrySetting"
|
||||
import { UserInfo } from "@shared/UserInfo"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import { DEFAULT_MCP_DISPLAY_MODE, McpDisplayMode } from "@shared/McpDisplayMode"
|
||||
import { migrateEnableCheckpointsSetting, migrateMcpMarketplaceEnableSetting } from "./state-migrations"
|
||||
/*
|
||||
Storage
|
||||
@@ -124,7 +125,9 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
awsBedrockUsePromptCache,
|
||||
awsBedrockEndpoint,
|
||||
awsProfile,
|
||||
awsBedrockApiKey,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
@@ -156,6 +159,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
fireworksModelMaxTokens,
|
||||
userInfo,
|
||||
qwenApiLine,
|
||||
moonshotApiLine,
|
||||
liteLlmApiKey,
|
||||
telemetrySetting,
|
||||
asksageApiKey,
|
||||
@@ -163,6 +167,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
xaiApiKey,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
planActSeparateModelsSettingRaw,
|
||||
favoritedModelIds,
|
||||
@@ -171,7 +177,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
shellIntegrationTimeout,
|
||||
enableCheckpointsSettingRaw,
|
||||
mcpMarketplaceEnabledRaw,
|
||||
mcpRichDisplayEnabled,
|
||||
mcpDisplayMode,
|
||||
mcpResponsesCollapsedRaw,
|
||||
globalWorkflowToggles,
|
||||
terminalReuseEnabled,
|
||||
@@ -183,6 +189,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
claudeCodePath,
|
||||
groqModelId,
|
||||
groqModelInfo,
|
||||
] = await Promise.all([
|
||||
getGlobalState(context, "isNewUser") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "welcomeViewCompleted") as Promise<boolean | undefined>,
|
||||
@@ -197,7 +205,9 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getGlobalState(context, "awsBedrockUsePromptCache") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "awsBedrockEndpoint") as Promise<string | undefined>,
|
||||
getGlobalState(context, "awsProfile") as Promise<string | undefined>,
|
||||
getSecret(context, "awsBedrockApiKey") as Promise<string | undefined>,
|
||||
getGlobalState(context, "awsUseProfile") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "awsAuthentication") as Promise<string | undefined>,
|
||||
getGlobalState(context, "vertexProjectId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "vertexRegion") as Promise<string | undefined>,
|
||||
getGlobalState(context, "openAiBaseUrl") as Promise<string | undefined>,
|
||||
@@ -229,6 +239,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getGlobalState(context, "fireworksModelMaxTokens") as Promise<number | undefined>,
|
||||
getGlobalState(context, "userInfo") as Promise<UserInfo | undefined>,
|
||||
getGlobalState(context, "qwenApiLine") as Promise<string | undefined>,
|
||||
getGlobalState(context, "moonshotApiLine") as Promise<string | undefined>,
|
||||
getSecret(context, "liteLlmApiKey") as Promise<string | undefined>,
|
||||
getGlobalState(context, "telemetrySetting") as Promise<TelemetrySetting | undefined>,
|
||||
getSecret(context, "asksageApiKey") as Promise<string | undefined>,
|
||||
@@ -236,6 +247,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getSecret(context, "xaiApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "sambanovaApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "cerebrasApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "groqApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "moonshotApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "nebiusApiKey") as Promise<string | undefined>,
|
||||
getGlobalState(context, "planActSeparateModelsSetting") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "favoritedModelIds") as Promise<string[] | undefined>,
|
||||
@@ -244,7 +257,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getGlobalState(context, "shellIntegrationTimeout") as Promise<number | undefined>,
|
||||
getGlobalState(context, "enableCheckpointsSetting") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "mcpMarketplaceEnabled") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "mcpRichDisplayEnabled") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "mcpDisplayMode") as Promise<McpDisplayMode | undefined>,
|
||||
getGlobalState(context, "mcpResponsesCollapsed") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "globalWorkflowToggles") as Promise<ClineRulesToggles | undefined>,
|
||||
getGlobalState(context, "terminalReuseEnabled") as Promise<boolean | undefined>,
|
||||
@@ -256,6 +269,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getGlobalState(context, "sapAiCoreTokenUrl") as Promise<string | undefined>,
|
||||
getGlobalState(context, "sapAiResourceGroup") as Promise<string | undefined>,
|
||||
getGlobalState(context, "claudeCodePath") as Promise<string | undefined>,
|
||||
getGlobalState(context, "groqModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "groqModelInfo") as Promise<ModelInfo | undefined>,
|
||||
])
|
||||
|
||||
const localClineRulesToggles = (await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles
|
||||
@@ -330,6 +345,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
const processingStart = performance.now()
|
||||
let apiProvider: ApiProvider
|
||||
if (storedApiProvider) {
|
||||
// Use the explicitly stored provider - this respects user's selection
|
||||
apiProvider = storedApiProvider
|
||||
} else {
|
||||
// Either new user or legacy user that doesn't have the apiProvider stored in state
|
||||
@@ -380,7 +396,9 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
awsBedrockUsePromptCache,
|
||||
awsBedrockEndpoint,
|
||||
awsProfile,
|
||||
awsBedrockApiKey,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
awsBedrockCustomSelected,
|
||||
awsBedrockCustomModelBaseId,
|
||||
vertexProjectId,
|
||||
@@ -407,6 +425,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
togetherModelId,
|
||||
qwenApiKey,
|
||||
qwenApiLine,
|
||||
moonshotApiLine,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
azureApiVersion,
|
||||
@@ -430,6 +449,10 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
xaiApiKey,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
groqModelId,
|
||||
groqModelInfo,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
favoritedModelIds,
|
||||
requestTimeoutMs,
|
||||
@@ -464,7 +487,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
previousModeAwsBedrockCustomModelBaseId,
|
||||
previousModeSapAiCoreModelId,
|
||||
mcpMarketplaceEnabled: mcpMarketplaceEnabled,
|
||||
mcpRichDisplayEnabled: mcpRichDisplayEnabled ?? true,
|
||||
mcpDisplayMode: mcpDisplayMode ?? DEFAULT_MCP_DISPLAY_MODE,
|
||||
mcpResponsesCollapsed: mcpResponsesCollapsed,
|
||||
telemetrySetting: telemetrySetting || "unset",
|
||||
planActSeparateModelsSetting,
|
||||
@@ -490,8 +513,10 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
awsUseCrossRegionInference,
|
||||
awsBedrockUsePromptCache,
|
||||
awsBedrockEndpoint,
|
||||
awsBedrockApiKey,
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
awsBedrockCustomSelected,
|
||||
awsBedrockCustomModelBaseId,
|
||||
vertexProjectId,
|
||||
@@ -530,6 +555,7 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
liteLlmApiKey,
|
||||
liteLlmUsePromptCache,
|
||||
qwenApiLine,
|
||||
moonshotApiLine,
|
||||
asksageApiKey,
|
||||
asksageApiUrl,
|
||||
xaiApiKey,
|
||||
@@ -538,6 +564,10 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
clineAccountId,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
groqModelId,
|
||||
groqModelInfo,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
favoritedModelIds,
|
||||
fireworksApiKey,
|
||||
@@ -575,6 +605,8 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
requestyModelInfo,
|
||||
togetherModelId,
|
||||
fireworksModelId,
|
||||
groqModelId,
|
||||
groqModelInfo,
|
||||
sapAiCoreModelId,
|
||||
|
||||
// Global state updates (27 keys)
|
||||
@@ -584,6 +616,7 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
awsBedrockEndpoint,
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
@@ -598,6 +631,7 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
liteLlmBaseUrl,
|
||||
liteLlmUsePromptCache,
|
||||
qwenApiLine,
|
||||
moonshotApiLine,
|
||||
asksageApiUrl,
|
||||
favoritedModelIds,
|
||||
requestTimeoutMs: apiConfiguration.requestTimeoutMs,
|
||||
@@ -617,6 +651,7 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
awsBedrockApiKey,
|
||||
openAiApiKey,
|
||||
geminiApiKey,
|
||||
openAiNativeApiKey,
|
||||
@@ -632,6 +667,8 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
xaiApiKey,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
@@ -658,6 +695,7 @@ export async function resetGlobalState(context: vscode.ExtensionContext) {
|
||||
"awsAccessKey",
|
||||
"awsSecretKey",
|
||||
"awsSessionToken",
|
||||
"awsBedrockApiKey",
|
||||
"openAiApiKey",
|
||||
"geminiApiKey",
|
||||
"openAiNativeApiKey",
|
||||
@@ -674,6 +712,8 @@ export async function resetGlobalState(context: vscode.ExtensionContext) {
|
||||
"xaiApiKey",
|
||||
"sambanovaApiKey",
|
||||
"cerebrasApiKey",
|
||||
"groqApiKey",
|
||||
"moonshotApiKey",
|
||||
"nebiusApiKey",
|
||||
]
|
||||
for (const key of secretKeys) {
|
||||
|
||||
+18
-7
@@ -81,7 +81,8 @@ import { refreshWorkflowToggles } from "../context/instructions/user-instruction
|
||||
import { MessageStateHandler } from "./message-state"
|
||||
import { TaskState } from "./TaskState"
|
||||
import { ToolExecutor } from "./ToolExecutor"
|
||||
import { formatErrorWithStatusCode, updateApiReqMsg } from "./utils"
|
||||
import { extractErrorDetails, formatErrorWithStatusCode, updateApiReqMsg } from "./utils"
|
||||
import { createDiffViewProvider, getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
|
||||
export const USE_EXPERIMENTAL_CLAUDE4_FEATURES = false
|
||||
|
||||
@@ -174,7 +175,7 @@ export class Task {
|
||||
this.urlContentFetcher = new UrlContentFetcher(context)
|
||||
this.browserSession = new BrowserSession(context, browserSettings)
|
||||
this.contextManager = new ContextManager()
|
||||
this.diffViewProvider = new DiffViewProvider(cwd)
|
||||
this.diffViewProvider = createDiffViewProvider()
|
||||
this.autoApprovalSettings = autoApprovalSettings
|
||||
this.browserSettings = browserSettings
|
||||
this.chatSettings = chatSettings
|
||||
@@ -1574,8 +1575,9 @@ export class Task {
|
||||
|
||||
await this.migrateDisableBrowserToolSetting()
|
||||
const disableBrowserTool = this.browserSettings.disableToolUse ?? false
|
||||
const modelInfo = this.api.getModel()
|
||||
// cline browser tool uses image recognition for navigation (requires model image support).
|
||||
const modelSupportsBrowserUse = this.api.getModel().info.supportsImages ?? false
|
||||
const modelSupportsBrowserUse = modelInfo.info.supportsImages ?? false
|
||||
|
||||
const supportsBrowserUse = modelSupportsBrowserUse && !disableBrowserTool // only enable browser use if the model supports it and the user hasn't disabled it
|
||||
|
||||
@@ -1660,6 +1662,17 @@ export class Task {
|
||||
const isOpenRouterContextWindowError = checkIsOpenRouterContextWindowError(error) && isOpenRouter
|
||||
const isAnthropicContextWindowError = checkIsAnthropicContextWindowError(error) && isAnthropic
|
||||
|
||||
const { statusCode, message, requestId } = extractErrorDetails(error)
|
||||
|
||||
// Capture provider failure telemetry
|
||||
telemetryService.captureProviderApiError({
|
||||
taskId: this.taskId,
|
||||
model: modelInfo.id,
|
||||
errorMessage: message,
|
||||
errorStatus: statusCode,
|
||||
requestId,
|
||||
})
|
||||
|
||||
if (isAnthropic && isAnthropicContextWindowError && !this.taskState.didAutomaticallyRetryFailedApiRequest) {
|
||||
this.taskState.conversationHistoryDeletedRange = this.contextManager.getNextTruncationRange(
|
||||
this.messageStateHandler.getApiConversationHistory(),
|
||||
@@ -1723,7 +1736,7 @@ export class Task {
|
||||
await this.messageStateHandler.updateClineMessage(lastApiReqStartedIndex, {
|
||||
text: JSON.stringify({
|
||||
...currentApiReqInfo, // Spread the modified info (with retryStatus removed)
|
||||
cancelReason: "retries_exhausted", // Indicate that automatic retries failed
|
||||
// cancelReason: "retries_exhausted", // Indicate that automatic retries failed
|
||||
streamingFailedMessage: errorMessage,
|
||||
} satisfies ClineApiReqInfo),
|
||||
})
|
||||
@@ -2053,7 +2066,7 @@ export class Task {
|
||||
content: userContent,
|
||||
})
|
||||
|
||||
telemetryService.captureConversationTurnEvent(this.taskId, currentProviderId, this.api.getModel().id, "user", true)
|
||||
telemetryService.captureConversationTurnEvent(this.taskId, currentProviderId, this.api.getModel().id, "user")
|
||||
|
||||
// since we sent off a placeholder api_req_started message to update the webview while waiting to actually start the API request (to load potential details for example), we need to update the text of that message
|
||||
const lastApiReqIndex = findLastIndex(this.messageStateHandler.getClineMessages(), (m) => m.say === "api_req_started")
|
||||
@@ -2123,7 +2136,6 @@ export class Task {
|
||||
currentProviderId,
|
||||
this.api.getModel().id,
|
||||
"assistant",
|
||||
true,
|
||||
{
|
||||
tokensIn: inputTokens,
|
||||
tokensOut: outputTokens,
|
||||
@@ -2303,7 +2315,6 @@ export class Task {
|
||||
currentProviderId,
|
||||
this.api.getModel().id,
|
||||
"assistant",
|
||||
true,
|
||||
{
|
||||
tokensIn: inputTokens,
|
||||
tokensOut: outputTokens,
|
||||
|
||||
@@ -6,13 +6,20 @@ import { calculateApiCostAnthropic } from "@/utils/cost"
|
||||
import { ApiHandler } from "@/api"
|
||||
|
||||
export function formatErrorWithStatusCode(error: any): string {
|
||||
const statusCode = error.status || error.statusCode || (error.response && error.response.status)
|
||||
const message = error.message ?? JSON.stringify(serializeError(error), null, 2)
|
||||
const { statusCode, message } = extractErrorDetails(error)
|
||||
|
||||
// Only prepend the statusCode if it's not already part of the message
|
||||
return statusCode && !message.includes(statusCode.toString()) ? `${statusCode} - ${message}` : message
|
||||
}
|
||||
|
||||
export function extractErrorDetails(error: any): { message: string; statusCode?: number; requestId?: string } {
|
||||
const statusCode = error.status || error.statusCode || (error.response && error.response?.status)
|
||||
const message = error.message ?? JSON.stringify(serializeError(error), null, 2)
|
||||
const requestId = error.request_id || error.response?.request_id || undefined
|
||||
|
||||
return { message, statusCode, requestId }
|
||||
}
|
||||
|
||||
export const showNotificationForApprovalIfAutoApprovalEnabled = (
|
||||
message: string,
|
||||
autoApprovalSettingsEnabled: boolean,
|
||||
|
||||
@@ -10,6 +10,8 @@ import path from "node:path"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import { Uri } from "vscode"
|
||||
import { ExtensionMessage } from "@/shared/ExtensionMessage"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
|
||||
export abstract class WebviewProvider {
|
||||
public static readonly sideBarId = "claude-dev.SidebarProvider" // used in package.json as the view's id. This value cannot be changed due to how vscode caches views based on their id, and updating the id would break existing instances of the extension.
|
||||
@@ -260,9 +262,16 @@ export abstract class WebviewProvider {
|
||||
try {
|
||||
await axios.get(`http://${localServerUrl}`)
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(
|
||||
"Cline: Local webview dev server is not running, HMR will not work. Please run 'npm run dev:webview' before launching the extension to enable HMR. Using bundled assets.",
|
||||
)
|
||||
// Only show the error message if not in development mode.
|
||||
if (!process.env.IS_DEV) {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message:
|
||||
"Cline: Local webview dev server is not running, HMR will not work. Please run 'npm run dev:webview' before launching the extension to enable HMR. Using bundled assets.",
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return this.getHtmlContent()
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import * as path from "path"
|
||||
import { Controller } from "@core/controller"
|
||||
import { HistoryItem } from "@shared/HistoryItem"
|
||||
import { ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
|
||||
/**
|
||||
* Registers development-only commands for task manipulation.
|
||||
@@ -96,7 +98,13 @@ export function registerTaskCommands(context: vscode.ExtensionContext, controlle
|
||||
// Update the UI to show the new tasks
|
||||
await controller.postStateToWebview()
|
||||
|
||||
vscode.window.showInformationMessage(`Created ${tasksCount} test tasks`)
|
||||
const message = `Created ${tasksCount} test tasks`
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
},
|
||||
)
|
||||
}),
|
||||
|
||||
+53
-31
@@ -32,12 +32,14 @@ import {
|
||||
import { sendFocusChatInputEvent } from "./core/controller/ui/subscribeToFocusChatInput"
|
||||
import { FileContextTracker } from "./core/context/context-tracking/FileContextTracker"
|
||||
import * as hostProviders from "@hosts/host-providers"
|
||||
import { vscodeHostBridgeClient } from "@/hosts/vscode/client/host-grpc-client"
|
||||
import { VscodeWebviewProvider } from "./core/webview/VscodeWebviewProvider"
|
||||
import { vscodeHostBridgeClient } from "@/hosts/vscode/hostbridge/client/host-grpc-client"
|
||||
import { VscodeWebviewProvider } from "./hosts/vscode/VscodeWebviewProvider"
|
||||
import { ExtensionContext } from "vscode"
|
||||
import { AuthService } from "./services/auth/AuthService"
|
||||
import { writeTextToClipboard, readTextFromClipboard } from "@/utils/env"
|
||||
|
||||
import { VscodeDiffViewProvider } from "./hosts/vscode/VscodeDiffViewProvider"
|
||||
import { getHostBridgeProvider } from "@hosts/host-providers"
|
||||
import { ShowMessageRequest, ShowMessageType } from "./shared/proto/host/window"
|
||||
/*
|
||||
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
|
||||
|
||||
@@ -104,7 +106,12 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
const message = `Cline has been updated to v${currentVersion}`
|
||||
await vscode.commands.executeCommand("claude-dev.SidebarProvider.focus")
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
vscode.window.showInformationMessage(message)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
// Record that we've shown the popup for this version.
|
||||
await context.globalState.update("clineLastPopupNotificationVersion", currentVersion)
|
||||
}
|
||||
@@ -294,24 +301,12 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
break
|
||||
}
|
||||
case "/auth": {
|
||||
const authService = AuthService.getInstance()
|
||||
console.log("Auth callback received:", uri.toString())
|
||||
|
||||
const token = query.get("idToken")
|
||||
const state = query.get("state")
|
||||
const provider = query.get("provider")
|
||||
|
||||
console.log("Auth callback received:", {
|
||||
token: token,
|
||||
state: state,
|
||||
provider: provider,
|
||||
})
|
||||
|
||||
// Validate state parameter
|
||||
if (!(authService.authNonce === state)) {
|
||||
vscode.window.showErrorMessage("Invalid auth state")
|
||||
return
|
||||
}
|
||||
console.log("Auth callback received:", { provider })
|
||||
|
||||
if (token) {
|
||||
await visibleWebview?.controller.handleAuthCallback(token, provider)
|
||||
@@ -368,7 +363,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
languageId,
|
||||
Array.isArray(diagnostics) ? diagnostics : undefined,
|
||||
)
|
||||
telemetryService.captureButtonClick("codeAction_addToChat", visibleWebview?.controller.task?.taskId, true)
|
||||
telemetryService.captureButtonClick("codeAction_addToChat", visibleWebview?.controller.task?.taskId)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -418,7 +413,12 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
// Ensure clipboard is restored even if an error occurs
|
||||
await writeTextToClipboard(tempCopyBuffer)
|
||||
console.error("Error getting terminal contents:", error)
|
||||
vscode.window.showErrorMessage("Failed to get terminal contents")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to get terminal contents",
|
||||
}),
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -540,7 +540,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
// Send to sidebar provider with diagnostics
|
||||
const visibleWebview = WebviewProvider.getVisibleInstance()
|
||||
await visibleWebview?.controller.fixWithCline(selectedText, filePath, languageId, diagnostics)
|
||||
telemetryService.captureButtonClick("codeAction_fixWithCline", visibleWebview?.controller.task?.taskId, true)
|
||||
telemetryService.captureButtonClick("codeAction_fixWithCline", visibleWebview?.controller.task?.taskId)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -554,7 +554,12 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
}
|
||||
const selectedText = editor.document.getText(range)
|
||||
if (!selectedText.trim()) {
|
||||
vscode.window.showInformationMessage("Please select some code to explain.")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Please select some code to explain.",
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
const filePath = editor.document.uri.fsPath
|
||||
@@ -562,7 +567,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
const fileMention = visibleWebview?.controller.getFileMentionFromPath(filePath) || filePath
|
||||
const prompt = `Explain the following code from ${fileMention}:\n\`\`\`${editor.document.languageId}\n${selectedText}\n\`\`\``
|
||||
await visibleWebview?.controller.initTask(prompt)
|
||||
telemetryService.captureButtonClick("codeAction_explainCode", visibleWebview?.controller.task?.taskId, true)
|
||||
telemetryService.captureButtonClick("codeAction_explainCode", visibleWebview?.controller.task?.taskId)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -576,7 +581,12 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
}
|
||||
const selectedText = editor.document.getText(range)
|
||||
if (!selectedText.trim()) {
|
||||
vscode.window.showInformationMessage("Please select some code to improve.")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Please select some code to improve.",
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
const filePath = editor.document.uri.fsPath
|
||||
@@ -584,7 +594,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
const fileMention = visibleWebview?.controller.getFileMentionFromPath(filePath) || filePath
|
||||
const prompt = `Improve the following code from ${fileMention} (e.g., suggest refactorings, optimizations, or better practices):\n\`\`\`${editor.document.languageId}\n${selectedText}\n\`\`\``
|
||||
await visibleWebview?.controller.initTask(prompt)
|
||||
telemetryService.captureButtonClick("codeAction_improveCode", visibleWebview?.controller.task?.taskId, true)
|
||||
telemetryService.captureButtonClick("codeAction_improveCode", visibleWebview?.controller.task?.taskId)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -641,11 +651,14 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
sendFocusChatInputEvent(clientId)
|
||||
} else {
|
||||
console.error("FocusChatInput: Could not find or activate a Cline webview to focus.")
|
||||
vscode.window.showErrorMessage(
|
||||
"Could not activate Cline view. Please try opening it manually from the Activity Bar.",
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Could not activate Cline view. Please try opening it manually from the Activity Bar.",
|
||||
}),
|
||||
)
|
||||
}
|
||||
telemetryService.captureButtonClick("command_focusChatInput", activeWebviewProvider?.controller.task?.taskId, true)
|
||||
telemetryService.captureButtonClick("command_focusChatInput", activeWebviewProvider?.controller.task?.taskId)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -653,7 +666,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("cline.openWalkthrough", async () => {
|
||||
await vscode.commands.executeCommand("workbench.action.openWalkthrough", "saoudrizwan.claude-dev#ClineWalkthrough")
|
||||
telemetryService.captureButtonClick("command_openWalkthrough", undefined, true)
|
||||
telemetryService.captureButtonClick("command_openWalkthrough")
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -677,6 +690,14 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
}),
|
||||
)
|
||||
|
||||
context.subscriptions.push(
|
||||
context.secrets.onDidChange((event) => {
|
||||
if (event.key === "clineAccountId") {
|
||||
AuthService.getInstance(context)?.restoreRefreshTokenAndRetrieveAuthInfo()
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
return createClineAPI(outputChannel, sidebarWebview.controller)
|
||||
}
|
||||
|
||||
@@ -686,7 +707,10 @@ function maybeSetupHostProviders(context: ExtensionContext) {
|
||||
const createWebview = function (type: WebviewProviderType) {
|
||||
return new VscodeWebviewProvider(context, outputChannel, type)
|
||||
}
|
||||
hostProviders.initializeHostProviders(createWebview, vscodeHostBridgeClient)
|
||||
const createDiffView = function () {
|
||||
return new VscodeDiffViewProvider()
|
||||
}
|
||||
hostProviders.initializeHostProviders(createWebview, createDiffView, vscodeHostBridgeClient)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -704,8 +728,6 @@ export async function deactivate() {
|
||||
// Dispose all webview instances
|
||||
await WebviewProvider.disposeAllInstances()
|
||||
|
||||
await telemetryService.sendCollectedEvents()
|
||||
|
||||
// Clean up test mode
|
||||
cleanupTestMode()
|
||||
await posthogClientProvider.shutdown()
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
WorkspaceServiceClientInterface,
|
||||
EnvServiceClientInterface,
|
||||
WindowServiceClientInterface,
|
||||
DiffServiceClientInterface,
|
||||
} from "@generated/hosts/host-bridge-client-types"
|
||||
|
||||
/**
|
||||
@@ -13,6 +14,7 @@ export interface HostBridgeClientProvider {
|
||||
workspaceClient: WorkspaceServiceClientInterface
|
||||
envClient: EnvServiceClientInterface
|
||||
windowClient: WindowServiceClientInterface
|
||||
diffClient: DiffServiceClientInterface
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
import { WebviewProvider } from "@core/webview"
|
||||
import { HostBridgeClientProvider } from "./host-provider-types"
|
||||
import { WebviewProviderType } from "@/shared/webview/types"
|
||||
import { DiffViewProvider } from "@/integrations/editor/DiffViewProvider"
|
||||
|
||||
/**
|
||||
* A function that creates WebviewProvider instances
|
||||
*/
|
||||
export type WebviewProviderCreator = (providerType: WebviewProviderType) => WebviewProvider
|
||||
|
||||
export type DiffViewProviderCreator = () => DiffViewProvider
|
||||
|
||||
let _webviewProviderCreator: WebviewProviderCreator | undefined
|
||||
let _diffViewProviderCreator: DiffViewProviderCreator | undefined
|
||||
let _hostBridgeProvider: HostBridgeClientProvider | undefined
|
||||
|
||||
export var isSetup: boolean = false
|
||||
|
||||
export function initializeHostProviders(
|
||||
webviewProviderCreator: WebviewProviderCreator,
|
||||
diffViewProviderCreator: DiffViewProviderCreator,
|
||||
hostBridgeProvider: HostBridgeClientProvider,
|
||||
) {
|
||||
_webviewProviderCreator = webviewProviderCreator
|
||||
_diffViewProviderCreator = diffViewProviderCreator
|
||||
_hostBridgeProvider = hostBridgeProvider
|
||||
isSetup = true
|
||||
}
|
||||
@@ -28,6 +34,13 @@ export function createWebviewProvider(providerType: WebviewProviderType): Webvie
|
||||
return _webviewProviderCreator(providerType)
|
||||
}
|
||||
|
||||
export function createDiffViewProvider(): DiffViewProvider {
|
||||
if (!_diffViewProviderCreator) {
|
||||
throw Error("Host providers not initialized")
|
||||
}
|
||||
return _diffViewProviderCreator()
|
||||
}
|
||||
|
||||
export function getHostBridgeProvider(): HostBridgeClientProvider {
|
||||
if (!_hostBridgeProvider) {
|
||||
throw Error("Host providers not initialized")
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { arePathsEqual } from "@/utils/path"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { DecorationController } from "@integrations/editor/DecorationController"
|
||||
import { DIFF_VIEW_URI_SCHEME, DiffViewProvider } from "@integrations/editor/DiffViewProvider"
|
||||
|
||||
export class VscodeDiffViewProvider extends DiffViewProvider {
|
||||
override async openDiffEditor(): Promise<void> {
|
||||
if (!this.absolutePath) {
|
||||
throw new Error("No file path set")
|
||||
}
|
||||
// get diagnostics before editing the file, we'll compare to diagnostics after editing to see if cline needs to fix anything
|
||||
this.preDiagnostics = vscode.languages.getDiagnostics()
|
||||
|
||||
// if the file was already open, close it (must happen after showing the diff view since if it's the only tab the column will close)
|
||||
this.documentWasOpen = false
|
||||
// close the tab if it's open (it's already been saved)
|
||||
const tabs = vscode.window.tabGroups.all
|
||||
.map((tg) => tg.tabs)
|
||||
.flat()
|
||||
.filter((tab) => tab.input instanceof vscode.TabInputText && arePathsEqual(tab.input.uri.fsPath, this.absolutePath))
|
||||
for (const tab of tabs) {
|
||||
if (!tab.isDirty) {
|
||||
await vscode.window.tabGroups.close(tab)
|
||||
}
|
||||
this.documentWasOpen = true
|
||||
}
|
||||
|
||||
const uri = vscode.Uri.file(this.absolutePath)
|
||||
// If this diff editor is already open (ie if a previous write file was interrupted) then we should activate that instead of opening a new diff
|
||||
const diffTab = vscode.window.tabGroups.all
|
||||
.flatMap((group) => group.tabs)
|
||||
.find(
|
||||
(tab) =>
|
||||
tab.input instanceof vscode.TabInputTextDiff &&
|
||||
tab.input?.original?.scheme === DIFF_VIEW_URI_SCHEME &&
|
||||
arePathsEqual(tab.input.modified.fsPath, uri.fsPath),
|
||||
)
|
||||
|
||||
if (diffTab && diffTab.input instanceof vscode.TabInputTextDiff) {
|
||||
// Use already open diff editor.
|
||||
this.activeDiffEditor = await vscode.window.showTextDocument(diffTab.input.modified, {
|
||||
preserveFocus: true,
|
||||
})
|
||||
} else {
|
||||
// Open new diff editor.
|
||||
this.activeDiffEditor = await new Promise<vscode.TextEditor>((resolve, reject) => {
|
||||
const fileName = path.basename(uri.fsPath)
|
||||
const fileExists = this.editType === "modify"
|
||||
const disposable = vscode.window.onDidChangeActiveTextEditor((editor) => {
|
||||
if (editor && arePathsEqual(editor.document.uri.fsPath, uri.fsPath)) {
|
||||
disposable.dispose()
|
||||
resolve(editor)
|
||||
}
|
||||
})
|
||||
vscode.commands.executeCommand(
|
||||
"vscode.diff",
|
||||
vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${fileName}`).with({
|
||||
query: Buffer.from(this.originalContent ?? "").toString("base64"),
|
||||
}),
|
||||
uri,
|
||||
`${fileName}: ${fileExists ? "Original ↔ Cline's Changes" : "New File"} (Editable)`,
|
||||
{
|
||||
preserveFocus: true,
|
||||
},
|
||||
)
|
||||
// This may happen on very slow machines ie project idx
|
||||
setTimeout(() => {
|
||||
disposable.dispose()
|
||||
reject(new Error("Failed to open diff editor, please try again..."))
|
||||
}, 10_000)
|
||||
})
|
||||
}
|
||||
|
||||
this.fadedOverlayController = new DecorationController("fadedOverlay", this.activeDiffEditor)
|
||||
this.activeLineController = new DecorationController("activeLine", this.activeDiffEditor)
|
||||
// Apply faded overlay to all lines initially
|
||||
this.fadedOverlayController.addLines(0, this.activeDiffEditor.document.lineCount)
|
||||
}
|
||||
|
||||
override async replaceText(
|
||||
content: string,
|
||||
rangeToReplace: { startLine: number; endLine: number },
|
||||
currentLine: number,
|
||||
): Promise<void> {
|
||||
const document = this.activeDiffEditor?.document
|
||||
if (!document) {
|
||||
throw new Error("User closed text editor, unable to edit file...")
|
||||
}
|
||||
|
||||
const edit = new vscode.WorkspaceEdit()
|
||||
const range = new vscode.Range(rangeToReplace.startLine, 0, rangeToReplace.endLine, 0)
|
||||
edit.replace(document.uri, range, content)
|
||||
await vscode.workspace.applyEdit(edit)
|
||||
|
||||
// Update decorations for the entire changed section
|
||||
this.activeLineController?.setActiveLine(currentLine)
|
||||
this.fadedOverlayController?.updateOverlayAfterLine(currentLine, document.lineCount)
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -4,8 +4,8 @@ import { sendThemeEvent } from "@core/controller/ui/subscribeToTheme"
|
||||
import { getTheme } from "@integrations/theme/getTheme"
|
||||
import * as vscode from "vscode"
|
||||
import { Uri } from "vscode"
|
||||
import { WebviewProvider } from "."
|
||||
import { sendDidBecomeVisibleEvent } from "../controller/ui/subscribeToDidBecomeVisible"
|
||||
import { WebviewProvider } from "@core/webview"
|
||||
import { sendDidBecomeVisibleEvent } from "@core/controller/ui/subscribeToDidBecomeVisible"
|
||||
|
||||
/*
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
@@ -1,5 +1,5 @@
|
||||
import { StreamingCallbacks } from "@/hosts/host-provider-types"
|
||||
import { HostServiceHandlerConfig, hostServiceHandlers } from "./host-grpc-service-config"
|
||||
import { HostServiceHandlerConfig, hostServiceHandlers } from "@generated/hosts/vscode/hostbridge-grpc-service-config"
|
||||
import { GrpcRequestRegistry } from "@core/controller/grpc-request-registry"
|
||||
|
||||
/**
|
||||
@@ -1,4 +1,4 @@
|
||||
import { StreamingResponseHandler } from "./host-grpc-handler"
|
||||
import { StreamingResponseHandler } from "./hostbridge-grpc-handler"
|
||||
|
||||
/**
|
||||
* Generic type for service method handlers
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import { GrpcHandler } from "../host-grpc-handler"
|
||||
import { GrpcHandler } from "@/hosts/vscode/hostbridge-grpc-handler"
|
||||
import { StreamingCallbacks } from "@/hosts/host-provider-types"
|
||||
|
||||
// Generic type for any protobuf service definition
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
import { createGrpcClient } from "@hosts/vscode/client/host-grpc-client-base"
|
||||
import { createGrpcClient } from "@hosts/vscode/hostbridge/client/host-grpc-client-base"
|
||||
import { HostBridgeClientProvider } from "@/hosts/host-provider-types"
|
||||
import * as host from "@shared/proto/index.host"
|
||||
|
||||
@@ -7,4 +7,5 @@ export const vscodeHostBridgeClient: HostBridgeClientProvider = {
|
||||
workspaceClient: createGrpcClient(host.WorkspaceServiceDefinition),
|
||||
envClient: createGrpcClient(host.EnvServiceDefinition),
|
||||
windowClient: createGrpcClient(host.WindowServiceDefinition),
|
||||
diffClient: createGrpcClient(host.DiffServiceDefinition),
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { OpenDiffRequest, OpenDiffResponse } from "@/shared/proto/index.host"
|
||||
|
||||
export async function openDiff(_request: OpenDiffRequest): Promise<OpenDiffResponse> {
|
||||
throw new Error("diffService.openDiff is not supported. Use the VscodeDiffViewProvider.")
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ReplaceTextRequest, ReplaceTextResponse } from "@/shared/proto/index.host"
|
||||
|
||||
export async function replaceText(_request: ReplaceTextRequest): Promise<ReplaceTextResponse> {
|
||||
throw new Error("diffService.replaceText is not supported. Use the VscodeDiffViewProvider.")
|
||||
}
|
||||
Vendored
Vendored
+2
-2
@@ -1,7 +1,7 @@
|
||||
import * as fs from "fs/promises"
|
||||
import * as fsSync from "fs"
|
||||
import { SubscribeToFileRequest, FileChangeEvent, FileChangeEvent_ChangeType } from "@shared/proto/host/watch"
|
||||
import { StreamingResponseHandler, getRequestRegistry } from "../host-grpc-handler"
|
||||
import { SubscribeToFileRequest, FileChangeEvent_ChangeType } from "@shared/proto/host/watch"
|
||||
import { StreamingResponseHandler, getRequestRegistry } from "@/hosts/vscode/hostbridge-grpc-handler"
|
||||
|
||||
// Debounce configuration
|
||||
const DEBOUNCE_DELAY = 100 // ms
|
||||
@@ -0,0 +1,26 @@
|
||||
import { window } from "vscode"
|
||||
import { SelectedResponse, ShowMessageRequest, ShowMessageType } from "@/shared/proto/index.host"
|
||||
|
||||
const DEFAULT_OPTIONS = { modal: false, items: [] } as const
|
||||
|
||||
export async function showMessage(request: ShowMessageRequest): Promise<SelectedResponse | undefined> {
|
||||
const { message, type, options } = request
|
||||
const { modal, detail, items } = { ...DEFAULT_OPTIONS, ...options }
|
||||
const option = { modal, detail }
|
||||
|
||||
let selectedOption: string | undefined = undefined
|
||||
|
||||
switch (type) {
|
||||
case ShowMessageType.ERROR:
|
||||
selectedOption = await window.showErrorMessage(message, option, ...items)
|
||||
break
|
||||
case ShowMessageType.WARNING:
|
||||
selectedOption = await window.showWarningMessage(message, option, ...items)
|
||||
break
|
||||
default:
|
||||
selectedOption = await window.showInformationMessage(message, option, ...items)
|
||||
break
|
||||
}
|
||||
|
||||
return SelectedResponse.create({ selectedOption })
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import deepEqual from "fast-deep-equal"
|
||||
import { getCwd } from "@/utils/path"
|
||||
|
||||
export function getNewDiagnostics(
|
||||
oldDiagnostics: [vscode.Uri, vscode.Diagnostic[]][],
|
||||
@@ -70,11 +71,11 @@ export function getNewDiagnostics(
|
||||
// // - New error in file3 (1:1)
|
||||
|
||||
// will return empty string if no problems with the given severity are found
|
||||
export function diagnosticsToProblemsString(
|
||||
export async function diagnosticsToProblemsString(
|
||||
diagnostics: [vscode.Uri, vscode.Diagnostic[]][],
|
||||
severities: vscode.DiagnosticSeverity[],
|
||||
cwd: string,
|
||||
): string {
|
||||
): Promise<string> {
|
||||
const cwd = await getCwd()
|
||||
let result = ""
|
||||
for (const [uri, fileDiagnostics] of diagnostics) {
|
||||
const problems = fileDiagnostics.filter((d) => severities.includes(d.severity))
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import * as fs from "fs/promises"
|
||||
import { createDirectoriesForFile } from "@utils/fs"
|
||||
import { arePathsEqual } from "@utils/path"
|
||||
import { arePathsEqual, getCwd } from "@utils/path"
|
||||
import { formatResponse } from "@core/prompts/responses"
|
||||
import { DecorationController } from "./DecorationController"
|
||||
import * as diff from "diff"
|
||||
@@ -10,50 +10,45 @@ import { diagnosticsToProblemsString, getNewDiagnostics } from "../diagnostics"
|
||||
import { detectEncoding } from "../misc/extract-text"
|
||||
import * as iconv from "iconv-lite"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { ShowTextDocumentRequest, ShowTextDocumentOptions, TextEditorInfo } from "@/shared/proto/host/window"
|
||||
import { ShowTextDocumentRequest, ShowTextDocumentOptions } from "@/shared/proto/host/window"
|
||||
|
||||
export const DIFF_VIEW_URI_SCHEME = "cline-diff"
|
||||
|
||||
export class DiffViewProvider {
|
||||
export abstract class DiffViewProvider {
|
||||
editType?: "create" | "modify"
|
||||
isEditing = false
|
||||
originalContent: string | undefined
|
||||
private createdDirs: string[] = []
|
||||
private documentWasOpen = false
|
||||
private relPath?: string
|
||||
private newContent?: string
|
||||
private activeDiffEditor?: vscode.TextEditor
|
||||
private fadedOverlayController?: DecorationController
|
||||
private activeLineController?: DecorationController
|
||||
protected documentWasOpen = false
|
||||
protected relPath?: string
|
||||
protected absolutePath?: string
|
||||
protected fileEncoding: string = "utf8"
|
||||
private streamedLines: string[] = []
|
||||
private preDiagnostics: [vscode.Uri, vscode.Diagnostic[]][] = []
|
||||
private fileEncoding: string = "utf8"
|
||||
private lastFirstVisibleLine: number = 0
|
||||
private shouldAutoScroll: boolean = true
|
||||
private scrollListener?: vscode.Disposable
|
||||
private newContent?: string
|
||||
|
||||
constructor(private cwd: string) {}
|
||||
protected activeDiffEditor?: vscode.TextEditor
|
||||
protected fadedOverlayController?: DecorationController
|
||||
protected activeLineController?: DecorationController
|
||||
protected preDiagnostics: [vscode.Uri, vscode.Diagnostic[]][] = []
|
||||
|
||||
async open(relPath: string): Promise<void> {
|
||||
this.relPath = relPath
|
||||
const fileExists = this.editType === "modify"
|
||||
const absolutePath = path.resolve(this.cwd, relPath)
|
||||
constructor() {}
|
||||
|
||||
public async open(relPath: string): Promise<void> {
|
||||
this.isEditing = true
|
||||
this.shouldAutoScroll = true
|
||||
this.lastFirstVisibleLine = 0
|
||||
this.relPath = relPath
|
||||
this.absolutePath = path.resolve(await getCwd(), relPath)
|
||||
const fileExists = this.editType === "modify"
|
||||
|
||||
// if the file is already open, ensure it's not dirty before getting its contents
|
||||
if (fileExists) {
|
||||
const existingDocument = vscode.workspace.textDocuments.find((doc) => arePathsEqual(doc.uri.fsPath, absolutePath))
|
||||
const existingDocument = vscode.workspace.textDocuments.find((doc) =>
|
||||
arePathsEqual(doc.uri.fsPath, this.absolutePath),
|
||||
)
|
||||
if (existingDocument && existingDocument.isDirty) {
|
||||
await existingDocument.save()
|
||||
}
|
||||
}
|
||||
|
||||
// get diagnostics before editing the file, we'll compare to diagnostics after editing to see if cline needs to fix anything
|
||||
this.preDiagnostics = vscode.languages.getDiagnostics()
|
||||
|
||||
if (fileExists) {
|
||||
const fileBuffer = await fs.readFile(absolutePath)
|
||||
const fileBuffer = await fs.readFile(this.absolutePath)
|
||||
this.fileEncoding = await detectEncoding(fileBuffer)
|
||||
this.originalContent = iconv.decode(fileBuffer, this.fileEncoding)
|
||||
} else {
|
||||
@@ -61,48 +56,30 @@ export class DiffViewProvider {
|
||||
this.fileEncoding = "utf8"
|
||||
}
|
||||
// for new files, create any necessary directories and keep track of new directories to delete if the user denies the operation
|
||||
this.createdDirs = await createDirectoriesForFile(absolutePath)
|
||||
this.createdDirs = await createDirectoriesForFile(this.absolutePath)
|
||||
// make sure the file exists before we open it
|
||||
if (!fileExists) {
|
||||
await fs.writeFile(absolutePath, "")
|
||||
await fs.writeFile(this.absolutePath, "")
|
||||
}
|
||||
// if the file was already open, close it (must happen after showing the diff view since if it's the only tab the column will close)
|
||||
this.documentWasOpen = false
|
||||
// close the tab if it's open (it's already saved above)
|
||||
const tabs = vscode.window.tabGroups.all
|
||||
.map((tg) => tg.tabs)
|
||||
.flat()
|
||||
.filter((tab) => tab.input instanceof vscode.TabInputText && arePathsEqual(tab.input.uri.fsPath, absolutePath))
|
||||
for (const tab of tabs) {
|
||||
if (!tab.isDirty) {
|
||||
await vscode.window.tabGroups.close(tab)
|
||||
}
|
||||
this.documentWasOpen = true
|
||||
}
|
||||
this.activeDiffEditor = await this.openDiffEditor()
|
||||
this.fadedOverlayController = new DecorationController("fadedOverlay", this.activeDiffEditor)
|
||||
this.activeLineController = new DecorationController("activeLine", this.activeDiffEditor)
|
||||
// Apply faded overlay to all lines initially
|
||||
this.fadedOverlayController.addLines(0, this.activeDiffEditor.document.lineCount)
|
||||
await this.openDiffEditor()
|
||||
this.scrollEditorToLine(0) // will this crash for new files?
|
||||
this.streamedLines = []
|
||||
|
||||
// Add scroll detection to disable auto-scrolling when user scrolls up
|
||||
this.scrollListener = vscode.window.onDidChangeTextEditorVisibleRanges((e: vscode.TextEditorVisibleRangesChangeEvent) => {
|
||||
if (e.textEditor === this.activeDiffEditor) {
|
||||
const currentFirstVisibleLine = e.visibleRanges[0]?.start.line || 0
|
||||
|
||||
// If the first visible line moved upward, user scrolled up
|
||||
// if (currentFirstVisibleLine < this.lastFirstVisibleLine) {
|
||||
// this.shouldAutoScroll = false
|
||||
// }
|
||||
|
||||
// Always update our tracking variable
|
||||
this.lastFirstVisibleLine = currentFirstVisibleLine
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a diff editor or viewer for the current file.
|
||||
*
|
||||
* This abstract method must be implemented by subclasses to create and display
|
||||
* a diff editor or viewer that shows the difference between the original and
|
||||
* modified content.
|
||||
*
|
||||
* Called automatically by the `open` method after ensuring the file exists and
|
||||
* creating any necessary directories.
|
||||
*
|
||||
* @returns A promise that resolves when the diff editor is open and ready
|
||||
*/
|
||||
protected abstract openDiffEditor(): Promise<void>
|
||||
|
||||
async update(
|
||||
accumulatedContent: string,
|
||||
isFinal: boolean,
|
||||
@@ -144,46 +121,38 @@ export class DiffViewProvider {
|
||||
|
||||
// Replace all content up to the current line with accumulated lines
|
||||
// This is necessary (as compared to inserting one line at a time) to handle cases where html tags on previous lines are auto closed for example
|
||||
const edit = new vscode.WorkspaceEdit()
|
||||
const rangeToReplace = new vscode.Range(0, 0, currentLine + 1, 0)
|
||||
const contentToReplace = accumulatedLines.slice(0, currentLine + 1).join("\n") + "\n"
|
||||
edit.replace(document.uri, rangeToReplace, contentToReplace)
|
||||
await vscode.workspace.applyEdit(edit)
|
||||
const rangeToReplace = { startLine: 0, endLine: currentLine + 1 }
|
||||
await this.replaceText(contentToReplace, rangeToReplace, currentLine)
|
||||
|
||||
// Update decorations for the entire changed section
|
||||
this.activeLineController.setActiveLine(currentLine)
|
||||
this.fadedOverlayController.updateOverlayAfterLine(currentLine, document.lineCount)
|
||||
|
||||
// Scroll to the actual change location if provided, otherwise use the old logic
|
||||
if (this.shouldAutoScroll) {
|
||||
if (changeLocation) {
|
||||
// We have the actual location of the change, scroll to it
|
||||
const targetLine = changeLocation.startLine
|
||||
this.scrollEditorToLine(targetLine)
|
||||
// Scroll to the actual change location if provided.
|
||||
if (changeLocation) {
|
||||
// We have the actual location of the change, scroll to it
|
||||
const targetLine = changeLocation.startLine
|
||||
this.scrollEditorToLine(targetLine)
|
||||
} else {
|
||||
// Fallback to the old logic for non-replacement updates
|
||||
if (diffLines.length <= 5) {
|
||||
// For small changes, just jump directly to the line
|
||||
this.scrollEditorToLine(currentLine)
|
||||
} else {
|
||||
// Fallback to the old logic for non-replacement updates
|
||||
if (diffLines.length <= 5) {
|
||||
// For small changes, just jump directly to the line
|
||||
this.scrollEditorToLine(currentLine)
|
||||
} else {
|
||||
// For larger changes, create a quick scrolling animation
|
||||
const startLine = this.streamedLines.length
|
||||
const endLine = currentLine
|
||||
const totalLines = endLine - startLine
|
||||
const numSteps = 10 // Adjust this number to control animation speed
|
||||
const stepSize = Math.max(1, Math.floor(totalLines / numSteps))
|
||||
// For larger changes, create a quick scrolling animation
|
||||
const startLine = this.streamedLines.length
|
||||
const endLine = currentLine
|
||||
const totalLines = endLine - startLine
|
||||
const numSteps = 10 // Adjust this number to control animation speed
|
||||
const stepSize = Math.max(1, Math.floor(totalLines / numSteps))
|
||||
|
||||
// Create and await the smooth scrolling animation
|
||||
for (let line = startLine; line <= endLine; line += stepSize) {
|
||||
this.activeDiffEditor?.revealRange(
|
||||
new vscode.Range(line, 0, line, 0),
|
||||
vscode.TextEditorRevealType.InCenter,
|
||||
)
|
||||
await new Promise((resolve) => setTimeout(resolve, 16)) // ~60fps
|
||||
}
|
||||
// Ensure we end at the final line
|
||||
this.scrollEditorToLine(currentLine)
|
||||
// Create and await the smooth scrolling animation
|
||||
for (let line = startLine; line <= endLine; line += stepSize) {
|
||||
this.activeDiffEditor?.revealRange(
|
||||
new vscode.Range(line, 0, line, 0),
|
||||
vscode.TextEditorRevealType.InCenter,
|
||||
)
|
||||
await new Promise((resolve) => setTimeout(resolve, 16)) // ~60fps
|
||||
}
|
||||
// Ensure we end at the final line
|
||||
this.scrollEditorToLine(currentLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,6 +180,24 @@ export class DiffViewProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces text in the diff editor with the specified content.
|
||||
*
|
||||
* This abstract method must be implemented by subclasses to handle the actual
|
||||
* text replacement in their specific diff editor implementation. It's called
|
||||
* during the streaming update process to progressively show changes.
|
||||
*
|
||||
* @param content The new content to insert into the document
|
||||
* @param rangeToReplace An object specifying the line range to replace
|
||||
* @param currentLine The current line number being edited, used for scroll positioning
|
||||
* @returns A promise that resolves when the text replacement is complete
|
||||
*/
|
||||
abstract replaceText(
|
||||
content: string,
|
||||
rangeToReplace: { startLine: number; endLine: number },
|
||||
currentLine: number,
|
||||
): Promise<void>
|
||||
|
||||
async saveChanges(): Promise<{
|
||||
newProblemsMessage: string | undefined
|
||||
userEdits: string | undefined
|
||||
@@ -225,7 +212,6 @@ export class DiffViewProvider {
|
||||
finalContent: undefined,
|
||||
}
|
||||
}
|
||||
const absolutePath = path.resolve(this.cwd, this.relPath)
|
||||
const updatedDocument = this.activeDiffEditor.document
|
||||
|
||||
// get the contents before save operation which may do auto-formatting
|
||||
@@ -240,7 +226,7 @@ export class DiffViewProvider {
|
||||
|
||||
await getHostBridgeProvider().windowClient.showTextDocument(
|
||||
ShowTextDocumentRequest.create({
|
||||
path: absolutePath,
|
||||
path: this.absolutePath,
|
||||
options: ShowTextDocumentOptions.create({
|
||||
preview: false,
|
||||
preserveFocus: true,
|
||||
@@ -267,13 +253,9 @@ export class DiffViewProvider {
|
||||
initial fix is usually correct and it may just take time for linters to catch up.
|
||||
*/
|
||||
const postDiagnostics = vscode.languages.getDiagnostics()
|
||||
const newProblems = diagnosticsToProblemsString(
|
||||
getNewDiagnostics(this.preDiagnostics, postDiagnostics),
|
||||
[
|
||||
vscode.DiagnosticSeverity.Error, // only including errors since warnings can be distracting (if user wants to fix warnings they can use the @problems mention)
|
||||
],
|
||||
this.cwd,
|
||||
) // will be empty string if no errors
|
||||
const newProblems = await diagnosticsToProblemsString(getNewDiagnostics(this.preDiagnostics, postDiagnostics), [
|
||||
vscode.DiagnosticSeverity.Error, // only including errors since warnings can be distracting (if user wants to fix warnings they can use the @problems mention)
|
||||
]) // will be empty string if no errors
|
||||
const newProblemsMessage =
|
||||
newProblems.length > 0 ? `\n\nNew problems detected after saving the file:\n${newProblems}` : ""
|
||||
|
||||
@@ -313,24 +295,23 @@ export class DiffViewProvider {
|
||||
}
|
||||
|
||||
async revertChanges(): Promise<void> {
|
||||
if (!this.relPath || !this.activeDiffEditor) {
|
||||
if (!this.absolutePath || !this.activeDiffEditor) {
|
||||
return
|
||||
}
|
||||
const fileExists = this.editType === "modify"
|
||||
const updatedDocument = this.activeDiffEditor.document
|
||||
const absolutePath = path.resolve(this.cwd, this.relPath)
|
||||
if (!fileExists) {
|
||||
if (updatedDocument.isDirty) {
|
||||
await updatedDocument.save()
|
||||
}
|
||||
await this.closeAllDiffViews()
|
||||
await fs.unlink(absolutePath)
|
||||
await fs.unlink(this.absolutePath)
|
||||
// Remove only the directories we created, in reverse order
|
||||
for (let i = this.createdDirs.length - 1; i >= 0; i--) {
|
||||
await fs.rmdir(this.createdDirs[i])
|
||||
console.log(`Directory ${this.createdDirs[i]} has been deleted.`)
|
||||
}
|
||||
console.log(`File ${absolutePath} has been deleted.`)
|
||||
console.log(`File ${this.absolutePath} has been deleted.`)
|
||||
} else {
|
||||
// revert document
|
||||
const edit = new vscode.WorkspaceEdit()
|
||||
@@ -342,11 +323,11 @@ export class DiffViewProvider {
|
||||
// Apply the edit and save, since contents shouldn't have changed this won't show in local history unless of course the user made changes and saved during the edit
|
||||
await vscode.workspace.applyEdit(edit)
|
||||
await updatedDocument.save()
|
||||
console.log(`File ${absolutePath} has been reverted to its original content.`)
|
||||
console.log(`File ${this.absolutePath} has been reverted to its original content.`)
|
||||
if (this.documentWasOpen) {
|
||||
await getHostBridgeProvider().windowClient.showTextDocument(
|
||||
ShowTextDocumentRequest.create({
|
||||
path: absolutePath,
|
||||
path: this.absolutePath,
|
||||
options: ShowTextDocumentOptions.create({
|
||||
preview: false,
|
||||
preserveFocus: true,
|
||||
@@ -373,65 +354,6 @@ export class DiffViewProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private async openDiffEditor(): Promise<vscode.TextEditor> {
|
||||
if (!this.relPath) {
|
||||
throw new Error("No file path set")
|
||||
}
|
||||
const uri = vscode.Uri.file(path.resolve(this.cwd, this.relPath))
|
||||
// If this diff editor is already open (ie if a previous write file was interrupted) then we should activate that instead of opening a new diff
|
||||
const diffTab = vscode.window.tabGroups.all
|
||||
.flatMap((group) => group.tabs)
|
||||
.find(
|
||||
(tab) =>
|
||||
tab.input instanceof vscode.TabInputTextDiff &&
|
||||
tab.input?.original?.scheme === DIFF_VIEW_URI_SCHEME &&
|
||||
arePathsEqual(tab.input.modified.fsPath, uri.fsPath),
|
||||
)
|
||||
if (diffTab && diffTab.input instanceof vscode.TabInputTextDiff) {
|
||||
const editorInfo = await getHostBridgeProvider().windowClient.showTextDocument(
|
||||
ShowTextDocumentRequest.create({
|
||||
path: diffTab.input.modified.fsPath,
|
||||
options: ShowTextDocumentOptions.create({
|
||||
preserveFocus: true,
|
||||
}),
|
||||
}),
|
||||
)
|
||||
// Find the editor that matches the returned path
|
||||
const editor = vscode.window.visibleTextEditors.find((e) => e.document.uri.fsPath === editorInfo.documentPath)
|
||||
if (!editor) {
|
||||
throw new Error("Failed to find opened text editor")
|
||||
}
|
||||
return editor
|
||||
}
|
||||
// Open new diff editor
|
||||
return new Promise<vscode.TextEditor>((resolve, reject) => {
|
||||
const fileName = path.basename(uri.fsPath)
|
||||
const fileExists = this.editType === "modify"
|
||||
const disposable = vscode.window.onDidChangeActiveTextEditor((editor) => {
|
||||
if (editor && arePathsEqual(editor.document.uri.fsPath, uri.fsPath)) {
|
||||
disposable.dispose()
|
||||
resolve(editor)
|
||||
}
|
||||
})
|
||||
vscode.commands.executeCommand(
|
||||
"vscode.diff",
|
||||
vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${fileName}`).with({
|
||||
query: Buffer.from(this.originalContent ?? "").toString("base64"),
|
||||
}),
|
||||
uri,
|
||||
`${fileName}: ${fileExists ? "Original ↔ Cline's Changes" : "New File"} (Editable)`,
|
||||
{
|
||||
preserveFocus: true,
|
||||
},
|
||||
)
|
||||
// This may happen on very slow machines ie project idx
|
||||
setTimeout(() => {
|
||||
disposable.dispose()
|
||||
reject(new Error("Failed to open diff editor, please try again..."))
|
||||
}, 10_000)
|
||||
})
|
||||
}
|
||||
|
||||
private scrollEditorToLine(line: number) {
|
||||
if (this.activeDiffEditor) {
|
||||
const scrollLine = line + 4
|
||||
@@ -476,15 +398,5 @@ export class DiffViewProvider {
|
||||
this.activeLineController = undefined
|
||||
this.streamedLines = []
|
||||
this.preDiagnostics = []
|
||||
|
||||
// Clean up the scroll listener
|
||||
if (this.scrollListener) {
|
||||
this.scrollListener.dispose()
|
||||
this.scrollListener = undefined
|
||||
}
|
||||
|
||||
// Reset auto-scroll state
|
||||
this.shouldAutoScroll = true
|
||||
this.lastFirstVisibleLine = 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import * as vscode from "vscode"
|
||||
import { getWorkingState } from "@utils/git"
|
||||
import { writeTextToClipboard } from "@utils/env"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { ShowTextDocumentRequest } from "@/shared/proto/host/window"
|
||||
|
||||
import { ShowMessageType, ShowTextDocumentRequest, ShowMessageRequest } from "@/shared/proto/host/window"
|
||||
/**
|
||||
* Formats the git diff into a prompt for the AI
|
||||
* @param gitDiff The git diff to format
|
||||
@@ -61,7 +59,12 @@ export function extractCommitMessage(aiResponse: string): string {
|
||||
*/
|
||||
export async function copyCommitMessageToClipboard(message: string): Promise<void> {
|
||||
await writeTextToClipboard(message)
|
||||
vscode.window.showInformationMessage("Commit message copied to clipboard")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Commit message copied to clipboard",
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,13 +76,19 @@ export async function showCommitMessageOptions(message: string): Promise<void> {
|
||||
const applyAction = "Apply to Git Input"
|
||||
const editAction = "Edit Message"
|
||||
|
||||
const selectedAction = await vscode.window.showInformationMessage(
|
||||
"Commit message generated",
|
||||
{ modal: false, detail: message },
|
||||
copyAction,
|
||||
applyAction,
|
||||
editAction,
|
||||
)
|
||||
const selectedAction = (
|
||||
await getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Commit message generated",
|
||||
options: {
|
||||
modal: false,
|
||||
detail: message,
|
||||
items: [copyAction, applyAction, editAction],
|
||||
},
|
||||
}),
|
||||
)
|
||||
)?.selectedOption
|
||||
|
||||
// Handle user dismissing the dialog (selectedAction is undefined)
|
||||
if (!selectedAction) {
|
||||
@@ -111,13 +120,28 @@ async function applyCommitMessageToGitInput(message: string): Promise<void> {
|
||||
if (api && api.repositories.length > 0) {
|
||||
const repo = api.repositories[0]
|
||||
repo.inputBox.value = message
|
||||
vscode.window.showInformationMessage("Commit message applied to Git input")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Commit message applied to Git input",
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
vscode.window.showErrorMessage("No Git repositories found")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "No Git repositories found",
|
||||
}),
|
||||
)
|
||||
await copyCommitMessageToClipboard(message)
|
||||
}
|
||||
} else {
|
||||
vscode.window.showErrorMessage("Git extension not found")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Git extension not found",
|
||||
}),
|
||||
)
|
||||
await copyCommitMessageToClipboard(message)
|
||||
}
|
||||
}
|
||||
@@ -137,5 +161,10 @@ async function editCommitMessage(message: string): Promise<void> {
|
||||
path: document.uri.fsPath,
|
||||
}),
|
||||
)
|
||||
vscode.window.showInformationMessage("Edit the commit message and copy when ready")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Edit the commit message and copy when ready",
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import os from "os"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { ShowTextDocumentRequest, ShowTextDocumentOptions } from "@/shared/proto/host/window"
|
||||
import { ShowTextDocumentRequest, ShowTextDocumentOptions, ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { writeFile } from "@utils/fs"
|
||||
|
||||
export async function downloadTask(dateTs: number, conversationHistory: Anthropic.MessageParam[]) {
|
||||
@@ -48,8 +48,11 @@ export async function downloadTask(dateTs: number, conversationHistory: Anthropi
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to save markdown file: ${error instanceof Error ? error.message : String(error)}`,
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to save markdown file: ${error instanceof Error ? error.message : String(error)}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,18 @@ import * as os from "os"
|
||||
import * as vscode from "vscode"
|
||||
import { arePathsEqual } from "@utils/path"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { ShowTextDocumentRequest, ShowTextDocumentOptions } from "@/shared/proto/host/window"
|
||||
import { ShowTextDocumentRequest, ShowTextDocumentOptions, ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { writeFile } from "@utils/fs"
|
||||
|
||||
export async function openImage(dataUri: string) {
|
||||
const matches = dataUri.match(/^data:image\/([a-zA-Z]+);base64,(.+)$/)
|
||||
if (!matches) {
|
||||
vscode.window.showErrorMessage("Invalid data URI format")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Invalid data URI format",
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
const [, format, base64Data] = matches
|
||||
@@ -19,7 +24,12 @@ export async function openImage(dataUri: string) {
|
||||
await writeFile(tempFilePath, new Uint8Array(imageBuffer))
|
||||
await vscode.commands.executeCommand("vscode.open", vscode.Uri.file(tempFilePath))
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`Error opening image: ${error}`)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Error opening image: ${error}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,14 +54,14 @@ export async function openFile(absolutePath: string) {
|
||||
}
|
||||
} catch {} // not essential, sometimes tab operations fail
|
||||
|
||||
const document = await vscode.workspace.openTextDocument(uri)
|
||||
await getHostBridgeProvider().windowClient.showTextDocument(
|
||||
ShowTextDocumentRequest.create({
|
||||
path: document.uri.fsPath,
|
||||
options: ShowTextDocumentOptions.create({ preview: false }),
|
||||
}),
|
||||
)
|
||||
await getHostBridgeProvider().windowClient.showTextDocument({
|
||||
path: uri.fsPath,
|
||||
options: { preview: false },
|
||||
})
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`Could not open file!`)
|
||||
getHostBridgeProvider().windowClient.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Could not open file!`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import sizeOf from "image-size"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { ShowOpenDialogueRequest } from "@/shared/proto/host/window"
|
||||
import { ShowMessageRequest, ShowMessageType, ShowOpenDialogueRequest } from "@/shared/proto/host/window"
|
||||
|
||||
/**
|
||||
* Supports processing of images and other file types
|
||||
@@ -46,14 +46,22 @@ export async function selectFiles(imagesAllowed: boolean): Promise<{ images: str
|
||||
const dimensions = sizeOf(uint8Array) // Get dimensions from Uint8Array
|
||||
if (dimensions.width! > 7500 || dimensions.height! > 7500) {
|
||||
console.warn(`Image dimensions exceed 7500px, skipping: ${filePath}`)
|
||||
vscode.window.showErrorMessage(
|
||||
`Image too large: ${path.basename(filePath)} was skipped (dimensions exceed 7500px).`,
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Image too large: ${path.basename(filePath)} was skipped (dimensions exceed 7500px).`,
|
||||
}),
|
||||
)
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error reading file or getting dimensions for ${filePath}:`, error)
|
||||
vscode.window.showErrorMessage(`Could not read dimensions for ${path.basename(filePath)}, skipping.`)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Could not read dimensions for ${path.basename(filePath)}, skipping.`,
|
||||
}),
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -68,12 +76,22 @@ export async function selectFiles(imagesAllowed: boolean): Promise<{ images: str
|
||||
const stats = await fs.stat(filePath)
|
||||
if (stats.size > 20 * 1000 * 1024) {
|
||||
console.warn(`File too large, skipping: ${filePath}`)
|
||||
vscode.window.showErrorMessage(`File too large: ${path.basename(filePath)} was skipped (size exceeds 20MB).`)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `File too large: ${path.basename(filePath)} was skipped (size exceeds 20MB).`,
|
||||
}),
|
||||
)
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error checking file size for ${filePath}:`, error)
|
||||
vscode.window.showErrorMessage(`Could not check file size for ${path.basename(filePath)}, skipping.`)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Could not check file size for ${path.basename(filePath)}, skipping.`,
|
||||
}),
|
||||
)
|
||||
return null
|
||||
}
|
||||
return { type: "file", data: filePath }
|
||||
|
||||
@@ -238,8 +238,8 @@ export class ClineAccountService {
|
||||
console.error("Error switching account:", error)
|
||||
throw error
|
||||
} finally {
|
||||
// Request a new authentication token
|
||||
await this._authService.refreshAuth()
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import vscode from "vscode"
|
||||
import crypto from "crypto"
|
||||
import { EmptyRequest, String } from "../../shared/proto/common"
|
||||
import { AuthState } from "../../shared/proto/account"
|
||||
import { AuthState, UserInfo } from "../../shared/proto/account"
|
||||
import { StreamingResponseHandler, getRequestRegistry } from "@/core/controller/grpc-handler"
|
||||
import { FirebaseAuthProvider } from "./providers/FirebaseAuthProvider"
|
||||
import { Controller } from "@/core/controller"
|
||||
@@ -22,15 +21,35 @@ const availableAuthProviders = {
|
||||
// Add other providers here as needed
|
||||
}
|
||||
|
||||
export interface ClineAuthInfo {
|
||||
idToken: string
|
||||
userInfo: ClineAccountUserInfo
|
||||
}
|
||||
|
||||
export interface ClineAccountUserInfo {
|
||||
createdAt: string
|
||||
displayName: string
|
||||
email: string
|
||||
id: string
|
||||
organizations: ClineAccountOrganization[]
|
||||
}
|
||||
|
||||
export interface ClineAccountOrganization {
|
||||
active: boolean
|
||||
memberId: string
|
||||
name: string
|
||||
organizationId: string
|
||||
roles: string[]
|
||||
}
|
||||
|
||||
// TODO: Add logic to handle multiple webviews getting auth updates.
|
||||
|
||||
export class AuthService {
|
||||
private static instance: AuthService | null = null
|
||||
private _config: ServiceConfig
|
||||
private _authenticated: boolean = false
|
||||
private _user: any = null
|
||||
private _provider: any = null
|
||||
private _authNonce: string | null = null
|
||||
private _clineAuthInfo: ClineAuthInfo | null = null
|
||||
private _provider: { provider: FirebaseAuthProvider } | null = null
|
||||
private _activeAuthStatusUpdateSubscriptions = new Set<[Controller, StreamingResponseHandler]>()
|
||||
private _context: vscode.ExtensionContext
|
||||
|
||||
@@ -100,6 +119,7 @@ export class AuthService {
|
||||
})
|
||||
|
||||
this._setProvider(authProviders.find((authProvider) => authProvider.name === providerName).name)
|
||||
|
||||
this._context = context
|
||||
}
|
||||
|
||||
@@ -118,7 +138,7 @@ export class AuthService {
|
||||
}
|
||||
AuthService.instance = new AuthService(context, config || {}, authProvider)
|
||||
}
|
||||
if (context) {
|
||||
if (context !== undefined) {
|
||||
AuthService.instance.context = context
|
||||
}
|
||||
return AuthService.instance
|
||||
@@ -136,18 +156,20 @@ export class AuthService {
|
||||
this._setProvider(providerName)
|
||||
}
|
||||
|
||||
get authNonce(): string | null {
|
||||
return this._authNonce
|
||||
}
|
||||
|
||||
async getAuthToken(): Promise<string | null> {
|
||||
if (!this._user) {
|
||||
if (!this._clineAuthInfo) {
|
||||
return null
|
||||
}
|
||||
|
||||
// TODO: This may need to be dependant on the auth provider
|
||||
// Return the ID token from the user object
|
||||
return this._provider.provider.getAuthToken(this._user)
|
||||
const idToken = this._clineAuthInfo.idToken
|
||||
const shouldRefreshIdToken = await this._provider?.provider.shouldRefreshIdToken(idToken)
|
||||
if (shouldRefreshIdToken) {
|
||||
// Retrieves the stored id token and refreshes it, then updates this._clineAuthInfo
|
||||
await this.restoreRefreshTokenAndRetrieveAuthInfo()
|
||||
if (!this._clineAuthInfo) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return this._clineAuthInfo.idToken
|
||||
}
|
||||
|
||||
private _setProvider(providerName: string): void {
|
||||
@@ -160,9 +182,17 @@ export class AuthService {
|
||||
}
|
||||
|
||||
getInfo(): AuthState {
|
||||
let user = null
|
||||
if (this._user && this._authenticated) {
|
||||
user = this._provider.provider.convertUserData(this._user)
|
||||
// TODO: this logic should be cleaner, but this will determine the authentication state for the webview -- if a user object is returned then the webview assumes authenticated, otherwise it assumes logged out (we previously returned a UserInfo object with empty fields, and this represented a broken logged in state)
|
||||
let user: any = null
|
||||
if (this._clineAuthInfo && this._authenticated) {
|
||||
const userInfo = this._clineAuthInfo.userInfo
|
||||
user = UserInfo.create({
|
||||
// TODO: create proto for new user info type
|
||||
uid: userInfo?.id,
|
||||
displayName: userInfo?.displayName,
|
||||
email: userInfo?.email,
|
||||
photoUrl: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
return AuthState.create({
|
||||
@@ -170,33 +200,26 @@ export class AuthService {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the auth nonce to null.
|
||||
* This is typically called after a successful authentication.
|
||||
*/
|
||||
resetAuthNonce(): void {
|
||||
this._authNonce = null
|
||||
}
|
||||
|
||||
async createAuthRequest(): Promise<String> {
|
||||
if (!this._authenticated) {
|
||||
// Generate nonce for state validation
|
||||
this._authNonce = crypto.randomBytes(32).toString("hex")
|
||||
|
||||
const uriScheme = vscode.env.uriScheme
|
||||
const authUrl = vscode.Uri.parse(
|
||||
`${this._config.URI}?state=${encodeURIComponent(this._authNonce)}&callback_url=${encodeURIComponent(`${uriScheme || "vscode"}://saoudrizwan.claude-dev/auth`)}`,
|
||||
)
|
||||
await vscode.env.openExternal(authUrl)
|
||||
return String.create({
|
||||
value: authUrl.toString(),
|
||||
})
|
||||
} else {
|
||||
if (this._authenticated) {
|
||||
this.sendAuthStatusUpdate()
|
||||
return String.create({
|
||||
value: "Already authenticated",
|
||||
})
|
||||
return String.create({ value: "Already authenticated" })
|
||||
}
|
||||
|
||||
if (!this._config.URI) {
|
||||
throw new Error("Authentication URI is not configured")
|
||||
}
|
||||
|
||||
const callbackUrl = `${vscode.env.uriScheme || "vscode"}://saoudrizwan.claude-dev/auth`
|
||||
|
||||
// Use URL object for more graceful query construction
|
||||
const authUrl = new URL(this._config.URI)
|
||||
authUrl.searchParams.set("callback_url", callbackUrl)
|
||||
|
||||
const authUrlString = authUrl.toString()
|
||||
|
||||
await vscode.env.openExternal(vscode.Uri.parse(authUrlString))
|
||||
return String.create({ value: authUrlString })
|
||||
}
|
||||
|
||||
async handleDeauth(): Promise<void> {
|
||||
@@ -205,8 +228,7 @@ export class AuthService {
|
||||
}
|
||||
|
||||
try {
|
||||
await this._provider.provider.signOut()
|
||||
this._user = null
|
||||
this._clineAuthInfo = null
|
||||
this._authenticated = false
|
||||
this.sendAuthStatusUpdate()
|
||||
} catch (error) {
|
||||
@@ -221,12 +243,11 @@ export class AuthService {
|
||||
}
|
||||
|
||||
try {
|
||||
this._user = await this._provider.provider.signIn(this._context, token, provider)
|
||||
this._clineAuthInfo = await this._provider.provider.signIn(this._context, token, provider)
|
||||
this._authenticated = true
|
||||
|
||||
await this.sendAuthStatusUpdate()
|
||||
this.setupAutoRefreshAuth()
|
||||
return this._user
|
||||
// return this._clineAuthInfo
|
||||
} catch (error) {
|
||||
console.error("Error signing in with custom token:", error)
|
||||
throw error
|
||||
@@ -245,59 +266,29 @@ export class AuthService {
|
||||
* Restores the authentication token from the extension's storage.
|
||||
* This is typically called when the extension is activated.
|
||||
*/
|
||||
async restoreAuthToken(): Promise<void> {
|
||||
async restoreRefreshTokenAndRetrieveAuthInfo(): Promise<void> {
|
||||
if (!this._provider || !this._provider.provider) {
|
||||
throw new Error("Auth provider is not set")
|
||||
}
|
||||
|
||||
try {
|
||||
this._user = await this._provider.provider.restoreAuthCredential(this._context)
|
||||
if (this._user) {
|
||||
this._clineAuthInfo = await this._provider.provider.retrieveClineAuthInfo(this._context)
|
||||
if (this._clineAuthInfo) {
|
||||
this._authenticated = true
|
||||
await this.sendAuthStatusUpdate()
|
||||
this.setupAutoRefreshAuth()
|
||||
// Setup auto-refresh for the auth token
|
||||
} else {
|
||||
console.warn("No user found after restoring auth token")
|
||||
this._authenticated = false
|
||||
this._user = null
|
||||
this._clineAuthInfo = null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error restoring auth token:", error)
|
||||
this._authenticated = false
|
||||
this._user = null
|
||||
this._clineAuthInfo = null
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the authentication status and sends an update to all subscribers.
|
||||
*/
|
||||
async refreshAuth(): Promise<void> {
|
||||
if (!this._user) {
|
||||
console.warn("No user is authenticated, skipping auth refresh")
|
||||
return
|
||||
}
|
||||
|
||||
await this._provider.provider.refreshAuthToken()
|
||||
this.sendAuthStatusUpdate()
|
||||
}
|
||||
|
||||
private setupAutoRefreshAuth(): void {
|
||||
// Set timeoutDuration to refresh the auth token 5 minutes before it expires
|
||||
const timeoutDuration = Math.floor(this._user.stsTokenManager.expirationTime - 5 * 60000 - Date.now()) // Milliseconds until 5 minutes before expiration
|
||||
setTimeout(() => this._autoRefreshAuth(), timeoutDuration)
|
||||
}
|
||||
|
||||
private async _autoRefreshAuth(): Promise<void> {
|
||||
if (!this._user) {
|
||||
console.warn("No user is authenticated, skipping auth refresh")
|
||||
return
|
||||
}
|
||||
await this.refreshAuth()
|
||||
this.setupAutoRefreshAuth() // Reschedule the next auto-refresh
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to authStatusUpdate events
|
||||
* @param controller The controller instance
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
import { getSecret, storeSecret } from "@/core/storage/state"
|
||||
import { ErrorService } from "@/services/error/ErrorService"
|
||||
import axios from "axios"
|
||||
import { initializeApp } from "firebase/app"
|
||||
import {
|
||||
AuthCredential,
|
||||
GoogleAuthProvider,
|
||||
GithubAuthProvider,
|
||||
OAuthCredential,
|
||||
User,
|
||||
UserCredential,
|
||||
getAuth,
|
||||
signInWithCredential,
|
||||
signOut,
|
||||
} from "firebase/auth"
|
||||
import { GithubAuthProvider, GoogleAuthProvider, User, getAuth, signInWithCredential } from "firebase/auth"
|
||||
import { ExtensionContext } from "vscode"
|
||||
import { ClineAccountUserInfo, ClineAuthInfo } from "../AuthService"
|
||||
import { jwtDecode } from "jwt-decode"
|
||||
|
||||
export class FirebaseAuthProvider {
|
||||
private _config: any
|
||||
@@ -29,80 +22,16 @@ export class FirebaseAuthProvider {
|
||||
this._config = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the authentication token of the current user.
|
||||
* @returns {Promise<string | null>} A promise that resolves to the authentication token of the current user, or null if no user is signed in.
|
||||
*/
|
||||
async getAuthToken(): Promise<string | null> {
|
||||
const user = getAuth().currentUser
|
||||
const idToken = user ? await user.getIdToken() : null
|
||||
return idToken
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the refresh token of the current user.
|
||||
* @returns {Promise<string | null>} A promise that resolves to the refresh token of the current user, or null if no user is signed in.
|
||||
*/
|
||||
async getRefreshToken(): Promise<string | null> {
|
||||
const user = getAuth().currentUser
|
||||
const refreshToken = user ? user.refreshToken : null
|
||||
return refreshToken
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the authentication token of the current user.
|
||||
* @returns {Promise<string | null>} A promise that resolves to the refreshed authentication token of the current user, or null if no user is signed in.
|
||||
*/
|
||||
async refreshAuthToken(): Promise<string | null> {
|
||||
const user = getAuth().currentUser
|
||||
const idToken = user ? await user.getIdToken(true) : null
|
||||
return idToken
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts Firebase User object to a generic user object.
|
||||
* @param user - The Firebase User object.
|
||||
* @returns {User} A generic user object.
|
||||
*/
|
||||
convertUserData(user: User) {
|
||||
return {
|
||||
uid: user.uid,
|
||||
email: user.email,
|
||||
displayName: user.displayName,
|
||||
photoUrl: user.photoURL,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs out the current user from Firebase.
|
||||
* @returns {Promise<void>} A promise that resolves when the user is signed out.
|
||||
*/
|
||||
async signOut(): Promise<void> {
|
||||
signOut(getAuth(initializeApp(Object.assign({}, this._config))))
|
||||
.then(() => {
|
||||
console.log("User signed out successfully.")
|
||||
})
|
||||
.catch((error) => {
|
||||
ErrorService.logMessage("Firebase sign-out error", "error")
|
||||
ErrorService.logException(error)
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the authentication token using a provided token.
|
||||
* @param token - The authentication token to store.
|
||||
* @returns {Promise<User>} A promise that resolves with the authenticated user.
|
||||
* @throws {Error} Throws an error if the storage fails.
|
||||
*/
|
||||
private async _storeAuthCredential(context: ExtensionContext, credential: AuthCredential): Promise<void> {
|
||||
try {
|
||||
await storeSecret(context, "clineAccountId", JSON.stringify(credential.toJSON()))
|
||||
} catch (error) {
|
||||
ErrorService.logMessage("Firebase store token error", "error")
|
||||
ErrorService.logException(error)
|
||||
throw error
|
||||
async shouldRefreshIdToken(existingIdToken: string): Promise<boolean> {
|
||||
const decodedToken = jwtDecode(existingIdToken)
|
||||
const exp = decodedToken.exp || 0 // 1752297633
|
||||
const expirationTime = exp * 1000
|
||||
const currentTime = Date.now()
|
||||
const fiveMinutesInMs = 5 * 60 * 1000
|
||||
if (currentTime > expirationTime - fiveMinutesInMs) {
|
||||
return true // id token is expired or about to be expired
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,31 +40,55 @@ export class FirebaseAuthProvider {
|
||||
* @returns {Promise<User>} A promise that resolves with the authenticated user.
|
||||
* @throws {Error} Throws an error if the restoration fails.
|
||||
*/
|
||||
async restoreAuthCredential(context: ExtensionContext): Promise<User | null> {
|
||||
const credentialJSON = await getSecret(context, "clineAccountId")
|
||||
if (!credentialJSON) {
|
||||
async retrieveClineAuthInfo(context: ExtensionContext): Promise<ClineAuthInfo | null> {
|
||||
const userRefreshToken = await getSecret(context, "clineAccountId")
|
||||
if (!userRefreshToken) {
|
||||
console.error("No stored authentication credential found.")
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const credentialData: AuthCredential = OAuthCredential.fromJSON(credentialJSON) as AuthCredential
|
||||
const userCredential = await this._signInWithCredential(credentialData)
|
||||
return userCredential.user
|
||||
} catch (error) {
|
||||
ErrorService.logMessage("Firebase restore token error", "error")
|
||||
ErrorService.logException(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
// Exchange refresh token for new access token using Firebase's secure token endpoint
|
||||
// https://stackoverflow.com/questions/38233687/how-to-use-the-firebase-refreshtoken-to-reauthenticate/57119131#57119131
|
||||
const firebaseApiKey = this._config.apiKey
|
||||
const googleAccessTokenResponse = await axios.post(
|
||||
`https://securetoken.googleapis.com/v1/token?key=${firebaseApiKey}`,
|
||||
`grant_type=refresh_token&refresh_token=${encodeURIComponent(userRefreshToken)}`,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
async _signInWithCredential(credential: AuthCredential): Promise<UserCredential> {
|
||||
const firebaseConfig = Object.assign({}, this._config)
|
||||
const app = initializeApp(firebaseConfig)
|
||||
const auth = getAuth(app)
|
||||
try {
|
||||
return await signInWithCredential(auth, credential)
|
||||
// console.log("googleAccessTokenResponse", googleAccessTokenResponse)
|
||||
|
||||
// This returns an object with access_token, expires_in (3600), id_token (can be used as bearer token to authenticate requests, we'll use this in the future instead of firebase but need to be aware of how we use firebase sdk for e.g. user info like the profile image), project_id, refresh_token, token_type (always Bearer), and user_id
|
||||
const idToken = googleAccessTokenResponse.data.id_token
|
||||
// const idTokenExpirationDate = new Date(Date.now() + googleAccessTokenResponse.data.expires_in * 1000)
|
||||
|
||||
// Now retrieve the user info from the backend (this was an easy solution to keep providing user profile details like name and email, but we should move to using the fetchMe() function instead)
|
||||
// Fetch user info from Cline API
|
||||
// TODO: consolidate with fetchMe() instead of making the call directly here
|
||||
const userResponse = await axios.get("https://api.cline.bot/api/v1/users/me", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${idToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
// Store user data
|
||||
const userInfo: ClineAccountUserInfo = userResponse.data.data
|
||||
|
||||
return { idToken, userInfo }
|
||||
|
||||
// let userObject = JSON.parse(credentialJSON)
|
||||
// let user = User.
|
||||
// userObject = User.constructor._fromJSON(auth, user2);
|
||||
// const credentialData: AuthCredential = OAuthCredential.fromJSON(credentialJSON) as AuthCredential
|
||||
// const userCredential = await this._signInWithCredential(context, credentialData)
|
||||
// return userCredential.user
|
||||
} catch (error) {
|
||||
ErrorService.logMessage("Firebase sign-in with credential error", "error")
|
||||
console.error("Firebase restore token error", error)
|
||||
ErrorService.logMessage("Firebase restore token error", "error")
|
||||
ErrorService.logException(error)
|
||||
throw error
|
||||
}
|
||||
@@ -146,10 +99,9 @@ export class FirebaseAuthProvider {
|
||||
* @returns {Promise<User>} A promise that resolves with the authenticated user.
|
||||
* @throws {Error} Throws an error if the sign-in fails.
|
||||
*/
|
||||
async signIn(context: ExtensionContext, token: string, provider: string): Promise<User> {
|
||||
async signIn(context: ExtensionContext, token: string, provider: string): Promise<ClineAuthInfo | null> {
|
||||
try {
|
||||
let credential
|
||||
let userCredential
|
||||
switch (provider) {
|
||||
case "google":
|
||||
credential = GoogleAuthProvider.credential(token)
|
||||
@@ -160,9 +112,25 @@ export class FirebaseAuthProvider {
|
||||
default:
|
||||
throw new Error(`Unsupported provider: ${provider}`)
|
||||
}
|
||||
this._storeAuthCredential(context, credential)
|
||||
userCredential = await this._signInWithCredential(credential)
|
||||
return userCredential.user
|
||||
// we've received the short-lived tokens from google/github, now we need to sign in to firebase with them
|
||||
const firebaseConfig = Object.assign({}, this._config)
|
||||
const app = initializeApp(firebaseConfig)
|
||||
const auth = getAuth(app)
|
||||
// this signs the user into firebase sdk internally
|
||||
const userCredential = (await signInWithCredential(auth, credential)).user
|
||||
// const userRefreshToken = await userCredential.getIdToken()
|
||||
|
||||
// store the long-lived refresh token in secret storage
|
||||
try {
|
||||
await storeSecret(context, "clineAccountId", userCredential.refreshToken)
|
||||
} catch (error) {
|
||||
ErrorService.logMessage("Firebase store token error", "error")
|
||||
ErrorService.logException(error)
|
||||
throw error
|
||||
}
|
||||
|
||||
// userCredential = await this._signInWithCredential(context, credential)
|
||||
return await this.retrieveClineAuthInfo(context)
|
||||
} catch (error) {
|
||||
ErrorService.logMessage("Firebase sign-in error", "error")
|
||||
ErrorService.logException(error)
|
||||
|
||||
+62
-23
@@ -20,10 +20,8 @@ import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { z } from "zod"
|
||||
import { FileChangeEvent_ChangeType, SubscribeToFileRequest } from "../../shared/proto/host/watch"
|
||||
import { Metadata } from "../../shared/proto/common"
|
||||
import {
|
||||
DEFAULT_MCP_TIMEOUT_SECONDS,
|
||||
McpMode,
|
||||
McpResource,
|
||||
McpResourceResponse,
|
||||
McpResourceTemplate,
|
||||
@@ -33,15 +31,14 @@ import {
|
||||
MIN_MCP_TIMEOUT_SECONDS,
|
||||
} from "@shared/mcp"
|
||||
import { fileExistsAtPath } from "@utils/fs"
|
||||
import { arePathsEqual } from "@utils/path"
|
||||
import { secondsToMs } from "@utils/time"
|
||||
import { GlobalFileNames } from "@core/storage/disk"
|
||||
import { ExtensionMessage } from "@shared/ExtensionMessage"
|
||||
import { DEFAULT_REQUEST_TIMEOUT_MS } from "./constants"
|
||||
import { Transport, McpConnection, McpTransportType, McpServerConfig } from "./types"
|
||||
import { McpConnection, McpServerConfig } from "./types"
|
||||
import { BaseConfigSchema, ServerConfigSchema, McpSettingsSchema } from "./schemas"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
export class McpHub {
|
||||
getMcpServersPath: () => Promise<string>
|
||||
private getSettingsDirectoryPath: () => Promise<string>
|
||||
@@ -112,8 +109,11 @@ export class McpHub {
|
||||
try {
|
||||
config = JSON.parse(content)
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(
|
||||
"Invalid MCP settings format. Please ensure your settings follow the correct JSON format.",
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Invalid MCP settings format. Please ensure your settings follow the correct JSON format.",
|
||||
}),
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
@@ -121,7 +121,12 @@ export class McpHub {
|
||||
// Validate against schema
|
||||
const result = McpSettingsSchema.safeParse(config)
|
||||
if (!result.success) {
|
||||
vscode.window.showErrorMessage("Invalid MCP settings schema.")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Invalid MCP settings schema.",
|
||||
}),
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -143,9 +148,9 @@ export class McpHub {
|
||||
}),
|
||||
{
|
||||
onResponse: async (response) => {
|
||||
console.log(
|
||||
`[DEBUG] MCP settings ${response.type === FileChangeEvent_ChangeType.CHANGED ? "changed" : "event"}`,
|
||||
)
|
||||
// console.log(
|
||||
// `[DEBUG] MCP settings ${response.type === FileChangeEvent_ChangeType.CHANGED ? "changed" : "event"}`,
|
||||
// )
|
||||
|
||||
// Only process the file if it was changed (not created or deleted)
|
||||
if (response.type === FileChangeEvent_ChangeType.CHANGED) {
|
||||
@@ -153,7 +158,12 @@ export class McpHub {
|
||||
if (settings) {
|
||||
try {
|
||||
await this.updateServerConnections(settings.mcpServers)
|
||||
vscode.window.showInformationMessage("MCP servers updated")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "MCP servers updated",
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Failed to process MCP settings change:", error)
|
||||
}
|
||||
@@ -164,7 +174,7 @@ export class McpHub {
|
||||
console.error("Error watching MCP settings file:", error)
|
||||
},
|
||||
onComplete: () => {
|
||||
console.log("[DEBUG] MCP settings file watch completed")
|
||||
//console.log("[DEBUG] MCP settings file watch completed")
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -403,8 +413,11 @@ export class McpHub {
|
||||
console.log(`[MCP Fallback Notification] ${name}:`, JSON.stringify(notification, null, 2))
|
||||
|
||||
// Show in VS Code for visibility
|
||||
vscode.window.showInformationMessage(
|
||||
`MCP ${name}: ${notification.method || "unknown"} - ${JSON.stringify(notification.params || {})}`,
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: `MCP ${name}: ${notification.method || "unknown"} - ${JSON.stringify(notification.params || {})}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
console.log(`[MCP Debug] Successfully set fallback notification handler for ${name}`)
|
||||
@@ -658,7 +671,12 @@ export class McpHub {
|
||||
const connection = this.connections.find((conn) => conn.server.name === serverName)
|
||||
const config = connection?.server.config
|
||||
if (config) {
|
||||
vscode.window.showInformationMessage(`Restarting ${serverName} MCP server...`)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: `Restarting ${serverName} MCP server...`,
|
||||
}),
|
||||
)
|
||||
connection.server.status = "connecting"
|
||||
connection.server.error = ""
|
||||
await this.notifyWebviewOfServerChanges()
|
||||
@@ -667,10 +685,20 @@ export class McpHub {
|
||||
await this.deleteConnection(serverName)
|
||||
// Try to connect again using existing config
|
||||
await this.connectToServer(serverName, JSON.parse(config), "internal")
|
||||
vscode.window.showInformationMessage(`${serverName} MCP server connected`)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: `${serverName} MCP server connected`,
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(`Failed to restart connection for ${serverName}:`, error)
|
||||
vscode.window.showErrorMessage(`Failed to connect to ${serverName} MCP server`)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to connect to ${serverName} MCP server`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -756,8 +784,11 @@ export class McpHub {
|
||||
if (error instanceof Error) {
|
||||
console.error("Error details:", error.message, error.stack)
|
||||
}
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to update server state: ${error instanceof Error ? error.message : String(error)}`,
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to update server state: ${error instanceof Error ? error.message : String(error)}`,
|
||||
}),
|
||||
)
|
||||
throw error
|
||||
}
|
||||
@@ -915,7 +946,12 @@ export class McpHub {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to update autoApprove settings:", error)
|
||||
vscode.window.showErrorMessage("Failed to update autoApprove settings")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to update autoApprove settings",
|
||||
}),
|
||||
)
|
||||
throw error // Re-throw to ensure the error is properly handled
|
||||
}
|
||||
}
|
||||
@@ -1033,8 +1069,11 @@ export class McpHub {
|
||||
if (error instanceof Error) {
|
||||
console.error("Error details:", error.message, error.stack)
|
||||
}
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to update server timeout: ${error instanceof Error ? error.message : String(error)}`,
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to update server timeout: ${error instanceof Error ? error.message : String(error)}`,
|
||||
}),
|
||||
)
|
||||
throw error
|
||||
}
|
||||
|
||||
@@ -12,16 +12,6 @@ import { posthogClientProvider } from "../PostHogClientProvider"
|
||||
* Respects user privacy settings and VSCode's global telemetry configuration
|
||||
*/
|
||||
|
||||
interface CollectedTasks {
|
||||
taskId: string
|
||||
collection: Collection[]
|
||||
}
|
||||
|
||||
interface Collection {
|
||||
event: string
|
||||
properties: any
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents telemetry event categories that can be individually enabled or disabled
|
||||
* When adding a new category, add it both here and to the initial values in telemetryCategoryEnabled
|
||||
@@ -29,6 +19,11 @@ interface Collection {
|
||||
*/
|
||||
type TelemetryCategory = "checkpoints" | "browser"
|
||||
|
||||
/**
|
||||
* Maximum length for error messages to prevent excessive data
|
||||
*/
|
||||
const MAX_ERROR_MESSAGE_LENGTH = 500
|
||||
|
||||
class TelemetryService {
|
||||
// Map to control specific telemetry categories (event types)
|
||||
private telemetryCategoryEnabled: Map<TelemetryCategory, boolean> = new Map([
|
||||
@@ -36,8 +31,6 @@ class TelemetryService {
|
||||
["browser", true], // Browser telemetry enabled
|
||||
])
|
||||
|
||||
// Stores events when collect=true
|
||||
private collectedTasks: CollectedTasks[] = []
|
||||
// Event constants for tracking user interactions and system events
|
||||
private static readonly EVENTS = {
|
||||
// Task-related events for tracking conversation and execution flow
|
||||
@@ -83,8 +76,8 @@ class TelemetryService {
|
||||
BROWSER_ERROR: "task.browser_error",
|
||||
// Tracks Gemini API specific performance metrics
|
||||
GEMINI_API_PERFORMANCE: "task.gemini_api_performance",
|
||||
// Collection of all task events
|
||||
TASK_COLLECTION: "task.collection",
|
||||
// Tracks when API providers return errors
|
||||
PROVIDER_API_ERROR: "task.provider_api_error",
|
||||
},
|
||||
// UI interaction events for tracking user engagement
|
||||
UI: {
|
||||
@@ -190,15 +183,13 @@ class TelemetryService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures a telemetry event if telemetry is enabled or collects if collect=true
|
||||
* Captures a telemetry event if telemetry is enabled
|
||||
* @param event The event to capture with its properties
|
||||
* @param collect If true, store the event in collectedEvents instead of sending to PostHog
|
||||
*/
|
||||
public capture(event: { event: string; properties?: any }, collect: boolean = false): void {
|
||||
public capture(event: { event: string; properties?: any }): void {
|
||||
if (!this.telemetryEnabled) {
|
||||
return
|
||||
}
|
||||
const taskId = event.properties.taskId
|
||||
|
||||
const propertiesWithVersion = this.addProperties(event.properties)
|
||||
|
||||
@@ -207,19 +198,7 @@ class TelemetryService {
|
||||
properties: propertiesWithVersion,
|
||||
}
|
||||
|
||||
if (collect && taskId) {
|
||||
const existingTask = this.collectedTasks.find((task) => task.taskId === taskId)
|
||||
if (existingTask) {
|
||||
existingTask.collection.push(capturedEvent)
|
||||
} else {
|
||||
this.collectedTasks.push({
|
||||
taskId,
|
||||
collection: [capturedEvent],
|
||||
})
|
||||
}
|
||||
} else {
|
||||
this.client.capture({ ...capturedEvent, distinctId: this.distinctId })
|
||||
}
|
||||
this.client.capture({ ...capturedEvent, distinctId: this.distinctId })
|
||||
}
|
||||
|
||||
public captureExtensionActivated(installId: string) {
|
||||
@@ -236,47 +215,35 @@ class TelemetryService {
|
||||
* Records when a new task/conversation is started
|
||||
* @param taskId Unique identifier for the new task
|
||||
* @param apiProvider Optional API provider
|
||||
* @param collect If true, collect event instead of sending
|
||||
*/
|
||||
public captureTaskCreated(taskId: string, apiProvider?: string, collect: boolean = false) {
|
||||
this.capture(
|
||||
{
|
||||
event: TelemetryService.EVENTS.TASK.CREATED,
|
||||
properties: { taskId, apiProvider },
|
||||
},
|
||||
collect,
|
||||
)
|
||||
public captureTaskCreated(taskId: string, apiProvider?: string) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.CREATED,
|
||||
properties: { taskId, apiProvider },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when a task/conversation is restarted
|
||||
* @param taskId Unique identifier for the new task
|
||||
* @param apiProvider Optional API provider
|
||||
* @param collect If true, collect event instead of sending
|
||||
*/
|
||||
public captureTaskRestarted(taskId: string, apiProvider?: string, collect: boolean = false) {
|
||||
this.capture(
|
||||
{
|
||||
event: TelemetryService.EVENTS.TASK.RESTARTED,
|
||||
properties: { taskId, apiProvider },
|
||||
},
|
||||
collect,
|
||||
)
|
||||
public captureTaskRestarted(taskId: string, apiProvider?: string) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.RESTARTED,
|
||||
properties: { taskId, apiProvider },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when cline calls the task completion_result tool signifying that cline is done with the task
|
||||
* @param taskId Unique identifier for the task
|
||||
* @param collect If true, collect event instead of sending
|
||||
*/
|
||||
public captureTaskCompleted(taskId: string, collect: boolean = false) {
|
||||
this.capture(
|
||||
{
|
||||
event: TelemetryService.EVENTS.TASK.COMPLETED,
|
||||
properties: { taskId },
|
||||
},
|
||||
collect,
|
||||
)
|
||||
public captureTaskCompleted(taskId: string) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.COMPLETED,
|
||||
properties: { taskId },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -285,7 +252,6 @@ class TelemetryService {
|
||||
* @param provider The API provider (e.g., OpenAI, Anthropic)
|
||||
* @param model The specific model used (e.g., GPT-4, Claude)
|
||||
* @param source The source of the message ("user" | "model"). Used to track message patterns and identify when users need to correct the model's responses.
|
||||
* @param collect If true, collect event instead of sending
|
||||
* @param tokenUsage Optional token usage data
|
||||
*/
|
||||
public captureConversationTurnEvent(
|
||||
@@ -293,7 +259,6 @@ class TelemetryService {
|
||||
provider: string = "unknown",
|
||||
model: string = "unknown",
|
||||
source: "user" | "assistant",
|
||||
collect: boolean = false,
|
||||
tokenUsage: {
|
||||
tokensIn?: number
|
||||
tokensOut?: number
|
||||
@@ -317,13 +282,10 @@ class TelemetryService {
|
||||
...tokenUsage,
|
||||
}
|
||||
|
||||
this.capture(
|
||||
{
|
||||
event: TelemetryService.EVENTS.TASK.CONVERSATION_TURN,
|
||||
properties,
|
||||
},
|
||||
collect,
|
||||
)
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.CONVERSATION_TURN,
|
||||
properties,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -333,19 +295,16 @@ class TelemetryService {
|
||||
* @param tokensOut Number of output tokens generated
|
||||
* @param model The model used for token calculation
|
||||
*/
|
||||
public captureTokenUsage(taskId: string, tokensIn: number, tokensOut: number, model: string, collect: boolean = false) {
|
||||
this.capture(
|
||||
{
|
||||
event: TelemetryService.EVENTS.TASK.TOKEN_USAGE,
|
||||
properties: {
|
||||
taskId,
|
||||
tokensIn,
|
||||
tokensOut,
|
||||
model,
|
||||
},
|
||||
public captureTokenUsage(taskId: string, tokensIn: number, tokensOut: number, model: string) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.TOKEN_USAGE,
|
||||
properties: {
|
||||
taskId,
|
||||
tokensIn,
|
||||
tokensOut,
|
||||
model,
|
||||
},
|
||||
collect,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -353,17 +312,14 @@ class TelemetryService {
|
||||
* @param taskId Unique identifier for the task
|
||||
* @param mode The mode being switched to (plan or act)
|
||||
*/
|
||||
public captureModeSwitch(taskId: string, mode: "plan" | "act", collect: boolean = false) {
|
||||
this.capture(
|
||||
{
|
||||
event: TelemetryService.EVENTS.TASK.MODE_SWITCH,
|
||||
properties: {
|
||||
taskId,
|
||||
mode,
|
||||
},
|
||||
public captureModeSwitch(taskId: string, mode: "plan" | "act") {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.MODE_SWITCH,
|
||||
properties: {
|
||||
taskId,
|
||||
mode,
|
||||
},
|
||||
collect,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -371,18 +327,15 @@ class TelemetryService {
|
||||
* @param taskId Unique identifier for the task
|
||||
* @param feedbackType The type of feedback ("thumbs_up" or "thumbs_down")
|
||||
*/
|
||||
public captureTaskFeedback(taskId: string, feedbackType: TaskFeedbackType, collect: boolean = false) {
|
||||
public captureTaskFeedback(taskId: string, feedbackType: TaskFeedbackType) {
|
||||
console.info("TelemetryService: Capturing task feedback", { taskId, feedbackType })
|
||||
this.capture(
|
||||
{
|
||||
event: TelemetryService.EVENTS.TASK.FEEDBACK,
|
||||
properties: {
|
||||
taskId,
|
||||
feedbackType,
|
||||
},
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.FEEDBACK,
|
||||
properties: {
|
||||
taskId,
|
||||
feedbackType,
|
||||
},
|
||||
collect,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// Tool events
|
||||
@@ -393,27 +346,17 @@ class TelemetryService {
|
||||
* @param autoApproved Whether the tool was auto-approved based on settings
|
||||
* @param success Whether the tool execution was successful
|
||||
*/
|
||||
public captureToolUsage(
|
||||
taskId: string,
|
||||
tool: string,
|
||||
modelId: string,
|
||||
autoApproved: boolean,
|
||||
success: boolean,
|
||||
collect: boolean = false,
|
||||
) {
|
||||
this.capture(
|
||||
{
|
||||
event: TelemetryService.EVENTS.TASK.TOOL_USED,
|
||||
properties: {
|
||||
taskId,
|
||||
tool,
|
||||
autoApproved,
|
||||
success,
|
||||
modelId,
|
||||
},
|
||||
public captureToolUsage(taskId: string, tool: string, modelId: string, autoApproved: boolean, success: boolean) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.TOOL_USED,
|
||||
properties: {
|
||||
taskId,
|
||||
tool,
|
||||
autoApproved,
|
||||
success,
|
||||
modelId,
|
||||
},
|
||||
collect,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -426,23 +369,19 @@ class TelemetryService {
|
||||
taskId: string,
|
||||
action: "shadow_git_initialized" | "commit_created" | "restored" | "diff_generated",
|
||||
durationMs?: number,
|
||||
collect: boolean = false,
|
||||
) {
|
||||
if (!this.isCategoryEnabled("checkpoints")) {
|
||||
return
|
||||
}
|
||||
|
||||
this.capture(
|
||||
{
|
||||
event: TelemetryService.EVENTS.TASK.CHECKPOINT_USED,
|
||||
properties: {
|
||||
taskId,
|
||||
action,
|
||||
durationMs,
|
||||
},
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.CHECKPOINT_USED,
|
||||
properties: {
|
||||
taskId,
|
||||
action,
|
||||
durationMs,
|
||||
},
|
||||
collect,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -450,18 +389,15 @@ class TelemetryService {
|
||||
* @param taskId Unique identifier for the task
|
||||
* @param errorType Type of error that occurred (e.g., "search_not_found", "invalid_format")
|
||||
*/
|
||||
public captureDiffEditFailure(taskId: string, modelId: string, errorType?: string, collect: boolean = false) {
|
||||
this.capture(
|
||||
{
|
||||
event: TelemetryService.EVENTS.TASK.DIFF_EDIT_FAILED,
|
||||
properties: {
|
||||
taskId,
|
||||
errorType,
|
||||
modelId,
|
||||
},
|
||||
public captureDiffEditFailure(taskId: string, modelId: string, errorType?: string) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.DIFF_EDIT_FAILED,
|
||||
properties: {
|
||||
taskId,
|
||||
errorType,
|
||||
modelId,
|
||||
},
|
||||
collect,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -470,50 +406,41 @@ class TelemetryService {
|
||||
* @param provider Provider of the selected model
|
||||
* @param taskId Optional task identifier if model was selected during a task
|
||||
*/
|
||||
public captureModelSelected(model: string, provider: string, taskId?: string, collect: boolean = false) {
|
||||
this.capture(
|
||||
{
|
||||
event: TelemetryService.EVENTS.UI.MODEL_SELECTED,
|
||||
properties: {
|
||||
model,
|
||||
provider,
|
||||
taskId,
|
||||
},
|
||||
public captureModelSelected(model: string, provider: string, taskId?: string) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.UI.MODEL_SELECTED,
|
||||
properties: {
|
||||
model,
|
||||
provider,
|
||||
taskId,
|
||||
},
|
||||
collect,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when a historical task is loaded from storage
|
||||
* @param taskId Unique identifier for the historical task
|
||||
*/
|
||||
public captureHistoricalTaskLoaded(taskId: string, collect: boolean = false) {
|
||||
this.capture(
|
||||
{
|
||||
event: TelemetryService.EVENTS.TASK.HISTORICAL_LOADED,
|
||||
properties: {
|
||||
taskId,
|
||||
},
|
||||
public captureHistoricalTaskLoaded(taskId: string) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.HISTORICAL_LOADED,
|
||||
properties: {
|
||||
taskId,
|
||||
},
|
||||
collect,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when the retry button is clicked for failed operations
|
||||
* @param taskId Unique identifier for the task being retried
|
||||
*/
|
||||
public captureRetryClicked(taskId: string, collect: boolean = false) {
|
||||
this.capture(
|
||||
{
|
||||
event: TelemetryService.EVENTS.TASK.RETRY_CLICKED,
|
||||
properties: {
|
||||
taskId,
|
||||
},
|
||||
public captureRetryClicked(taskId: string) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.RETRY_CLICKED,
|
||||
properties: {
|
||||
taskId,
|
||||
},
|
||||
collect,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -521,24 +448,21 @@ class TelemetryService {
|
||||
* @param taskId Unique identifier for the task
|
||||
* @param browserSettings The browser settings being used
|
||||
*/
|
||||
public captureBrowserToolStart(taskId: string, browserSettings: BrowserSettings, collect: boolean = false) {
|
||||
public captureBrowserToolStart(taskId: string, browserSettings: BrowserSettings) {
|
||||
if (!this.isCategoryEnabled("browser")) {
|
||||
return
|
||||
}
|
||||
|
||||
this.capture(
|
||||
{
|
||||
event: TelemetryService.EVENTS.TASK.BROWSER_TOOL_START,
|
||||
properties: {
|
||||
taskId,
|
||||
viewport: browserSettings.viewport,
|
||||
isRemote: !!browserSettings.remoteBrowserEnabled,
|
||||
remoteBrowserHost: browserSettings.remoteBrowserHost,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.BROWSER_TOOL_START,
|
||||
properties: {
|
||||
taskId,
|
||||
viewport: browserSettings.viewport,
|
||||
isRemote: !!browserSettings.remoteBrowserEnabled,
|
||||
remoteBrowserHost: browserSettings.remoteBrowserHost,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
collect,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -553,25 +477,21 @@ class TelemetryService {
|
||||
duration: number
|
||||
actions?: string[]
|
||||
},
|
||||
collect: boolean = false,
|
||||
) {
|
||||
if (!this.isCategoryEnabled("browser")) {
|
||||
return
|
||||
}
|
||||
|
||||
this.capture(
|
||||
{
|
||||
event: TelemetryService.EVENTS.TASK.BROWSER_TOOL_END,
|
||||
properties: {
|
||||
taskId,
|
||||
actionCount: stats.actionCount,
|
||||
duration: stats.duration,
|
||||
actions: stats.actions,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.BROWSER_TOOL_END,
|
||||
properties: {
|
||||
taskId,
|
||||
actionCount: stats.actionCount,
|
||||
duration: stats.duration,
|
||||
actions: stats.actions,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
collect,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -591,25 +511,21 @@ class TelemetryService {
|
||||
isRemote?: boolean
|
||||
[key: string]: any
|
||||
},
|
||||
collect: boolean = false,
|
||||
) {
|
||||
if (!this.isCategoryEnabled("browser")) {
|
||||
return
|
||||
}
|
||||
|
||||
this.capture(
|
||||
{
|
||||
event: TelemetryService.EVENTS.TASK.BROWSER_ERROR,
|
||||
properties: {
|
||||
taskId,
|
||||
errorType,
|
||||
errorMessage,
|
||||
context,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.BROWSER_ERROR,
|
||||
properties: {
|
||||
taskId,
|
||||
errorType,
|
||||
errorMessage,
|
||||
context,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
collect,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -618,18 +534,15 @@ class TelemetryService {
|
||||
* @param qty The quantity of options that were presented
|
||||
* @param mode The mode in which the option was selected ("plan" or "act")
|
||||
*/
|
||||
public captureOptionSelected(taskId: string, qty: number, mode: "plan" | "act", collect: boolean = false) {
|
||||
this.capture(
|
||||
{
|
||||
event: TelemetryService.EVENTS.TASK.OPTION_SELECTED,
|
||||
properties: {
|
||||
taskId,
|
||||
qty,
|
||||
mode,
|
||||
},
|
||||
public captureOptionSelected(taskId: string, qty: number, mode: "plan" | "act") {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.OPTION_SELECTED,
|
||||
properties: {
|
||||
taskId,
|
||||
qty,
|
||||
mode,
|
||||
},
|
||||
collect,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -638,18 +551,15 @@ class TelemetryService {
|
||||
* @param qty The quantity of options that were presented
|
||||
* @param mode The mode in which the custom response was provided ("plan" or "act")
|
||||
*/
|
||||
public captureOptionsIgnored(taskId: string, qty: number, mode: "plan" | "act", collect: boolean = false) {
|
||||
this.capture(
|
||||
{
|
||||
event: TelemetryService.EVENTS.TASK.OPTIONS_IGNORED,
|
||||
properties: {
|
||||
taskId,
|
||||
qty,
|
||||
mode,
|
||||
},
|
||||
public captureOptionsIgnored(taskId: string, qty: number, mode: "plan" | "act") {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.OPTIONS_IGNORED,
|
||||
properties: {
|
||||
taskId,
|
||||
qty,
|
||||
mode,
|
||||
},
|
||||
collect,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -657,7 +567,6 @@ class TelemetryService {
|
||||
* @param taskId Unique identifier for the task
|
||||
* @param modelId Specific Gemini model ID
|
||||
* @param data Performance data including TTFT, durations, token counts, cache stats, and API success status
|
||||
* @param collect If true, collect event instead of sending
|
||||
*/
|
||||
public captureGeminiApiPerformance(
|
||||
taskId: string,
|
||||
@@ -674,19 +583,15 @@ class TelemetryService {
|
||||
apiError?: string
|
||||
throughputTokensPerSec?: number
|
||||
},
|
||||
collect: boolean = false,
|
||||
) {
|
||||
this.capture(
|
||||
{
|
||||
event: TelemetryService.EVENTS.TASK.GEMINI_API_PERFORMANCE,
|
||||
properties: {
|
||||
taskId,
|
||||
modelId,
|
||||
...data,
|
||||
},
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.GEMINI_API_PERFORMANCE,
|
||||
properties: {
|
||||
taskId,
|
||||
modelId,
|
||||
...data,
|
||||
},
|
||||
collect,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -694,30 +599,50 @@ class TelemetryService {
|
||||
* @param model The name of the model the user has interacted with
|
||||
* @param isFavorited Whether the model is being favorited (true) or unfavorited (false)
|
||||
*/
|
||||
public captureModelFavoritesUsage(model: string, isFavorited: boolean, collect: boolean = false) {
|
||||
this.capture(
|
||||
{
|
||||
event: TelemetryService.EVENTS.UI.MODEL_FAVORITE_TOGGLED,
|
||||
properties: {
|
||||
model,
|
||||
isFavorited,
|
||||
},
|
||||
public captureModelFavoritesUsage(model: string, isFavorited: boolean) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.UI.MODEL_FAVORITE_TOGGLED,
|
||||
properties: {
|
||||
model,
|
||||
isFavorited,
|
||||
},
|
||||
collect,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
public captureButtonClick(button: string, taskId?: string, collect: boolean = false) {
|
||||
this.capture(
|
||||
{
|
||||
event: TelemetryService.EVENTS.UI.BUTTON_CLICKED,
|
||||
properties: {
|
||||
button,
|
||||
taskId,
|
||||
},
|
||||
public captureButtonClick(button: string, taskId?: string) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.UI.BUTTON_CLICKED,
|
||||
properties: {
|
||||
button,
|
||||
taskId,
|
||||
},
|
||||
collect,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Records telemetry when an API provider returns an error
|
||||
* @param taskId Unique identifier for the task
|
||||
* @param model Identifier of the model used
|
||||
* @param requestId Unique identifier for the specific API request
|
||||
* @param errorMessage Detailed error message from the API provider
|
||||
* @param errorStatus HTTP status code of the error response, if available
|
||||
* @param collect Optional flag to determine if the event should be collected for batch sending
|
||||
*/
|
||||
public captureProviderApiError(args: {
|
||||
taskId: string
|
||||
model: string
|
||||
errorMessage: string
|
||||
errorStatus?: number | undefined
|
||||
requestId?: string | undefined
|
||||
}) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.PROVIDER_API_ERROR,
|
||||
properties: {
|
||||
...args,
|
||||
errorMessage: args.errorMessage.substring(0, MAX_ERROR_MESSAGE_LENGTH), // Truncate long error messages
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -738,39 +663,6 @@ class TelemetryService {
|
||||
return this.telemetryCategoryEnabled.get(category) ?? true
|
||||
}
|
||||
|
||||
public async sendCollectedEvents(taskId?: string): Promise<void> {
|
||||
if (!this.telemetryEnabled) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.collectedTasks.length > 0) {
|
||||
if (taskId) {
|
||||
const task = this.collectedTasks.find((t) => t.taskId === taskId)
|
||||
if (task) {
|
||||
this.capture(
|
||||
{
|
||||
event: TelemetryService.EVENTS.TASK.TASK_COLLECTION,
|
||||
properties: { taskId, events: task.collection },
|
||||
},
|
||||
false,
|
||||
)
|
||||
this.collectedTasks = this.collectedTasks.filter((t) => t.taskId !== taskId)
|
||||
}
|
||||
} else {
|
||||
for (const task of this.collectedTasks) {
|
||||
this.capture(
|
||||
{
|
||||
event: TelemetryService.EVENTS.TASK.TASK_COLLECTION,
|
||||
properties: { taskId: task.taskId, events: task.collection },
|
||||
},
|
||||
false,
|
||||
)
|
||||
this.collectedTasks = this.collectedTasks.filter((t) => t.taskId !== task.taskId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async shutdown(): Promise<void> {
|
||||
await this.client.shutdown()
|
||||
}
|
||||
|
||||
@@ -76,3 +76,6 @@ export interface OrganizationUsageTransaction {
|
||||
totalTokens: number
|
||||
userId: string
|
||||
}
|
||||
|
||||
// Used in cline.ts provider and in webview-ui/src/components/chat/ChatRow.tsx to display the login button
|
||||
export const CLINE_ACCOUNT_AUTH_ERROR_MESSAGE = "Unauthorized: Please sign in to Cline before trying again."
|
||||
|
||||
@@ -7,6 +7,7 @@ import { HistoryItem } from "./HistoryItem"
|
||||
import { TelemetrySetting } from "./TelemetrySetting"
|
||||
import { ClineRulesToggles } from "./cline-rules"
|
||||
import { UserInfo } from "./UserInfo"
|
||||
import { McpDisplayMode, DEFAULT_MCP_DISPLAY_MODE } from "./McpDisplayMode"
|
||||
|
||||
// webview will hold state
|
||||
export interface ExtensionMessage {
|
||||
@@ -37,7 +38,7 @@ export interface ExtensionState {
|
||||
clineMessages: ClineMessage[]
|
||||
currentTaskItem?: HistoryItem
|
||||
mcpMarketplaceEnabled?: boolean
|
||||
mcpRichDisplayEnabled: boolean
|
||||
mcpDisplayMode: McpDisplayMode
|
||||
planActSeparateModelsSetting: boolean
|
||||
enableCheckpointsSetting?: boolean
|
||||
platform: Platform
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Represents the different display modes available for MCP responses
|
||||
*/
|
||||
export type McpDisplayMode = "rich" | "plain" | "markdown"
|
||||
|
||||
/**
|
||||
* Default display mode for MCP responses
|
||||
*/
|
||||
export const DEFAULT_MCP_DISPLAY_MODE: McpDisplayMode = "plain"
|
||||
+131
-1
@@ -20,6 +20,7 @@ export type ApiProvider =
|
||||
| "vscode-lm"
|
||||
| "cline"
|
||||
| "litellm"
|
||||
| "moonshot"
|
||||
| "nebius"
|
||||
| "fireworks"
|
||||
| "asksage"
|
||||
@@ -27,6 +28,7 @@ export type ApiProvider =
|
||||
| "sambanova"
|
||||
| "cerebras"
|
||||
| "sapaicore"
|
||||
| "groq"
|
||||
|
||||
export interface ApiHandlerOptions {
|
||||
apiModelId?: string
|
||||
@@ -50,8 +52,10 @@ export interface ApiHandlerOptions {
|
||||
awsRegion?: string
|
||||
awsUseCrossRegionInference?: boolean
|
||||
awsBedrockUsePromptCache?: boolean
|
||||
awsAuthentication?: string
|
||||
awsUseProfile?: boolean
|
||||
awsProfile?: string
|
||||
awsBedrockApiKey?: string
|
||||
awsBedrockEndpoint?: string
|
||||
awsBedrockCustomSelected?: boolean
|
||||
awsBedrockCustomModelBaseId?: BedrockModelId
|
||||
@@ -86,6 +90,8 @@ export interface ApiHandlerOptions {
|
||||
azureApiVersion?: string
|
||||
vsCodeLmModelSelector?: LanguageModelChatSelector
|
||||
qwenApiLine?: string
|
||||
moonshotApiLine?: string
|
||||
moonshotApiKey?: string
|
||||
nebiusApiKey?: string
|
||||
asksageApiUrl?: string
|
||||
asksageApiKey?: string
|
||||
@@ -94,6 +100,9 @@ export interface ApiHandlerOptions {
|
||||
reasoningEffort?: string
|
||||
sambanovaApiKey?: string
|
||||
cerebrasApiKey?: string
|
||||
groqApiKey?: string
|
||||
groqModelId?: string
|
||||
groqModelInfo?: ModelInfo
|
||||
requestTimeoutMs?: number
|
||||
sapAiCoreClientId?: string
|
||||
sapAiCoreClientSecret?: string
|
||||
@@ -2094,7 +2103,8 @@ export const xaiModels = {
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 3.0, // will have different pricing for long context vs short context
|
||||
outputPrice: 6.0,
|
||||
cacheReadsPrice: 0.75,
|
||||
outputPrice: 15.0,
|
||||
},
|
||||
"grok-3-beta": {
|
||||
maxTokens: 8192,
|
||||
@@ -2397,6 +2407,95 @@ export const cerebrasModels = {
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
||||
// Groq
|
||||
// https://console.groq.com/docs/models
|
||||
// https://groq.com/pricing/
|
||||
export type GroqModelId = keyof typeof groqModels
|
||||
export const groqDefaultModelId: GroqModelId = "moonshotai/kimi-k2-instruct"
|
||||
export const groqModels = {
|
||||
// Compound Beta Models - Hybrid architectures optimized for tool use
|
||||
"compound-beta": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 128000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0,
|
||||
outputPrice: 0.0,
|
||||
description:
|
||||
"Compound model using Llama 4 Scout for core reasoning with Llama 3.3 70B for routing and tool use. Excellent for plan/act workflows.",
|
||||
},
|
||||
"compound-beta-mini": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 128000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0,
|
||||
outputPrice: 0.0,
|
||||
description: "Lightweight compound model for faster inference while maintaining tool use capabilities.",
|
||||
},
|
||||
// DeepSeek Models - Reasoning-optimized
|
||||
"deepseek-r1-distill-llama-70b": {
|
||||
maxTokens: 131072,
|
||||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.75,
|
||||
outputPrice: 0.99,
|
||||
description:
|
||||
"DeepSeek R1 reasoning capabilities distilled into Llama 70B architecture. Excellent for complex problem-solving and planning.",
|
||||
},
|
||||
// Llama 4 Models
|
||||
"meta-llama/llama-4-maverick-17b-128e-instruct": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 131072,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.2,
|
||||
outputPrice: 0.6,
|
||||
description: "Meta's Llama 4 Maverick 17B model with 128 experts, supports vision and multimodal tasks.",
|
||||
},
|
||||
"meta-llama/llama-4-scout-17b-16e-instruct": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 131072,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.11,
|
||||
outputPrice: 0.34,
|
||||
description: "Meta's Llama 4 Scout 17B model with 16 experts, optimized for fast inference and general tasks.",
|
||||
},
|
||||
// Llama 3.3 Models
|
||||
"llama-3.3-70b-versatile": {
|
||||
maxTokens: 32768,
|
||||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.59,
|
||||
outputPrice: 0.79,
|
||||
description: "Meta's latest Llama 3.3 70B model optimized for versatile use cases with excellent performance and speed.",
|
||||
},
|
||||
// Llama 3.1 Models - Fast inference
|
||||
"llama-3.1-8b-instant": {
|
||||
maxTokens: 131072,
|
||||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.05,
|
||||
outputPrice: 0.08,
|
||||
description: "Fast and efficient Llama 3.1 8B model optimized for speed, low latency, and reliable tool execution.",
|
||||
},
|
||||
// Mistral Models
|
||||
"moonshotai/kimi-k2-instruct": {
|
||||
maxTokens: 16384,
|
||||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 1.0,
|
||||
outputPrice: 3.0,
|
||||
description:
|
||||
"Kimi K2 is Moonshot AI's state-of-the-art Mixture-of-Experts (MoE) language model with 1 trillion total parameters and 32 billion activated parameters.",
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
||||
// Requesty
|
||||
// https://requesty.ai/models
|
||||
export const requestyDefaultModelId = "anthropic/claude-3-7-sonnet-latest"
|
||||
@@ -2549,3 +2648,34 @@ export const sapAiCoreModels = {
|
||||
description: sapAiCoreModelDescription,
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
||||
// Moonshot AI Studio
|
||||
// https://platform.moonshot.ai/docs/pricing/chat
|
||||
export const moonshotModels = {
|
||||
"kimi-k2-0711-preview": {
|
||||
maxTokens: 131_072,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.6,
|
||||
outputPrice: 2.5,
|
||||
},
|
||||
"moonshot-v1-128k-vision-preview": {
|
||||
maxTokens: 131_072,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 2,
|
||||
outputPrice: 5,
|
||||
},
|
||||
"kimi-thinking-preview": {
|
||||
maxTokens: 131_072,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 30,
|
||||
outputPrice: 30,
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
export type MoonshotModelId = keyof typeof moonshotModels
|
||||
export const moonshotDefaultModelId = "kimi-k2-0711-preview" satisfies MoonshotModelId
|
||||
|
||||
@@ -222,6 +222,8 @@ function convertApiProviderToProto(provider: string | undefined): ProtoApiProvid
|
||||
return ProtoApiProvider.CLINE
|
||||
case "litellm":
|
||||
return ProtoApiProvider.LITELLM
|
||||
case "moonshot":
|
||||
return ProtoApiProvider.MOONSHOT
|
||||
case "nebius":
|
||||
return ProtoApiProvider.NEBIUS
|
||||
case "fireworks":
|
||||
@@ -234,6 +236,8 @@ function convertApiProviderToProto(provider: string | undefined): ProtoApiProvid
|
||||
return ProtoApiProvider.SAMBANOVA
|
||||
case "cerebras":
|
||||
return ProtoApiProvider.CEREBRAS
|
||||
case "groq":
|
||||
return ProtoApiProvider.GROQ
|
||||
case "sapaicore":
|
||||
return ProtoApiProvider.SAPAICORE
|
||||
case "claude-code":
|
||||
@@ -282,6 +286,8 @@ function convertProtoToApiProvider(provider: ProtoApiProvider): ApiProvider {
|
||||
return "cline"
|
||||
case ProtoApiProvider.LITELLM:
|
||||
return "litellm"
|
||||
case ProtoApiProvider.MOONSHOT:
|
||||
return "moonshot"
|
||||
case ProtoApiProvider.NEBIUS:
|
||||
return "nebius"
|
||||
case ProtoApiProvider.FIREWORKS:
|
||||
@@ -294,6 +300,8 @@ function convertProtoToApiProvider(provider: ProtoApiProvider): ApiProvider {
|
||||
return "sambanova"
|
||||
case ProtoApiProvider.CEREBRAS:
|
||||
return "cerebras"
|
||||
case ProtoApiProvider.GROQ:
|
||||
return "groq"
|
||||
case ProtoApiProvider.SAPAICORE:
|
||||
return "sapaicore"
|
||||
case ProtoApiProvider.CLAUDE_CODE:
|
||||
@@ -328,7 +336,9 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
|
||||
awsUseCrossRegionInference: config.awsUseCrossRegionInference,
|
||||
awsBedrockUsePromptCache: config.awsBedrockUsePromptCache,
|
||||
awsUseProfile: config.awsUseProfile,
|
||||
awsAuthentication: config.awsAuthentication,
|
||||
awsProfile: config.awsProfile,
|
||||
awsBedrockApiKey: config.awsBedrockApiKey,
|
||||
awsBedrockEndpoint: config.awsBedrockEndpoint,
|
||||
awsBedrockCustomSelected: config.awsBedrockCustomSelected,
|
||||
awsBedrockCustomModelBaseId: config.awsBedrockCustomModelBaseId as string | undefined,
|
||||
@@ -362,6 +372,8 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
|
||||
azureApiVersion: config.azureApiVersion,
|
||||
vsCodeLmModelSelector: config.vsCodeLmModelSelector,
|
||||
qwenApiLine: config.qwenApiLine,
|
||||
moonshotApiLine: config.moonshotApiLine,
|
||||
moonshotApiKey: config.moonshotApiKey,
|
||||
nebiusApiKey: config.nebiusApiKey,
|
||||
asksageApiUrl: config.asksageApiUrl,
|
||||
asksageApiKey: config.asksageApiKey,
|
||||
@@ -370,6 +382,9 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
|
||||
reasoningEffort: config.reasoningEffort,
|
||||
sambanovaApiKey: config.sambanovaApiKey,
|
||||
cerebrasApiKey: config.cerebrasApiKey,
|
||||
groqApiKey: config.groqApiKey,
|
||||
groqModelId: config.groqModelId,
|
||||
groqModelInfo: convertModelInfoToProtoOpenRouter(config.groqModelInfo),
|
||||
requestTimeoutMs: config.requestTimeoutMs,
|
||||
apiProvider: config.apiProvider ? convertApiProviderToProto(config.apiProvider) : undefined,
|
||||
favoritedModelIds: config.favoritedModelIds || [],
|
||||
@@ -407,7 +422,9 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
|
||||
awsUseCrossRegionInference: protoConfig.awsUseCrossRegionInference,
|
||||
awsBedrockUsePromptCache: protoConfig.awsBedrockUsePromptCache,
|
||||
awsUseProfile: protoConfig.awsUseProfile,
|
||||
awsAuthentication: protoConfig.awsAuthentication,
|
||||
awsProfile: protoConfig.awsProfile,
|
||||
awsBedrockApiKey: protoConfig.awsBedrockApiKey,
|
||||
awsBedrockEndpoint: protoConfig.awsBedrockEndpoint,
|
||||
awsBedrockCustomSelected: protoConfig.awsBedrockCustomSelected,
|
||||
awsBedrockCustomModelBaseId: protoConfig.awsBedrockCustomModelBaseId as BedrockModelId | undefined,
|
||||
@@ -441,6 +458,8 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
|
||||
azureApiVersion: protoConfig.azureApiVersion,
|
||||
vsCodeLmModelSelector: protoConfig.vsCodeLmModelSelector,
|
||||
qwenApiLine: protoConfig.qwenApiLine,
|
||||
moonshotApiLine: protoConfig.moonshotApiLine,
|
||||
moonshotApiKey: protoConfig.moonshotApiKey,
|
||||
nebiusApiKey: protoConfig.nebiusApiKey,
|
||||
asksageApiUrl: protoConfig.asksageApiUrl,
|
||||
asksageApiKey: protoConfig.asksageApiKey,
|
||||
@@ -449,6 +468,9 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
|
||||
reasoningEffort: protoConfig.reasoningEffort,
|
||||
sambanovaApiKey: protoConfig.sambanovaApiKey,
|
||||
cerebrasApiKey: protoConfig.cerebrasApiKey,
|
||||
groqApiKey: protoConfig.groqApiKey,
|
||||
groqModelId: protoConfig.groqModelId,
|
||||
groqModelInfo: convertProtoToModelInfo(protoConfig.groqModelInfo),
|
||||
requestTimeoutMs: protoConfig.requestTimeoutMs,
|
||||
apiProvider: protoConfig.apiProvider !== undefined ? convertProtoToApiProvider(protoConfig.apiProvider) : undefined,
|
||||
favoritedModelIds: protoConfig.favoritedModelIds.length > 0 ? protoConfig.favoritedModelIds : undefined,
|
||||
|
||||
@@ -30,6 +30,7 @@ export function convertApiConfigurationToProtoApiConfiguration(config: ApiConfig
|
||||
qwenApiKey: config.qwenApiKey,
|
||||
doubaoApiKey: config.doubaoApiKey,
|
||||
mistralApiKey: config.mistralApiKey,
|
||||
moonshotApiKey: config.moonshotApiKey,
|
||||
nebiusApiKey: config.nebiusApiKey,
|
||||
asksageApiKey: config.asksageApiKey,
|
||||
xaiApiKey: config.xaiApiKey,
|
||||
@@ -60,7 +61,9 @@ export function convertApiConfigurationToProtoApiConfiguration(config: ApiConfig
|
||||
awsUseCrossRegionInference: config.awsUseCrossRegionInference,
|
||||
awsBedrockUsePromptCache: config.awsBedrockUsePromptCache,
|
||||
awsUseProfile: config.awsUseProfile,
|
||||
awsAuthentication: config.awsAuthentication,
|
||||
awsProfile: config.awsProfile,
|
||||
awsBedrockApiKey: config.awsBedrockApiKey,
|
||||
awsBedrockEndpoint: config.awsBedrockEndpoint,
|
||||
|
||||
// Vertex AI fields
|
||||
@@ -99,6 +102,9 @@ export function convertApiConfigurationToProtoApiConfiguration(config: ApiConfig
|
||||
// Qwen specific
|
||||
qwenApiLine: config.qwenApiLine,
|
||||
|
||||
// Moonshot specific
|
||||
moonshotApiLine: config.moonshotApiLine,
|
||||
|
||||
// OpenRouter specific
|
||||
openrouterProviderSorting: config.openRouterProviderSorting,
|
||||
|
||||
@@ -150,6 +156,7 @@ export function convertProtoApiConfigurationToApiConfiguration(protoConfig: Prot
|
||||
qwenApiKey: protoConfig.qwenApiKey,
|
||||
doubaoApiKey: protoConfig.doubaoApiKey,
|
||||
mistralApiKey: protoConfig.mistralApiKey,
|
||||
moonshotApiKey: protoConfig.moonshotApiKey,
|
||||
nebiusApiKey: protoConfig.nebiusApiKey,
|
||||
asksageApiKey: protoConfig.asksageApiKey,
|
||||
xaiApiKey: protoConfig.xaiApiKey,
|
||||
@@ -177,6 +184,8 @@ export function convertProtoApiConfigurationToApiConfiguration(protoConfig: Prot
|
||||
awsBedrockUsePromptCache: protoConfig.awsBedrockUsePromptCache,
|
||||
awsUseProfile: protoConfig.awsUseProfile,
|
||||
awsProfile: protoConfig.awsProfile,
|
||||
awsAuthentication: protoConfig.awsAuthentication,
|
||||
awsBedrockApiKey: protoConfig.awsBedrockApiKey,
|
||||
awsBedrockEndpoint: protoConfig.awsBedrockEndpoint,
|
||||
|
||||
// Vertex AI fields
|
||||
@@ -215,6 +224,9 @@ export function convertProtoApiConfigurationToApiConfiguration(protoConfig: Prot
|
||||
// Qwen specific
|
||||
qwenApiLine: protoConfig.qwenApiLine,
|
||||
|
||||
// Moonshot specific
|
||||
moonshotApiLine: protoConfig.moonshotApiLine,
|
||||
|
||||
// OpenRouter specific
|
||||
openRouterProviderSorting: protoConfig.openrouterProviderSorting,
|
||||
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
// Public PostHog key (safe for open source)
|
||||
export const posthogConfig = {
|
||||
const posthogProdConfig = {
|
||||
apiKey: "phc_qfOAGxZw2TL5O8p9KYd9ak3bPBFzfjC8fy5L6jNWY7K",
|
||||
host: "https://data.cline.bot",
|
||||
uiHost: "https://us.posthog.com",
|
||||
}
|
||||
|
||||
// Public PostHog key for Development Environment project
|
||||
const posthogDevEnvConfig = {
|
||||
apiKey: "phc_uY24EJXNBcc9kwO1K8TJUl5hPQntGM6LL1Mtrz0CBD4",
|
||||
host: "https://data.cline.bot",
|
||||
uiHost: "https://us.i.posthog.com",
|
||||
}
|
||||
|
||||
export const posthogConfig = process.env.IS_DEV === "true" ? posthogDevEnvConfig : posthogProdConfig
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { DiffViewProvider } from "@/integrations/editor/DiffViewProvider"
|
||||
|
||||
export class ExternalDiffViewProvider extends DiffViewProvider {
|
||||
private activeDiffEditorId: string | undefined
|
||||
override async openDiffEditor(): Promise<void> {
|
||||
if (!this.absolutePath) {
|
||||
return
|
||||
}
|
||||
const response = await getHostBridgeProvider().diffClient.openDiff({
|
||||
path: this.absolutePath,
|
||||
content: this.originalContent ?? "",
|
||||
})
|
||||
this.activeDiffEditorId = response.diffId
|
||||
}
|
||||
override async replaceText(
|
||||
content: string,
|
||||
rangeToReplace: { startLine: number; endLine: number },
|
||||
_currentLine: number,
|
||||
): Promise<void> {
|
||||
await getHostBridgeProvider().diffClient.replaceText({
|
||||
diffId: this.activeDiffEditorId,
|
||||
content: content,
|
||||
startLine: rangeToReplace.startLine,
|
||||
endLine: rangeToReplace.endLine,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -13,11 +13,15 @@ import { ExternalHostBridgeClientManager } from "./host-bridge-client-manager"
|
||||
import { ExternalWebviewProvider } from "./ExternalWebviewProvider"
|
||||
import { WebviewProviderType } from "@/shared/webview/types"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import { ExternalDiffViewProvider } from "./ExternalDiffviewProvider"
|
||||
|
||||
export const PROTOBUS_PORT = 26040
|
||||
export const HOSTBRIDGE_PORT = 26041
|
||||
|
||||
async function main() {
|
||||
log("Starting standalone service...")
|
||||
|
||||
hostProviders.initializeHostProviders(createWebview, new ExternalHostBridgeClientManager())
|
||||
hostProviders.initializeHostProviders(createWebview, createDiffView, new ExternalHostBridgeClientManager())
|
||||
activate(extensionContext)
|
||||
const controller = new Controller(extensionContext, outputChannel, postMessage, uuidv4())
|
||||
startProtobusService(controller)
|
||||
@@ -41,7 +45,7 @@ function startProtobusService(controller: Controller) {
|
||||
reflection.addToServer(server)
|
||||
|
||||
// Start the server.
|
||||
const host = process.env.PROTOBUS_ADDRESS || "127.0.0.1:50051"
|
||||
const host = process.env.PROTOBUS_ADDRESS || `127.0.0.1:${PROTOBUS_PORT}`
|
||||
server.bindAsync(host, grpc.ServerCredentials.createInsecure(), (err) => {
|
||||
if (err) {
|
||||
log(`Error: Failed to bind to ${host}, port may be unavailable. ${err.message}`)
|
||||
@@ -60,9 +64,12 @@ function getProtobusServiceNames(packageDefinition: { [x: string]: any }): strin
|
||||
return protobusServiceNames
|
||||
}
|
||||
|
||||
const createWebview = () => {
|
||||
function createWebview() {
|
||||
return new ExternalWebviewProvider(extensionContext, outputChannel, WebviewProviderType.SIDEBAR)
|
||||
}
|
||||
function createDiffView() {
|
||||
return new ExternalDiffViewProvider()
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a Promise-based handler function to make it compatible with gRPC's callback-based API.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user