Compare commits

...
Author SHA1 Message Date
celestial-vault 6ca086d7a1 lazily initialize provider sdks with error catching 2025-07-04 16:57:59 -05:00
github-actions[bot] 6c2c0780ee v3.18.2 Release Notes (#4581) 2025-07-02 22:15:45 -07:00
pashpashpashandCline Evaluation 021a014012 setting claude 4 as best model (#4637)
Co-authored-by: Cline Evaluation <cline@example.com>
2025-07-02 21:45:30 -07:00
Ara ef1a68b5e3 Adding a troubleshooting guide for terminal problems (#4592) 2025-07-02 19:08:42 -07:00
Tomás Barreiro 72029fe205 feat: Introduce Thinking Budget customization for Claude Code (#4618) 2025-07-03 06:37:32 +05:30
celestial-vault 2af151e736 [SettingsView] [ApiConfig Section] save on change (#4554)
* refactor out apiconfig section

* add general settings section

* duplicate import

* move terminal, browser, and feature settings

* move files to sections folder

* refactor out debug section

* pull out about section

* implement save on change and remove confirmation modals for api config section

* add doc strings to new hook functions

* add debounced text field for smooth typing

* use context value directly for apiProvider dropdown value; remove unecessary memo; remove keys from ApiOptions

* add welcomeViewCompleted state boolean to control welcome view showing

* refactor other sections to save-on-change; remove form diff calculation logic

* cleanup

* remove memo

* make welcomeViewCompleted context value initial value false
2025-07-02 19:39:49 -05:00
canvrno 64963c4e9c confirmation popup when deleting tasks (#4627) 2025-07-01 22:41:58 -07:00
Toshii 52571ccee8 base (#4600) 2025-07-01 17:08:28 -07:00
87322feeb7 Fall back to getting current terminal content when shell integration API fails to return output (#4605)
* Revert to when terminal process worked more reliably

* Get last terminal output if no output is retrieved

* Fix getting terminal output for when shell integration unavailable

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Revert removing previous changes

* Update TerminalProcess to emit current terminal contents instead of a silent command completion message

* Revert when first chunk fails message

* Revert error title

* Fixing tests with fake timers

---------

Co-authored-by: Dennise Bartlett <bartlett.dc.1@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Cline Evaluation <cline@example.com>
2025-07-01 16:57:36 -07:00
kevinneung 27f8372c5b add cline.walkthrough to command handler (#4621)
added cline.walkthrough to command handler, since it would throw an error otherwise
2025-07-02 05:20:02 +05:30
Sarah Fortune b3d3e9861f Use the host bridge to get the cwd in autoApprove.ts (#4601)
Update the ToolExecutor to use await because the approval is now async.
2025-07-01 16:01:33 -07:00
pashpashpashandCline Evaluation 01178909ee adding unique remote git urls to context on first message env variables (#4622)
Co-authored-by: Cline Evaluation <cline@example.com>
2025-07-01 15:15:10 -07:00
Sarah Fortune c982216113 Cleanup: Remove unused property and param postMessage from ClineAccountService (#4620)
* Remove unused property and param postMessage

* Remove unused property and param postMessage

Remove unused import.
2025-07-01 14:44:41 -07:00
Sarah Fortune da6f705df2 Remove unused file get-python-env.ts (#4604) 2025-07-01 14:06:14 -07:00
AraandCline Evaluation c7548a7f52 Fixing Bugs in ChatView with Primary/Secondary Buttons and related issues (#4553)
* Splitting chat view into multiple modular files

* Adding Comments and removing redundancies

* Fixing Bugs in ChatView with Primary/Secondary Buttons and related issues

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-07-01 12:58:18 -07:00
Tomás Barreiro e5f78a0456 Do not read auth variables from the user env when using Claude Code (#4591) 2025-07-01 12:09:58 +05:30
112 changed files with 2377 additions and 2117 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Refactor chat view into multiple modular files
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Include litellm_session_id as part of chat completion requests
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Add Claude Sonnet 4 and Opus 4 model in SAP AI Core provider.
+9
View File
@@ -1,5 +1,14 @@
# Changelog
## [3.18.2]
- Fix issue where terminal output would not be captured if shell integration fails by falling back to capturing the terminal content.
- Add confirmation popup when deleting tasks
- Add support for Claude Sonnet 4 and Opus 4 model in SAP AI Core provider (Thanks @lizzzcai!)
- Add support for `litellm_session_id` to group requests in a single session (Thanks @jorgegarciarey!)
- Add "Thinking Budget" customization for Claude Code (Thanks @BarreiroT!)
- Fix issue where the extension would use the user's environment variables for authentication when using Claude Code (Thanks @BarreiroT!)
## [3.18.1]
- Add support for Claude 4 Sonnet in SAP AI Core provider (Thanks @GTxx!)
+4
View File
@@ -170,6 +170,10 @@
"running-models-locally/ollama"
]
},
{
"group": "Troubleshooting",
"pages": ["troubleshooting/terminal-quick-fixes", "troubleshooting/terminal-integration-guide"]
},
{
"group": "More Info",
"pages": ["more-info/telemetry"]
@@ -58,3 +58,16 @@ When you use the terminal mention in your message, here's what happens behind th
6. The AI can now "see" the complete terminal output with all formatting preserved
This process happens automatically whenever you use the terminal mention, giving the AI access to your command results, error messages, and other terminal output without you having to copy it manually.
## Troubleshooting Terminal Issues
If you're experiencing issues with terminal mentions or terminal integration in general (such as "Shell Integration Unavailable" or commands not showing output), please refer to our comprehensive [Terminal Integration Troubleshooting Guide](/troubleshooting/terminal-integration-guide).
Common issues include:
- Terminal mentions not capturing output
- "Shell Integration Unavailable" messages in Cline chat
- Commands executing but output not visible to Cline
- Terminal integration working inconsistently
The troubleshooting guide provides platform-specific solutions and detailed configuration steps to resolve these issues.
@@ -74,8 +74,25 @@ This approach ensures that all terminal output, including colors and formatting,
- **Select specific output when needed**: By default, the integration captures all terminal content, but you can also select specific lines before right-clicking to focus on just the relevant output.
- **Combine with file mentions**: After sending terminal output to Cline, you can enhance your question by mentioning relevant files using the @ mentions feature.
- **Combine terminal outputs with file mentions**: After sending terminal output to Cline, you can enhance your question by mentioning relevant files using the @ mentions feature.
- **Use for build and test output**: Terminal integration is particularly useful for understanding complex build errors or test failures that span multiple lines.
- **Contextualize build & test outputs with the terminal**: Terminal integration is particularly useful for understanding complex build errors or test failures that span multiple lines.
Next time you're staring at a cryptic error message in your terminal, try using Cline's terminal integration instead of copying and pasting. You'll get more accurate help because Cline can see the complete terminal context with proper formatting.
## Troubleshooting Terminal Issues
If you're experiencing issues with terminal integration, such as "Shell Integration Unavailable" or commands not showing output, please refer to our comprehensive [Terminal Integration Troubleshooting Guide](/troubleshooting/terminal-integration-guide).
The troubleshooting guide covers:
- Common terminal integration issues and quick fixes
- Platform-specific solutions for Windows, macOS, and Linux
- Shell-specific configurations for zsh, bash, PowerShell, and more
- Advanced debugging techniques
- Terminal settings optimization
<Tip>
**Quick Fix**: Most terminal issues can be resolved by switching to bash in the Cline settings and increasing the shell
integration timeout to 10 seconds.
</Tip>
@@ -0,0 +1,399 @@
---
title: "Terminal Integration Troubleshooting Guide"
sidebarTitle: "Terminal Troubleshooting"
description: "Complete guide to resolving terminal integration issues in Cline"
---
This guide helps you resolve terminal integration issues in Cline. Terminal integration is crucial for Cline to execute commands and read their output, enabling it to understand errors, test results, and command responses.
<Tip>
If you're experiencing terminal issues, try switching to a simpler shell like `bash` in the Cline settings, under "Terminal Settings"
This resolves most terminal integration problems.
</Tip>
## Quick Diagnosis Flowchart
Follow this flowchart to quickly identify your issue:
```mermaid
graph TD
A[Terminal Issue] --> B{Can Cline execute commands?}
B -->|No| C[Shell Integration Unavailable]
B -->|Yes| D{Can Cline see the output?}
D -->|No| E[Output Capture Failed]
D -->|Yes| F{Is the output corrupted?}
F -->|Yes| G[Character Filtering Issue]
F -->|No| H{Does the command hang?}
H -->|Yes| I[Long-Running Command Issue]
H -->|No| J[Check Terminal Settings]
C --> K[Try Solution 1]
E --> L[Try Solution 2]
G --> M[Try Solution 3]
I --> N[Try Solution 4]
style A fill:#f9f,stroke:#333,stroke-width:2px
style K fill:#9f9,stroke:#333,stroke-width:2px
style L fill:#9f9,stroke:#333,stroke-width:2px
style M fill:#9f9,stroke:#333,stroke-width:2px
style N fill:#9f9,stroke:#333,stroke-width:2px
```
## Common Issues & Quick Solutions
### 1. Shell Integration Unavailable
**Symptoms:**
- Message: "Shell Integration Unavailable"
- Commands execute but Cline can't read output
- Terminal works fine manually but not with Cline
**Quick Solutions:**
#### macOS
- **Switch to bash**
1. Go to Cline Settings
2. Left-Click the **"Terminal Settings"** tab
3. Navigate to **"Default Terminal Profile"** and select **"bash"** from the drop-down menu
- **Disable Oh-My-Zsh temporarily**:
1. If using zsh, enter `mv ~/.zshrc ~/.zshrc.backup` into the terminal
2. Restart VSCode
- **Set environment**:
1.a For Zsh users, use one of the following Zsh commands to edit your shell profile:
- `nano ~/.zshrc`
- `vim ~/.zshrc`
- `code ~/.zshrc`
1.b For Bash users
- nano ~/.bash_profile
2. Add the following to your shell config: `export TERM=xterm-256color`
3. Save your configuration
#### Windows
- **Use PowerShell 7**
1. Install from Microsoft Store
2. Go to Cline Settings
3. Left-Click the **"Terminal Settings"** tab
4. Navigate to **"Default Terminal Profile"** and select **"PowerShell 7"** from the drop-down menu
- **Disable Windows ConPTY**
1. Navigate to your VSCode Settings
2. Enter "Integrated: Windows Enable Conpty" into the Settings searchbar
3. Uncheck the option
- **Try Command Prompt**
1. Go to Cline Settings
2. Left-Click the **"Terminal Settings"** tab
3. Navigate to **"Default Terminal Profile"** and select **"Command Prompt"** from the drop-down menu
#### Linux
- **Use bash**
1. Go to Cline Settings
2. Left-Click the **"Terminal Settings"** tab
3. Navigate to **"Default Terminal Profile"** and select **"bash"** from the drop-down menu
- **Check permissions**
1. Ensure VSCode has terminal access permissions
- **Disable custom prompts**
1. Comment out prompt customizations in `.bashrc`
### 2. Command Output Not Visible
**Symptoms:**
- Cline states in chat: "[Command is running but producing no output]"
- Commands complete but Cline doesn't see results
- Commands work sometimes but not consistently
**Solutions:**
- **Increase Shell Integration Timeout**
1. Within Cline, left-click the **Settings** button in the top right-hand corner of the chat window
2. Once in the **Settings** window, left-click the **"Terminal Settings"** tab from the left-hand column
3. Navigate to "Shell integration timeout (seconds)" and enter **"10"** into the text field
- **Disable Terminal Reuse**
1. Within Cline, left-click the **Settings** button in the top right-hand corner of the chat window
2. Once in the **Settings** window, left-click the **"Terminal Settings"** tab from the left-hand column
3. Look for **"Enable aggressive terminal reuse"**, and **uncheck** this option
- **Check for interfering extensions**
1. Disable other terminal-related VSCode extensions
### 3. Character Filtering Issues
**Symptoms:**
- Commas missing from output (JSON appears corrupted)
- Special characters stripped from terminal output
- Syntax errors that don't appear when running manually
**Solution:**
This is a known bug in output processing. Workarounds:
- Recommend AI to use file output instead
1. Tell Cline in chat or Cline rules, to use `command > output.txt` before reading the file/s
<Tip>
This family of issues is only partially solved in the latest Cline versions, so if you still face this, create a GitHub issue
if it is a persistent problem.
</Tip>
### 4. Long-Running Commands & Progress Bars
**Symptoms:**
- Docker builds never complete in Cline
- Progress bars consume thousands of tokens
- The Cline button "Proceed while running" doesn't work properly in chat
<Tip>
This family of issues has been solved in latest Cline versions but if you still face any issues, then create a GitHub issue
for this.
</Tip>
## Terminal Settings Explained
Access these in Cline by clicking the settings icon, and navigating to the "Terminal Settings" section:
### Default Terminal Profile
- **What it does**: Selects which shell Cline uses for commands
- **When to change**: If experiencing shell integration issues with your default shell
- **Recommended**: - macOS: bash (if zsh has issues) - Windows: PowerShell 7 - Linux: bash
### Shell Integration Timeout
- **What it does**: How long Cline waits for the terminal to be ready
- **Default**: 4 seconds
- **When to increase**:
- Slow shell startup (heavy .zshrc/.bashrc)
- WSL environments
- SSH connections
- **Recommended**: - Start with 10 seconds if having issues
### Enable Aggressive Terminal Reuse
- **What it does**: Reuses existing terminals even if not in the correct directory
- **When to disable**:
- Commands execute in wrong directory
- Virtual environment issues
- Terminal state corruption
- **Trade-off**: - Disabling creates more terminals but ensures clean state
### Terminal Output Line Limit
- **What it does**: Limits how many lines Cline reads from terminal output
- **Default**: 500 lines
- **When to adjust**:
- Increase for verbose build outputs
- Decrease if hitting token limits
- Set to 100 for commands with progress bars
## Platform-Specific Solutions
### macOS Issues
#### Oh-My-Zsh Conflicts
Oh-My-Zsh often interferes with shell integration. Solutions:
1. Create a minimal `.zshrc` for VSCode:
```bash
# ~/.zshrc-vscode
export TERM=xterm-256color
export PAGER=cat
# Minimal PATH and environment setup
```
2. Configure VSCode to use it:
```json
{
"terminal.integrated.env.osx": {
"ZDOTDIR": "~/.zshrc-vscode"
}
}
```
#### macOS 15+ Issues
Recent macOS versions have stricter terminal permissions:
1. System Preferences → Privacy & Security → Developer Tools
2. Add Visual Studio Code
3. Restart VSCode completely
### Windows Issues
#### PowerShell Execution Policy
If commands fail silently:
```powershell
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
```
#### WSL Integration
For WSL issues:
1. Use WSL extension for VSCode
2. Open folder in WSL: `code .` from WSL terminal
3. Select "WSL Bash" as terminal profile in Cline
#### Path Issues
Windows path problems:
1. Use forward slashes in Cline: `C:/Users/...`
2. Quote paths with spaces: `"C:/Program Files/..."`
3. Avoid `~` - use full paths
### Linux/SSH/Container Issues
#### SSH Connections
For remote development:
1. Install Cline on the remote machine, not locally
2. Use SSH extension's integrated terminal
3. Increase timeout to 15+ seconds
#### Docker Containers
When developing in containers:
1. Install Cline in the container
2. Use Dev Containers extension
3. Ensure shell integration scripts are available
## Shell-Specific Fixes
### Zsh
```bash
# Add to ~/.zshrc
export TERM=xterm-256color
export PAGER=cat
# Disable fancy prompts for VSCode
if [[ "$TERM_PROGRAM" == "vscode" ]]; then
PS1="%n@%m %1~ %# "
fi
```
### Bash
```bash
# Add to ~/.bashrc
export TERM=xterm-256color
export PAGER=cat
# Simple prompt for VSCode
if [[ "$TERM_PROGRAM" == "vscode" ]]; then
PS1='\u@\h:\w\$ '
fi
```
### Fish
```fish
# Add to ~/.config/fish/config.fish
set -x TERM xterm-256color
set -x PAGER cat
# Disable fancy features in VSCode
if test "$TERM_PROGRAM" = "vscode"
function fish_prompt
echo (whoami)'@'(hostname)':'(pwd)'> '
end
end
```
### PowerShell
```powershell
# Add to $PROFILE
$env:PAGER = "cat"
# Disable progress bars
$ProgressPreference = 'SilentlyContinue'
```
## Advanced Troubleshooting
### Debug Mode
Enable terminal debugging to see what's happening:
1. Open VSCode Command Palette (Cmd/Ctrl+Shift+P)
2. Run: "Developer: Set Log Level..."
3. Choose "Trace"
4. Check Output panel → "Cline" for terminal logs
### Manual Shell Integration Test
Test if shell integration works at all:
```bash
# In VSCode terminal
echo $TERM_PROGRAM # Should show "vscode"
echo $VSCODE_SHELL_INTEGRATION # Should be "1"
```
## FAQ
### Why does Cline create so many terminals?
When shell integration fails, Cline can't reuse terminals safely (they might be running long processes). Enable shell integration or adjust the terminal reuse setting.
### Can I use my custom shell (nushell, xonsh, etc.)?
Cline officially supports bash, zsh, fish, and PowerShell. Custom shells may work but aren't guaranteed. Use bash as a fallback.
### Why do some commands work but others don't?
Commands that use interactive features (pagers, progress bars, curses) often fail. Set `PAGER=cat` and use non-interactive flags.
### How do I know if shell integration is working?
Working integration shows command output in Cline's chat. Failed integration shows "Shell Integration Unavailable" or "[Command is running but producing no output]".
## Still Having Issues?
If you've tried everything:
1. **Collect Debug Info**:
```bash
echo "Shell: $SHELL"
echo "Term: $TERM"
echo "VSCode: $TERM_PROGRAM"
which bash
bash --version
```
2. **Report the Issue**:
- Use `/reportbug` in Cline github issues
- Include your debug info
- Mention which solutions you tried
<Tip>
Remember: Most terminal issues are resolved by switching to bash and increasing the timeout. Start there before trying complex
solutions.
</Tip>
@@ -0,0 +1,51 @@
---
title: "Terminal Quick Fixes"
sidebarTitle: "Terminal Quick Fixes"
description: "Quick solutions for common terminal issues"
---
**Here is a list of common fixes, starting with the most applicable:**
- **Switch to bash** (solves most instances)
1. Within Cline, left-click the **Settings** button in the top right-hand corner of the chat window
2. Once in the **Settings** window, left-click the **"Terminal Settings"** tab from the left-hand column
3. Navigate to **"Default Terminal Profile"** and select **"bash"** from the drop-down
- **Increase timeout**
1. Within Cline, left-click the **Settings** button in the top right-hand corner of the chat window
2. Once in the **Settings** window, left-click the **"Terminal Settings"** tab from the left-hand column
3. Navigate to "Shell integration timeout (seconds)" and enter **"10"** into the text field
- **Disable terminal reuse**
1. Within Cline, left-click the **Settings** button in the top right-hand corner of the chat window
2. Once in the **Settings** window, left-click the **"Terminal Settings"** tab from the left-hand column
3. Look for **"Enable aggressive terminal reuse"**, and **uncheck** this option
## Platform-Specific Fixes
### macOS + Oh-My-Zsh
```bash
# Create minimal config for VSCode
echo 'export TERM=xterm-256color' > ~/.zshrc-vscode
echo 'export PAGER=cat' >> ~/.zshrc-vscode
```
### Windows PowerShell
```powershell
# Run as Administrator
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
```
### WSL
- Open folder from WSL: `code .`
- Select **"WSL Bash"** in Cline settings, under **"Terminal Settings"**
- Increase **"Shell integration timeout (seconds)"** to **15**
## Full Guide
For detailed troubleshooting, see the [Complete Terminal Troubleshooting Guide](/troubleshooting/terminal-integration-guide).
@@ -492,6 +492,12 @@ async function constructNewFileContentV1(diffContent: string, originalContent: s
if (isReplaceBlockEnd(line)) {
// Finished one replace block
if (searchMatchIndex === -1) {
throw new Error(
`The SEARCH block:\n${currentSearchContent.trimEnd()}\n...is malformatted.`,
)
}
// Store this replacement
replacements.push({
start: searchMatchIndex,
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.18.1",
"version": "3.18.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.18.1",
"version": "3.18.2",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.37.0",
+1 -1
View File
@@ -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.1",
"version": "3.18.2",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
+1
View File
@@ -18,6 +18,7 @@ service StateService {
rpc updateAutoApprovalSettings(AutoApprovalSettingsRequest) returns (Empty);
rpc updateSettings(UpdateSettingsRequest) returns (Empty);
rpc updateTelemetrySetting(TelemetrySettingRequest) returns (Empty);
rpc setWelcomeViewCompleted(BooleanRequest) returns (Empty);
}
message State {
+6 -3
View File
@@ -45,8 +45,10 @@ describe("OllamaHandler", () => {
this.skip()
}
this.timeout(5000)
// Ensure client is initialized
const client = (handler as any).ensureClient()
// Mock the Ollama client's chat method
const chatStub = sinon.stub(handler["client"], "chat").resolves({
const chatStub = sinon.stub(client, "chat").resolves({
[Symbol.asyncIterator]: async function* () {
yield {
message: { content: "Hello, world!" },
@@ -139,8 +141,9 @@ describe("OllamaHandler", () => {
// Restore real timers for this test
clock.restore()
// Mock the Ollama client's chat method to fail on first call and succeed on second
const chatStub = sinon.stub(handler["client"], "chat")
// Ensure client is initialized and mock the Ollama client's chat method to fail on first call and succeed on second
const client = (handler as any).ensureClient()
const chatStub = sinon.stub(client, "chat")
// First call throws an error
chatStub.onFirstCall().rejects(new Error("API Error"))
+22 -7
View File
@@ -7,18 +7,33 @@ import { ApiStream } from "../transform/stream"
export class AnthropicHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: Anthropic
private client: Anthropic | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new Anthropic({
apiKey: this.options.apiKey,
baseURL: this.options.anthropicBaseUrl || undefined,
})
}
private ensureClient(): Anthropic {
if (!this.client) {
if (!this.options.apiKey) {
throw new Error("Anthropic API key is required")
}
try {
this.client = new Anthropic({
apiKey: this.options.apiKey,
baseURL: this.options.anthropicBaseUrl || undefined,
})
} catch (error) {
throw new Error(`Error creating Anthropic client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
let stream: AnthropicStream<Anthropic.RawMessageStreamEvent>
const modelId = model.id
@@ -44,7 +59,7 @@ export class AnthropicHandler implements ApiHandler {
)
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
stream = await this.client.messages.create(
stream = await client.messages.create(
{
model: modelId,
thinking: reasoningOn ? { type: "enabled", budget_tokens: budget_tokens } : undefined,
@@ -118,7 +133,7 @@ export class AnthropicHandler implements ApiHandler {
break
}
default: {
stream = await this.client.messages.create({
stream = await client.messages.create({
model: modelId,
max_tokens: model.info.maxTokens || 8192,
temperature: 0,
+22 -11
View File
@@ -7,26 +7,37 @@ import { ApiStream } from "@api/transform/stream"
export class CerebrasHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: Cerebras
private client: Cerebras | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
}
// Clean and validate the API key
const cleanApiKey = this.options.cerebrasApiKey?.trim()
private ensureClient(): Cerebras {
if (!this.client) {
// Clean and validate the API key
const cleanApiKey = this.options.cerebrasApiKey?.trim()
if (!cleanApiKey) {
throw new Error("Cerebras API key is required")
if (!cleanApiKey) {
throw new Error("Cerebras API key is required")
}
try {
this.client = new Cerebras({
apiKey: cleanApiKey,
timeout: 30000, // 30 second timeout
})
} catch (error) {
throw new Error(`Error creating Cerebras client: ${error.message}`)
}
}
this.client = new Cerebras({
apiKey: cleanApiKey,
timeout: 30000, // 30 second timeout
})
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
// Convert Anthropic messages to Cerebras format
const cerebrasMessages: Array<{
role: "system" | "user" | "assistant"
@@ -65,7 +76,7 @@ export class CerebrasHandler implements ApiHandler {
}
try {
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: this.getModel().id,
messages: cerebrasMessages,
temperature: 0,
+1
View File
@@ -27,6 +27,7 @@ export class ClaudeCodeHandler implements ApiHandler {
messages: filteredMessages,
path: this.options.claudeCodePath,
modelId: this.getModel().id,
thinkingBudgetTokens: this.options.thinkingBudgetTokens,
})
// Usage is included with assistant messages,
+25 -11
View File
@@ -10,28 +10,42 @@ import { withRetry } from "../retry"
export class ClineHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private client: OpenAI | undefined
lastGenerationId?: string
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.cline.bot/v1",
apiKey: this.options.clineApiKey || "",
defaultHeaders: {
"HTTP-Referer": "https://cline.bot", // Optional, for including your app on cline.bot rankings.
"X-Title": "Cline", // Optional. Shows in rankings on cline.bot.
"X-Task-ID": this.options.taskId || "", // Include the task ID in the request headers
},
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.clineApiKey) {
throw new Error("You don't seem to be logged in to a Cline account.")
}
try {
this.client = new OpenAI({
baseURL: "https://api.cline.bot/v1",
apiKey: this.options.clineApiKey || "",
defaultHeaders: {
"HTTP-Referer": "https://cline.bot", // Optional, for including your app on cline.bot rankings.
"X-Title": "Cline", // Optional. Shows in rankings on cline.bot.
"X-Task-ID": this.options.taskId || "", // Include the task ID in the request headers
},
})
} catch (error) {
throw new Error(`Error creating Cline client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
this.lastGenerationId = undefined
const stream = await createOpenRouterStream(
this.client,
client,
systemPrompt,
messages,
this.getModel(),
+20 -6
View File
@@ -10,14 +10,27 @@ import { convertToR1Format } from "../transform/r1-format"
export class DeepSeekHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.deepseek.com/v1",
apiKey: this.options.deepSeekApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.deepSeekApiKey) {
throw new Error("DeepSeek API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.deepseek.com/v1",
apiKey: this.options.deepSeekApiKey,
})
} catch (error) {
throw new Error(`Error creating DeepSeek client: ${error.message}`)
}
}
return this.client
}
private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {
@@ -54,6 +67,7 @@ export class DeepSeekHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const isDeepseekReasoner = model.id.includes("deepseek-reasoner")
@@ -67,7 +81,7 @@ export class DeepSeekHandler implements ApiHandler {
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
max_completion_tokens: model.info.maxTokens,
messages: openAiMessages,
+20 -6
View File
@@ -8,13 +8,26 @@ import { withRetry } from "../retry"
export class DoubaoHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://ark.cn-beijing.volces.com/api/v3/",
apiKey: this.options.doubaoApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.doubaoApiKey) {
throw new Error("Doubao API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://ark.cn-beijing.volces.com/api/v3/",
apiKey: this.options.doubaoApiKey,
})
} catch (error) {
throw new Error(`Error creating Doubao client: ${error.message}`)
}
}
return this.client
}
getModel(): { id: DoubaoModelId; info: ModelInfo } {
@@ -31,12 +44,13 @@ export class DoubaoHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
max_completion_tokens: model.info.maxTokens,
messages: openAiMessages,
+20 -6
View File
@@ -15,18 +15,32 @@ import { ApiStream } from "../transform/stream"
export class FireworksHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.fireworks.ai/inference/v1",
apiKey: this.options.fireworksApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.fireworksApiKey) {
throw new Error("Fireworks API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.fireworks.ai/inference/v1",
apiKey: this.options.fireworksApiKey,
})
} catch (error) {
throw new Error(`Error creating Fireworks client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const modelId = this.options.fireworksModelId ?? ""
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
@@ -34,7 +48,7 @@ export class FireworksHandler implements ApiHandler {
...convertToOpenAiMessages(messages),
]
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: modelId,
...(this.options.fireworksModelMaxCompletionTokens
? { max_completion_tokens: this.options.fireworksModelMaxCompletionTokens }
+35 -18
View File
@@ -38,30 +38,45 @@ interface GeminiHandlerOptions extends ApiHandlerOptions {
*/
export class GeminiHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: GoogleGenAI
private client: GoogleGenAI | undefined
constructor(options: GeminiHandlerOptions) {
// Store the options
this.options = options
}
if (options.isVertex) {
// Initialize with Vertex AI configuration
const project = this.options.vertexProjectId ?? "not-provided"
const location = this.options.vertexRegion ?? "not-provided"
private ensureClient(): GoogleGenAI {
if (!this.client) {
const options = this.options as GeminiHandlerOptions
this.client = new GoogleGenAI({
vertexai: true,
project,
location,
})
} else {
// Initialize with standard API key
if (!options.geminiApiKey) {
throw new Error("API key is required for Google Gemini when not using Vertex AI")
if (options.isVertex) {
// Initialize with Vertex AI configuration
const project = this.options.vertexProjectId ?? "not-provided"
const location = this.options.vertexRegion ?? "not-provided"
try {
this.client = new GoogleGenAI({
vertexai: true,
project,
location,
})
} catch (error) {
throw new Error(`Error creating Gemini Vertex AI client: ${error.message}`)
}
} else {
// Initialize with standard API key
if (!options.geminiApiKey) {
throw new Error("API key is required for Google Gemini when not using Vertex AI")
}
try {
this.client = new GoogleGenAI({ apiKey: options.geminiApiKey })
} catch (error) {
throw new Error(`Error creating Gemini client: ${error.message}`)
}
}
this.client = new GoogleGenAI({ apiKey: options.geminiApiKey })
}
return this.client
}
/**
@@ -80,6 +95,7 @@ export class GeminiHandler implements ApiHandler {
maxDelay: 15000,
})
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const { id: modelId, info } = this.getModel()
const contents = messages.map(convertAnthropicMessageToGemini)
@@ -117,7 +133,7 @@ export class GeminiHandler implements ApiHandler {
let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined
try {
const result = await this.client.models.generateContentStream({
const result = await client.models.generateContentStream({
model: modelId,
contents: contents,
config: {
@@ -351,6 +367,7 @@ export class GeminiHandler implements ApiHandler {
*/
async countTokens(content: Array<any>): Promise<number> {
try {
const client = this.ensureClient()
const { id: model } = this.getModel()
// Convert content to Gemini format
@@ -362,7 +379,7 @@ export class GeminiHandler implements ApiHandler {
})
// Use Gemini's token counting API
const response = await this.client.models.countTokens({
const response = await client.models.countTokens({
model,
contents: [{ parts: geminiContent }],
})
+22 -7
View File
@@ -8,21 +8,35 @@ import { withRetry } from "../retry"
export class LiteLlmHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: this.options.liteLlmBaseUrl || "http://localhost:4000",
apiKey: this.options.liteLlmApiKey || "noop",
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.liteLlmApiKey) {
throw new Error("LiteLLM API key is required")
}
try {
this.client = new OpenAI({
baseURL: this.options.liteLlmBaseUrl || "http://localhost:4000",
apiKey: this.options.liteLlmApiKey || "noop",
})
} catch (error) {
throw new Error(`Error creating LiteLLM client: ${error.message}`)
}
}
return this.client
}
async calculateCost(prompt_tokens: number, completion_tokens: number): Promise<number | undefined> {
// Reference: https://github.com/BerriAI/litellm/blob/122ee634f434014267af104814022af1d9a0882f/litellm/proxy/spend_tracking/spend_management_endpoints.py#L1473
const client = this.ensureClient()
const modelId = this.options.liteLlmModelId || liteLlmDefaultModelId
try {
const response = await fetch(`${this.client.baseURL}/spend/calculate`, {
const response = await fetch(`${client.baseURL}/spend/calculate`, {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -54,6 +68,7 @@ export class LiteLlmHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const formattedMessages = convertToOpenAiMessages(messages)
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
role: "system",
@@ -101,7 +116,7 @@ export class LiteLlmHandler implements ApiHandler {
return message
})
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: this.options.liteLlmModelId || liteLlmDefaultModelId,
messages: [enhancedSystemMessage, ...enhancedMessages],
temperature,
+17 -6
View File
@@ -8,25 +8,36 @@ import { withRetry } from "../retry"
export class LmStudioHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: (this.options.lmStudioBaseUrl || "http://localhost:1234") + "/v1",
apiKey: "noop",
})
}
private ensureClient(): OpenAI {
if (!this.client) {
try {
this.client = new OpenAI({
baseURL: (this.options.lmStudioBaseUrl || "http://localhost:1234") + "/v1",
apiKey: "noop",
})
} catch (error) {
throw new Error(`Error creating LM Studio client: ${error.message}`)
}
}
return this.client
}
@withRetry({ retryAllErrors: true })
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
try {
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: this.getModel().id,
messages: openAiMessages,
stream: true,
+19 -5
View File
@@ -8,18 +8,32 @@ import { ApiStream } from "../transform/stream"
export class MistralHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: Mistral
private client: Mistral | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new Mistral({
apiKey: this.options.mistralApiKey,
})
}
private ensureClient(): Mistral {
if (!this.client) {
if (!this.options.mistralApiKey) {
throw new Error("Mistral API key is required")
}
try {
this.client = new Mistral({
apiKey: this.options.mistralApiKey,
})
} catch (error) {
throw new Error(`Error creating Mistral client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const stream = await this.client.chat
const client = this.ensureClient()
const stream = await client.chat
.stream({
model: this.getModel().id,
// max_completion_tokens: this.getModel().info.maxTokens,
+20 -7
View File
@@ -8,24 +8,37 @@ import { convertToR1Format } from "../transform/r1-format"
import { nebiusDefaultModelId, nebiusModels, type ModelInfo, type ApiHandlerOptions, type NebiusModelId } from "../../shared/api"
export class NebiusHandler implements ApiHandler {
private client: OpenAI
private client: OpenAI | undefined
constructor(private readonly options: ApiHandlerOptions) {
this.client = new OpenAI({
baseURL: "https://api.studio.nebius.ai/v1",
apiKey: this.options.nebiusApiKey,
})
constructor(private readonly options: ApiHandlerOptions) {}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.nebiusApiKey) {
throw new Error("Nebius API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.studio.nebius.ai/v1",
apiKey: this.options.nebiusApiKey,
})
} catch (error) {
throw new Error(`Error creating Nebius 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[] = model.id.includes("DeepSeek-R1")
? convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)]
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
messages: openAiMessages,
temperature: 0,
+14 -3
View File
@@ -8,15 +8,26 @@ import { withRetry } from "../retry"
export class OllamaHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: Ollama
private client: Ollama | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new Ollama({ host: this.options.ollamaBaseUrl || "http://localhost:11434" })
}
private ensureClient(): Ollama {
if (!this.client) {
try {
this.client = new Ollama({ host: this.options.ollamaBaseUrl || "http://localhost:11434" })
} catch (error) {
throw new Error(`Error creating Ollama client: ${error.message}`)
}
}
return this.client
}
@withRetry({ retryAllErrors: true })
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const ollamaMessages: Message[] = [{ role: "system", content: systemPrompt }, ...convertToOllamaMessages(messages)]
try {
@@ -27,7 +38,7 @@ export class OllamaHandler implements ApiHandler {
})
// Create the actual API request promise
const apiPromise = this.client.chat({
const apiPromise = client.chat({
model: this.getModel().id,
messages: ollamaMessages,
stream: true,
+21 -7
View File
@@ -10,13 +10,26 @@ import type { ChatCompletionReasoningEffort } from "openai/resources/chat/comple
export class OpenAiNativeHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
apiKey: this.options.openAiNativeApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.openAiNativeApiKey) {
throw new Error("OpenAI API key is required")
}
try {
this.client = new OpenAI({
apiKey: this.options.openAiNativeApiKey,
})
} catch (error: any) {
throw new Error(`Error creating OpenAI client: ${error.message}`)
}
}
return this.client
}
private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {
@@ -38,6 +51,7 @@ export class OpenAiNativeHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
switch (model.id) {
@@ -45,7 +59,7 @@ export class OpenAiNativeHandler implements ApiHandler {
case "o1-preview":
case "o1-mini": {
// o1 doesn't support streaming, non-1 temp, or system prompt
const response = await this.client.chat.completions.create({
const response = await client.chat.completions.create({
model: model.id,
messages: [{ role: "user", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
})
@@ -61,7 +75,7 @@ export class OpenAiNativeHandler implements ApiHandler {
case "o4-mini":
case "o3":
case "o3-mini": {
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
messages: [{ role: "developer", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
stream: true,
@@ -85,7 +99,7 @@ export class OpenAiNativeHandler implements ApiHandler {
break
}
default: {
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
// max_completion_tokens: this.getModel().info.maxTokens,
temperature: 0,
+36 -22
View File
@@ -10,35 +10,49 @@ import type { ChatCompletionReasoningEffort } from "openai/resources/chat/comple
export class OpenAiHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
// Azure API shape slightly differs from the core API shape: https://github.com/openai/openai-node?tab=readme-ov-file#microsoft-azure-openai
// Use azureApiVersion to determine if this is an Azure endpoint, since the URL may not always contain 'azure.com'
if (
this.options.azureApiVersion ||
((this.options.openAiBaseUrl?.toLowerCase().includes("azure.com") ||
this.options.openAiBaseUrl?.toLowerCase().includes("azure.us")) &&
!this.options.openAiModelId?.toLowerCase().includes("deepseek"))
) {
this.client = new AzureOpenAI({
baseURL: this.options.openAiBaseUrl,
apiKey: this.options.openAiApiKey,
apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
defaultHeaders: this.options.openAiHeaders,
})
} else {
this.client = new OpenAI({
baseURL: this.options.openAiBaseUrl,
apiKey: this.options.openAiApiKey,
defaultHeaders: this.options.openAiHeaders,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.openAiApiKey) {
throw new Error("OpenAI API key is required")
}
try {
// Azure API shape slightly differs from the core API shape: https://github.com/openai/openai-node?tab=readme-ov-file#microsoft-azure-openai
// Use azureApiVersion to determine if this is an Azure endpoint, since the URL may not always contain 'azure.com'
if (
this.options.azureApiVersion ||
((this.options.openAiBaseUrl?.toLowerCase().includes("azure.com") ||
this.options.openAiBaseUrl?.toLowerCase().includes("azure.us")) &&
!this.options.openAiModelId?.toLowerCase().includes("deepseek"))
) {
this.client = new AzureOpenAI({
baseURL: this.options.openAiBaseUrl,
apiKey: this.options.openAiApiKey,
apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
defaultHeaders: this.options.openAiHeaders,
})
} else {
this.client = new OpenAI({
baseURL: this.options.openAiBaseUrl,
apiKey: this.options.openAiApiKey,
defaultHeaders: this.options.openAiHeaders,
})
}
} catch (error: any) {
throw new Error(`Error creating OpenAI client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const modelId = this.options.openAiModelId ?? ""
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
const isR1FormatRequired = this.options.openAiModelInfo?.isR1FormatRequired ?? false
@@ -68,7 +82,7 @@ export class OpenAiHandler implements ApiHandler {
reasoningEffort = (this.options.reasoningEffort as ChatCompletionReasoningEffort) || "medium"
}
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: modelId,
messages: openAiMessages,
temperature,
+24 -10
View File
@@ -11,27 +11,41 @@ import { OpenRouterErrorResponse } from "./types"
export class OpenRouterHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private client: OpenAI | undefined
lastGenerationId?: string
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://openrouter.ai/api/v1",
apiKey: this.options.openRouterApiKey,
defaultHeaders: {
"HTTP-Referer": "https://cline.bot", // Optional, for including your app on openrouter.ai rankings.
"X-Title": "Cline", // Optional. Shows in rankings on openrouter.ai.
},
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.openRouterApiKey) {
throw new Error("OpenRouter API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://openrouter.ai/api/v1",
apiKey: this.options.openRouterApiKey,
defaultHeaders: {
"HTTP-Referer": "https://cline.bot", // Optional, for including your app on openrouter.ai rankings.
"X-Title": "Cline", // Optional. Shows in rankings on openrouter.ai.
},
})
} catch (error: any) {
throw new Error(`Error creating OpenRouter client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
this.lastGenerationId = undefined
const stream = await createOpenRouterStream(
this.client,
client,
systemPrompt,
messages,
this.getModel(),
+23 -9
View File
@@ -18,17 +18,30 @@ import { withRetry } from "../retry"
export class QwenHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL:
this.options.qwenApiLine === "china"
? "https://dashscope.aliyuncs.com/compatible-mode/v1"
: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
apiKey: this.options.qwenApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.qwenApiKey) {
throw new Error("Alibaba API key is required")
}
try {
this.client = new OpenAI({
baseURL:
this.options.qwenApiLine === "china"
? "https://dashscope.aliyuncs.com/compatible-mode/v1"
: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
apiKey: this.options.qwenApiKey,
})
} catch (error: any) {
throw new Error(`Error creating Alibaba client: ${error.message}`)
}
}
return this.client
}
getModel(): { id: MainlandQwenModelId | InternationalQwenModelId; info: ModelInfo } {
@@ -51,6 +64,7 @@ export class QwenHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const isDeepseekReasoner = model.id.includes("deepseek-r1")
const isReasoningModelFamily = model.id.includes("qwen3") || ["qwen-plus-latest", "qwen-turbo-latest"].includes(model.id)
@@ -76,7 +90,7 @@ export class QwenHandler implements ApiHandler {
temperature = undefined
}
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
max_completion_tokens: model.info.maxTokens,
messages: openAiMessages,
+24 -10
View File
@@ -19,22 +19,36 @@ interface RequestyUsage extends OpenAI.CompletionUsage {
export class RequestyHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://router.requesty.ai/v1",
apiKey: this.options.requestyApiKey,
defaultHeaders: {
"HTTP-Referer": "https://cline.bot",
"X-Title": "Cline",
},
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.requestyApiKey) {
throw new Error("Requesty API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://router.requesty.ai/v1",
apiKey: this.options.requestyApiKey,
defaultHeaders: {
"HTTP-Referer": "https://cline.bot",
"X-Title": "Cline",
},
})
} catch (error: any) {
throw new Error(`Error creating Requesty 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[] = [
@@ -57,7 +71,7 @@ export class RequestyHandler implements ApiHandler {
: {}
// @ts-ignore-next-line
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
max_tokens: model.info.maxTokens || undefined,
messages: openAiMessages,
+20 -6
View File
@@ -9,18 +9,32 @@ import { convertToR1Format } from "@api/transform/r1-format"
export class SambanovaHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.sambanova.ai/v1",
apiKey: this.options.sambanovaApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.sambanovaApiKey) {
throw new Error("SambaNova API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.sambanova.ai/v1",
apiKey: this.options.sambanovaApiKey,
})
} catch (error: any) {
throw new Error(`Error creating SambaNova client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
@@ -34,7 +48,7 @@ export class SambanovaHandler implements ApiHandler {
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: this.getModel().id,
messages: openAiMessages,
temperature: 0,
+20 -6
View File
@@ -9,18 +9,32 @@ import { convertToR1Format } from "@api/transform/r1-format"
export class TogetherHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.together.xyz/v1",
apiKey: this.options.togetherApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.togetherApiKey) {
throw new Error("Together API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.together.xyz/v1",
apiKey: this.options.togetherApiKey,
})
} catch (error: any) {
throw new Error(`Error creating Together client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const modelId = this.options.togetherModelId ?? ""
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
@@ -33,7 +47,7 @@ export class TogetherHandler implements ApiHandler {
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: modelId,
messages: openAiMessages,
temperature: 0,
+43 -16
View File
@@ -7,25 +7,49 @@ import { ApiStream } from "@api/transform/stream"
import { GeminiHandler } from "./gemini"
export class VertexHandler implements ApiHandler {
private geminiHandler: GeminiHandler
private clientAnthropic: AnthropicVertex
private geminiHandler: GeminiHandler | undefined
private clientAnthropic: AnthropicVertex | undefined
private options: ApiHandlerOptions
constructor(options: ApiHandlerOptions) {
this.options = options
}
// Create a GeminiHandler with isVertex flag for Gemini models
this.geminiHandler = new GeminiHandler({
...options,
isVertex: true,
})
private ensureGeminiHandler(): GeminiHandler {
if (!this.geminiHandler) {
try {
// Create a GeminiHandler with isVertex flag for Gemini models
this.geminiHandler = new GeminiHandler({
...this.options,
isVertex: true,
})
} catch (error: any) {
throw new Error(`Error creating Vertex AI Gemini handler: ${error.message}`)
}
}
return this.geminiHandler
}
// Initialize Anthropic client for Claude models
this.clientAnthropic = new AnthropicVertex({
projectId: this.options.vertexProjectId,
// https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#regions
region: this.options.vertexRegion,
})
private ensureAnthropicClient(): AnthropicVertex {
if (!this.clientAnthropic) {
if (!this.options.vertexProjectId) {
throw new Error("Vertex AI project ID is required")
}
if (!this.options.vertexRegion) {
throw new Error("Vertex AI region is required")
}
try {
// Initialize Anthropic client for Claude models
this.clientAnthropic = new AnthropicVertex({
projectId: this.options.vertexProjectId,
// https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#regions
region: this.options.vertexRegion,
})
} catch (error: any) {
throw new Error(`Error creating Vertex AI Anthropic client: ${error.message}`)
}
}
return this.clientAnthropic
}
@withRetry()
@@ -35,10 +59,13 @@ export class VertexHandler implements ApiHandler {
// For Gemini models, use the GeminiHandler
if (!modelId.includes("claude")) {
yield* this.geminiHandler.createMessage(systemPrompt, messages)
const geminiHandler = this.ensureGeminiHandler()
yield* geminiHandler.createMessage(systemPrompt, messages)
return
}
const clientAnthropic = this.ensureAnthropicClient()
// Claude implementation
let budget_tokens = this.options.thinkingBudgetTokens || 0
const reasoningOn =
@@ -63,7 +90,7 @@ export class VertexHandler implements ApiHandler {
)
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
stream = await this.clientAnthropic.beta.messages.create(
stream = await clientAnthropic.beta.messages.create(
{
model: modelId,
max_tokens: model.info.maxTokens || 8192,
@@ -125,7 +152,7 @@ export class VertexHandler implements ApiHandler {
break
}
default: {
stream = await this.clientAnthropic.beta.messages.create({
stream = await clientAnthropic.beta.messages.create({
model: modelId,
max_tokens: model.info.maxTokens || 8192,
temperature: 0,
+20 -6
View File
@@ -9,18 +9,32 @@ import { withRetry } from "../retry"
export class XAIHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.x.ai/v1",
apiKey: this.options.xaiApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.xaiApiKey) {
throw new Error("xAI API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.x.ai/v1",
apiKey: this.options.xaiApiKey,
})
} catch (error: any) {
throw new Error(`Error creating xAI client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const modelId = this.getModel().id
// ensure reasoning effort is either "low" or "high" for grok-3-mini
let reasoningEffort: ChatCompletionReasoningEffort | undefined
@@ -30,7 +44,7 @@ export class XAIHandler implements ApiHandler {
reasoningEffort = undefined
}
}
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: modelId,
max_completion_tokens: this.getModel().info.maxTokens,
temperature: 0,
+39
View File
@@ -156,6 +156,45 @@ replaced
expected: "line2\nreplaced\nline4",
isFinal: true,
},
{
name: "malformed diff - missing separator",
original: "line1\nline2\nline3",
diff: `------- SEARCH
line2
+++++++ REPLACE
replaced`,
shouldThrow: true,
},
{
name: "malformed diff - trailing space on separator",
original: "line1\nline2\nline3",
diff: `------- SEARCH
line2
=======
replaced
+++++++ REPLACE`,
shouldThrow: true,
},
{
name: "malformed diff - double replace markers",
original: "line1\nline2\nline3",
diff: `------- SEARCH
line2
+++++++ REPLACE
first replacement
+++++++ REPLACE`,
shouldThrow: true,
},
{
name: "malformed diff - malformed separator with dashes",
original: "line1\nline2\nline3",
diff: `------- SEARCH
line2
------- =======
replaced
+++++++ REPLACE`,
shouldThrow: true,
},
]
//.filter(({name}) => name === "multiple ordered replacements")
//.filter(({name}) => name === "delete then replace")
+4
View File
@@ -380,6 +380,10 @@ async function constructNewFileContentV1(diffContent: string, originalContent: s
if (isReplaceBlockEnd(line)) {
// Finished one replace block
if (searchMatchIndex === -1) {
throw new Error(`The SEARCH block:\n${currentSearchContent.trimEnd()}\n...is malformatted.`)
}
// Store this replacement
replacements.push({
start: searchMatchIndex,
+6 -7
View File
@@ -78,13 +78,10 @@ export class Controller {
(msg) => this.postMessageToWebview(msg),
this.context.extension?.packageJSON?.version ?? "1.0.0",
)
this.accountService = new ClineAccountService(
(msg) => this.postMessageToWebview(msg),
async () => {
const { apiConfiguration } = await this.getStateToPostToWebview()
return apiConfiguration?.clineApiKey
},
)
this.accountService = new ClineAccountService(async () => {
const { apiConfiguration } = await this.getStateToPostToWebview()
return apiConfiguration?.clineApiKey
})
// Clean up legacy checkpoints
cleanupLegacyCheckpoints(this.context.globalStorageUri.fsPath, this.outputChannel).catch((error) => {
@@ -848,6 +845,7 @@ export class Controller {
terminalReuseEnabled,
defaultTerminalProfile,
isNewUser,
welcomeViewCompleted,
mcpResponsesCollapsed,
terminalOutputLineLimit,
} = await getAllExtensionState(this.context)
@@ -902,6 +900,7 @@ export class Controller {
terminalReuseEnabled,
defaultTerminalProfile,
isNewUser,
welcomeViewCompleted: welcomeViewCompleted as boolean, // Can be undefined but is set to either true or false by the migration that runs on extension launch in extension.ts
mcpResponsesCollapsed,
terminalOutputLineLimit,
}
@@ -0,0 +1,25 @@
import type { BooleanRequest } from "../../../shared/proto/common"
import { Empty } from "../../../shared/proto/common"
import type { Controller } from "../index"
import { updateGlobalState } from "../../storage/state"
/**
* Sets the welcomeViewCompleted flag to the specified boolean value
* @param controller The controller instance
* @param request The boolean request containing the value to set
* @returns Empty response
*/
export async function setWelcomeViewCompleted(controller: Controller, request: BooleanRequest): Promise<Empty> {
try {
// Update the global state to set welcomeViewCompleted to the requested value
await updateGlobalState(controller.context, "welcomeViewCompleted", request.value)
await controller.postStateToWebview()
console.log(`Welcome view completed set to: ${request.value}`)
return Empty.create({})
} catch (error) {
console.error("Failed to set welcome view completed:", error)
throw error
}
}
@@ -1,5 +1,6 @@
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 { TaskMethodHandler } from "./index"
@@ -20,6 +21,18 @@ export const deleteTasksWithIds: TaskMethodHandler = async (
throw new Error("Missing task IDs")
}
const taskCount = request.value.length
const message =
taskCount === 1
? "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")
if (userChoice === undefined) {
return Empty.create()
}
for (const id of request.value) {
await deleteTaskWithId(controller, id)
}
+3
View File
@@ -35,6 +35,9 @@ Otherwise, if you have not completed the task and do not need additional informa
tooManyMistakes: (feedback?: string) =>
`You seem to be having trouble proceeding. The user has provided the following feedback to help guide you:\n<feedback>\n${feedback}\n</feedback>`,
autoApprovalMaxReached: (feedback?: string) =>
`Auto-approval limit reached. The user has provided the following feedback to help guide you:\n<feedback>\n${feedback}\n</feedback>`,
missingToolParameterError: (paramName: string) =>
`Missing value for required parameter '${paramName}'. Please retry with complete response.\n\n${toolUseInstructionsReminder}`,
+1
View File
@@ -71,6 +71,7 @@ export type GlobalStateKey =
| "terminalReuseEnabled"
| "defaultTerminalProfile"
| "isNewUser"
| "welcomeViewCompleted"
| "terminalOutputLineLimit"
| "mcpRichDisplayEnabled"
| "sapAiCoreTokenUrl"
+53 -1
View File
@@ -2,7 +2,7 @@ import * as vscode from "vscode"
import { ensureRulesDirectoryExists } from "./disk"
import fs from "fs/promises"
import path from "path"
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from "./state"
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState, getAllExtensionState } from "./state"
import { GlobalStateKey } from "./state-keys"
export async function migratePlanActGlobalToWorkspaceStorage(context: vscode.ExtensionContext) {
@@ -145,3 +145,55 @@ export async function migrateModeFromWorkspaceStorageToControllerState(context:
// Continue execution - migration failure shouldn't break extension startup
}
}
export async function migrateWelcomeViewCompleted(context: vscode.ExtensionContext) {
try {
// Check if welcomeViewCompleted is already set
const welcomeViewCompleted = await getGlobalState(context, "welcomeViewCompleted")
if (welcomeViewCompleted === undefined) {
console.log("Migrating welcomeViewCompleted setting...")
// Get all extension state to check for existing API keys
const extensionState = await getAllExtensionState(context)
const config = extensionState.apiConfiguration
// This is the original logic used for checking is the welcome view should be shown
// It was located in the ExtensionStateContextProvider
const hasKey = config
? [
config.apiKey,
config.openRouterApiKey,
config.awsRegion,
config.vertexProjectId,
config.openAiApiKey,
config.ollamaModelId,
config.lmStudioModelId,
config.liteLlmApiKey,
config.geminiApiKey,
config.openAiNativeApiKey,
config.deepSeekApiKey,
config.requestyApiKey,
config.togetherApiKey,
config.qwenApiKey,
config.doubaoApiKey,
config.mistralApiKey,
config.vsCodeLmModelSelector,
config.clineApiKey,
config.asksageApiKey,
config.xaiApiKey,
config.sambanovaApiKey,
config.sapAiCoreClientId,
].some((key) => key !== undefined)
: false
// Set welcomeViewCompleted based on whether user has keys
await updateGlobalState(context, "welcomeViewCompleted", hasKey)
console.log(`Migration: Set welcomeViewCompleted to ${hasKey} based on existing API keys`)
}
} catch (error) {
console.error("Failed to migrate welcomeViewCompleted:", error)
// Continue execution - migration failure shouldn't break extension startup
}
}
+3
View File
@@ -55,6 +55,7 @@ export async function getWorkspaceState(context: vscode.ExtensionContext, key: s
export async function getAllExtensionState(context: vscode.ExtensionContext) {
const [
isNewUser,
welcomeViewCompleted,
apiKey,
openRouterApiKey,
clineApiKey,
@@ -128,6 +129,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
claudeCodePath,
] = await Promise.all([
getGlobalState(context, "isNewUser") as Promise<boolean | undefined>,
getGlobalState(context, "welcomeViewCompleted") as Promise<boolean | undefined>,
getSecret(context, "apiKey") as Promise<string | undefined>,
getSecret(context, "openRouterApiKey") as Promise<string | undefined>,
getSecret(context, "clineApiKey") as Promise<string | undefined>,
@@ -388,6 +390,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
sapAiCoreModelId,
},
isNewUser: isNewUser ?? true,
welcomeViewCompleted,
lastShownAnnouncementId,
taskHistory,
autoApprovalSettings: autoApprovalSettings || DEFAULT_AUTO_APPROVAL_SETTINGS, // default value can be 0 or empty string
+14 -11
View File
@@ -63,7 +63,10 @@ export class ToolExecutor {
return this.autoApprover.shouldAutoApproveTool(toolName)
}
private shouldAutoApproveToolWithPath(blockname: ToolUseName, autoApproveActionpath: string | undefined): boolean {
private async shouldAutoApproveToolWithPath(
blockname: ToolUseName,
autoApproveActionpath: string | undefined,
): Promise<boolean> {
return this.autoApprover.shouldAutoApproveToolWithPath(blockname, autoApproveActionpath)
}
@@ -573,7 +576,7 @@ export class ToolExecutor {
// update gui message
const partialMessage = JSON.stringify(sharedMessageProps)
if (this.shouldAutoApproveToolWithPath(block.name, relPath)) {
if (await this.shouldAutoApproveToolWithPath(block.name, relPath)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool") // in case the user changes auto-approval settings mid stream
await this.say("tool", partialMessage, undefined, undefined, block.partial)
} else {
@@ -645,7 +648,7 @@ export class ToolExecutor {
// )
// : undefined,
} satisfies ClineSayTool)
if (this.shouldAutoApproveToolWithPath(block.name, relPath)) {
if (await this.shouldAutoApproveToolWithPath(block.name, relPath)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
await this.say("tool", completeMessage, undefined, undefined, false)
this.taskState.consecutiveAutoApprovedRequestsCount++
@@ -782,7 +785,7 @@ export class ToolExecutor {
content: undefined,
operationIsLocatedInWorkspace: await isLocatedInWorkspace(relPath),
} satisfies ClineSayTool)
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
if (await this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
await this.say("tool", partialMessage, undefined, undefined, block.partial)
} else {
@@ -813,7 +816,7 @@ export class ToolExecutor {
content: absolutePath,
operationIsLocatedInWorkspace: await isLocatedInWorkspace(relPath),
} satisfies ClineSayTool)
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
if (await this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
await this.say("tool", completeMessage, undefined, undefined, false) // need to be sending partialValue bool, since undefined has its own purpose in that the message is treated neither as a partial or completion of a partial, but as a single complete message
this.taskState.consecutiveAutoApprovedRequestsCount++
@@ -864,7 +867,7 @@ export class ToolExecutor {
content: "",
operationIsLocatedInWorkspace: await isLocatedInWorkspace(block.params.path),
} satisfies ClineSayTool)
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
if (await this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
await this.say("tool", partialMessage, undefined, undefined, block.partial)
} else {
@@ -896,7 +899,7 @@ export class ToolExecutor {
content: result,
operationIsLocatedInWorkspace: await isLocatedInWorkspace(block.params.path),
} satisfies ClineSayTool)
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
if (await this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
await this.say("tool", completeMessage, undefined, undefined, false)
this.taskState.consecutiveAutoApprovedRequestsCount++
@@ -939,7 +942,7 @@ export class ToolExecutor {
content: "",
operationIsLocatedInWorkspace: await isLocatedInWorkspace(block.params.path),
} satisfies ClineSayTool)
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
if (await this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
await this.say("tool", partialMessage, undefined, undefined, block.partial)
} else {
@@ -968,7 +971,7 @@ export class ToolExecutor {
content: result,
operationIsLocatedInWorkspace: await isLocatedInWorkspace(block.params.path),
} satisfies ClineSayTool)
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
if (await this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
await this.say("tool", completeMessage, undefined, undefined, false)
this.taskState.consecutiveAutoApprovedRequestsCount++
@@ -1015,7 +1018,7 @@ export class ToolExecutor {
content: "",
operationIsLocatedInWorkspace: await isLocatedInWorkspace(block.params.path),
} satisfies ClineSayTool)
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
if (await this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
await this.say("tool", partialMessage, undefined, undefined, block.partial)
} else {
@@ -1052,7 +1055,7 @@ export class ToolExecutor {
content: results,
operationIsLocatedInWorkspace: await isLocatedInWorkspace(block.params.path),
} satisfies ClineSayTool)
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
if (await this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
await this.say("tool", completeMessage, undefined, undefined, false)
this.taskState.consecutiveAutoApprovedRequestsCount++
+42 -2
View File
@@ -35,6 +35,7 @@ import pTimeout from "p-timeout"
import pWaitFor from "p-wait-for"
import * as path from "path"
import * as vscode from "vscode"
import { getGitRemoteUrls } from "@utils/git"
import { parseAssistantMessageV2, parseAssistantMessageV3, ToolUseName } from "@core/assistant-message"
import {
@@ -1436,7 +1437,10 @@ export class Task {
try {
const { response, text, images, files } = await this.ask("command_output", chunk)
if (response === "yesButtonClicked") {
// proceed while running
// proceed while running - but still capture user feedback if provided
if (text || (images && images.length > 0) || (files && files.length > 0)) {
userFeedback = { text, images, files }
}
} else {
userFeedback = { text, images, files }
}
@@ -1927,12 +1931,42 @@ export class Task {
message: `Cline has auto-approved ${this.autoApprovalSettings.maxRequests.toString()} API requests.`,
})
}
await this.ask(
const { response, text, images, files } = await this.ask(
"auto_approval_max_req_reached",
`Cline has auto-approved ${this.autoApprovalSettings.maxRequests.toString()} API requests. Would you like to reset the count and proceed with the task?`,
)
// if we get past the promise it means the user approved and did not start a new task
this.taskState.consecutiveAutoApprovedRequestsCount = 0
// Process user feedback if provided
if (response === "messageResponse") {
// Display the user's message in the chat UI
await this.say("user_feedback", text, images, files)
// This userContent is for the *next* API call.
const feedbackUserContent: UserContent = []
feedbackUserContent.push({
type: "text",
text: formatResponse.autoApprovalMaxReached(text),
})
if (images && images.length > 0) {
feedbackUserContent.push(...formatResponse.imageBlocks(images))
}
let fileContentString = ""
if (files && files.length > 0) {
fileContentString = await processFilesIntoText(files)
}
if (fileContentString) {
feedbackUserContent.push({
type: "text",
text: fileContentString,
})
}
userContent = feedbackUserContent
}
}
// get previous api req's index to check token usage and determine if we need to truncate conversation history
@@ -2547,6 +2581,12 @@ export class Task {
const result = formatResponse.formatFilesList(cwd, files, didHitLimit, this.clineIgnoreController)
details += result
}
// Add git remote URLs section
const gitRemotes = await getGitRemoteUrls(cwd)
if (gitRemotes.length > 0) {
details += `\n\n# Git Remote URLs\n${gitRemotes.join("\n")}`
}
}
// Add context window usage information
+3 -5
View File
@@ -1,11 +1,8 @@
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
import { ToolUseName } from "@core/assistant-message"
import * as path from "path"
import * as vscode from "vscode"
import os from "os"
export const cwd =
vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop")
import { getCwd } from "@/utils/path"
export class AutoApprove {
autoApprovalSettings: AutoApprovalSettings
@@ -54,9 +51,10 @@ export class AutoApprove {
// Check if the tool should be auto-approved based on the settings
// and the path of the action. Returns true if the tool should be auto-approved
// based on the user's settings and the path of the action.
shouldAutoApproveToolWithPath(blockname: ToolUseName, autoApproveActionpath: string | undefined): boolean {
async shouldAutoApproveToolWithPath(blockname: ToolUseName, autoApproveActionpath: string | undefined): Promise<boolean> {
let isLocalRead: boolean = false
if (autoApproveActionpath) {
const cwd = await getCwd(path.join(os.homedir(), "Desktop"))
const absolutePath = path.resolve(cwd, autoApproveActionpath)
isLocalRead = absolutePath.startsWith(cwd)
} else {
+12
View File
@@ -26,6 +26,7 @@ import {
migratePlanActGlobalToWorkspaceStorage,
migrateCustomInstructionsToGlobalRules,
migrateModeFromWorkspaceStorageToControllerState,
migrateWelcomeViewCompleted,
} from "./core/storage/state-migrations"
import { sendFocusChatInputEvent } from "./core/controller/ui/subscribeToFocusChatInput"
@@ -68,6 +69,9 @@ export async function activate(context: vscode.ExtensionContext) {
// Migrate mode from workspace storage to controller state (one-time cleanup)
await migrateModeFromWorkspaceStorageToControllerState(context)
// Migrate welcomeViewCompleted setting based on existing API keys (one-time cleanup)
await migrateWelcomeViewCompleted(context)
// Clean up orphaned file context warnings (startup cleanup)
await FileContextTracker.cleanupOrphanedWarnings(context)
@@ -640,6 +644,14 @@ export async function activate(context: vscode.ExtensionContext) {
}),
)
// Register the openWalkthrough command handler
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)
}),
)
// Register the generateGitCommitMessage command handler
context.subscriptions.push(
vscode.commands.registerCommand("cline.generateGitCommitMessage", async () => {
+14 -6
View File
@@ -11,6 +11,7 @@ type ClaudeCodeOptions = {
messages: Anthropic.Messages.MessageParam[]
path?: string
modelId?: string
thinkingBudgetTokens?: number
}
type ProcessState = {
@@ -107,7 +108,7 @@ const claudeCodeTools = [
const CLAUDE_CODE_TIMEOUT = 600000 // 10 minutes
function runProcess({ systemPrompt, messages, path, modelId }: ClaudeCodeOptions) {
function runProcess({ systemPrompt, messages, path, modelId, thinkingBudgetTokens }: ClaudeCodeOptions) {
const claudePath = path || "claude"
const args = [
@@ -128,15 +129,22 @@ function runProcess({ systemPrompt, messages, path, modelId }: ClaudeCodeOptions
args.push("--model", modelId)
}
const env: NodeJS.ProcessEnv = {
...process.env,
// The default is 32000. However, I've gotten larger responses, so we increase it unless the user specified it.
CLAUDE_CODE_MAX_OUTPUT_TOKENS: process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS || "64000",
MAX_THINKING_TOKENS: (thinkingBudgetTokens || 0).toString(),
}
// We don't want to consume the user's ANTHROPIC_API_KEY,
// and will allow Claude Code to resolve auth by itself
delete env["ANTHROPIC_API_KEY"]
const claudeCodeProcess = execa(claudePath, args, {
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
env: {
...process.env,
// The default is 32000. However, I've gotten larger responses, so we increase it unless the user specified it.
CLAUDE_CODE_MAX_OUTPUT_TOKENS: process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS || "64000",
},
env,
cwd,
maxBuffer: 1024 * 1024 * 1000,
timeout: CLAUDE_CODE_TIMEOUT,
@@ -60,7 +60,14 @@ describe("TerminalProcess (Integration Tests)", () => {
const emitSpy = sandbox.spy(process, "emit")
// Run a simple command
await process.run(terminal, "echo test")
const runPromise = process.run(terminal, "echo test")
// If terminal doesn't have shell integration, advance timer
if (!terminal.shellIntegration) {
await sandbox.clock.tickAsync(3000)
}
await runPromise
// Verify that the continue event was emitted
;(emitSpy as sinon.SinonSpy).calledWith("continue").should.be.true()
@@ -76,7 +83,14 @@ describe("TerminalProcess (Integration Tests)", () => {
const emitSpy = sandbox.spy(process, "emit")
// Run a command that produces predictable output
await process.run(terminal, "echo 'Line 1' && echo 'Line 2'")
const runPromise = process.run(terminal, "echo 'Line 1' && echo 'Line 2'")
// If terminal doesn't have shell integration, advance timer
if (!terminal.shellIntegration) {
await sandbox.clock.tickAsync(3000)
}
await runPromise
// Check that the events were emitted
;(emitSpy as sinon.SinonSpy).calledWith("completed").should.be.true()
@@ -92,7 +106,14 @@ describe("TerminalProcess (Integration Tests)", () => {
const emitSpy = sandbox.spy(process, "emit")
// Run a command that lists files
await process.run(terminal, "ls -la")
const runPromise = process.run(terminal, "ls -la")
// If terminal doesn't have shell integration, advance timer
if (!terminal.shellIntegration) {
await sandbox.clock.tickAsync(3000)
}
await runPromise
// Verify that the continue event was emitted
;(emitSpy as sinon.SinonSpy).calledWith("continue").should.be.true()
@@ -130,7 +151,14 @@ describe("TerminalProcess (Integration Tests)", () => {
const emitSpy = sandbox.spy(process, "emit")
// Run a command that produces predictable output
await process.run(terminal, "echo 'Line 1' 'Line 2'")
const runPromise = process.run(terminal, "echo 'Line 1' 'Line 2'")
// If terminal doesn't have shell integration, advance timer
if (!terminal.shellIntegration) {
await sandbox.clock.tickAsync(3000)
}
await runPromise
// Check that the events were emitted
;(emitSpy as sinon.SinonSpy).calledWith("completed").should.be.true()
@@ -146,7 +174,14 @@ describe("TerminalProcess (Integration Tests)", () => {
const emitSpy = sandbox.spy(process, "emit")
// Run a command that produces predictable output
await process.run(terminal, "echo \"Line 1\" && echo 'Line 2'")
const runPromise = process.run(terminal, "echo \"Line 1\" && echo 'Line 2'")
// If terminal doesn't have shell integration, advance timer
if (!terminal.shellIntegration) {
await sandbox.clock.tickAsync(3000)
}
await runPromise
// Check that the events were emitted
;(emitSpy as sinon.SinonSpy).calledWith("completed").should.be.true()
@@ -169,8 +204,14 @@ describe("TerminalProcess (Integration Tests)", () => {
// Spy on the emit function to verify events
const emitSpy = sandbox.spy(process, "emit")
// Run the command
await process.run(terminal, "test-command")
// Run the command - this returns a promise
const runPromise = process.run(terminal, "test-command")
// Advance the fake timer by 3 seconds to trigger the setTimeout
await sandbox.clock.tickAsync(3000)
// Now wait for the promise to resolve
await runPromise
// Check that the correct methods were called and events emitted
sendTextStub.calledWith("test-command", true).should.be.true()
+25 -2
View File
@@ -2,6 +2,7 @@ import { EventEmitter } from "events"
import { stripAnsi } from "./ansiUtils"
import * as vscode from "vscode"
import { Logger } from "@services/logging/Logger"
import { getLatestTerminalOutput } from "./get-latest-output"
export interface TerminalProcessEvents {
line: [line: string]
@@ -31,6 +32,19 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
// super()
async run(terminal: vscode.Terminal, command: string) {
// When command does not produce any output, we can assume the shell integration API failed and as a fallback return the current terminal contents
const emitCurrentTerminalContents = async () => {
try {
const terminalSnapshot = await getLatestTerminalOutput()
if (terminalSnapshot && terminalSnapshot.trim()) {
const fallbackMessage = `The command's output could not be captured due to some technical issue, however it has been executed successfully. Here's the current terminal's content to help you get the command's output:\n\n${terminalSnapshot}`
this.emit("line", fallbackMessage)
}
} catch (error) {
console.error("Error capturing terminal output:", error)
}
}
// Clear any existing grace period timer from previous commands
if (this.gracePeriodTimer) {
clearTimeout(this.gracePeriodTimer)
@@ -249,7 +263,8 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
if (!didEmitEmptyLine) {
console.log(`[TerminalProcess] Emitting fallback empty line for no-output command`)
this.emit("line", "") // empty line to show proceed button
this.emit("line", "[Command completed with no output]")
// this.emit("line", "[Command completed with no output]")
await emitCurrentTerminalContents()
didEmitEmptyLine = true
}
}
@@ -295,7 +310,8 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
console.log(`[TerminalProcess] WARNING: Process completed but no output was captured`)
// Ensure we emit at least one line for UI feedback
if (!didEmitEmptyLine) {
this.emit("line", "[Command completed silently]")
// this.emit("line", "[Command completed silently]")
await emitCurrentTerminalContents()
}
}
@@ -314,7 +330,14 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
this.startGracePeriod()
}
} else {
// no shell integration detected, we'll fallback to running the command and capturing the terminal's output after some time
terminal.sendText(command, true)
// wait 3 seconds for the command to run
await new Promise((resolve) => setTimeout(resolve, 3000))
// For terminals without shell integration, also try to capture terminal content
await emitCurrentTerminalContents()
// For terminals without shell integration, we can't know when the command completes
// So we'll just emit the continue event after a delay
this.emit("completed")
@@ -1,43 +0,0 @@
import { getCwd } from "@/utils/path"
import * as vscode from "vscode"
/*
Used to get user's current python environment (unnecessary now that we use the IDE's terminal)
${await (async () => {
try {
const pythonEnvPath = await getPythonEnvPath()
if (pythonEnvPath) {
return `\nPython Environment: ${pythonEnvPath}`
}
} catch {}
return ""
})()}
*/
export async function getPythonEnvPath(): Promise<string | undefined> {
const pythonExtension = vscode.extensions.getExtension("ms-python.python")
if (!pythonExtension) {
return undefined
}
// Ensure the Python extension is activated
if (!pythonExtension.isActive) {
// if the python extension is not active, we can assume the project is not a python project
return undefined
}
// Access the Python extension API
const pythonApi = pythonExtension.exports
// Get the active environment path for the current workspace
const workspaceFolder = await getCwd()
if (!workspaceFolder) {
return undefined
}
// Get the active python environment path for the current workspace
const pythonEnv = await pythonApi?.environments?.getActiveEnvironmentPath(workspaceFolder)
if (pythonEnv && pythonEnv.path) {
return pythonEnv.path
} else {
return undefined
}
}
+1 -6
View File
@@ -4,14 +4,9 @@ import { ExtensionMessage } from "@shared/ExtensionMessage"
export class ClineAccountService {
private readonly baseUrl = "https://api.cline.bot/v1"
private postMessageToWebview: (message: ExtensionMessage) => Promise<void>
private getClineApiKey: () => Promise<string | undefined>
constructor(
postMessageToWebview: (message: ExtensionMessage) => Promise<void>,
getClineApiKey: () => Promise<string | undefined>,
) {
this.postMessageToWebview = postMessageToWebview
constructor(getClineApiKey: () => Promise<string | undefined>) {
this.getClineApiKey = getClineApiKey
}
-1
View File
@@ -9,7 +9,6 @@ import * as path from "path"
import { Logger } from "../logging/Logger"
import { createTestServer, shutdownTestServer } from "./TestServer"
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { GetWorkspacePathsRequest } from "@/shared/proto/index.host"
// State variable
let isTestMode = false
+1
View File
@@ -60,6 +60,7 @@ export const DEFAULT_PLATFORM = "unknown"
export interface ExtensionState {
isNewUser: boolean
welcomeViewCompleted: boolean
apiConfiguration?: ApiConfiguration
autoApprovalSettings: AutoApprovalSettings
browserSettings: BrowserSettings
+36
View File
@@ -185,6 +185,42 @@ export async function getWorkingState(cwd: string): Promise<string> {
}
}
export async function getGitRemoteUrls(cwd: string): Promise<string[]> {
try {
const isInstalled = await checkGitInstalled()
if (!isInstalled) {
return []
}
const isRepo = await checkGitRepo(cwd)
if (!isRepo) {
return []
}
const { stdout } = await execAsync("git remote -v", { cwd })
if (!stdout.trim()) {
return []
}
// Parse output to extract unique URLs
// git remote -v output format: "remoteName remoteUrl (fetch|push)"
const remotes = stdout
.trim()
.split("\n")
.filter((line) => line.includes("(fetch)")) // Only fetch URLs to avoid duplicates
.map((line) => {
const match = line.match(/^(\S+)\s+(\S+)\s+\(fetch\)$/)
return match ? { name: match[1], url: match[2] } : null
})
.filter((remote): remote is { name: string; url: string } => remote !== null)
return remotes.map((remote) => `${remote.name}: ${remote.url}`)
} catch (error) {
console.error("Error getting git remotes:", error)
return []
}
}
function truncateOutput(content: string): string {
if (!GIT_OUTPUT_LINE_LIMIT) {
return content
+14 -8
View File
@@ -1353,7 +1353,7 @@ export const ChatRowContent = memo(
style={{
display: "flex",
flexDirection: "column",
backgroundColor: "rgba(255, 191, 0, 0.1)",
backgroundColor: "var(--vscode-textBlockQuote-background)",
padding: 8,
borderRadius: 3,
fontSize: 12,
@@ -1368,19 +1368,19 @@ export const ChatRowContent = memo(
className="codicon codicon-warning"
style={{
marginRight: 8,
fontSize: 18,
color: "#FFA500",
fontSize: 14,
color: "var(--vscode-descriptionForeground)",
}}></i>
<span
style={{
fontWeight: 500,
color: "#FFA500",
color: "var(--vscode-foreground)",
}}>
Shell Integration Unavailable
</span>
</div>
<div>
Cline won't be able to view the command's output. Please update VSCode (
<div style={{ color: "var(--vscode-foreground)", opacity: 0.8 }}>
Cline may have trouble viewing the command's output. Please update VSCode (
<code>CMD/CTRL + Shift + P</code> "Update") and make sure you're using a supported
shell: zsh, bash, fish, or PowerShell (<code>CMD/CTRL + Shift + P</code> "Terminal:
Select Default Profile").{" "}
@@ -1551,7 +1551,10 @@ export const ChatRowContent = memo(
<OptionsButtons
options={options}
selected={selected}
isActive={isLast && lastModifiedMessage?.ask === "followup"}
isActive={
(isLast && lastModifiedMessage?.ask === "followup") ||
(!selected && options && options.length > 0)
}
inputValue={inputValue}
/>
{quoteButtonState.visible && (
@@ -1640,7 +1643,10 @@ export const ChatRowContent = memo(
<OptionsButtons
options={options}
selected={selected}
isActive={isLast && lastModifiedMessage?.ask === "plan_mode_respond"}
isActive={
(isLast && lastModifiedMessage?.ask === "plan_mode_respond") ||
(!selected && options && options.length > 0)
}
inputValue={inputValue}
/>
{quoteButtonState.visible && (
@@ -1725,7 +1725,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
apiErrorMessage={undefined}
modelIdErrorMessage={undefined}
isPopup={true}
saveImmediately={true} // Ensure popup saves immediately
/>
</ModelSelectorTooltip>
)}
@@ -66,7 +66,7 @@ export const MessagesArea: React.FC<MessagesAreaProps> = ({
<div style={{ flexGrow: 1, display: "flex" }} ref={scrollContainerRef}>
<Virtuoso
ref={virtuosoRef}
key={task.ts} // trick to make sure virtuoso re-renders when task changes
key={task.ts} // trick to make sure virtuoso re-renders when task changes, and we use initialTopMostItemIndex to start at the bottom
className="scrollable"
style={{
flexGrow: 1,
@@ -75,11 +75,12 @@ export const MessagesArea: React.FC<MessagesAreaProps> = ({
components={{
Footer: () => <div style={{ height: 5 }} />, // Add empty padding at the bottom
}}
// increasing top by 3_000 to prevent jumping around when user collapses a row
increaseViewportBy={{
top: 3_000,
bottom: Number.MAX_SAFE_INTEGER,
}}
data={groupedMessages}
}} // hack to make sure the last message is always rendered to get truly perfect scroll to bottom animation when new messages are added (Number.MAX_SAFE_INTEGER is safe for arithmetic operations, which is all virtuoso uses this value for in src/sizeRangeSystem.ts)
data={groupedMessages} // messages is the raw format returned by extension, modifiedMessages is the manipulated structure that combines certain messages of related type, and visibleMessages is the filtered structure that removes messages that should not be rendered
itemContent={itemContent}
atBottomStateChange={(isAtBottom) => {
setIsAtBottom(isAtBottom)
@@ -88,7 +89,7 @@ export const MessagesArea: React.FC<MessagesAreaProps> = ({
}
setShowScrollToBottom(disableAutoScrollRef.current && !isAtBottom)
}}
atBottomThreshold={10}
atBottomThreshold={10} // anything lower causes issues with followOutput
initialTopMostItemIndex={groupedMessages.length - 1}
/>
</div>
@@ -39,55 +39,3 @@ export const useIsStreaming = (
return false
}, [modifiedMessages, clineAsk, enableButtons, primaryButtonText])
}
/**
* Component that shows a visual streaming indicator
* Can be used to show loading states, typing indicators, etc.
*/
export const StreamingVisualIndicator: React.FC<{ isStreaming: boolean }> = ({ isStreaming }) => {
if (!isStreaming) return null
return (
<div
style={{
display: "flex",
alignItems: "center",
padding: "8px 16px",
color: "var(--vscode-descriptionForeground)",
fontSize: "12px",
}}>
<div
style={{
display: "flex",
gap: "4px",
marginRight: "8px",
}}>
{[0, 1, 2].map((i) => (
<div
key={i}
style={{
width: "4px",
height: "4px",
borderRadius: "50%",
backgroundColor: "var(--vscode-progressBar-background)",
animation: `pulse 1.4s infinite ease-in-out ${i * 0.16}s`,
}}
/>
))}
</div>
<span>Cline is thinking...</span>
<style>{`
@keyframes pulse {
0%, 80%, 100% {
opacity: 0.3;
transform: scale(0.8);
}
40% {
opacity: 1;
transform: scale(1);
}
}
`}</style>
</div>
)
}
@@ -3,4 +3,4 @@
*/
export { MessageRenderer, createMessageRenderer } from "./MessageRenderer"
export { useIsStreaming, StreamingVisualIndicator } from "./StreamingIndicator"
export { useIsStreaming } from "./StreamingIndicator"
@@ -57,6 +57,8 @@ export function useMessageHandlers(messages: ClineMessage[], chatState: ChatStat
case "resume_task":
case "resume_completed_task":
case "mistake_limit_reached":
case "auto_approval_max_req_reached":
case "api_req_failed":
case "new_task":
case "condense":
case "report_bug":
@@ -111,34 +113,69 @@ export function useMessageHandlers(messages: ClineMessage[], chatState: ChatStat
switch (clineAsk) {
case "api_req_failed":
case "command":
case "command_output":
case "tool":
case "browser_action_launch":
case "use_mcp_server":
case "resume_task":
// For approval buttons, if there's input content, send it as a proper user message
// If there's no input content, just approve the action
if (trimmedInput || (images && images.length > 0) || (files && files.length > 0)) {
// Send as a regular message so it appears in the conversation
await handleSendMessage(trimmedInput || "", images || [], files || [])
} else {
// No input content, just approve the action
await TaskServiceClient.askResponse(
AskResponseRequest.create({
responseType: "yesButtonClicked",
}),
)
// Clear input state after sending (only when no content was sent as a message)
setInputValue("")
setActiveQuote(null)
setSelectedImages([])
setSelectedFiles([])
}
break
case "mistake_limit_reached":
case "auto_approval_max_req_reached":
case "command_output":
// For proceed buttons, if there's input content, send it as a proper user message
// If there's no input content, just proceed with the action
if (trimmedInput || (images && images.length > 0) || (files && files.length > 0)) {
await TaskServiceClient.askResponse(
AskResponseRequest.create({
responseType: "yesButtonClicked",
text: trimmedInput,
images: images,
files: files,
}),
)
// Send as a regular message so it appears in the conversation
await handleSendMessage(trimmedInput || "", images || [], files || [])
} else {
// No input content, just proceed with the action
await TaskServiceClient.askResponse(
AskResponseRequest.create({
responseType: "yesButtonClicked",
}),
)
// Clear input state after sending (only when no content was sent as a message)
setInputValue("")
setActiveQuote(null)
setSelectedImages([])
setSelectedFiles([])
}
break
case "resume_task":
// For resume_task, if there's input content, send it as a proper user message
// If there's no input content, just resume the task
if (trimmedInput || (images && images.length > 0) || (files && files.length > 0)) {
// Send as a regular message so it appears in the conversation
await handleSendMessage(trimmedInput || "", images || [], files || [])
} else {
// No input content, just resume the task
await TaskServiceClient.askResponse(
AskResponseRequest.create({
responseType: "yesButtonClicked",
}),
)
// Clear input state after sending (only when no content was sent as a message)
setInputValue("")
setActiveQuote(null)
setSelectedImages([])
setSelectedFiles([])
}
// Clear input state after sending
setInputValue("")
setActiveQuote(null)
setSelectedImages([])
setSelectedFiles([])
break
case "completion_result":
case "resume_completed_task":
@@ -185,6 +222,7 @@ export function useMessageHandlers(messages: ClineMessage[], chatState: ChatStat
setSendingDisabled,
setEnableButtons,
chatState,
handleSendMessage,
],
)
@@ -32,8 +32,6 @@ export function useScrollBehavior(
const [showScrollToBottom, setShowScrollToBottom] = useState(false)
const [isAtBottom, setIsAtBottom] = useState(false)
const [pendingScrollToMessage, setPendingScrollToMessage] = useState<number | null>(null)
// Smooth scroll to bottom with debounce
const scrollToBottomSmooth = useMemo(
() =>
debounce(
@@ -49,7 +47,7 @@ export function useScrollBehavior(
[],
)
// Instant scroll to bottom
// Smooth scroll to bottom with debounce
const scrollToBottomAuto = useCallback(() => {
virtuosoRef.current?.scrollTo({
top: Number.MAX_SAFE_INTEGER,
@@ -57,7 +55,6 @@ export function useScrollBehavior(
})
}, [])
// Scroll to specific message
const scrollToMessage = useCallback(
(messageIndex: number) => {
setPendingScrollToMessage(messageIndex)
@@ -113,7 +110,7 @@ export function useScrollBehavior(
[messages, visibleMessages, groupedMessages],
)
// Toggle row expansion with scroll handling
// scroll when user toggles certain rows
const toggleRowExpansion = useCallback(
(ts: number) => {
const isCollapsing = expandedRows[ts] ?? false
@@ -165,10 +162,9 @@ export function useScrollBehavior(
}
}
},
[groupedMessages, expandedRows, scrollToBottomAuto, isAtBottom, setExpandedRows],
[groupedMessages, expandedRows, scrollToBottomAuto, isAtBottom],
)
// Handle row height changes
const handleRowHeightChange = useCallback(
(isTaller: boolean) => {
if (!disableAutoScrollRef.current) {
@@ -184,23 +180,21 @@ export function useScrollBehavior(
[scrollToBottomSmooth, scrollToBottomAuto],
)
// Auto-scroll when new messages arrive
useEffect(() => {
if (!disableAutoScrollRef.current) {
setTimeout(() => {
scrollToBottomSmooth()
}, 50)
// return () => clearTimeout(timer) // dont cleanup since if visibleMessages.length changes it cancels.
}
}, [groupedMessages.length, scrollToBottomSmooth])
// Handle pending scroll to message
useEffect(() => {
if (pendingScrollToMessage !== null) {
scrollToMessage(pendingScrollToMessage)
}
}, [pendingScrollToMessage, groupedMessages, scrollToMessage])
// Handle wheel events to detect manual scrolling
const handleWheel = useCallback((event: Event) => {
const wheelEvent = event as WheelEvent
if (wheelEvent.deltaY && wheelEvent.deltaY < 0) {
@@ -210,8 +204,7 @@ export function useScrollBehavior(
}
}
}, [])
useEvent("wheel", handleWheel, window, { passive: true })
useEvent("wheel", handleWheel, window, { passive: true }) // passive improves scrolling performance
return {
virtuosoRef,
@@ -20,23 +20,23 @@ export function filterVisibleMessages(messages: ClineMessage[]): ClineMessage[]
return messages.filter((message) => {
switch (message.ask) {
case "completion_result":
// don't show a chat row for a completion_result ask without text
// don't show a chat row for a completion_result ask without text. This specific type of message only occurs if cline wants to execute a command as part of its completion result, in which case we interject the completion_result tool with the execute_command tool.
if (message.text === "") {
return false
}
break
case "api_req_failed":
case "api_req_failed": // this message is used to update the latest api_req_started that the request failed
case "resume_task":
case "resume_completed_task":
return false
}
switch (message.say) {
case "api_req_finished":
case "api_req_retried":
case "deleted_api_reqs":
case "api_req_finished": // combineApiRequests removes this from modifiedMessages anyways
case "api_req_retried": // this message is used to update the latest api_req_started that the request was retried
case "deleted_api_reqs": // aggregated api_req metrics from deleted messages
return false
case "text":
// Sometimes cline returns an empty text message, we don't want to render these
// Sometimes cline returns an empty text message, we don't want to render these. (We also use a say text for user messages, so in case they just sent images we still render that)
if ((message.text ?? "") === "" && (message.images?.length ?? 0) === 0) {
return false
}
@@ -7,7 +7,7 @@ const DeleteTaskButton: React.FC<{
taskSize: string
taskId?: string
}> = ({ taskSize, taskId }) => (
<HeroTooltip content="Delete Task & Checkpoints">
<HeroTooltip content="Delete Task">
<VSCodeButton
appearance="icon"
onClick={() => taskId && TaskServiceClient.deleteTasksWithIds(StringArrayRequest.create({ value: [taskId] }))}
+37 -213
View File
@@ -1,11 +1,8 @@
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ModelsServiceClient } from "@/services/grpc-client"
import { ApiConfiguration } from "@shared/api"
import { StringRequest } from "@shared/proto/common"
import { UpdateApiConfigurationRequest } from "@shared/proto/models"
import { convertApiConfigurationToProto } from "@shared/proto-conversions/models/api-configuration-conversion"
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { memo, useCallback, useEffect, useMemo, useState } from "react"
import { useCallback, useEffect, useState } from "react"
import { useInterval } from "react-use"
import styled from "styled-components"
import { OPENROUTER_MODEL_PICKER_Z_INDEX } from "./OpenRouterModelPicker"
@@ -38,13 +35,13 @@ import { NebiusProvider } from "./providers/NebiusProvider"
import { LiteLlmProvider } from "./providers/LiteLlmProvider"
import { VSCodeLmProvider } from "./providers/VSCodeLmProvider"
import { LMStudioProvider } from "./providers/LMStudioProvider"
import { useApiConfigurationHandlers } from "./utils/useApiConfigurationHandlers"
interface ApiOptionsProps {
showModelOptions: boolean
apiErrorMessage?: string
modelIdErrorMessage?: string
isPopup?: boolean
saveImmediately?: boolean // Add prop to control immediate saving
}
// This is necessary to ensure dropdown opens downward, important for when this is used in popup
@@ -71,52 +68,15 @@ declare module "vscode" {
}
}
const ApiOptions = ({
showModelOptions,
apiErrorMessage,
modelIdErrorMessage,
isPopup,
saveImmediately = false, // Default to false
}: ApiOptionsProps) => {
const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, isPopup }: ApiOptionsProps) => {
// Use full context state for immediate save payload
const extensionState = useExtensionState()
const { apiConfiguration, setApiConfiguration, uriScheme } = extensionState
const { apiConfiguration, uriScheme } = useExtensionState()
const selectedProvider = apiConfiguration?.apiProvider
const { handleFieldChange } = useApiConfigurationHandlers()
const [ollamaModels, setOllamaModels] = useState<string[]>([])
const [modelConfigurationSelected, setModelConfigurationSelected] = useState(false)
const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
const newValue = event.target.value
// Update local state
setApiConfiguration({
...apiConfiguration,
[field]: newValue,
})
// If the field is the provider AND saveImmediately is true, save it immediately using the full context state
if (saveImmediately && field === "apiProvider") {
// Use apiConfiguration from the full extensionState context to send the most complete data
const currentFullApiConfig = extensionState.apiConfiguration
// Convert to proto format and send via gRPC
const updatedConfig = {
...currentFullApiConfig,
apiProvider: newValue,
}
const protoConfig = convertApiConfigurationToProto(updatedConfig)
ModelsServiceClient.updateApiConfigurationProto(
UpdateApiConfigurationRequest.create({
apiConfiguration: protoConfig,
}),
).catch((error) => {
console.error("Failed to update API configuration:", error)
})
}
}
const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo(() => {
return normalizeApiConfiguration(apiConfiguration)
}, [apiConfiguration])
// Poll ollama/vscode-lm models
const requestLocalModels = useCallback(async () => {
@@ -161,7 +121,7 @@ const ApiOptions = ({
<VSCodeDropdown
id="api-provider"
value={selectedProvider}
onChange={handleInputChange("apiProvider")}
onChange={(e: any) => handleFieldChange("apiProvider", e.target.value)}
style={{
minWidth: 130,
position: "relative",
@@ -196,241 +156,105 @@ const ApiOptions = ({
</DropdownContainer>
{apiConfiguration && selectedProvider === "cline" && (
<ClineProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<ClineProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "asksage" && (
<AskSageProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<AskSageProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "anthropic" && (
<AnthropicProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
setApiConfiguration={setApiConfiguration}
/>
<AnthropicProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "claude-code" && (
<ClaudeCodeProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<ClaudeCodeProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "openai-native" && (
<OpenAINativeProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<OpenAINativeProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "qwen" && (
<QwenProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
setApiConfiguration={setApiConfiguration}
/>
<QwenProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "doubao" && (
<DoubaoProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<DoubaoProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "mistral" && (
<MistralProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<MistralProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "openrouter" && (
<OpenRouterProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
uriScheme={uriScheme}
/>
<OpenRouterProvider showModelOptions={showModelOptions} isPopup={isPopup} uriScheme={uriScheme} />
)}
{apiConfiguration && selectedProvider === "deepseek" && (
<DeepSeekProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<DeepSeekProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "together" && (
<TogetherProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<TogetherProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "openai" && (
<OpenAICompatibleProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<OpenAICompatibleProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "sambanova" && (
<SambanovaProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<SambanovaProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "bedrock" && (
<BedrockProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
setApiConfiguration={setApiConfiguration}
/>
<BedrockProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "vertex" && (
<VertexProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
setApiConfiguration={setApiConfiguration}
/>
<VertexProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "gemini" && (
<GeminiProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
setApiConfiguration={setApiConfiguration}
/>
<GeminiProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "requesty" && (
<RequestyProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<RequestyProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "fireworks" && (
<FireworksProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<FireworksProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "vscode-lm" && (
<VSCodeLmProvider apiConfiguration={apiConfiguration} handleInputChange={handleInputChange} />
)}
{apiConfiguration && selectedProvider === "vscode-lm" && <VSCodeLmProvider />}
{apiConfiguration && selectedProvider === "litellm" && (
<LiteLlmProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
setApiConfiguration={setApiConfiguration}
/>
<LiteLlmProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "lmstudio" && (
<LMStudioProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<LMStudioProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "ollama" && (
<OllamaProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
setApiConfiguration={setApiConfiguration}
/>
<OllamaProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "nebius" && (
<NebiusProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<NebiusProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "xai" && (
<XaiProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
setApiConfiguration={setApiConfiguration}
/>
<XaiProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "cerebras" && (
<CerebrasProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<CerebrasProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "sapaicore" && (
<SapAiCoreProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<SapAiCoreProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiErrorMessage && (
@@ -457,4 +281,4 @@ const ApiOptions = ({
)
}
export default memo(ApiOptions)
export default ApiOptions
@@ -1,7 +1,7 @@
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { StateServiceClient } from "@/services/grpc-client"
import { openRouterDefaultModelId } from "@shared/api"
import { ApiConfiguration, openRouterDefaultModelId } from "@shared/api"
import { StringRequest } from "@shared/proto/common"
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import Fuse from "fuse.js"
@@ -14,6 +14,7 @@ import { ModelInfoView } from "./common/ModelInfoView"
import { normalizeApiConfiguration } from "./utils/providerUtils"
import FeaturedModelCard from "./FeaturedModelCard"
import ThinkingBudgetSlider from "./ThinkingBudgetSlider"
import { useApiConfigurationHandlers } from "./utils/useApiConfigurationHandlers"
// Star icon for favorites
const StarIcon = ({ isFavorite, onClick }: { isFavorite: boolean; onClick: (e: React.MouseEvent) => void }) => {
@@ -43,13 +44,13 @@ export interface OpenRouterModelPickerProps {
// Featured models for Cline provider
const featuredModels = [
{
id: "google/gemini-2.5-pro",
description: "Large 1M context window, great value",
id: "anthropic/claude-sonnet-4",
description: "Recommended for agentic coding in Cline",
label: "Best",
},
{
id: "anthropic/claude-sonnet-4",
description: "Recommended for agentic coding in Cline",
id: "google/gemini-2.5-pro",
description: "Large 1M context window, great value",
label: "Trending",
},
{
@@ -60,7 +61,8 @@ const featuredModels = [
]
const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }) => {
const { apiConfiguration, setApiConfiguration, openRouterModels, refreshOpenRouterModels } = useExtensionState()
const { handleFieldsChange } = useApiConfigurationHandlers()
const { apiConfiguration, openRouterModels, refreshOpenRouterModels } = useExtensionState()
const [searchTerm, setSearchTerm] = useState(apiConfiguration?.openRouterModelId || openRouterDefaultModelId)
const [isDropdownVisible, setIsDropdownVisible] = useState(false)
const [selectedIndex, setSelectedIndex] = useState(-1)
@@ -70,14 +72,13 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
const handleModelChange = (newModelId: string) => {
// could be setting invalid model id/undefined info but validation will catch it
setApiConfiguration({
...apiConfiguration,
...{
openRouterModelId: newModelId,
openRouterModelInfo: openRouterModels[newModelId],
},
})
setSearchTerm(newModelId)
handleFieldsChange({
openRouterModelId: newModelId,
openRouterModelInfo: openRouterModels[newModelId],
})
}
const { selectedModelId, selectedModelInfo } = useMemo(() => {
@@ -306,9 +307,7 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
{hasInfo ? (
<>
{showBudgetSlider && (
<ThinkingBudgetSlider apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
)}
{showBudgetSlider && <ThinkingBudgetSlider />}
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
</>
@@ -327,8 +326,8 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
If you're unsure which model to choose, Cline works best with{" "}
<VSCodeLink
style={{ display: "inline", fontSize: "inherit" }}
onClick={() => handleModelChange("google/gemini-2.5-pro")}>
google/gemini-2.5-pro.
onClick={() => handleModelChange("anthropic/claude-sonnet-4")}>
anthropic/claude-sonnet-4.
</VSCodeLink>
You can also try searching "free" for no-cost options currently available.
</>
@@ -1,13 +1,24 @@
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import React from "react"
import { ChatSettings } from "@shared/ChatSettings"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { updateSetting } from "./utils/settingsHandlers"
import { convertChatSettingsToProtoChatSettings } from "@shared/proto-conversions/state/chat-settings-conversion"
interface PreferredLanguageSettingProps {
chatSettings: ChatSettings
setChatSettings: (settings: ChatSettings) => void
}
const PreferredLanguageSetting: React.FC = () => {
const { chatSettings } = useExtensionState()
const handleLanguageChange = (newLanguage: string) => {
if (!chatSettings) return
const updatedChatSettings = {
...chatSettings,
preferredLanguage: newLanguage,
}
const protoChatSettings = convertChatSettingsToProtoChatSettings(updatedChatSettings)
updateSetting("chatSettings", protoChatSettings)
}
const PreferredLanguageSetting: React.FC<PreferredLanguageSettingProps> = ({ chatSettings, setChatSettings }) => {
return (
<div style={{}}>
<label htmlFor="preferred-language-dropdown" className="block mb-1 text-sm font-medium">
@@ -17,11 +28,7 @@ const PreferredLanguageSetting: React.FC<PreferredLanguageSettingProps> = ({ cha
id="preferred-language-dropdown"
currentValue={chatSettings.preferredLanguage || "English"}
onChange={(e: any) => {
const newLanguage = e.target.value
setChatSettings({
...chatSettings,
preferredLanguage: newLanguage,
}) // This constructs a full ChatSettings object
handleLanguageChange(e.target.value)
}}
style={{ width: "100%" }}>
<VSCodeOption value="English">English</VSCodeOption>
@@ -5,7 +5,7 @@ import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from
import { useRemark } from "react-remark"
import { useMount } from "react-use"
import styled from "styled-components"
import { requestyDefaultModelId, requestyDefaultModelInfo } from "../../../../src/shared/api"
import { requestyDefaultModelId, requestyDefaultModelInfo } from "@shared/api"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { ModelsServiceClient } from "../../services/grpc-client"
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
@@ -13,13 +13,15 @@ import { highlight } from "../history/HistoryView"
import { ModelInfoView } from "./common/ModelInfoView"
import { normalizeApiConfiguration } from "./utils/providerUtils"
import ThinkingBudgetSlider from "./ThinkingBudgetSlider"
import { useApiConfigurationHandlers } from "./utils/useApiConfigurationHandlers"
export interface RequestyModelPickerProps {
isPopup?: boolean
}
const RequestyModelPicker: React.FC<RequestyModelPickerProps> = ({ isPopup }) => {
const { apiConfiguration, setApiConfiguration, requestyModels, setRequestyModels } = useExtensionState()
const { apiConfiguration, requestyModels, setRequestyModels } = useExtensionState()
const { handleFieldsChange } = useApiConfigurationHandlers()
const [searchTerm, setSearchTerm] = useState(apiConfiguration?.requestyModelId || requestyDefaultModelId)
const [isDropdownVisible, setIsDropdownVisible] = useState(false)
const [selectedIndex, setSelectedIndex] = useState(-1)
@@ -29,12 +31,10 @@ const RequestyModelPicker: React.FC<RequestyModelPickerProps> = ({ isPopup }) =>
const handleModelChange = (newModelId: string) => {
// could be setting invalid model id/undefined info but validation will catch it
setApiConfiguration({
...apiConfiguration,
...{
requestyModelId: newModelId,
requestyModelInfo: requestyModels[newModelId],
},
handleFieldsChange({
requestyModelId: newModelId,
requestyModelInfo: requestyModels[newModelId],
})
setSearchTerm(newModelId)
}
@@ -226,9 +226,7 @@ const RequestyModelPicker: React.FC<RequestyModelPickerProps> = ({ isPopup }) =>
{hasInfo ? (
<>
{showBudgetSlider && (
<ThinkingBudgetSlider apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
)}
{showBudgetSlider && <ThinkingBudgetSlider />}
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
</>
) : (
@@ -1,19 +1,14 @@
import { UnsavedChangesDialog } from "@/components/common/AlertDialog"
import HeroTooltip from "@/components/common/HeroTooltip"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { StateServiceClient } from "@/services/grpc-client"
import { validateApiConfiguration, validateModelId } from "@/utils/validate"
import { ExtensionMessage } from "@shared/ExtensionMessage"
import { StringRequest } from "@shared/proto/common"
import { PlanActMode, ResetStateRequest, TogglePlanActModeRequest, UpdateSettingsRequest } from "@shared/proto/state"
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { PlanActMode, ResetStateRequest, TogglePlanActModeRequest } from "@shared/proto/state"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { CheckCheck, FlaskConical, Info, LucideIcon, Settings, SquareMousePointer, SquareTerminal, Webhook } from "lucide-react"
import { memo, useCallback, useEffect, useRef, useState } from "react"
import { useCallback, useEffect, useRef, useState } from "react"
import { useEvent } from "react-use"
import { Tab, TabContent, TabHeader, TabList, TabTrigger } from "../common/Tab"
import { BrowserSettings } from "@shared/BrowserSettings"
import FeatureSettingsSection from "./sections/FeatureSettingsSection"
import Section from "./Section"
import SectionHeader from "./SectionHeader"
import TerminalSettingsSection from "./sections/TerminalSettingsSection"
import ApiConfigurationSection from "./sections/ApiConfigurationSection"
@@ -21,8 +16,6 @@ import GeneralSettingsSection from "./sections/GeneralSettingsSection"
import BrowserSettingsSection from "./sections/BrowserSettingsSection"
import DebugSection from "./sections/DebugSection"
import AboutSection from "./sections/AboutSection"
import { convertApiConfigurationToProtoApiConfiguration } from "@shared/proto-conversions/state/settings-conversion"
import { convertChatSettingsToProtoChatSettings } from "@shared/proto-conversions/state/chat-settings-conversion"
const IS_DEV = process.env.IS_DEV
@@ -107,437 +100,12 @@ type SettingsViewProps = {
}
const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
// Track if there are unsaved changes
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
// State for the unsaved changes dialog
const [isUnsavedChangesDialogOpen, setIsUnsavedChangesDialogOpen] = useState(false)
// Store the action to perform after confirmation
const pendingAction = useRef<() => void>()
// Track active tab
const [activeTab, setActiveTab] = useState<string>(targetSection || SETTINGS_TABS[0].id)
// Track if we're currently switching modes
const [isSwitchingMode, setIsSwitchingMode] = useState(false)
// Track pending mode switch when there are unsaved changes
const [pendingModeSwitch, setPendingModeSwitch] = useState<"plan" | "act" | null>(null)
const {
apiConfiguration,
version,
openRouterModels,
telemetrySetting,
setTelemetrySetting,
chatSettings,
setChatSettings,
planActSeparateModelsSetting,
setPlanActSeparateModelsSetting,
enableCheckpointsSetting,
setEnableCheckpointsSetting,
mcpMarketplaceEnabled,
setMcpMarketplaceEnabled,
mcpRichDisplayEnabled,
setMcpRichDisplayEnabled,
shellIntegrationTimeout,
setShellIntegrationTimeout,
terminalOutputLineLimit,
setTerminalOutputLineLimit,
terminalReuseEnabled,
setTerminalReuseEnabled,
defaultTerminalProfile,
setDefaultTerminalProfile,
mcpResponsesCollapsed,
setMcpResponsesCollapsed,
setApiConfiguration,
browserSettings,
} = useExtensionState()
// Local state for browser settings
const [localBrowserSettings, setLocalBrowserSettings] = useState<BrowserSettings>(browserSettings)
// Store the original state to detect changes
const originalState = useRef({
apiConfiguration,
telemetrySetting,
planActSeparateModelsSetting,
enableCheckpointsSetting,
mcpMarketplaceEnabled,
mcpRichDisplayEnabled,
mcpResponsesCollapsed,
chatSettings,
shellIntegrationTimeout,
terminalReuseEnabled,
terminalOutputLineLimit,
defaultTerminalProfile,
browserSettings,
})
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
const handleSubmit = async (withoutDone: boolean = false) => {
const apiValidationResult = validateApiConfiguration(apiConfiguration)
const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels)
// setApiErrorMessage(apiValidationResult)
// setModelIdErrorMessage(modelIdValidationResult)
let apiConfigurationToSubmit = apiConfiguration
if (!apiValidationResult && !modelIdValidationResult) {
// vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
// vscode.postMessage({
// type: "telemetrySetting",
// text: telemetrySetting,
// })
// console.log("handleSubmit", withoutDone)
// vscode.postMessage({
// type: "separateModeSetting",
// text: separateModeSetting,
// })
} else {
// if the api configuration is invalid, we don't save it
apiConfigurationToSubmit = undefined
}
try {
await StateServiceClient.updateSettings(
UpdateSettingsRequest.create({
planActSeparateModelsSetting,
telemetrySetting,
enableCheckpointsSetting,
mcpMarketplaceEnabled,
mcpRichDisplayEnabled,
shellIntegrationTimeout,
terminalReuseEnabled,
mcpResponsesCollapsed,
apiConfiguration: apiConfigurationToSubmit
? convertApiConfigurationToProtoApiConfiguration(apiConfigurationToSubmit)
: undefined,
chatSettings: chatSettings ? convertChatSettingsToProtoChatSettings(chatSettings) : undefined,
terminalOutputLineLimit,
}),
)
// Update default terminal profile if it has changed
if (defaultTerminalProfile !== originalState.current.defaultTerminalProfile) {
await StateServiceClient.updateDefaultTerminalProfile({
value: defaultTerminalProfile || "default",
} as StringRequest)
}
// Update browser settings if they have changed
if (JSON.stringify(localBrowserSettings) !== JSON.stringify(originalState.current.browserSettings)) {
const { BrowserServiceClient } = await import("@/services/grpc-client")
const { UpdateBrowserSettingsRequest } = await import("@shared/proto/browser")
await BrowserServiceClient.updateBrowserSettings(
UpdateBrowserSettingsRequest.create({
metadata: {},
viewport: localBrowserSettings.viewport,
remoteBrowserEnabled: localBrowserSettings.remoteBrowserEnabled,
remoteBrowserHost: localBrowserSettings.remoteBrowserHost,
chromeExecutablePath: localBrowserSettings.chromeExecutablePath,
disableToolUse: localBrowserSettings.disableToolUse,
}),
)
}
// Update the original state to reflect the saved changes
originalState.current = {
apiConfiguration,
telemetrySetting,
planActSeparateModelsSetting,
enableCheckpointsSetting,
mcpMarketplaceEnabled,
mcpRichDisplayEnabled,
mcpResponsesCollapsed,
chatSettings,
shellIntegrationTimeout,
terminalReuseEnabled,
terminalOutputLineLimit,
defaultTerminalProfile,
browserSettings: localBrowserSettings,
}
} catch (error) {
console.error("Failed to update settings:", error)
}
if (!withoutDone) {
onDone()
}
}
useEffect(() => {
setApiErrorMessage(undefined)
setModelIdErrorMessage(undefined)
}, [apiConfiguration])
// Track the previous mode to detect mode switches
const previousMode = useRef(chatSettings.mode)
// Update original state when mode changes
useEffect(() => {
// Detect if the mode has changed
if (previousMode.current !== chatSettings.mode) {
// Mode has changed, update the original state immediately to reflect the new apiConfiguration and chatSettings
originalState.current = {
...originalState.current,
apiConfiguration: apiConfiguration,
chatSettings: chatSettings,
}
// Update the previous mode reference
previousMode.current = chatSettings.mode
}
}, [chatSettings.mode, apiConfiguration, chatSettings])
// Check for unsaved changes by comparing current state with original state
useEffect(() => {
// Don't check for changes while switching modes
if (isSwitchingMode) {
return
}
const hasChanges =
JSON.stringify(apiConfiguration) !== JSON.stringify(originalState.current.apiConfiguration) ||
telemetrySetting !== originalState.current.telemetrySetting ||
planActSeparateModelsSetting !== originalState.current.planActSeparateModelsSetting ||
enableCheckpointsSetting !== originalState.current.enableCheckpointsSetting ||
mcpMarketplaceEnabled !== originalState.current.mcpMarketplaceEnabled ||
mcpRichDisplayEnabled !== originalState.current.mcpRichDisplayEnabled ||
JSON.stringify(chatSettings) !== JSON.stringify(originalState.current.chatSettings) ||
mcpResponsesCollapsed !== originalState.current.mcpResponsesCollapsed ||
shellIntegrationTimeout !== originalState.current.shellIntegrationTimeout ||
terminalOutputLineLimit !== originalState.current.terminalOutputLineLimit ||
terminalReuseEnabled !== originalState.current.terminalReuseEnabled ||
defaultTerminalProfile !== originalState.current.defaultTerminalProfile ||
JSON.stringify(localBrowserSettings) !== JSON.stringify(originalState.current.browserSettings)
setHasUnsavedChanges(hasChanges)
}, [
apiConfiguration,
telemetrySetting,
planActSeparateModelsSetting,
enableCheckpointsSetting,
mcpMarketplaceEnabled,
mcpRichDisplayEnabled,
mcpResponsesCollapsed,
chatSettings,
shellIntegrationTimeout,
terminalReuseEnabled,
terminalOutputLineLimit,
defaultTerminalProfile,
isSwitchingMode,
])
// Handle cancel button click
const handleCancel = useCallback(() => {
if (hasUnsavedChanges) {
// Show confirmation dialog
setIsUnsavedChangesDialogOpen(true)
pendingAction.current = () => {
// Reset all tracked state to original values
setTelemetrySetting(originalState.current.telemetrySetting)
setPlanActSeparateModelsSetting(originalState.current.planActSeparateModelsSetting)
setChatSettings(originalState.current.chatSettings)
if (typeof setApiConfiguration === "function") {
setApiConfiguration(originalState.current.apiConfiguration ?? {})
}
if (typeof setEnableCheckpointsSetting === "function") {
setEnableCheckpointsSetting(
typeof originalState.current.enableCheckpointsSetting === "boolean"
? originalState.current.enableCheckpointsSetting
: false,
)
}
if (typeof setMcpMarketplaceEnabled === "function") {
setMcpMarketplaceEnabled(
typeof originalState.current.mcpMarketplaceEnabled === "boolean"
? originalState.current.mcpMarketplaceEnabled
: false,
)
}
if (typeof setMcpRichDisplayEnabled === "function") {
setMcpRichDisplayEnabled(
typeof originalState.current.mcpRichDisplayEnabled === "boolean"
? originalState.current.mcpRichDisplayEnabled
: true,
)
}
// Reset terminal settings
if (typeof setShellIntegrationTimeout === "function") {
setShellIntegrationTimeout(originalState.current.shellIntegrationTimeout)
}
if (typeof setTerminalOutputLineLimit === "function") {
setTerminalOutputLineLimit(originalState.current.terminalOutputLineLimit)
}
if (typeof setTerminalReuseEnabled === "function") {
setTerminalReuseEnabled(originalState.current.terminalReuseEnabled ?? true)
}
if (typeof setDefaultTerminalProfile === "function") {
setDefaultTerminalProfile(originalState.current.defaultTerminalProfile ?? "default")
}
if (typeof setMcpResponsesCollapsed === "function") {
setMcpResponsesCollapsed(originalState.current.mcpResponsesCollapsed ?? false)
}
// Reset browser settings
setLocalBrowserSettings(originalState.current.browserSettings)
// Close settings view
onDone()
}
} else {
// No changes, just close
onDone()
}
}, [
hasUnsavedChanges,
onDone,
setTelemetrySetting,
setPlanActSeparateModelsSetting,
setChatSettings,
setApiConfiguration,
setEnableCheckpointsSetting,
setMcpMarketplaceEnabled,
setMcpRichDisplayEnabled,
setMcpResponsesCollapsed,
])
// Handle confirmation dialog actions
const handleConfirmDiscard = useCallback(async () => {
setIsUnsavedChangesDialogOpen(false)
// Check if this is for a mode switch
if (pendingModeSwitch) {
// Reset all state to original values (discard changes)
setTelemetrySetting(originalState.current.telemetrySetting)
setPlanActSeparateModelsSetting(originalState.current.planActSeparateModelsSetting)
setChatSettings(originalState.current.chatSettings)
if (typeof setApiConfiguration === "function") {
setApiConfiguration(originalState.current.apiConfiguration ?? {})
}
if (typeof setEnableCheckpointsSetting === "function") {
setEnableCheckpointsSetting(
typeof originalState.current.enableCheckpointsSetting === "boolean"
? originalState.current.enableCheckpointsSetting
: false,
)
}
if (typeof setMcpMarketplaceEnabled === "function") {
setMcpMarketplaceEnabled(
typeof originalState.current.mcpMarketplaceEnabled === "boolean"
? originalState.current.mcpMarketplaceEnabled
: false,
)
}
if (typeof setMcpRichDisplayEnabled === "function") {
setMcpRichDisplayEnabled(
typeof originalState.current.mcpRichDisplayEnabled === "boolean"
? originalState.current.mcpRichDisplayEnabled
: true,
)
}
// Reset terminal settings
if (typeof setShellIntegrationTimeout === "function") {
setShellIntegrationTimeout(originalState.current.shellIntegrationTimeout)
}
if (typeof setTerminalOutputLineLimit === "function") {
setTerminalOutputLineLimit(originalState.current.terminalOutputLineLimit)
}
if (typeof setTerminalReuseEnabled === "function") {
setTerminalReuseEnabled(originalState.current.terminalReuseEnabled ?? true)
}
if (typeof setDefaultTerminalProfile === "function") {
setDefaultTerminalProfile(originalState.current.defaultTerminalProfile ?? "default")
}
if (typeof setMcpResponsesCollapsed === "function") {
setMcpResponsesCollapsed(originalState.current.mcpResponsesCollapsed ?? false)
}
// Now perform the mode switch
const targetMode = pendingModeSwitch
setPendingModeSwitch(null)
setIsSwitchingMode(true)
try {
await StateServiceClient.togglePlanActMode(
TogglePlanActModeRequest.create({
chatSettings: {
mode: targetMode === "plan" ? PlanActMode.PLAN : PlanActMode.ACT,
preferredLanguage: chatSettings.preferredLanguage,
openAiReasoningEffort: chatSettings.openAIReasoningEffort,
},
}),
)
} catch (error) {
console.error("Failed to toggle Plan/Act mode:", error)
} finally {
setIsSwitchingMode(false)
}
} else if (pendingAction.current) {
// Regular cancel button flow
pendingAction.current()
pendingAction.current = undefined
}
}, [
pendingModeSwitch,
setTelemetrySetting,
setPlanActSeparateModelsSetting,
setChatSettings,
setApiConfiguration,
setEnableCheckpointsSetting,
setMcpMarketplaceEnabled,
setMcpRichDisplayEnabled,
setShellIntegrationTimeout,
setTerminalOutputLineLimit,
setTerminalReuseEnabled,
setDefaultTerminalProfile,
setMcpResponsesCollapsed,
chatSettings.preferredLanguage,
chatSettings.openAIReasoningEffort,
])
// Handle save and switch for mode changes
const handleSaveAndSwitch = useCallback(async () => {
setIsUnsavedChangesDialogOpen(false)
if (pendingModeSwitch) {
// Save the current settings first
await handleSubmit(true)
// Now perform the mode switch
const targetMode = pendingModeSwitch
setPendingModeSwitch(null)
setIsSwitchingMode(true)
try {
await StateServiceClient.togglePlanActMode(
TogglePlanActModeRequest.create({
chatSettings: {
mode: targetMode === "plan" ? PlanActMode.PLAN : PlanActMode.ACT,
preferredLanguage: chatSettings.preferredLanguage,
openAiReasoningEffort: chatSettings.openAIReasoningEffort,
},
}),
)
} catch (error) {
console.error("Failed to toggle Plan/Act mode:", error)
} finally {
setIsSwitchingMode(false)
}
}
}, [pendingModeSwitch, handleSubmit, chatSettings.preferredLanguage, chatSettings.openAIReasoningEffort])
const handleCancelDiscard = useCallback(() => {
setIsUnsavedChangesDialogOpen(false)
pendingAction.current = undefined
setPendingModeSwitch(null)
}, [])
// validate as soon as the component is mounted
/*
useEffect will use stale values of variables if they are not included in the dependency array.
so trying to use useEffect with a dependency array of only one value for example will use any
other variables' old values. In most cases you don't want this, and should opt to use react-use
hooks.
// uses someVar and anotherVar
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [someVar])
If we only want to run code once on mount we can use react-use's useEffectOnce or useMount
*/
const { version, chatSettings } = useExtensionState()
const handleMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
@@ -596,16 +164,7 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
return
}
// Check if there are unsaved changes
if (hasUnsavedChanges) {
// Store the pending mode switch
setPendingModeSwitch(tab)
// Show the unsaved changes dialog
setIsUnsavedChangesDialogOpen(true)
return
}
// No unsaved changes, proceed with the switch
// All settings save immediately, so we can switch modes directly
setIsSwitchingMode(true)
try {
@@ -627,9 +186,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
}
}
// Track active tab
const [activeTab, setActiveTab] = useState<string>(targetSection || SETTINGS_TABS[0].id)
// Update active tab when targetSection changes
useEffect(() => {
if (targetSection) {
@@ -680,12 +236,8 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
<h3 className="text-[var(--vscode-foreground)] m-0">Settings</h3>
</div>
<div className="flex gap-2">
<VSCodeButton appearance="secondary" onClick={handleCancel}>
Cancel
</VSCodeButton>
<VSCodeButton onClick={() => handleSubmit(false)} disabled={!hasUnsavedChanges}>
Save
</VSCodeButton>
{/* All settings now save immediately, so only show Done button */}
<VSCodeButton onClick={onDone}>Done</VSCodeButton>
</div>
</TabHeader>
@@ -763,39 +315,20 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
{/* API Configuration Tab */}
{activeTab === "api-config" && (
<ApiConfigurationSection
planActSeparateModelsSetting={planActSeparateModelsSetting}
chatSettings={chatSettings}
isSwitchingMode={isSwitchingMode}
apiErrorMessage={apiErrorMessage}
modelIdErrorMessage={modelIdErrorMessage}
handlePlanActModeChange={handlePlanActModeChange}
setPlanActSeparateModelsSetting={setPlanActSeparateModelsSetting}
renderSectionHeader={renderSectionHeader}
/>
)}
{/* General Settings Tab */}
{activeTab === "general" && (
<GeneralSettingsSection
chatSettings={chatSettings}
setChatSettings={setChatSettings}
telemetrySetting={telemetrySetting}
setTelemetrySetting={setTelemetrySetting}
renderSectionHeader={renderSectionHeader}
/>
)}
{activeTab === "general" && <GeneralSettingsSection renderSectionHeader={renderSectionHeader} />}
{/* Feature Settings Tab */}
{activeTab === "features" && <FeatureSettingsSection renderSectionHeader={renderSectionHeader} />}
{/* Browser Settings Tab */}
{activeTab === "browser" && (
<BrowserSettingsSection
localBrowserSettings={localBrowserSettings}
onBrowserSettingsChange={setLocalBrowserSettings}
renderSectionHeader={renderSectionHeader}
/>
)}
{activeTab === "browser" && <BrowserSettingsSection renderSectionHeader={renderSectionHeader} />}
{/* Terminal Settings Tab */}
{activeTab === "terminal" && <TerminalSettingsSection renderSectionHeader={renderSectionHeader} />}
@@ -813,26 +346,8 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
)
})()}
</div>
{/* Unsaved Changes Dialog */}
<UnsavedChangesDialog
open={isUnsavedChangesDialogOpen}
onOpenChange={setIsUnsavedChangesDialogOpen}
onConfirm={handleConfirmDiscard}
onCancel={handleCancelDiscard}
onSave={pendingModeSwitch ? handleSaveAndSwitch : undefined}
title={pendingModeSwitch ? "Save Changes?" : "Unsaved Changes"}
description={
pendingModeSwitch
? `Do you want to save your changes to ${chatSettings.mode === "plan" ? "Plan" : "Act"} mode before switching to ${pendingModeSwitch === "plan" ? "Plan" : "Act"} mode?`
: "You have unsaved changes. Are you sure you want to discard them?"
}
confirmText={pendingModeSwitch ? "Switch Without Saving" : "Discard Changes"}
saveText="Save & Switch"
showSaveOption={!!pendingModeSwitch}
/>
</Tab>
)
}
export default memo(SettingsView)
export default SettingsView
@@ -1,12 +1,13 @@
import React from "react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { updateSetting } from "./utils/settingsHandlers"
const TerminalOutputLineLimitSlider: React.FC = () => {
const { terminalOutputLineLimit, setTerminalOutputLineLimit } = useExtensionState()
const { terminalOutputLineLimit } = useExtensionState()
const handleSliderChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(event.target.value, 10)
setTerminalOutputLineLimit(value)
updateSetting("terminalOutputLineLimit", value)
}
return (
@@ -1,7 +1,9 @@
import { memo, useCallback, useState } from "react"
import { memo, useCallback, useEffect, useMemo, useState } from "react"
import { anthropicModels, ApiConfiguration, geminiDefaultModelId, geminiModels, ModelInfo } from "@shared/api"
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import styled from "styled-components"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "./utils/useApiConfigurationHandlers"
// Constants
const DEFAULT_MIN_VALID_TOKENS = 1024
@@ -80,26 +82,30 @@ const RangeInput = styled.input<{ $value: number; $min: number; $max: number }>`
`
interface ThinkingBudgetSliderProps {
apiConfiguration: ApiConfiguration | undefined
setApiConfiguration: (apiConfiguration: ApiConfiguration) => void
maxBudget?: number
}
const ThinkingBudgetSlider = ({ apiConfiguration, setApiConfiguration, maxBudget }: ThinkingBudgetSliderProps) => {
const maxTokens =
apiConfiguration?.apiProvider === "gemini"
? geminiModels[geminiDefaultModelId].maxTokens
: anthropicModels["claude-3-7-sonnet-20250219"].maxTokens
const ThinkingBudgetSlider = ({ maxBudget }: ThinkingBudgetSliderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const [isEnabled, setIsEnabled] = useState<boolean>((apiConfiguration?.thinkingBudgetTokens || 0) > 0)
const maxTokens = useMemo(
() =>
apiConfiguration?.apiProvider === "gemini"
? geminiModels[geminiDefaultModelId].maxTokens
: anthropicModels["claude-3-7-sonnet-20250219"].maxTokens,
[apiConfiguration?.apiProvider],
)
// use maxBudget prop if provided, otherwise apply the percentage cap to maxTokens
const maxSliderValue = (() => {
const maxSliderValue = useMemo(() => {
if (maxBudget !== undefined) {
return maxBudget
}
return Math.floor(maxTokens * MAX_PERCENTAGE)
})()
const isEnabled = (apiConfiguration?.thinkingBudgetTokens || 0) > 0
}, [maxBudget, maxTokens])
// Add local state for the slider value
const [localValue, setLocalValue] = useState(apiConfiguration?.thinkingBudgetTokens || 0)
@@ -110,20 +116,16 @@ const ThinkingBudgetSlider = ({ apiConfiguration, setApiConfiguration, maxBudget
}, [])
const handleSliderComplete = () => {
setApiConfiguration({
...apiConfiguration,
thinkingBudgetTokens: localValue,
})
handleFieldChange("thinkingBudgetTokens", localValue)
}
const handleToggleChange = (event: any) => {
const isChecked = (event.target as HTMLInputElement).checked
const newValue = isChecked ? DEFAULT_MIN_VALID_TOKENS : 0
setIsEnabled(isChecked)
setLocalValue(newValue)
setApiConfiguration({
...apiConfiguration,
thinkingBudgetTokens: newValue,
})
handleFieldChange("thinkingBudgetTokens", newValue)
}
return (
@@ -1,11 +1,12 @@
import { useDebouncedInput } from "../utils/useDebouncedInput"
import { VSCodeTextField, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
/**
* Props for the ApiKeyField component
*/
interface ApiKeyFieldProps {
value: string
onChange: (e: any) => void
initialValue: string
onChange: (value: string) => void
providerName: string
signupUrl?: string
placeholder?: string
@@ -16,34 +17,43 @@ interface ApiKeyFieldProps {
* A reusable component for API key input fields with standard styling and help text for signing up for key
*/
export const ApiKeyField = ({
value,
initialValue,
onChange,
providerName,
signupUrl,
placeholder = "Enter API Key...",
helpText,
}: ApiKeyFieldProps) => (
<div>
<VSCodeTextField value={value} style={{ width: "100%" }} type="password" onInput={onChange} placeholder={placeholder}>
<span style={{ fontWeight: 500 }}>{providerName} API Key</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
{helpText || "This key is stored locally and only used to make API requests from this extension."}
{!value && signupUrl && (
<VSCodeLink
href={signupUrl}
style={{
display: "inline",
fontSize: "inherit",
}}>
You can get a{/^[aeiou]/i.test(providerName) ? "n" : ""} {providerName} API key by signing up here.
</VSCodeLink>
)}
</p>
</div>
)
}: ApiKeyFieldProps) => {
const [localValue, setLocalValue] = useDebouncedInput(initialValue, onChange)
return (
<div>
<VSCodeTextField
value={localValue}
style={{ width: "100%" }}
type="password"
onInput={(e: any) => setLocalValue(e.target.value)}
placeholder={placeholder}>
<span style={{ fontWeight: 500 }}>{providerName} API Key</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
{helpText || "This key is stored locally and only used to make API requests from this extension."}
{!localValue && signupUrl && (
<VSCodeLink
href={signupUrl}
style={{
display: "inline",
fontSize: "inherit",
}}>
You can get a{/^[aeiou]/i.test(providerName) ? "n" : ""} {providerName} API key by signing up here.
</VSCodeLink>
)}
</p>
</div>
)
}
@@ -1,11 +1,12 @@
import { useState, useEffect } from "react"
import { VSCodeCheckbox, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { useDebouncedInput } from "../utils/useDebouncedInput"
/**
* Props for the BaseUrlField component
*/
interface BaseUrlFieldProps {
value: string | undefined
initialValue: string | undefined
onChange: (value: string) => void
defaultValue?: string
label?: string
@@ -16,24 +17,19 @@ interface BaseUrlFieldProps {
* A reusable component for toggling and entering custom base URLs
*/
export const BaseUrlField = ({
value,
initialValue,
onChange,
defaultValue = "",
label = "Use custom base URL",
placeholder = "Default: https://api.example.com",
}: BaseUrlFieldProps) => {
const [isEnabled, setIsEnabled] = useState(!!value)
// When value changes externally, update isEnabled state
useEffect(() => {
setIsEnabled(!!value)
}, [value])
const [isEnabled, setIsEnabled] = useState(!!initialValue)
const [localValue, setLocalValue] = useDebouncedInput(initialValue || "", onChange)
const handleToggle = (e: any) => {
const checked = e.target.checked === true
setIsEnabled(checked)
if (!checked) {
onChange("")
setLocalValue("")
}
}
@@ -45,10 +41,10 @@ export const BaseUrlField = ({
{isEnabled && (
<VSCodeTextField
value={value || ""}
value={localValue}
style={{ width: "100%", marginTop: 3 }}
type="url"
onInput={(e: any) => onChange(e.target.value)}
onInput={(e: any) => setLocalValue(e.target.value)}
placeholder={placeholder}
/>
)}
@@ -0,0 +1,33 @@
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { useDebouncedInput } from "../utils/useDebouncedInput"
/**
* Props for the DebouncedTextField component
*/
interface DebouncedTextFieldProps {
// Custom props for debouncing functionality
initialValue: string
onChange: (value: string) => void
// Common VSCodeTextField props
style?: React.CSSProperties
type?: "text" | "password" | "url"
placeholder?: string
id?: string
children?: React.ReactNode
disabled?: boolean
}
/**
* A wrapper around VSCodeTextField that automatically handles debounced input
* to prevent excessive API calls while typing
*/
export const DebouncedTextField = ({ initialValue, onChange, children, ...otherProps }: DebouncedTextFieldProps) => {
const [localValue, setLocalValue] = useDebouncedInput(initialValue, onChange)
return (
<VSCodeTextField {...otherProps} value={localValue} onInput={(e: any) => setLocalValue(e.target.value)}>
{children}
</VSCodeTextField>
)
}
@@ -1,55 +1,50 @@
import { ApiConfiguration, anthropicModels } from "@shared/api"
import { anthropicModels } from "@shared/api"
import { ApiKeyField } from "../common/ApiKeyField"
import { BaseUrlField } from "../common/BaseUrlField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import ThinkingBudgetSlider from "../ThinkingBudgetSlider"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
// Anthropic models that support thinking/reasoning mode
const SUPPORTED_THINKING_MODELS = ["claude-3-7-sonnet-20250219", "claude-sonnet-4-20250514", "claude-opus-4-20250514"]
export const SUPPORTED_ANTHROPIC_THINKING_MODELS = [
"claude-3-7-sonnet-20250219",
"claude-sonnet-4-20250514",
"claude-opus-4-20250514",
]
/**
* Props for the AnthropicProvider component
*/
interface AnthropicProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
setApiConfiguration?: (config: ApiConfiguration) => void
}
/**
* The Anthropic provider configuration component
*/
export const AnthropicProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
setApiConfiguration,
}: AnthropicProviderProps) => {
export const AnthropicProvider = ({ showModelOptions, isPopup }: AnthropicProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
// Create a wrapper for handling field changes more directly
const handleFieldChange = (field: keyof ApiConfiguration) => (value: string) => {
handleInputChange(field)({ target: { value } })
}
return (
<div>
<ApiKeyField
value={apiConfiguration?.apiKey || ""}
onChange={handleInputChange("apiKey")}
initialValue={apiConfiguration?.apiKey || ""}
onChange={(value) => handleFieldChange("apiKey", value)}
providerName="Anthropic"
signupUrl="https://console.anthropic.com/settings/keys"
/>
<BaseUrlField
value={apiConfiguration?.anthropicBaseUrl}
onChange={handleFieldChange("anthropicBaseUrl")}
initialValue={apiConfiguration?.anthropicBaseUrl}
onChange={(value) => handleFieldChange("anthropicBaseUrl", value)}
placeholder="Default: https://api.anthropic.com"
label="Use custom base URL"
/>
@@ -59,16 +54,12 @@ export const AnthropicProvider = ({
<ModelSelector
models={anthropicModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
onChange={(e) => handleFieldChange("apiModelId", e.target.value)}
label="Model"
/>
{SUPPORTED_THINKING_MODELS.includes(selectedModelId) && setApiConfiguration && (
<ThinkingBudgetSlider
apiConfiguration={apiConfiguration}
setApiConfiguration={setApiConfiguration}
maxBudget={selectedModelInfo.thinkingConfig?.maxBudget}
/>
{SUPPORTED_ANTHROPIC_THINKING_MODELS.includes(selectedModelId) && (
<ThinkingBudgetSlider maxBudget={selectedModelInfo.thinkingConfig?.maxBudget} />
)}
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
@@ -1,16 +1,16 @@
import { ApiConfiguration, askSageModels, askSageDefaultURL } from "@shared/api"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { ApiKeyField } from "../common/ApiKeyField"
import { DebouncedTextField } from "../common/DebouncedTextField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
/**
* Props for the AskSageProvider component
*/
interface AskSageProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -18,34 +18,37 @@ interface AskSageProviderProps {
/**
* The AskSage provider configuration component
*/
export const AskSageProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: AskSageProviderProps) => {
export const AskSageProvider = ({ showModelOptions, isPopup }: AskSageProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
return (
<div>
<ApiKeyField
value={apiConfiguration?.asksageApiKey || ""}
onChange={handleInputChange("asksageApiKey")}
initialValue={apiConfiguration?.asksageApiKey || ""}
onChange={(value) => handleFieldChange("asksageApiKey", value)}
providerName="AskSage"
helpText="This key is stored locally and only used to make API requests from this extension."
/>
<VSCodeTextField
value={apiConfiguration?.asksageApiUrl || askSageDefaultURL}
<DebouncedTextField
initialValue={apiConfiguration?.asksageApiUrl || askSageDefaultURL}
onChange={(value) => handleFieldChange("asksageApiUrl", value)}
style={{ width: "100%" }}
type="url"
onInput={handleInputChange("asksageApiUrl")}
placeholder="Enter AskSage API URL...">
<span style={{ fontWeight: 500 }}>AskSage API URL</span>
</VSCodeTextField>
</DebouncedTextField>
{showModelOptions && (
<>
<ModelSelector
models={askSageModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
onChange={(e) => handleFieldChange("apiModelId", e.target.value)}
label="Model"
/>
@@ -1,36 +1,26 @@
import { ApiConfiguration, bedrockDefaultModelId, bedrockModels } from "@shared/api"
import {
VSCodeCheckbox,
VSCodeDropdown,
VSCodeOption,
VSCodeRadio,
VSCodeRadioGroup,
VSCodeTextField,
} from "@vscode/webview-ui-toolkit/react"
import { bedrockDefaultModelId, bedrockModels } from "@shared/api"
import { VSCodeCheckbox, VSCodeDropdown, VSCodeOption, VSCodeRadio, VSCodeRadioGroup } from "@vscode/webview-ui-toolkit/react"
import { useState } from "react"
import { DebouncedTextField } from "../common/DebouncedTextField"
import { ModelInfoView } from "../common/ModelInfoView"
import { DropdownContainer } from "../common/ModelSelector"
import ThinkingBudgetSlider from "../ThinkingBudgetSlider"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
// Z-index constants for proper dropdown layering
const DROPDOWN_Z_INDEX = 1000
interface BedrockProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
setApiConfiguration: (config: ApiConfiguration) => void
}
export const BedrockProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
setApiConfiguration,
}: BedrockProviderProps) => {
export const BedrockProvider = ({ showModelOptions, isPopup }: BedrockProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange, handleFieldsChange } = useApiConfigurationHandlers()
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
const [awsEndpointSelected, setAwsEndpointSelected] = useState(!!apiConfiguration?.awsBedrockEndpoint)
@@ -46,49 +36,47 @@ export const BedrockProvider = ({
onChange={(e) => {
const value = (e.target as HTMLInputElement)?.value
const useProfile = value === "profile"
setApiConfiguration({
...apiConfiguration,
awsUseProfile: useProfile,
})
handleFieldChange("awsUseProfile", useProfile)
}}>
<VSCodeRadio value="credentials">AWS Credentials</VSCodeRadio>
<VSCodeRadio value="profile">AWS Profile</VSCodeRadio>
</VSCodeRadioGroup>
{apiConfiguration?.awsUseProfile ? (
<VSCodeTextField
value={apiConfiguration?.awsProfile || ""}
<DebouncedTextField
initialValue={apiConfiguration?.awsProfile || ""}
onChange={(value) => handleFieldChange("awsProfile", value)}
style={{ width: "100%" }}
onInput={handleInputChange("awsProfile")}
placeholder="Enter profile name (default if empty)">
<span style={{ fontWeight: 500 }}>AWS Profile Name</span>
</VSCodeTextField>
</DebouncedTextField>
) : (
<>
<VSCodeTextField
value={apiConfiguration?.awsAccessKey || ""}
<DebouncedTextField
initialValue={apiConfiguration?.awsAccessKey || ""}
onChange={(value) => handleFieldChange("awsAccessKey", value)}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("awsAccessKey")}
placeholder="Enter Access Key...">
<span style={{ fontWeight: 500 }}>AWS Access Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.awsSecretKey || ""}
</DebouncedTextField>
<DebouncedTextField
initialValue={apiConfiguration?.awsSecretKey || ""}
onChange={(value) => handleFieldChange("awsSecretKey", value)}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("awsSecretKey")}
placeholder="Enter Secret Key...">
<span style={{ fontWeight: 500 }}>AWS Secret Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.awsSessionToken || ""}
</DebouncedTextField>
<DebouncedTextField
initialValue={apiConfiguration?.awsSessionToken || ""}
onChange={(value) => handleFieldChange("awsSessionToken", value)}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("awsSessionToken")}
placeholder="Enter Session Token...">
<span style={{ fontWeight: 500 }}>AWS Session Token</span>
</VSCodeTextField>
</DebouncedTextField>
</>
)}
@@ -100,7 +88,7 @@ export const BedrockProvider = ({
id="aws-region-dropdown"
value={apiConfiguration?.awsRegion || ""}
style={{ width: "100%" }}
onChange={handleInputChange("awsRegion")}>
onChange={(e: any) => handleFieldChange("awsRegion", e.target.value)}>
<VSCodeOption value="">Select a region...</VSCodeOption>
{/* The user will have to choose a region that supports the model they use, but this shouldn't be a problem since they'd have to request access for it in that region in the first place. */}
<VSCodeOption value="us-east-1">us-east-1</VSCodeOption>
@@ -139,21 +127,18 @@ export const BedrockProvider = ({
const isChecked = e.target.checked === true
setAwsEndpointSelected(isChecked)
if (!isChecked) {
setApiConfiguration({
...apiConfiguration,
awsBedrockEndpoint: "",
})
handleFieldChange("awsBedrockEndpoint", "")
}
}}>
Use custom VPC endpoint
</VSCodeCheckbox>
{awsEndpointSelected && (
<VSCodeTextField
value={apiConfiguration?.awsBedrockEndpoint || ""}
<DebouncedTextField
initialValue={apiConfiguration?.awsBedrockEndpoint || ""}
onChange={(value) => handleFieldChange("awsBedrockEndpoint", value)}
style={{ width: "100%", marginTop: 3, marginBottom: 5 }}
type="url"
onInput={handleInputChange("awsBedrockEndpoint")}
placeholder="Enter VPC Endpoint URL (optional)"
/>
)}
@@ -162,10 +147,8 @@ export const BedrockProvider = ({
checked={apiConfiguration?.awsUseCrossRegionInference || false}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setApiConfiguration({
...apiConfiguration,
awsUseCrossRegionInference: isChecked,
})
handleFieldChange("awsUseCrossRegionInference", isChecked)
}}>
Use cross-region inference
</VSCodeCheckbox>
@@ -176,10 +159,7 @@ export const BedrockProvider = ({
checked={apiConfiguration?.awsBedrockUsePromptCache || false}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setApiConfiguration({
...apiConfiguration,
awsBedrockUsePromptCache: isChecked,
})
handleFieldChange("awsBedrockUsePromptCache", isChecked)
}}>
Use prompt caching
</VSCodeCheckbox>
@@ -218,8 +198,8 @@ export const BedrockProvider = ({
value={apiConfiguration?.awsBedrockCustomSelected ? "custom" : selectedModelId}
onChange={(e: any) => {
const isCustom = e.target.value === "custom"
setApiConfiguration({
...apiConfiguration,
handleFieldsChange({
apiModelId: isCustom ? "" : e.target.value,
awsBedrockCustomSelected: isCustom,
awsBedrockCustomModelBaseId: bedrockDefaultModelId,
@@ -254,16 +234,14 @@ export const BedrockProvider = ({
Select "Custom" when using the Application Inference Profile in Bedrock. Enter the Application
Inference Profile ARN in the Model ID field.
</p>
<label htmlFor="bedrock-model-input">
<span style={{ fontWeight: 500 }}>Model ID</span>
</label>
<VSCodeTextField
<DebouncedTextField
id="bedrock-model-input"
value={apiConfiguration?.apiModelId || ""}
initialValue={apiConfiguration?.apiModelId || ""}
onChange={(value) => handleFieldChange("apiModelId", value)}
style={{ width: "100%", marginTop: 3 }}
onInput={handleInputChange("apiModelId")}
placeholder="Enter custom model ID..."
/>
placeholder="Enter custom model ID...">
<span style={{ fontWeight: 500 }}>Model ID</span>
</DebouncedTextField>
<label htmlFor="bedrock-base-model-dropdown">
<span style={{ fontWeight: 500 }}>Base Inference Model</span>
</label>
@@ -271,7 +249,7 @@ export const BedrockProvider = ({
<VSCodeDropdown
id="bedrock-base-model-dropdown"
value={apiConfiguration?.awsBedrockCustomModelBaseId || bedrockDefaultModelId}
onChange={handleInputChange("awsBedrockCustomModelBaseId")}
onChange={(e: any) => handleFieldChange("awsBedrockCustomModelBaseId", e.target.value)}
style={{ width: "100%" }}>
<VSCodeOption value="">Select a model...</VSCodeOption>
{Object.keys(bedrockModels).map((modelId) => (
@@ -300,7 +278,7 @@ export const BedrockProvider = ({
apiConfiguration?.awsBedrockCustomModelBaseId === "anthropic.claude-sonnet-4-20250514-v1:0") ||
(apiConfiguration?.awsBedrockCustomSelected &&
apiConfiguration?.awsBedrockCustomModelBaseId === "anthropic.claude-opus-4-20250514-v1:0")) && (
<ThinkingBudgetSlider apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
<ThinkingBudgetSlider />
)}
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
@@ -1,15 +1,15 @@
import { ApiConfiguration, cerebrasModels } from "@shared/api"
import { cerebrasModels } from "@shared/api"
import { ApiKeyField } from "../common/ApiKeyField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { useExtensionState } from "@/context/ExtensionStateContext"
/**
* Props for the CerebrasProvider component
*/
interface CerebrasProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -17,15 +17,18 @@ interface CerebrasProviderProps {
/**
* The Cerebras provider configuration component
*/
export const CerebrasProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: CerebrasProviderProps) => {
export const CerebrasProvider = ({ showModelOptions, isPopup }: CerebrasProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
return (
<div>
<ApiKeyField
value={apiConfiguration?.cerebrasApiKey || ""}
onChange={handleInputChange("cerebrasApiKey")}
initialValue={apiConfiguration?.cerebrasApiKey || ""}
onChange={(value) => handleFieldChange("cerebrasApiKey", value)}
providerName="Cerebras"
signupUrl="https://cloud.cerebras.ai/"
/>
@@ -35,7 +38,7 @@ export const CerebrasProvider = ({ apiConfiguration, handleInputChange, showMode
<ModelSelector
models={cerebrasModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
label="Model"
/>
@@ -1,15 +1,17 @@
import { ApiConfiguration, claudeCodeModels } from "@shared/api"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { claudeCodeModels } from "@shared/api"
import { DebouncedTextField } from "../common/DebouncedTextField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { SUPPORTED_ANTHROPIC_THINKING_MODELS } from "./AnthropicProvider"
import ThinkingBudgetSlider from "../ThinkingBudgetSlider"
/**
* Props for the ClaudeCodeProvider component
*/
interface ClaudeCodeProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -17,25 +19,23 @@ interface ClaudeCodeProviderProps {
/**
* The Claude Code provider configuration component
*/
export const ClaudeCodeProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
}: ClaudeCodeProviderProps) => {
export const ClaudeCodeProvider = ({ showModelOptions, isPopup }: ClaudeCodeProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
return (
<div>
<VSCodeTextField
value={apiConfiguration?.claudeCodePath || ""}
<DebouncedTextField
initialValue={apiConfiguration?.claudeCodePath || ""}
onChange={(value) => handleFieldChange("claudeCodePath", value)}
style={{ width: "100%", marginTop: 3 }}
type="text"
onInput={handleInputChange("claudeCodePath")}
placeholder="Default: claude">
<span style={{ fontWeight: 500 }}>Claude Code CLI Path</span>
</VSCodeTextField>
</DebouncedTextField>
<p
style={{
@@ -51,10 +51,14 @@ export const ClaudeCodeProvider = ({
<ModelSelector
models={claudeCodeModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
label="Model"
/>
{SUPPORTED_ANTHROPIC_THINKING_MODELS.includes(selectedModelId) && (
<ThinkingBudgetSlider maxBudget={selectedModelInfo.thinkingConfig?.maxBudget} />
)}
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
</>
)}
@@ -4,13 +4,13 @@ import { useState } from "react"
import { ClineAccountInfoCard } from "../ClineAccountInfoCard"
import OpenRouterModelPicker, { OPENROUTER_MODEL_PICKER_Z_INDEX } from "../OpenRouterModelPicker"
import { DropdownContainer } from "../common/ModelSelector"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { useExtensionState } from "@/context/ExtensionStateContext"
/**
* Props for the ClineProvider component
*/
interface ClineProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -18,13 +18,11 @@ interface ClineProviderProps {
/**
* The Cline provider configuration component
*/
export const ClineProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: ClineProviderProps) => {
const [providerSortingSelected, setProviderSortingSelected] = useState(!!apiConfiguration?.openRouterProviderSorting)
export const ClineProvider = ({ showModelOptions, isPopup }: ClineProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Create a wrapper for handling field changes more directly
const handleFieldChange = (field: keyof ApiConfiguration) => (value: any) => {
handleInputChange(field)({ target: { value } })
}
const [providerSortingSelected, setProviderSortingSelected] = useState(!!apiConfiguration?.openRouterProviderSorting)
return (
<div>
@@ -43,7 +41,7 @@ export const ClineProvider = ({ apiConfiguration, handleInputChange, showModelOp
const isChecked = e.target.checked === true
setProviderSortingSelected(isChecked)
if (!isChecked) {
handleFieldChange("openRouterProviderSorting")("")
handleFieldChange("openRouterProviderSorting", "")
}
}}>
Sort underlying provider routing
@@ -56,7 +54,7 @@ export const ClineProvider = ({ apiConfiguration, handleInputChange, showModelOp
style={{ width: "100%", marginTop: 3 }}
value={apiConfiguration?.openRouterProviderSorting}
onChange={(e: any) => {
handleFieldChange("openRouterProviderSorting")(e.target.value)
handleFieldChange("openRouterProviderSorting", e.target.value)
}}>
<VSCodeOption value="">Default</VSCodeOption>
<VSCodeOption value="price">Price</VSCodeOption>
@@ -3,13 +3,13 @@ import { ApiKeyField } from "../common/ApiKeyField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { useExtensionState } from "@/context/ExtensionStateContext"
/**
* Props for the DeepSeekProvider component
*/
interface DeepSeekProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -17,15 +17,18 @@ interface DeepSeekProviderProps {
/**
* The DeepSeek provider configuration component
*/
export const DeepSeekProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: DeepSeekProviderProps) => {
export const DeepSeekProvider = ({ showModelOptions, isPopup }: DeepSeekProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
return (
<div>
<ApiKeyField
value={apiConfiguration?.deepSeekApiKey || ""}
onChange={handleInputChange("deepSeekApiKey")}
initialValue={apiConfiguration?.deepSeekApiKey || ""}
onChange={(value) => handleFieldChange("deepSeekApiKey", value)}
providerName="DeepSeek"
signupUrl="https://www.deepseek.com/"
/>
@@ -35,7 +38,7 @@ export const DeepSeekProvider = ({ apiConfiguration, handleInputChange, showMode
<ModelSelector
models={deepSeekModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
label="Model"
/>
@@ -3,13 +3,13 @@ import { ApiKeyField } from "../common/ApiKeyField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
/**
* Props for the DoubaoProvider component
*/
interface DoubaoProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -17,15 +17,18 @@ interface DoubaoProviderProps {
/**
* The ByteDance Doubao provider configuration component
*/
export const DoubaoProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: DoubaoProviderProps) => {
export const DoubaoProvider = ({ showModelOptions, isPopup }: DoubaoProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
return (
<div>
<ApiKeyField
value={apiConfiguration?.doubaoApiKey || ""}
onChange={handleInputChange("doubaoApiKey")}
initialValue={apiConfiguration?.doubaoApiKey || ""}
onChange={(value) => handleFieldChange("doubaoApiKey", value)}
providerName="Doubao"
signupUrl="https://console.volcengine.com/home"
/>
@@ -35,7 +38,7 @@ export const DoubaoProvider = ({ apiConfiguration, handleInputChange, showModelO
<ModelSelector
models={doubaoModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
label="Model"
/>
@@ -1,13 +1,13 @@
import { ApiConfiguration } from "@shared/api"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { ApiKeyField } from "../common/ApiKeyField"
import { DebouncedTextField } from "../common/DebouncedTextField"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { ApiConfiguration } from "@shared/api"
/**
* Props for the FireworksProvider component
*/
interface FireworksProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -15,10 +15,12 @@ interface FireworksProviderProps {
/**
* The Fireworks provider configuration component
*/
export const FireworksProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: FireworksProviderProps) => {
export const FireworksProvider = ({ showModelOptions, isPopup }: FireworksProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Handler for number input fields with validation
const handleNumberInputChange = (field: keyof ApiConfiguration) => (e: any) => {
const value = (e.target as HTMLInputElement).value
const handleNumberInputChange = (field: keyof ApiConfiguration, value: string) => {
if (!value) {
return
}
@@ -26,31 +28,27 @@ export const FireworksProvider = ({ apiConfiguration, handleInputChange, showMod
if (isNaN(num)) {
return
}
handleInputChange(field)({
target: {
value: num,
},
})
handleFieldChange(field, num)
}
return (
<div>
<ApiKeyField
value={apiConfiguration?.fireworksApiKey || ""}
onChange={handleInputChange("fireworksApiKey")}
initialValue={apiConfiguration?.fireworksApiKey || ""}
onChange={(value) => handleFieldChange("fireworksApiKey", value)}
providerName="Fireworks"
signupUrl="https://fireworks.ai/settings/users/api-keys"
/>
{showModelOptions && (
<>
<VSCodeTextField
value={apiConfiguration?.fireworksModelId || ""}
<DebouncedTextField
initialValue={apiConfiguration?.fireworksModelId || ""}
onChange={(value) => handleFieldChange("fireworksModelId", value)}
style={{ width: "100%" }}
onInput={handleInputChange("fireworksModelId")}
placeholder={"Enter Model ID..."}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
</DebouncedTextField>
<p
style={{
fontSize: "12px",
@@ -62,20 +60,20 @@ export const FireworksProvider = ({ apiConfiguration, handleInputChange, showMod
models. Less capable models may not work as expected.)
</span>
</p>
<VSCodeTextField
value={apiConfiguration?.fireworksModelMaxCompletionTokens?.toString() || ""}
<DebouncedTextField
initialValue={apiConfiguration?.fireworksModelMaxCompletionTokens?.toString() || ""}
onChange={(value) => handleNumberInputChange("fireworksModelMaxCompletionTokens", value)}
style={{ width: "100%", marginBottom: 8 }}
onInput={handleNumberInputChange("fireworksModelMaxCompletionTokens")}
placeholder={"2000"}>
<span style={{ fontWeight: 500 }}>Max Completion Tokens</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.fireworksModelMaxTokens?.toString() || ""}
</DebouncedTextField>
<DebouncedTextField
initialValue={apiConfiguration?.fireworksModelMaxTokens?.toString() || ""}
onChange={(value) => handleNumberInputChange("fireworksModelMaxTokens", value)}
style={{ width: "100%", marginBottom: 8 }}
onInput={handleNumberInputChange("fireworksModelMaxTokens")}
placeholder={"4000"}>
<span style={{ fontWeight: 500 }}>Max Context Tokens</span>
</VSCodeTextField>
</DebouncedTextField>
</>
)}
</div>
@@ -1,10 +1,12 @@
import { ApiConfiguration, geminiModels } from "@shared/api"
import { geminiModels } from "@shared/api"
import { ApiKeyField } from "../common/ApiKeyField"
import { BaseUrlField } from "../common/BaseUrlField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import ThinkingBudgetSlider from "../ThinkingBudgetSlider"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { useExtensionState } from "@/context/ExtensionStateContext"
// Gemini models that support thinking/reasoning mode
const SUPPORTED_THINKING_MODELS = ["gemini-2.5-pro", "gemini-2.5-flash"]
@@ -13,43 +15,32 @@ const SUPPORTED_THINKING_MODELS = ["gemini-2.5-pro", "gemini-2.5-flash"]
* Props for the GeminiProvider component
*/
interface GeminiProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
setApiConfiguration?: (config: ApiConfiguration) => void
}
/**
* The Gemini provider configuration component
*/
export const GeminiProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
setApiConfiguration,
}: GeminiProviderProps) => {
export const GeminiProvider = ({ showModelOptions, isPopup }: GeminiProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
// Create a wrapper for handling field changes more directly
const handleFieldChange = (field: keyof ApiConfiguration) => (value: string) => {
handleInputChange(field)({ target: { value } })
}
return (
<div>
<ApiKeyField
value={apiConfiguration?.geminiApiKey || ""}
onChange={handleInputChange("geminiApiKey")}
initialValue={apiConfiguration?.geminiApiKey || ""}
onChange={(value) => handleFieldChange("geminiApiKey", value)}
providerName="Gemini"
signupUrl="https://aistudio.google.com/apikey"
/>
<BaseUrlField
value={apiConfiguration?.geminiBaseUrl}
onChange={handleFieldChange("geminiBaseUrl")}
initialValue={apiConfiguration?.geminiBaseUrl}
onChange={(value) => handleFieldChange("geminiBaseUrl", value)}
placeholder="Default: https://generativelanguage.googleapis.com"
label="Use custom base URL"
/>
@@ -59,16 +50,12 @@ export const GeminiProvider = ({
<ModelSelector
models={geminiModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
label="Model"
/>
{SUPPORTED_THINKING_MODELS.includes(selectedModelId) && setApiConfiguration && (
<ThinkingBudgetSlider
apiConfiguration={apiConfiguration}
setApiConfiguration={setApiConfiguration}
maxBudget={selectedModelInfo.thinkingConfig?.maxBudget}
/>
{SUPPORTED_THINKING_MODELS.includes(selectedModelId) && (
<ThinkingBudgetSlider maxBudget={selectedModelInfo.thinkingConfig?.maxBudget} />
)}
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
@@ -1,17 +1,17 @@
import { ApiConfiguration } from "@shared/api"
import { VSCodeTextField, VSCodeRadioGroup, VSCodeRadio, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { VSCodeRadioGroup, VSCodeRadio, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { useState, useCallback, useEffect } from "react"
import { useInterval } from "react-use"
import { DebouncedTextField } from "../common/DebouncedTextField"
import { ModelsServiceClient } from "@/services/grpc-client"
import { StringRequest } from "@shared/proto/common"
import { BaseUrlField } from "../common/BaseUrlField"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { useExtensionState } from "@/context/ExtensionStateContext"
/**
* Props for the LMStudioProvider component
*/
interface LMStudioProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -19,7 +19,10 @@ interface LMStudioProviderProps {
/**
* The LM Studio provider configuration component
*/
export const LMStudioProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: LMStudioProviderProps) => {
export const LMStudioProvider = ({ showModelOptions, isPopup }: LMStudioProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const [lmStudioModels, setLmStudioModels] = useState<string[]>([])
// Poll LM Studio models
@@ -48,19 +51,19 @@ export const LMStudioProvider = ({ apiConfiguration, handleInputChange, showMode
return (
<div>
<BaseUrlField
value={apiConfiguration?.lmStudioBaseUrl}
onChange={(value) => handleInputChange("lmStudioBaseUrl")({ target: { value } })}
initialValue={apiConfiguration?.lmStudioBaseUrl}
onChange={(value) => handleFieldChange("lmStudioBaseUrl", value)}
placeholder="Default: http://localhost:1234"
label="Use custom base URL"
/>
<VSCodeTextField
value={apiConfiguration?.lmStudioModelId || ""}
<DebouncedTextField
initialValue={apiConfiguration?.lmStudioModelId || ""}
onChange={(value) => handleFieldChange("lmStudioModelId", value)}
style={{ width: "100%" }}
onInput={handleInputChange("lmStudioModelId")}
placeholder={"e.g. meta-llama-3.1-8b-instruct"}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
</DebouncedTextField>
{lmStudioModels.length > 0 && (
<VSCodeRadioGroup
@@ -71,9 +74,7 @@ export const LMStudioProvider = ({ apiConfiguration, handleInputChange, showMode
const value = (e.target as HTMLInputElement)?.value
// need to check value first since radio group returns empty string sometimes
if (value) {
handleInputChange("lmStudioModelId")({
target: { value },
})
handleFieldChange("lmStudioModelId", value)
}
}}>
{lmStudioModels.map((model) => (
@@ -1,32 +1,29 @@
import { useState } from "react"
import { ApiConfiguration, liteLlmModelInfoSaneDefaults } from "@shared/api"
import { VSCodeTextField, VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { liteLlmModelInfoSaneDefaults } from "@shared/api"
import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { DebouncedTextField } from "../common/DebouncedTextField"
import { getAsVar, VSC_DESCRIPTION_FOREGROUND } from "@/utils/vscStyles"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { ModelInfoView } from "../common/ModelInfoView"
import ThinkingBudgetSlider from "../ThinkingBudgetSlider"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { useExtensionState } from "@/context/ExtensionStateContext"
/**
* Props for the LiteLlmProvider component
*/
interface LiteLlmProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
setApiConfiguration: (config: ApiConfiguration) => void
}
/**
* The LiteLLM provider configuration component
*/
export const LiteLlmProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
setApiConfiguration,
}: LiteLlmProviderProps) => {
export const LiteLlmProvider = ({ showModelOptions, isPopup }: LiteLlmProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
@@ -35,29 +32,29 @@ export const LiteLlmProvider = ({
return (
<div>
<VSCodeTextField
value={apiConfiguration?.liteLlmBaseUrl || ""}
<DebouncedTextField
initialValue={apiConfiguration?.liteLlmBaseUrl || ""}
onChange={(value) => handleFieldChange("liteLlmBaseUrl", value)}
style={{ width: "100%" }}
type="url"
onInput={handleInputChange("liteLlmBaseUrl")}
placeholder={"Default: http://localhost:4000"}>
<span style={{ fontWeight: 500 }}>Base URL (optional)</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.liteLlmApiKey || ""}
</DebouncedTextField>
<DebouncedTextField
initialValue={apiConfiguration?.liteLlmApiKey || ""}
onChange={(value) => handleFieldChange("liteLlmApiKey", value)}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("liteLlmApiKey")}
placeholder="Default: noop">
<span style={{ fontWeight: 500 }}>API Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.liteLlmModelId || ""}
</DebouncedTextField>
<DebouncedTextField
initialValue={apiConfiguration?.liteLlmModelId || ""}
onChange={(value) => handleFieldChange("liteLlmModelId", value)}
style={{ width: "100%" }}
onInput={handleInputChange("liteLlmModelId")}
placeholder={"e.g. anthropic/claude-sonnet-4-20250514"}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
</DebouncedTextField>
<div style={{ display: "flex", flexDirection: "column", marginTop: 10, marginBottom: 10 }}>
{selectedModelInfo.supportsPromptCache && (
@@ -66,10 +63,8 @@ export const LiteLlmProvider = ({
checked={apiConfiguration?.liteLlmUsePromptCache || false}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setApiConfiguration({
...apiConfiguration,
liteLlmUsePromptCache: isChecked,
})
handleFieldChange("liteLlmUsePromptCache", isChecked)
}}
style={{ fontWeight: 500, color: "var(--vscode-charts-green)" }}>
Use prompt caching (GA)
@@ -82,7 +77,7 @@ export const LiteLlmProvider = ({
</div>
<>
<ThinkingBudgetSlider apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
<ThinkingBudgetSlider />
<p
style={{
fontSize: "12px",
@@ -130,83 +125,69 @@ export const LiteLlmProvider = ({
? apiConfiguration.liteLlmModelInfo
: { ...liteLlmModelInfoSaneDefaults }
modelInfo.supportsImages = isChecked
setApiConfiguration({
...apiConfiguration,
liteLlmModelInfo: modelInfo,
})
handleFieldChange("liteLlmModelInfo", modelInfo)
}}>
Supports Images
</VSCodeCheckbox>
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
<VSCodeTextField
value={
<DebouncedTextField
initialValue={
apiConfiguration?.liteLlmModelInfo?.contextWindow
? apiConfiguration.liteLlmModelInfo.contextWindow.toString()
: liteLlmModelInfoSaneDefaults.contextWindow?.toString()
: (liteLlmModelInfoSaneDefaults.contextWindow?.toString() ?? "")
}
style={{ flex: 1 }}
onInput={(input: any) => {
onChange={(value) => {
const modelInfo = apiConfiguration?.liteLlmModelInfo
? apiConfiguration.liteLlmModelInfo
: { ...liteLlmModelInfoSaneDefaults }
modelInfo.contextWindow = Number(input.target.value)
setApiConfiguration({
...apiConfiguration,
liteLlmModelInfo: modelInfo,
})
modelInfo.contextWindow = Number(value)
handleFieldChange("liteLlmModelInfo", modelInfo)
}}>
<span style={{ fontWeight: 500 }}>Context Window Size</span>
</VSCodeTextField>
<VSCodeTextField
value={
</DebouncedTextField>
<DebouncedTextField
initialValue={
apiConfiguration?.liteLlmModelInfo?.maxTokens
? apiConfiguration.liteLlmModelInfo.maxTokens.toString()
: liteLlmModelInfoSaneDefaults.maxTokens?.toString()
: (liteLlmModelInfoSaneDefaults.maxTokens?.toString() ?? "")
}
style={{ flex: 1 }}
onInput={(input: any) => {
onChange={(value) => {
const modelInfo = apiConfiguration?.liteLlmModelInfo
? apiConfiguration.liteLlmModelInfo
: { ...liteLlmModelInfoSaneDefaults }
modelInfo.maxTokens = input.target.value
setApiConfiguration({
...apiConfiguration,
liteLlmModelInfo: modelInfo,
})
modelInfo.maxTokens = Number(value)
handleFieldChange("liteLlmModelInfo", modelInfo)
}}>
<span style={{ fontWeight: 500 }}>Max Output Tokens</span>
</VSCodeTextField>
</DebouncedTextField>
</div>
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
<VSCodeTextField
value={
<DebouncedTextField
initialValue={
apiConfiguration?.liteLlmModelInfo?.temperature !== undefined
? apiConfiguration.liteLlmModelInfo.temperature.toString()
: liteLlmModelInfoSaneDefaults.temperature?.toString()
: (liteLlmModelInfoSaneDefaults.temperature?.toString() ?? "")
}
onInput={(input: any) => {
onChange={(value) => {
const modelInfo = apiConfiguration?.liteLlmModelInfo
? apiConfiguration.liteLlmModelInfo
: { ...liteLlmModelInfoSaneDefaults }
// Check if the input ends with a decimal point or has trailing zeros after decimal
const value = input.target.value
const shouldPreserveFormat = value.endsWith(".") || (value.includes(".") && value.endsWith("0"))
modelInfo.temperature =
value === ""
? liteLlmModelInfoSaneDefaults.temperature
: shouldPreserveFormat
? value // Keep as string to preserve decimal format
: parseFloat(value)
value === "" ? liteLlmModelInfoSaneDefaults.temperature : parseFloat(value)
setApiConfiguration({
...apiConfiguration,
liteLlmModelInfo: modelInfo,
})
handleFieldChange("liteLlmModelInfo", modelInfo)
}}>
<span style={{ fontWeight: 500 }}>Temperature</span>
</VSCodeTextField>
</DebouncedTextField>
</div>
</>
)}
@@ -3,13 +3,13 @@ import { ApiKeyField } from "../common/ApiKeyField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
/**
* Props for the MistralProvider component
*/
interface MistralProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -17,15 +17,18 @@ interface MistralProviderProps {
/**
* The Mistral provider configuration component
*/
export const MistralProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: MistralProviderProps) => {
export const MistralProvider = ({ showModelOptions, isPopup }: MistralProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
return (
<div>
<ApiKeyField
value={apiConfiguration?.mistralApiKey || ""}
onChange={handleInputChange("mistralApiKey")}
initialValue={apiConfiguration?.mistralApiKey || ""}
onChange={(value) => handleFieldChange("mistralApiKey", value)}
providerName="Mistral"
signupUrl="https://console.mistral.ai/codestral"
/>
@@ -35,7 +38,7 @@ export const MistralProvider = ({ apiConfiguration, handleInputChange, showModel
<ModelSelector
models={mistralModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
label="Model"
/>
@@ -1,15 +1,15 @@
import { ApiConfiguration, nebiusModels } from "@shared/api"
import { nebiusModels } from "@shared/api"
import { ApiKeyField } from "../common/ApiKeyField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { useExtensionState } from "@/context/ExtensionStateContext"
/**
* Props for the NebiusProvider component
*/
interface NebiusProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -17,15 +17,18 @@ interface NebiusProviderProps {
/**
* The Nebius AI Studio provider configuration component
*/
export const NebiusProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: NebiusProviderProps) => {
export const NebiusProvider = ({ showModelOptions, isPopup }: NebiusProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
return (
<div>
<ApiKeyField
value={apiConfiguration?.nebiusApiKey || ""}
onChange={handleInputChange("nebiusApiKey")}
initialValue={apiConfiguration?.nebiusApiKey || ""}
onChange={(value) => handleFieldChange("nebiusApiKey", value)}
providerName="Nebius"
signupUrl="https://studio.nebius.com/settings/api-keys"
helpText="This key is stored locally and only used to make API requests from this extension. (Note: Cline uses complex prompts and works best with Claude models. Less capable models may not work as expected.)"
@@ -36,7 +39,7 @@ export const NebiusProvider = ({ apiConfiguration, handleInputChange, showModelO
<ModelSelector
models={nebiusModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
label="Model"
/>
@@ -1,33 +1,29 @@
import { ApiConfiguration } from "@shared/api"
import { VSCodeTextField, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { useState, useCallback, useEffect } from "react"
import { useInterval } from "react-use"
import { DebouncedTextField } from "../common/DebouncedTextField"
import { ModelsServiceClient } from "@/services/grpc-client"
import { StringRequest } from "@shared/proto/common"
import OllamaModelPicker from "../OllamaModelPicker"
import { BaseUrlField } from "../common/BaseUrlField"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
/**
* Props for the OllamaProvider component
*/
interface OllamaProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
setApiConfiguration: (config: ApiConfiguration) => void
}
/**
* The Ollama provider configuration component
*/
export const OllamaProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
setApiConfiguration,
}: OllamaProviderProps) => {
export const OllamaProvider = ({ showModelOptions, isPopup }: OllamaProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const [ollamaModels, setOllamaModels] = useState<string[]>([])
// Poll ollama models
@@ -56,8 +52,8 @@ export const OllamaProvider = ({
return (
<div>
<BaseUrlField
value={apiConfiguration?.ollamaBaseUrl}
onChange={(value) => handleInputChange("ollamaBaseUrl")({ target: { value } })}
initialValue={apiConfiguration?.ollamaBaseUrl}
onChange={(value) => handleFieldChange("ollamaBaseUrl", value)}
placeholder="Default: http://localhost:11434"
label="Use custom base URL"
/>
@@ -70,10 +66,7 @@ export const OllamaProvider = ({
ollamaModels={ollamaModels}
selectedModelId={apiConfiguration?.ollamaModelId || ""}
onModelChange={(modelId) => {
setApiConfiguration({
...apiConfiguration,
ollamaModelId: modelId,
})
handleFieldChange("ollamaModelId", modelId)
}}
placeholder={ollamaModels.length > 0 ? "Search and select a model..." : "e.g. llama3.1"}
/>
@@ -92,33 +85,29 @@ export const OllamaProvider = ({
</p>
)}
<VSCodeTextField
value={apiConfiguration?.ollamaApiOptionsCtxNum || "32768"}
<DebouncedTextField
initialValue={apiConfiguration?.ollamaApiOptionsCtxNum || "32768"}
onChange={(value) => handleFieldChange("ollamaApiOptionsCtxNum", value)}
style={{ width: "100%" }}
onInput={handleInputChange("ollamaApiOptionsCtxNum")}
placeholder={"e.g. 32768"}>
<span style={{ fontWeight: 500 }}>Model Context Window</span>
</VSCodeTextField>
</DebouncedTextField>
{showModelOptions && (
<>
<VSCodeTextField
value={apiConfiguration?.requestTimeoutMs ? apiConfiguration.requestTimeoutMs.toString() : "30000"}
style={{ width: "100%" }}
onInput={(e: any) => {
const value = e.target.value
<DebouncedTextField
initialValue={apiConfiguration?.requestTimeoutMs ? apiConfiguration.requestTimeoutMs.toString() : "30000"}
onChange={(value) => {
// Convert to number, with validation
const numValue = parseInt(value, 10)
if (!isNaN(numValue) && numValue > 0) {
setApiConfiguration({
...apiConfiguration,
requestTimeoutMs: numValue,
})
handleFieldChange("requestTimeoutMs", numValue)
}
}}
style={{ width: "100%" }}
placeholder="Default: 30000 (30 seconds)">
<span style={{ fontWeight: 500 }}>Request Timeout (ms)</span>
</VSCodeTextField>
</DebouncedTextField>
<p style={{ fontSize: "12px", marginTop: 3, color: "var(--vscode-descriptionForeground)" }}>
Maximum time in milliseconds to wait for API responses before timing out.
</p>
@@ -2,19 +2,20 @@ import { ApiConfiguration, azureOpenAiDefaultApiVersion, openAiModelInfoSaneDefa
import { OpenAiModelsRequest } from "@shared/proto/models"
import { ModelsServiceClient } from "@/services/grpc-client"
import { getAsVar, VSC_DESCRIPTION_FOREGROUND } from "@/utils/vscStyles"
import { VSCodeTextField, VSCodeButton, VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { VSCodeButton, VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { useCallback, useEffect, useRef, useState } from "react"
import { DebouncedTextField } from "../common/DebouncedTextField"
import { ModelInfoView } from "../common/ModelInfoView"
import { ApiKeyField } from "../common/ApiKeyField"
import { BaseUrlField } from "../common/BaseUrlField"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
/**
* Props for the OpenAICompatibleProvider component
*/
interface OpenAICompatibleProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -22,12 +23,10 @@ interface OpenAICompatibleProviderProps {
/**
* The OpenAI Compatible provider configuration component
*/
export const OpenAICompatibleProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
}: OpenAICompatibleProviderProps) => {
export const OpenAICompatibleProvider = ({ showModelOptions, isPopup }: OpenAICompatibleProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const [modelConfigurationSelected, setModelConfigurationSelected] = useState(false)
// Get the normalized configuration
@@ -65,38 +64,34 @@ export const OpenAICompatibleProvider = ({
return (
<div>
<VSCodeTextField
value={apiConfiguration?.openAiBaseUrl || ""}
<DebouncedTextField
initialValue={apiConfiguration?.openAiBaseUrl || ""}
onChange={(value) => {
handleFieldChange("openAiBaseUrl", value)
debouncedRefreshOpenAiModels(value, apiConfiguration?.openAiApiKey)
}}
style={{ width: "100%", marginBottom: 10 }}
type="url"
onInput={(e: any) => {
const baseUrl = e.target.value
handleInputChange("openAiBaseUrl")({ target: { value: baseUrl } })
debouncedRefreshOpenAiModels(baseUrl, apiConfiguration?.openAiApiKey)
}}
placeholder={"Enter base URL..."}>
<span style={{ fontWeight: 500 }}>Base URL</span>
</VSCodeTextField>
</DebouncedTextField>
<ApiKeyField
value={apiConfiguration?.openAiApiKey || ""}
onChange={(e: any) => {
const apiKey = e.target.value
handleInputChange("openAiApiKey")({ target: { value: apiKey } })
debouncedRefreshOpenAiModels(apiConfiguration?.openAiBaseUrl, apiKey)
initialValue={apiConfiguration?.openAiApiKey || ""}
onChange={(value) => {
handleFieldChange("openAiApiKey", value)
debouncedRefreshOpenAiModels(apiConfiguration?.openAiBaseUrl, value)
}}
providerName="OpenAI Compatible"
/>
<VSCodeTextField
value={apiConfiguration?.openAiModelId || ""}
<DebouncedTextField
initialValue={apiConfiguration?.openAiModelId || ""}
onChange={(value) => handleFieldChange("openAiModelId", value)}
style={{ width: "100%", marginBottom: 10 }}
onInput={handleInputChange("openAiModelId")}
placeholder={"Enter Model ID..."}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
</DebouncedTextField>
{/* OpenAI Compatible Custom Headers */}
{(() => {
@@ -111,11 +106,7 @@ export const OpenAICompatibleProvider = ({
const headerCount = Object.keys(currentHeaders).length
const newKey = `header${headerCount + 1}`
currentHeaders[newKey] = ""
handleInputChange("openAiHeaders")({
target: {
value: currentHeaders,
},
})
handleFieldChange("openAiHeaders", currentHeaders)
}}>
Add Header
</VSCodeButton>
@@ -123,38 +114,29 @@ export const OpenAICompatibleProvider = ({
<div>
{headerEntries.map(([key, value], index) => (
<div key={index} style={{ display: "flex", gap: 5, marginTop: 5 }}>
<VSCodeTextField
value={key}
<DebouncedTextField
initialValue={key}
style={{ width: "40%" }}
placeholder="Header name"
onInput={(e: any) => {
onChange={(newValue) => {
const currentHeaders = apiConfiguration?.openAiHeaders ?? {}
const newValue = e.target.value
if (newValue && newValue !== key) {
const { [key]: _, ...rest } = currentHeaders
handleInputChange("openAiHeaders")({
target: {
value: {
...rest,
[newValue]: value,
},
},
handleFieldChange("openAiHeaders", {
...rest,
[newValue]: value,
})
}
}}
/>
<VSCodeTextField
value={value}
<DebouncedTextField
initialValue={value}
style={{ width: "40%" }}
placeholder="Header value"
onInput={(e: any) => {
handleInputChange("openAiHeaders")({
target: {
value: {
...(apiConfiguration?.openAiHeaders ?? {}),
[key]: e.target.value,
},
},
onChange={(newValue) => {
handleFieldChange("openAiHeaders", {
...(apiConfiguration?.openAiHeaders ?? {}),
[key]: newValue,
})
}}
/>
@@ -162,11 +144,7 @@ export const OpenAICompatibleProvider = ({
appearance="secondary"
onClick={() => {
const { [key]: _, ...rest } = apiConfiguration?.openAiHeaders ?? {}
handleInputChange("openAiHeaders")({
target: {
value: rest,
},
})
handleFieldChange("openAiHeaders", rest)
}}>
Remove
</VSCodeButton>
@@ -178,8 +156,8 @@ export const OpenAICompatibleProvider = ({
})()}
<BaseUrlField
value={apiConfiguration?.azureApiVersion}
onChange={(value) => handleInputChange("azureApiVersion")({ target: { value } })}
initialValue={apiConfiguration?.azureApiVersion}
onChange={(value) => handleFieldChange("azureApiVersion", value)}
label="Set Azure API version"
placeholder={`Default: ${azureOpenAiDefaultApiVersion}`}
/>
@@ -217,9 +195,7 @@ export const OpenAICompatibleProvider = ({
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
modelInfo.supportsImages = isChecked
handleInputChange("openAiModelInfo")({
target: { value: modelInfo },
})
handleFieldChange("openAiModelInfo", modelInfo)
}}>
Supports Images
</VSCodeCheckbox>
@@ -232,9 +208,7 @@ export const OpenAICompatibleProvider = ({
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
modelInfo.supportsImages = isChecked
handleInputChange("openAiModelInfo")({
target: { value: modelInfo },
})
handleFieldChange("openAiModelInfo", modelInfo)
}}>
Supports browser use
</VSCodeCheckbox>
@@ -248,122 +222,108 @@ export const OpenAICompatibleProvider = ({
: { ...openAiModelInfoSaneDefaults }
modelInfo = { ...modelInfo, isR1FormatRequired: isChecked }
handleInputChange("openAiModelInfo")({
target: { value: modelInfo },
})
handleFieldChange("openAiModelInfo", modelInfo)
}}>
Enable R1 messages format
</VSCodeCheckbox>
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
<VSCodeTextField
value={
<DebouncedTextField
initialValue={
apiConfiguration?.openAiModelInfo?.contextWindow
? apiConfiguration.openAiModelInfo.contextWindow.toString()
: openAiModelInfoSaneDefaults.contextWindow?.toString()
: (openAiModelInfoSaneDefaults.contextWindow?.toString() ?? "")
}
style={{ flex: 1 }}
onInput={(input: any) => {
onChange={(value) => {
const modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
modelInfo.contextWindow = Number(input.target.value)
handleInputChange("openAiModelInfo")({
target: { value: modelInfo },
})
modelInfo.contextWindow = Number(value)
handleFieldChange("openAiModelInfo", modelInfo)
}}>
<span style={{ fontWeight: 500 }}>Context Window Size</span>
</VSCodeTextField>
</DebouncedTextField>
<VSCodeTextField
value={
<DebouncedTextField
initialValue={
apiConfiguration?.openAiModelInfo?.maxTokens
? apiConfiguration.openAiModelInfo.maxTokens.toString()
: openAiModelInfoSaneDefaults.maxTokens?.toString()
: (openAiModelInfoSaneDefaults.maxTokens?.toString() ?? "")
}
style={{ flex: 1 }}
onInput={(input: any) => {
onChange={(value) => {
const modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
modelInfo.maxTokens = input.target.value
handleInputChange("openAiModelInfo")({
target: { value: modelInfo },
})
modelInfo.maxTokens = Number(value)
handleFieldChange("openAiModelInfo", modelInfo)
}}>
<span style={{ fontWeight: 500 }}>Max Output Tokens</span>
</VSCodeTextField>
</DebouncedTextField>
</div>
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
<VSCodeTextField
value={
<DebouncedTextField
initialValue={
apiConfiguration?.openAiModelInfo?.inputPrice
? apiConfiguration.openAiModelInfo.inputPrice.toString()
: openAiModelInfoSaneDefaults.inputPrice?.toString()
: (openAiModelInfoSaneDefaults.inputPrice?.toString() ?? "")
}
style={{ flex: 1 }}
onInput={(input: any) => {
onChange={(value) => {
const modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
modelInfo.inputPrice = input.target.value
handleInputChange("openAiModelInfo")({
target: { value: modelInfo },
})
modelInfo.inputPrice = Number(value)
handleFieldChange("openAiModelInfo", modelInfo)
}}>
<span style={{ fontWeight: 500 }}>Input Price / 1M tokens</span>
</VSCodeTextField>
</DebouncedTextField>
<VSCodeTextField
value={
<DebouncedTextField
initialValue={
apiConfiguration?.openAiModelInfo?.outputPrice
? apiConfiguration.openAiModelInfo.outputPrice.toString()
: openAiModelInfoSaneDefaults.outputPrice?.toString()
: (openAiModelInfoSaneDefaults.outputPrice?.toString() ?? "")
}
style={{ flex: 1 }}
onInput={(input: any) => {
onChange={(value) => {
const modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
modelInfo.outputPrice = input.target.value
handleInputChange("openAiModelInfo")({
target: { value: modelInfo },
})
modelInfo.outputPrice = Number(value)
handleFieldChange("openAiModelInfo", modelInfo)
}}>
<span style={{ fontWeight: 500 }}>Output Price / 1M tokens</span>
</VSCodeTextField>
</DebouncedTextField>
</div>
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
<VSCodeTextField
value={
<DebouncedTextField
initialValue={
apiConfiguration?.openAiModelInfo?.temperature
? apiConfiguration.openAiModelInfo.temperature.toString()
: openAiModelInfoSaneDefaults.temperature?.toString()
: (openAiModelInfoSaneDefaults.temperature?.toString() ?? "")
}
onInput={(input: any) => {
onChange={(value) => {
const modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
// Check if the input ends with a decimal point or has trailing zeros after decimal
const value = input.target.value
const shouldPreserveFormat = value.endsWith(".") || (value.includes(".") && value.endsWith("0"))
modelInfo.temperature =
value === ""
? openAiModelInfoSaneDefaults.temperature
: shouldPreserveFormat
? value // Keep as string to preserve decimal format
? (value as any)
: parseFloat(value)
handleInputChange("openAiModelInfo")({
target: { value: modelInfo },
})
handleFieldChange("openAiModelInfo", modelInfo)
}}>
<span style={{ fontWeight: 500 }}>Temperature</span>
</VSCodeTextField>
</DebouncedTextField>
</div>
</>
)}
@@ -3,13 +3,13 @@ import { ApiKeyField } from "../common/ApiKeyField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
/**
* Props for the OpenAINativeProvider component
*/
interface OpenAINativeProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -17,20 +17,18 @@ interface OpenAINativeProviderProps {
/**
* The OpenAI (native) provider configuration component
*/
export const OpenAINativeProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
}: OpenAINativeProviderProps) => {
export const OpenAINativeProvider = ({ showModelOptions, isPopup }: OpenAINativeProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
return (
<div>
<ApiKeyField
value={apiConfiguration?.openAiNativeApiKey || ""}
onChange={handleInputChange("openAiNativeApiKey")}
initialValue={apiConfiguration?.openAiNativeApiKey || ""}
onChange={(value) => handleFieldChange("openAiNativeApiKey", value)}
providerName="OpenAI"
signupUrl="https://platform.openai.com/api-keys"
/>
@@ -40,7 +38,7 @@ export const OpenAINativeProvider = ({
<ModelSelector
models={openAiNativeModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
label="Model"
/>
@@ -1,5 +1,6 @@
import { ApiConfiguration } from "@shared/api"
import { VSCodeCheckbox, VSCodeDropdown, VSCodeOption, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { VSCodeCheckbox, VSCodeDropdown, VSCodeOption, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { DebouncedTextField } from "../common/DebouncedTextField"
import { DropdownContainer } from "../common/ModelSelector"
import { useState } from "react"
import { getOpenRouterAuthUrl } from "../utils/providerUtils"
@@ -7,6 +8,8 @@ import { useOpenRouterKeyInfo } from "../../ui/hooks/useOpenRouterKeyInfo"
import VSCodeButtonLink from "../../common/VSCodeButtonLink"
import OpenRouterModelPicker, { OPENROUTER_MODEL_PICKER_Z_INDEX } from "../OpenRouterModelPicker"
import { formatPrice } from "../utils/pricingUtils"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
/**
* Component to display OpenRouter balance information
@@ -48,8 +51,6 @@ const OpenRouterBalanceDisplay = ({ apiKey }: { apiKey: string }) => {
* Props for the OpenRouterProvider component
*/
interface OpenRouterProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
uriScheme?: string
@@ -58,28 +59,20 @@ interface OpenRouterProviderProps {
/**
* The OpenRouter provider configuration component
*/
export const OpenRouterProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
uriScheme,
}: OpenRouterProviderProps) => {
const [providerSortingSelected, setProviderSortingSelected] = useState(!!apiConfiguration?.openRouterProviderSorting)
export const OpenRouterProvider = ({ showModelOptions, isPopup, uriScheme }: OpenRouterProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Create a wrapper for handling field changes more directly
const handleFieldChange = (field: keyof ApiConfiguration) => (value: any) => {
handleInputChange(field)({ target: { value } })
}
const [providerSortingSelected, setProviderSortingSelected] = useState(!!apiConfiguration?.openRouterProviderSorting)
return (
<div>
<div>
<VSCodeTextField
value={apiConfiguration?.openRouterApiKey || ""}
<DebouncedTextField
initialValue={apiConfiguration?.openRouterApiKey || ""}
onChange={(value) => handleFieldChange("openRouterApiKey", value)}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("openRouterApiKey")}
placeholder="Enter API Key...">
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", width: "100%" }}>
<span style={{ fontWeight: 500 }}>OpenRouter API Key</span>
@@ -87,7 +80,7 @@ export const OpenRouterProvider = ({
<OpenRouterBalanceDisplay apiKey={apiConfiguration.openRouterApiKey} />
)}
</div>
</VSCodeTextField>
</DebouncedTextField>
{!apiConfiguration?.openRouterApiKey && (
<VSCodeButtonLink
href={getOpenRouterAuthUrl(uriScheme)}
@@ -115,7 +108,7 @@ export const OpenRouterProvider = ({
const isChecked = e.target.checked === true
setProviderSortingSelected(isChecked)
if (!isChecked) {
handleFieldChange("openRouterProviderSorting")("")
handleFieldChange("openRouterProviderSorting", "")
}
}}>
Sort underlying provider routing
@@ -128,7 +121,7 @@ export const OpenRouterProvider = ({
style={{ width: "100%", marginTop: 3 }}
value={apiConfiguration?.openRouterProviderSorting}
onChange={(e: any) => {
handleFieldChange("openRouterProviderSorting")(e.target.value)
handleFieldChange("openRouterProviderSorting", e.target.value)
}}>
<VSCodeOption value="">Default</VSCodeOption>
<VSCodeOption value="price">Price</VSCodeOption>
@@ -6,6 +6,8 @@ import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import ThinkingBudgetSlider from "../ThinkingBudgetSlider"
import { DROPDOWN_Z_INDEX } from "../ApiOptions"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
const SUPPORTED_THINKING_MODELS = [
"qwen3-235b-a22b",
@@ -24,23 +26,17 @@ const SUPPORTED_THINKING_MODELS = [
* Props for the QwenProvider component
*/
interface QwenProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
setApiConfiguration: (config: ApiConfiguration) => void
}
/**
* The Alibaba Qwen provider configuration component
*/
export const QwenProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
setApiConfiguration,
}: QwenProviderProps) => {
export const QwenProvider = ({ showModelOptions, isPopup }: QwenProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
@@ -56,7 +52,7 @@ export const QwenProvider = ({
<VSCodeDropdown
id="qwen-line-provider"
value={apiConfiguration?.qwenApiLine || "china"}
onChange={handleInputChange("qwenApiLine")}
onChange={(e: any) => handleFieldChange("qwenApiLine", e.target.value)}
style={{
minWidth: 130,
position: "relative",
@@ -76,8 +72,8 @@ export const QwenProvider = ({
</p>
<ApiKeyField
value={apiConfiguration?.qwenApiKey || ""}
onChange={handleInputChange("qwenApiKey")}
initialValue={apiConfiguration?.qwenApiKey || ""}
onChange={(value) => handleFieldChange("qwenApiKey", value)}
providerName="Qwen"
signupUrl="https://bailian.console.aliyun.com/"
/>
@@ -87,17 +83,13 @@ export const QwenProvider = ({
<ModelSelector
models={qwenModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
label="Model"
zIndex={DROPDOWN_Z_INDEX - 2}
/>
{SUPPORTED_THINKING_MODELS.includes(selectedModelId) && (
<ThinkingBudgetSlider
apiConfiguration={apiConfiguration}
setApiConfiguration={setApiConfiguration}
maxBudget={selectedModelInfo.thinkingConfig?.maxBudget}
/>
<ThinkingBudgetSlider maxBudget={selectedModelInfo.thinkingConfig?.maxBudget} />
)}
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
@@ -1,13 +1,12 @@
import { ApiConfiguration } from "@shared/api"
import { ApiKeyField } from "../common/ApiKeyField"
import RequestyModelPicker from "../RequestyModelPicker"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { useExtensionState } from "@/context/ExtensionStateContext"
/**
* Props for the RequestyProvider component
*/
interface RequestyProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -15,12 +14,15 @@ interface RequestyProviderProps {
/**
* The Requesty provider configuration component
*/
export const RequestyProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: RequestyProviderProps) => {
export const RequestyProvider = ({ showModelOptions, isPopup }: RequestyProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
return (
<div>
<ApiKeyField
value={apiConfiguration?.requestyApiKey || ""}
onChange={handleInputChange("requestyApiKey")}
initialValue={apiConfiguration?.requestyApiKey || ""}
onChange={(value) => handleFieldChange("requestyApiKey", value)}
providerName="Requesty"
signupUrl="https://app.requesty.ai/manage-api"
/>
@@ -3,13 +3,13 @@ import { ApiKeyField } from "../common/ApiKeyField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
/**
* Props for the SambanovaProvider component
*/
interface SambanovaProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -17,15 +17,18 @@ interface SambanovaProviderProps {
/**
* The SambaNova provider configuration component
*/
export const SambanovaProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: SambanovaProviderProps) => {
export const SambanovaProvider = ({ showModelOptions, isPopup }: SambanovaProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
return (
<div>
<ApiKeyField
value={apiConfiguration?.sambanovaApiKey || ""}
onChange={handleInputChange("sambanovaApiKey")}
initialValue={apiConfiguration?.sambanovaApiKey || ""}
onChange={(value) => handleFieldChange("sambanovaApiKey", value)}
providerName="SambaNova"
signupUrl="https://docs.sambanova.ai/cloud/docs/get-started/overview"
/>
@@ -35,7 +38,7 @@ export const SambanovaProvider = ({ apiConfiguration, handleInputChange, showMod
<ModelSelector
models={sambanovaModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
label="Model"
/>
@@ -1,15 +1,16 @@
import { ApiConfiguration, sapAiCoreModels } from "@shared/api"
import { VSCodeTextField, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { sapAiCoreModels } from "@shared/api"
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { DebouncedTextField } from "../common/DebouncedTextField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { useExtensionState } from "@/context/ExtensionStateContext"
/**
* Props for the SapAiCoreProvider component
*/
interface SapAiCoreProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -17,62 +18,65 @@ interface SapAiCoreProviderProps {
/**
* The SAP AI Core provider configuration component
*/
export const SapAiCoreProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: SapAiCoreProviderProps) => {
export const SapAiCoreProvider = ({ showModelOptions, isPopup }: SapAiCoreProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
return (
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
<VSCodeTextField
value={apiConfiguration?.sapAiCoreClientId || ""}
<DebouncedTextField
initialValue={apiConfiguration?.sapAiCoreClientId || ""}
onChange={(value) => handleFieldChange("sapAiCoreClientId", value)}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("sapAiCoreClientId")}
placeholder="Enter AI Core Client Id...">
<span style={{ fontWeight: 500 }}>AI Core Client Id</span>
</VSCodeTextField>
</DebouncedTextField>
{apiConfiguration?.sapAiCoreClientId && (
<p style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)" }}>
Client Id is set. To change it, please re-enter the value.
</p>
)}
<VSCodeTextField
value={apiConfiguration?.sapAiCoreClientSecret ? "********" : ""}
<DebouncedTextField
initialValue={apiConfiguration?.sapAiCoreClientSecret ? "********" : ""}
onChange={(value) => handleFieldChange("sapAiCoreClientSecret", value)}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("sapAiCoreClientSecret")}
placeholder="Enter AI Core Client Secret...">
<span style={{ fontWeight: 500 }}>AI Core Client Secret</span>
</VSCodeTextField>
</DebouncedTextField>
{apiConfiguration?.sapAiCoreClientSecret && (
<p style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)" }}>
Client Secret is set. To change it, please re-enter the value.
</p>
)}
<VSCodeTextField
value={apiConfiguration?.sapAiCoreBaseUrl || ""}
<DebouncedTextField
initialValue={apiConfiguration?.sapAiCoreBaseUrl || ""}
onChange={(value) => handleFieldChange("sapAiCoreBaseUrl", value)}
style={{ width: "100%" }}
onInput={handleInputChange("sapAiCoreBaseUrl")}
placeholder="Enter AI Core Base URL...">
<span style={{ fontWeight: 500 }}>AI Core Base URL</span>
</VSCodeTextField>
</DebouncedTextField>
<VSCodeTextField
value={apiConfiguration?.sapAiCoreTokenUrl || ""}
<DebouncedTextField
initialValue={apiConfiguration?.sapAiCoreTokenUrl || ""}
onChange={(value) => handleFieldChange("sapAiCoreTokenUrl", value)}
style={{ width: "100%" }}
onInput={handleInputChange("sapAiCoreTokenUrl")}
placeholder="Enter AI Core Auth URL...">
<span style={{ fontWeight: 500 }}>AI Core Auth URL</span>
</VSCodeTextField>
</DebouncedTextField>
<VSCodeTextField
value={apiConfiguration?.sapAiResourceGroup || ""}
<DebouncedTextField
initialValue={apiConfiguration?.sapAiResourceGroup || ""}
onChange={(value) => handleFieldChange("sapAiResourceGroup", value)}
style={{ width: "100%" }}
onInput={handleInputChange("sapAiResourceGroup")}
placeholder="Enter AI Core Resource Group...">
<span style={{ fontWeight: 500 }}>AI Core Resource Group</span>
</VSCodeTextField>
</DebouncedTextField>
<p
style={{
@@ -93,7 +97,7 @@ export const SapAiCoreProvider = ({ apiConfiguration, handleInputChange, showMod
<ModelSelector
models={sapAiCoreModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
label="Model"
/>
@@ -1,13 +1,13 @@
import { ApiConfiguration } from "@shared/api"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { DebouncedTextField } from "../common/DebouncedTextField"
import { ApiKeyField } from "../common/ApiKeyField"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
/**
* Props for the TogetherProvider component
*/
interface TogetherProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -15,21 +15,24 @@ interface TogetherProviderProps {
/**
* The Together provider configuration component
*/
export const TogetherProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: TogetherProviderProps) => {
export const TogetherProvider = ({ showModelOptions, isPopup }: TogetherProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
return (
<div>
<ApiKeyField
value={apiConfiguration?.togetherApiKey || ""}
onChange={handleInputChange("togetherApiKey")}
initialValue={apiConfiguration?.togetherApiKey || ""}
onChange={(value) => handleFieldChange("togetherApiKey", value)}
providerName="Together"
/>
<VSCodeTextField
value={apiConfiguration?.togetherModelId || ""}
<DebouncedTextField
initialValue={apiConfiguration?.togetherModelId || ""}
onChange={(value) => handleFieldChange("togetherModelId", value)}
style={{ width: "100%" }}
onInput={handleInputChange("togetherModelId")}
placeholder={"Enter Model ID..."}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
</DebouncedTextField>
<p
style={{
fontSize: "12px",
@@ -1,4 +1,3 @@
import { ApiConfiguration } from "@shared/api"
import { EmptyRequest } from "@shared/proto/common"
import { ModelsServiceClient } from "@/services/grpc-client"
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
@@ -6,14 +5,13 @@ import { useState, useCallback, useEffect } from "react"
import { useInterval } from "react-use"
import * as vscodemodels from "vscode"
import { DropdownContainer, DROPDOWN_Z_INDEX } from "../ApiOptions"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { useExtensionState } from "@/context/ExtensionStateContext"
interface VSCodeLmProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
}
export const VSCodeLmProvider = ({ apiConfiguration, handleInputChange }: VSCodeLmProviderProps) => {
export const VSCodeLmProvider = () => {
const [vsCodeLmModels, setVsCodeLmModels] = useState<vscodemodels.LanguageModelChatSelector[]>([])
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Poll VS Code LM models
const requestVsCodeLmModels = useCallback(async () => {
@@ -54,11 +52,8 @@ export const VSCodeLmProvider = ({ apiConfiguration, handleInputChange }: VSCode
return
}
const [vendor, family] = value.split("/")
handleInputChange("vsCodeLmModelSelector")({
target: {
value: { vendor, family },
},
})
handleFieldChange("vsCodeLmModelSelector", { vendor, family })
}}
style={{ width: "100%" }}>
<VSCodeOption value="">Select a model...</VSCodeOption>

Some files were not shown because too many files have changed in this diff Show More