mirror of
https://github.com/cline/cline.git
synced 2026-09-14 11:29:25 +08:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ca086d7a1 | ||
|
|
6c2c0780ee | ||
|
|
021a014012 | ||
|
|
ef1a68b5e3 | ||
|
|
72029fe205 | ||
|
|
2af151e736 | ||
|
|
64963c4e9c | ||
|
|
52571ccee8 | ||
|
|
87322feeb7 | ||
|
|
27f8372c5b | ||
|
|
b3d3e9861f | ||
|
|
01178909ee | ||
|
|
c982216113 | ||
|
|
da6f705df2 | ||
|
|
c7548a7f52 | ||
|
|
e5f78a0456 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Refactor chat view into multiple modular files
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Include litellm_session_id as part of chat completion requests
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add Claude Sonnet 4 and Opus 4 model in SAP AI Core provider.
|
||||
@@ -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!)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Generated
+2
-2
@@ -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
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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 }],
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ export type GlobalStateKey =
|
||||
| "terminalReuseEnabled"
|
||||
| "defaultTerminalProfile"
|
||||
| "isNewUser"
|
||||
| "welcomeViewCompleted"
|
||||
| "terminalOutputLineLimit"
|
||||
| "mcpRichDisplayEnabled"
|
||||
| "sapAiCoreTokenUrl"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -60,6 +60,7 @@ export const DEFAULT_PLATFORM = "unknown"
|
||||
|
||||
export interface ExtensionState {
|
||||
isNewUser: boolean
|
||||
welcomeViewCompleted: boolean
|
||||
apiConfiguration?: ApiConfiguration
|
||||
autoApprovalSettings: AutoApprovalSettings
|
||||
browserSettings: BrowserSettings
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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] }))}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user