Compare commits

..
Author SHA1 Message Date
Cline Evaluation fd431673ea Fixing linter stuff 2025-07-02 19:22:38 -06:00
Ara fe339ddcee Update terminal-integration-guide.mdx 2025-07-03 06:33:08 +05:30
Ara 18b8349e61 Update terminal-quick-fixes.mdx 2025-07-03 06:31:10 +05:30
Ara 70a3edf8cd Update terminal-quick-fixes.mdx 2025-07-03 06:28:26 +05:30
kevinneungandellipsis-dev[bot] d5e9cf1892 Update docs/troubleshooting/terminal-integration-guide.mdx
tweak Github to GitHub

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-02 16:48:56 -07:00
kevinneung f3600d38f6 Improved formatting, and increased detail
I reformatted these docs into a more hierarchal structure. I also added more navigation context around certain steps. I also removed emoji's.
2025-07-02 16:45:44 -07:00
Cline Evaluation b09edcd122 Adding a one stop guide for terminal problems 2025-06-30 16:24:31 -06:00
Cline Evaluation ddfcb370c2 Merge branch 'arafatkatze/terminal-problems-guide' of https://github.com/cline/cline into arafatkatze/terminal-problems-guide 2025-06-30 16:18:17 -06:00
Cline Evaluation c25145775a Revert package-lock.json changes 2025-06-30 16:17:49 -06:00
AraandCopilot b768c28d2f Update docs/troubleshooting/terminal-integration-guide.mdx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-01 03:47:06 +05:30
Cline Evaluation b04c74f57a Adding a one stop guide for terminal problems 2025-06-30 16:12:17 -06:00
Sarah Fortune c8b05cdf9c Remove unused property postMessageToWebview from Task (#4587)
Cleanup imports.
2025-06-30 14:03:41 -07:00
AraandCline Evaluation 9e69576fb0 Splitting chat view into multiple modular files (#4569)
* Splitting chat view into multiple modular files

* Adding Comments and removing redundancies

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-06-30 13:55:25 -07:00
Toshii e6760ed6cc race (#4583) 2025-06-30 11:33:00 -07:00
celestial-vault 27a531c86a [SettingsView.tsx] Extract About Section (#4537)
* refactor out apiconfig section

* add general settings section

* duplicate import

* move terminal, browser, and feature settings

* move files to sections folder

* refactor out debug section

* pull out about section
2025-06-30 10:47:18 -07:00
Lize Cai 09c773bd5f Add Claude 4 model to SAP AI Core Provider (#4418)
* add claude 4 support to sap aicore

Signed-off-by: Lize Cai <lize.cai@sap.com>

* add changelog

Signed-off-by: Lize Cai <lize.cai@sap.com>

* update model-utils to capture other naming for claude 4.

Signed-off-by: Lize Cai <lize.cai@sap.com>

* add claude 4 opus as well

Signed-off-by: Lize Cai <lize.cai@sap.com>

---------

Signed-off-by: Lize Cai <lize.cai@sap.com>
2025-06-30 10:24:45 -07:00
github-actions[bot] 037a781a6d v3.18.1 Release Notes
-   Add support for Claude 4 Sonnet in SAP AI Core provider (Thanks @GTxx!)
-   Fix ENAMETOOLONG error when using Claude Code provider with long conversation histories (Thanks @BarreiroT!)
-   Remove Gemini CLI provider because Google asked us to
-   Fix bug with "Delete All Tasks" functionality
2025-06-28 18:38:32 -07:00
AraandCline Evaluation 968b46a179 Revert "Adding Gemini CLI Provider with Oauth support (#4472)" (#4556)
* Revert "Adding Gemini CLI Provider with Oauth support (#4472)"

This reverts commit 890148407a.

* revert

* revert

* revert

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-06-28 18:20:32 -07:00
Toshii 5a777f28b6 fix browser row in chat view to prevent constant rerendering (#4542)
* base

* browser 2
2025-06-28 10:12:51 -07:00
Toshii fe9e5bff74 base (#4539) 2025-06-28 00:27:13 -07:00
celestial-vault e65605535a [SettingsView.tsx] Extract Debug Section (#4536)
* refactor out apiconfig section

* add general settings section

* duplicate import

* move terminal, browser, and feature settings

* move files to sections folder

* refactor out debug section
2025-06-27 16:04:28 -07:00
celestial-vault 3d100d835a [SettingsView.tsx] Extract Browser, Terminal, and Feature Sections (#4533)
* refactor out apiconfig section

* add general settings section

* duplicate import

* move terminal, browser, and feature settings

* move files to sections folder
2025-06-27 15:44:31 -07:00
celestial-vault b9d3814355 [SettingsView.tsx] Extract GeneralSettings Section (#4529)
* refactor out apiconfig section

* add general settings section

* duplicate import

* add missing type
2025-06-27 15:14:12 -07:00
xiongxiong 12fbc48629 feat. support anthropic--claude-4-sonnet in SAP AI Core provider (#4448) 2025-06-27 14:44:54 -07:00
Sarah Fortune d2f1e0cde0 Replace vscode.workspace.workspaceFolders with host bridge getWorkspacePaths (#4443)
* Use the host bridge in utils/path.ts

Update utils/path.ts to use the host bridge to get the workspace folders, instead of the vscode SDK.
Update callers to use await as the functions are now async.

* Replace vscode workspaceFolders in WorkspaceTracker

Make the cwd an instance property because await cannot be used at the top level.

* Use the host bridge getWorkspacePaths in FileContextTracker

Replace the vscode SDK getWorkspaceFolders with the util function getCwd (this is already switched to the host bridge).

* Fix test failure

Update the rootDir for the tests to be "." instead of "src". The changes to path.ts pull in new dependencies from the extension, which indirectly include files from the webview-ui.

```
Run npm run pretest

> claude-dev@3.18.0 pretest
> npm run compile-tests && npm run compile && npm run compile-standalone && npm run lint

> claude-dev@3.18.0 compile-tests
> node ./scripts/build-tests.js

node:child_process:957
    throw err;
    ^

Error: Command failed: tsc -p ./tsconfig.test.json --outDir out
    at genericNodeError (node:internal/errors:983:15)
    at wrappedFn (node:internal/errors:537:14)
    at checkExecSyncError (node:child_process:882:11)
    at execSync (node:child_process:954:15)
    at Object.<anonymous> (/home/runner/work/cline/cline/scripts/build-tests.js:55:1)
    at Module._compile (node:internal/modules/cjs/loader:1730:14)
    at Object..js (node:internal/modules/cjs/loader:1895:10)
    at Module.load (node:internal/modules/cjs/loader:1465:32)
    at Function._load (node:internal/modules/cjs/loader:1282:12)
    at TracingChannel.traceSync (node:diagnostics_channel:322:14) {
  status: 2,
  signal: null,
  output: [
    null,
    "src/services/test/TestServer.ts(8,35): error TS6059: File '/home/runner/work/cline/cline/webview-ui/src/services/grpc-client.ts' is not under 'rootDir' '/home/runner/work/cline/cline/src'. 'rootDir' is expected to contain all source files.\n" +
      "webview-ui/src/services/grpc-client-base.ts(1,24): error TS6059: File '/home/runner/work/cline/cline/webview-ui/src/utils/vscode.ts' is not under 'rootDir' '/home/runner/work/cline/cline/src'. 'rootDir' is expected to contain all source files.\n" +
      "webview-ui/src/services/grpc-client.ts(4,34): error TS6059: File '/home/runner/work/cline/cline/webview-ui/src/services/grpc-client-base.ts' is not under 'rootDir' '/home/runner/work/cline/cline/src'. 'rootDir' is expected to contain all source files.\n",
    ''
  ],
  pid: 2496,
  stdout: "src/services/test/TestServer.ts(8,35): error TS6059: File '/home/runner/work/cline/cline/webview-ui/src/services/grpc-client.ts' is not under 'rootDir' '/home/runner/work/cline/cline/src'. 'rootDir' is expected to contain all source files.\n" +
    "webview-ui/src/services/grpc-client-base.ts(1,24): error TS6059: File '/home/runner/work/cline/cline/webview-ui/src/utils/vscode.ts' is not under 'rootDir' '/home/runner/work/cline/cline/src'. 'rootDir' is expected to contain all source files.\n" +
    "webview-ui/src/services/grpc-client.ts(4,34): error TS6059: File '/home/runner/work/cline/cline/webview-ui/src/services/grpc-client-base.ts' is not under 'rootDir' '/home/runner/work/cline/cline/src'. 'rootDir' is expected to contain all source files.\n",
  stderr: ''
}
```

* Build the protos before compiling the tests.

The tests depend on generated files now, so compile the extension before the tests so that the protos are built.

* Set up the host providers in the integration test FileContextTracker.test.ts

* Reduce the amount of logging in grpc-service.ts

Just log the service registration, instead of every rpc.

* In the `clean` build target, also remove the compiled test code.

* Correct the alias mapping for the compiled test files.
2025-06-27 11:51:02 -07:00
celestial-vault efe2388e4a refactor out apiconfig section (#4528) 2025-06-27 10:57:54 -07:00
celestial-vault 247552fcb5 remove unused function (#4518) 2025-06-27 09:35:02 -07:00
ToshiiandStvLz 656e3276c6 lite llm provider ui (#4517)
* base 2

Co-authored-by: StvLz <lizarazo.steven@gmail.com>

* base 3

* base

---------

Co-authored-by: StvLz <lizarazo.steven@gmail.com>
2025-06-26 22:51:12 -07:00
ToshiiandStvLz 0e29f05e28 refactor nebius and remove unused logic (#4515)
* base 2

Co-authored-by: StvLz <lizarazo.steven@gmail.com>

* base 3

---------

Co-authored-by: StvLz <lizarazo.steven@gmail.com>
2025-06-26 22:39:56 -07:00
celestial-vault 985cb51c39 refactor lmstudio (#4514)
* refactor lmstudio

* merge conflict deleted stuff
2025-06-26 22:34:14 -07:00
celestial-vault abccde0e2a refactor out vscode lm (#4513) 2025-06-26 21:55:37 -07:00
celestial-vault ca984609ca refactor out cline (#4512) 2025-06-26 21:41:35 -07:00
celestial-vault 6690d392cd refactor out bedrock (#4510) 2025-06-26 21:29:27 -07:00
Toshii 40244f09fe base (#4511) 2025-06-26 21:23:43 -07:00
Toshii 9563a71c8a base (#4509) 2025-06-26 21:14:00 -07:00
83 changed files with 5866 additions and 5624 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix ENAMETOOLONG when calling Claude Code
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Refactor chat view into multiple modular files
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add Claude Sonnet 4 and Opus 4 model in SAP AI Core provider.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Removed deleteNonFavoritedTasks, moved popup to extension, cleaned up deletion logic
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
host bridge migration - clipboard
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Refactor task class, moving auto approve
+7
View File
@@ -1,5 +1,12 @@
# Changelog
## [3.18.1]
- Add support for Claude 4 Sonnet in SAP AI Core provider (Thanks @GTxx!)
- Fix ENAMETOOLONG error when using Claude Code provider with long conversation histories (Thanks @BarreiroT!)
- Remove Gemini CLI provider because Google asked us to
- Fix bug with "Delete All Tasks" functionality
## [3.18.0]
- Optimized Cline to work with the Claude 4 family of models, resulting in improved performance, reliability, and new capabilities
+4
View File
@@ -170,6 +170,10 @@
"running-models-locally/ollama"
]
},
{
"group": "Troubleshooting",
"pages": ["troubleshooting/terminal-quick-fixes", "troubleshooting/terminal-integration-guide"]
},
{
"group": "More Info",
"pages": ["more-info/telemetry"]
@@ -58,3 +58,16 @@ When you use the terminal mention in your message, here's what happens behind th
6. The AI can now "see" the complete terminal output with all formatting preserved
This process happens automatically whenever you use the terminal mention, giving the AI access to your command results, error messages, and other terminal output without you having to copy it manually.
## Troubleshooting Terminal Issues
If you're experiencing issues with terminal mentions or terminal integration in general (such as "Shell Integration Unavailable" or commands not showing output), please refer to our comprehensive [Terminal Integration Troubleshooting Guide](/troubleshooting/terminal-integration-guide).
Common issues include:
- Terminal mentions not capturing output
- "Shell Integration Unavailable" messages in Cline chat
- Commands executing but output not visible to Cline
- Terminal integration working inconsistently
The troubleshooting guide provides platform-specific solutions and detailed configuration steps to resolve these issues.
@@ -74,8 +74,25 @@ This approach ensures that all terminal output, including colors and formatting,
- **Select specific output when needed**: By default, the integration captures all terminal content, but you can also select specific lines before right-clicking to focus on just the relevant output.
- **Combine with file mentions**: After sending terminal output to Cline, you can enhance your question by mentioning relevant files using the @ mentions feature.
- **Combine terminal outputs with file mentions**: After sending terminal output to Cline, you can enhance your question by mentioning relevant files using the @ mentions feature.
- **Use for build and test output**: Terminal integration is particularly useful for understanding complex build errors or test failures that span multiple lines.
- **Contextualize build & test outputs with the terminal**: Terminal integration is particularly useful for understanding complex build errors or test failures that span multiple lines.
Next time you're staring at a cryptic error message in your terminal, try using Cline's terminal integration instead of copying and pasting. You'll get more accurate help because Cline can see the complete terminal context with proper formatting.
## Troubleshooting Terminal Issues
If you're experiencing issues with terminal integration, such as "Shell Integration Unavailable" or commands not showing output, please refer to our comprehensive [Terminal Integration Troubleshooting Guide](/troubleshooting/terminal-integration-guide).
The troubleshooting guide covers:
- Common terminal integration issues and quick fixes
- Platform-specific solutions for Windows, macOS, and Linux
- Shell-specific configurations for zsh, bash, PowerShell, and more
- Advanced debugging techniques
- Terminal settings optimization
<Tip>
**Quick Fix**: Most terminal issues can be resolved by switching to bash in the Cline settings and increasing the shell
integration timeout to 10 seconds.
</Tip>
@@ -0,0 +1,399 @@
---
title: "Terminal Integration Troubleshooting Guide"
sidebarTitle: "Terminal Troubleshooting"
description: "Complete guide to resolving terminal integration issues in Cline"
---
This guide helps you resolve terminal integration issues in Cline. Terminal integration is crucial for Cline to execute commands and read their output, enabling it to understand errors, test results, and command responses.
<Tip>
If you're experiencing terminal issues, try switching to a simpler shell like `bash` in the Cline settings, under "Terminal Settings"
This resolves most terminal integration problems.
</Tip>
## Quick Diagnosis Flowchart
Follow this flowchart to quickly identify your issue:
```mermaid
graph TD
A[Terminal Issue] --> B{Can Cline execute commands?}
B -->|No| C[Shell Integration Unavailable]
B -->|Yes| D{Can Cline see the output?}
D -->|No| E[Output Capture Failed]
D -->|Yes| F{Is the output corrupted?}
F -->|Yes| G[Character Filtering Issue]
F -->|No| H{Does the command hang?}
H -->|Yes| I[Long-Running Command Issue]
H -->|No| J[Check Terminal Settings]
C --> K[Try Solution 1]
E --> L[Try Solution 2]
G --> M[Try Solution 3]
I --> N[Try Solution 4]
style A fill:#f9f,stroke:#333,stroke-width:2px
style K fill:#9f9,stroke:#333,stroke-width:2px
style L fill:#9f9,stroke:#333,stroke-width:2px
style M fill:#9f9,stroke:#333,stroke-width:2px
style N fill:#9f9,stroke:#333,stroke-width:2px
```
## Common Issues & Quick Solutions
### 1. Shell Integration Unavailable
**Symptoms:**
- Message: "Shell Integration Unavailable"
- Commands execute but Cline can't read output
- Terminal works fine manually but not with Cline
**Quick Solutions:**
#### macOS
- **Switch to bash**
1. Go to Cline Settings
2. Left-Click the **"Terminal Settings"** tab
3. Navigate to **"Default Terminal Profile"** and select **"bash"** from the drop-down menu
- **Disable Oh-My-Zsh temporarily**:
1. If using zsh, enter `mv ~/.zshrc ~/.zshrc.backup` into the terminal
2. Restart VSCode
- **Set environment**:
1.a For Zsh users, use one of the following Zsh commands to edit your shell profile:
- `nano ~/.zshrc`
- `vim ~/.zshrc`
- `code ~/.zshrc`
1.b For Bash users
- nano ~/.bash_profile
2. Add the following to your shell config: `export TERM=xterm-256color`
3. Save your configuration
#### Windows
- **Use PowerShell 7**
1. Install from Microsoft Store
2. Go to Cline Settings
3. Left-Click the **"Terminal Settings"** tab
4. Navigate to **"Default Terminal Profile"** and select **"PowerShell 7"** from the drop-down menu
- **Disable Windows ConPTY**
1. Navigate to your VSCode Settings
2. Enter "Integrated: Windows Enable Conpty" into the Settings searchbar
3. Uncheck the option
- **Try Command Prompt**
1. Go to Cline Settings
2. Left-Click the **"Terminal Settings"** tab
3. Navigate to **"Default Terminal Profile"** and select **"Command Prompt"** from the drop-down menu
#### Linux
- **Use bash**
1. Go to Cline Settings
2. Left-Click the **"Terminal Settings"** tab
3. Navigate to **"Default Terminal Profile"** and select **"bash"** from the drop-down menu
- **Check permissions**
1. Ensure VSCode has terminal access permissions
- **Disable custom prompts**
1. Comment out prompt customizations in `.bashrc`
### 2. Command Output Not Visible
**Symptoms:**
- Cline states in chat: "[Command is running but producing no output]"
- Commands complete but Cline doesn't see results
- Commands work sometimes but not consistently
**Solutions:**
- **Increase Shell Integration Timeout**
1. Within Cline, left-click the **Settings** button in the top right-hand corner of the chat window
2. Once in the **Settings** window, left-click the **"Terminal Settings"** tab from the left-hand column
3. Navigate to "Shell integration timeout (seconds)" and enter **"10"** into the text field
- **Disable Terminal Reuse**
1. Within Cline, left-click the **Settings** button in the top right-hand corner of the chat window
2. Once in the **Settings** window, left-click the **"Terminal Settings"** tab from the left-hand column
3. Look for **"Enable aggressive terminal reuse"**, and **uncheck** this option
- **Check for interfering extensions**
1. Disable other terminal-related VSCode extensions
### 3. Character Filtering Issues
**Symptoms:**
- Commas missing from output (JSON appears corrupted)
- Special characters stripped from terminal output
- Syntax errors that don't appear when running manually
**Solution:**
This is a known bug in output processing. Workarounds:
- Recommend AI to use file output instead
1. Tell Cline in chat or Cline rules, to use `command > output.txt` before reading the file/s
<Tip>
This family of issues is only partially solved in the latest Cline versions, so if you still face this, create a GitHub issue
if it is a persistent problem.
</Tip>
### 4. Long-Running Commands & Progress Bars
**Symptoms:**
- Docker builds never complete in Cline
- Progress bars consume thousands of tokens
- The Cline button "Proceed while running" doesn't work properly in chat
<Tip>
This family of issues has been solved in latest Cline versions but if you still face any issues, then create a GitHub issue
for this.
</Tip>
## Terminal Settings Explained
Access these in Cline by clicking the settings icon, and navigating to the "Terminal Settings" section:
### Default Terminal Profile
- **What it does**: Selects which shell Cline uses for commands
- **When to change**: If experiencing shell integration issues with your default shell
- **Recommended**: - macOS: bash (if zsh has issues) - Windows: PowerShell 7 - Linux: bash
### Shell Integration Timeout
- **What it does**: How long Cline waits for the terminal to be ready
- **Default**: 4 seconds
- **When to increase**:
- Slow shell startup (heavy .zshrc/.bashrc)
- WSL environments
- SSH connections
- **Recommended**: - Start with 10 seconds if having issues
### Enable Aggressive Terminal Reuse
- **What it does**: Reuses existing terminals even if not in the correct directory
- **When to disable**:
- Commands execute in wrong directory
- Virtual environment issues
- Terminal state corruption
- **Trade-off**: - Disabling creates more terminals but ensures clean state
### Terminal Output Line Limit
- **What it does**: Limits how many lines Cline reads from terminal output
- **Default**: 500 lines
- **When to adjust**:
- Increase for verbose build outputs
- Decrease if hitting token limits
- Set to 100 for commands with progress bars
## Platform-Specific Solutions
### macOS Issues
#### Oh-My-Zsh Conflicts
Oh-My-Zsh often interferes with shell integration. Solutions:
1. Create a minimal `.zshrc` for VSCode:
```bash
# ~/.zshrc-vscode
export TERM=xterm-256color
export PAGER=cat
# Minimal PATH and environment setup
```
2. Configure VSCode to use it:
```json
{
"terminal.integrated.env.osx": {
"ZDOTDIR": "~/.zshrc-vscode"
}
}
```
#### macOS 15+ Issues
Recent macOS versions have stricter terminal permissions:
1. System Preferences → Privacy & Security → Developer Tools
2. Add Visual Studio Code
3. Restart VSCode completely
### Windows Issues
#### PowerShell Execution Policy
If commands fail silently:
```powershell
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
```
#### WSL Integration
For WSL issues:
1. Use WSL extension for VSCode
2. Open folder in WSL: `code .` from WSL terminal
3. Select "WSL Bash" as terminal profile in Cline
#### Path Issues
Windows path problems:
1. Use forward slashes in Cline: `C:/Users/...`
2. Quote paths with spaces: `"C:/Program Files/..."`
3. Avoid `~` - use full paths
### Linux/SSH/Container Issues
#### SSH Connections
For remote development:
1. Install Cline on the remote machine, not locally
2. Use SSH extension's integrated terminal
3. Increase timeout to 15+ seconds
#### Docker Containers
When developing in containers:
1. Install Cline in the container
2. Use Dev Containers extension
3. Ensure shell integration scripts are available
## Shell-Specific Fixes
### Zsh
```bash
# Add to ~/.zshrc
export TERM=xterm-256color
export PAGER=cat
# Disable fancy prompts for VSCode
if [[ "$TERM_PROGRAM" == "vscode" ]]; then
PS1="%n@%m %1~ %# "
fi
```
### Bash
```bash
# Add to ~/.bashrc
export TERM=xterm-256color
export PAGER=cat
# Simple prompt for VSCode
if [[ "$TERM_PROGRAM" == "vscode" ]]; then
PS1='\u@\h:\w\$ '
fi
```
### Fish
```fish
# Add to ~/.config/fish/config.fish
set -x TERM xterm-256color
set -x PAGER cat
# Disable fancy features in VSCode
if test "$TERM_PROGRAM" = "vscode"
function fish_prompt
echo (whoami)'@'(hostname)':'(pwd)'> '
end
end
```
### PowerShell
```powershell
# Add to $PROFILE
$env:PAGER = "cat"
# Disable progress bars
$ProgressPreference = 'SilentlyContinue'
```
## Advanced Troubleshooting
### Debug Mode
Enable terminal debugging to see what's happening:
1. Open VSCode Command Palette (Cmd/Ctrl+Shift+P)
2. Run: "Developer: Set Log Level..."
3. Choose "Trace"
4. Check Output panel → "Cline" for terminal logs
### Manual Shell Integration Test
Test if shell integration works at all:
```bash
# In VSCode terminal
echo $TERM_PROGRAM # Should show "vscode"
echo $VSCODE_SHELL_INTEGRATION # Should be "1"
```
## FAQ
### Why does Cline create so many terminals?
When shell integration fails, Cline can't reuse terminals safely (they might be running long processes). Enable shell integration or adjust the terminal reuse setting.
### Can I use my custom shell (nushell, xonsh, etc.)?
Cline officially supports bash, zsh, fish, and PowerShell. Custom shells may work but aren't guaranteed. Use bash as a fallback.
### Why do some commands work but others don't?
Commands that use interactive features (pagers, progress bars, curses) often fail. Set `PAGER=cat` and use non-interactive flags.
### How do I know if shell integration is working?
Working integration shows command output in Cline's chat. Failed integration shows "Shell Integration Unavailable" or "[Command is running but producing no output]".
## Still Having Issues?
If you've tried everything:
1. **Collect Debug Info**:
```bash
echo "Shell: $SHELL"
echo "Term: $TERM"
echo "VSCode: $TERM_PROGRAM"
which bash
bash --version
```
2. **Report the Issue**:
- Use `/reportbug` in Cline github issues
- Include your debug info
- Mention which solutions you tried
<Tip>
Remember: Most terminal issues are resolved by switching to bash and increasing the timeout. Start there before trying complex
solutions.
</Tip>
@@ -0,0 +1,51 @@
---
title: "Terminal Quick Fixes"
sidebarTitle: "Terminal Quick Fixes"
description: "Quick solutions for common terminal issues"
---
**Here is a list of common fixes, starting with the most applicable:**
- **Switch to bash** (solves most instances)
1. Within Cline, left-click the **Settings** button in the top right-hand corner of the chat window
2. Once in the **Settings** window, left-click the **"Terminal Settings"** tab from the left-hand column
3. Navigate to **"Default Terminal Profile"** and select **"bash"** from the drop-down
- **Increase timeout**
1. Within Cline, left-click the **Settings** button in the top right-hand corner of the chat window
2. Once in the **Settings** window, left-click the **"Terminal Settings"** tab from the left-hand column
3. Navigate to "Shell integration timeout (seconds)" and enter **"10"** into the text field
- **Disable terminal reuse**
1. Within Cline, left-click the **Settings** button in the top right-hand corner of the chat window
2. Once in the **Settings** window, left-click the **"Terminal Settings"** tab from the left-hand column
3. Look for **"Enable aggressive terminal reuse"**, and **uncheck** this option
## Platform-Specific Fixes
### macOS + Oh-My-Zsh
```bash
# Create minimal config for VSCode
echo 'export TERM=xterm-256color' > ~/.zshrc-vscode
echo 'export PAGER=cat' >> ~/.zshrc-vscode
```
### Windows PowerShell
```powershell
# Run as Administrator
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
```
### WSL
- Open folder from WSL: `code .`
- Select **"WSL Bash"** in Cline settings, under **"Terminal Settings"**
- Increase **"Shell integration timeout (seconds)"** to **15**
## Full Guide
For detailed troubleshooting, see the [Complete Terminal Troubleshooting Guide](/troubleshooting/terminal-integration-guide).
+60 -552
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.18.0",
"version": "3.18.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.18.0",
"version": "3.18.1",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.37.0",
@@ -45,7 +45,6 @@
"fzf": "^0.5.2",
"get-folder-size": "^5.0.0",
"globby": "^14.0.2",
"google-auth-library": "^10.1.0",
"grpc-health-check": "^2.0.2",
"iconv-lite": "^0.6.3",
"ignore": "^7.0.3",
@@ -202,75 +201,6 @@
"undici-types": "~5.26.4"
}
},
"node_modules/@anthropic-ai/vertex-sdk/node_modules/gaxios": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
"integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
"license": "Apache-2.0",
"dependencies": {
"extend": "^3.0.2",
"https-proxy-agent": "^7.0.1",
"is-stream": "^2.0.0",
"node-fetch": "^2.6.9",
"uuid": "^9.0.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@anthropic-ai/vertex-sdk/node_modules/gcp-metadata": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
"integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
"license": "Apache-2.0",
"dependencies": {
"gaxios": "^6.1.1",
"google-logging-utils": "^0.0.2",
"json-bigint": "^1.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@anthropic-ai/vertex-sdk/node_modules/google-auth-library": {
"version": "9.15.1",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
"integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
"license": "Apache-2.0",
"dependencies": {
"base64-js": "^1.3.0",
"ecdsa-sig-formatter": "^1.0.11",
"gaxios": "^6.1.1",
"gcp-metadata": "^6.1.0",
"gtoken": "^7.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@anthropic-ai/vertex-sdk/node_modules/google-logging-utils": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
"integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
}
},
"node_modules/@anthropic-ai/vertex-sdk/node_modules/gtoken": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
"integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
"license": "MIT",
"dependencies": {
"gaxios": "^6.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@asyncapi/parser": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@asyncapi/parser/-/parser-3.4.0.tgz",
@@ -2938,75 +2868,6 @@
"node": ">=18.0.0"
}
},
"node_modules/@google-cloud/vertexai/node_modules/gaxios": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
"integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
"license": "Apache-2.0",
"dependencies": {
"extend": "^3.0.2",
"https-proxy-agent": "^7.0.1",
"is-stream": "^2.0.0",
"node-fetch": "^2.6.9",
"uuid": "^9.0.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@google-cloud/vertexai/node_modules/gcp-metadata": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
"integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
"license": "Apache-2.0",
"dependencies": {
"gaxios": "^6.1.1",
"google-logging-utils": "^0.0.2",
"json-bigint": "^1.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@google-cloud/vertexai/node_modules/google-auth-library": {
"version": "9.15.1",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
"integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
"license": "Apache-2.0",
"dependencies": {
"base64-js": "^1.3.0",
"ecdsa-sig-formatter": "^1.0.11",
"gaxios": "^6.1.1",
"gcp-metadata": "^6.1.0",
"gtoken": "^7.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@google-cloud/vertexai/node_modules/google-logging-utils": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
"integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
}
},
"node_modules/@google-cloud/vertexai/node_modules/gtoken": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
"integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
"license": "MIT",
"dependencies": {
"gaxios": "^6.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@google/genai": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.0.0.tgz",
@@ -3025,75 +2886,6 @@
"@modelcontextprotocol/sdk": "^1.11.0"
}
},
"node_modules/@google/genai/node_modules/gaxios": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
"integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
"license": "Apache-2.0",
"dependencies": {
"extend": "^3.0.2",
"https-proxy-agent": "^7.0.1",
"is-stream": "^2.0.0",
"node-fetch": "^2.6.9",
"uuid": "^9.0.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@google/genai/node_modules/gcp-metadata": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
"integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
"license": "Apache-2.0",
"dependencies": {
"gaxios": "^6.1.1",
"google-logging-utils": "^0.0.2",
"json-bigint": "^1.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@google/genai/node_modules/google-auth-library": {
"version": "9.15.1",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
"integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
"license": "Apache-2.0",
"dependencies": {
"base64-js": "^1.3.0",
"ecdsa-sig-formatter": "^1.0.11",
"gaxios": "^6.1.1",
"gcp-metadata": "^6.1.0",
"gtoken": "^7.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@google/genai/node_modules/google-logging-utils": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
"integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
}
},
"node_modules/@google/genai/node_modules/gtoken": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
"integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
"license": "MIT",
"dependencies": {
"gaxios": "^6.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@grpc/grpc-js": {
"version": "1.9.15",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz",
@@ -9810,10 +9602,9 @@
}
},
"node_modules/bignumber.js": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz",
"integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==",
"license": "MIT",
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz",
"integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==",
"engines": {
"node": "*"
}
@@ -9999,8 +9790,7 @@
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
},
"node_modules/buffer-indexof-polyfill": {
"version": "1.0.2",
@@ -11658,7 +11448,6 @@
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
@@ -13297,29 +13086,6 @@
"pend": "~1.2.0"
}
},
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"dependencies": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
},
"engines": {
"node": "^12.20 || >= 14.13"
}
},
"node_modules/figures": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
@@ -13656,18 +13422,6 @@
"node": ">= 14"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"license": "MIT",
"dependencies": {
"fetch-blob": "^3.1.2"
},
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -13934,44 +13688,18 @@
}
},
"node_modules/gaxios": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.1.tgz",
"integrity": "sha512-Odju3uBUJyVCkW64nLD4wKLhbh93bh6vIg/ZIXkWiLPBrdgtc65+tls/qml+un3pr6JqYVFDZbbmLDQT68rTOQ==",
"license": "Apache-2.0",
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
"integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
"dependencies": {
"extend": "^3.0.2",
"https-proxy-agent": "^7.0.1",
"node-fetch": "^3.3.2"
"is-stream": "^2.0.0",
"node-fetch": "^2.6.9",
"uuid": "^9.0.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/gaxios/node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/gaxios/node_modules/node-fetch": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"license": "MIT",
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
"node": ">=14"
}
},
"node_modules/gcd": {
@@ -13981,17 +13709,15 @@
"dev": true
},
"node_modules/gcp-metadata": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-7.0.1.tgz",
"integrity": "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==",
"license": "Apache-2.0",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz",
"integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==",
"dependencies": {
"gaxios": "^7.0.0",
"google-logging-utils": "^1.0.0",
"gaxios": "^6.0.0",
"json-bigint": "^1.0.0"
},
"engines": {
"node": ">=18"
"node": ">=14"
}
},
"node_modules/get-caller-file": {
@@ -14264,28 +13990,18 @@
}
},
"node_modules/google-auth-library": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.1.0.tgz",
"integrity": "sha512-GspVjZj1RbyRWpQ9FbAXMKjFGzZwDKnUHi66JJ+tcjcu5/xYAP1pdlWotCuIkMwjfVsxxDvsGZXGLzRt72D0sQ==",
"version": "9.15.1",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
"integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
"license": "Apache-2.0",
"dependencies": {
"base64-js": "^1.3.0",
"ecdsa-sig-formatter": "^1.0.11",
"gaxios": "^7.0.0",
"gcp-metadata": "^7.0.0",
"google-logging-utils": "^1.0.0",
"gtoken": "^8.0.0",
"gaxios": "^6.1.1",
"gcp-metadata": "^6.1.0",
"gtoken": "^7.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/google-logging-utils": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.1.tgz",
"integrity": "sha512-rcX58I7nqpu4mbKztFeOAObbomBbHU2oIb/d3tJfF3dizGSApqtSwYJigGCooHdnMyQBIw8BrWyK96w3YXgr6A==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
}
@@ -14422,16 +14138,15 @@
}
},
"node_modules/gtoken": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz",
"integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==",
"license": "MIT",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
"integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
"dependencies": {
"gaxios": "^7.0.0",
"gaxios": "^6.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=18"
"node": ">=14.0.0"
}
},
"node_modules/has-bigints": {
@@ -16030,7 +15745,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
"license": "MIT",
"dependencies": {
"bignumber.js": "^9.0.0"
}
@@ -16144,12 +15858,11 @@
"license": "MIT"
},
"node_modules/jwa": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
"license": "MIT",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
"integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
@@ -16158,7 +15871,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
"license": "MIT",
"dependencies": {
"jwa": "^2.0.0",
"safe-buffer": "^5.0.1"
@@ -23688,15 +23400,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/web-tree-sitter": {
"version": "0.22.6",
"resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.22.6.tgz",
@@ -24457,55 +24160,6 @@
"requires": {
"undici-types": "~5.26.4"
}
},
"gaxios": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
"integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
"requires": {
"extend": "^3.0.2",
"https-proxy-agent": "^7.0.1",
"is-stream": "^2.0.0",
"node-fetch": "^2.6.9",
"uuid": "^9.0.1"
}
},
"gcp-metadata": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
"integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
"requires": {
"gaxios": "^6.1.1",
"google-logging-utils": "^0.0.2",
"json-bigint": "^1.0.0"
}
},
"google-auth-library": {
"version": "9.15.1",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
"integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
"requires": {
"base64-js": "^1.3.0",
"ecdsa-sig-formatter": "^1.0.11",
"gaxios": "^6.1.1",
"gcp-metadata": "^6.1.0",
"gtoken": "^7.0.0",
"jws": "^4.0.0"
}
},
"google-logging-utils": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
"integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="
},
"gtoken": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
"integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
"requires": {
"gaxios": "^6.0.0",
"jws": "^4.0.0"
}
}
}
},
@@ -26762,57 +26416,6 @@
"integrity": "sha512-35o5tIEMLW3JeFJOaaMNR2e5sq+6rpnhrF97PuAxeOm0GlqVTESKhkGj7a5B5mmJSSSU3hUfIhcQCRRsw4Ipzg==",
"requires": {
"google-auth-library": "^9.1.0"
},
"dependencies": {
"gaxios": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
"integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
"requires": {
"extend": "^3.0.2",
"https-proxy-agent": "^7.0.1",
"is-stream": "^2.0.0",
"node-fetch": "^2.6.9",
"uuid": "^9.0.1"
}
},
"gcp-metadata": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
"integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
"requires": {
"gaxios": "^6.1.1",
"google-logging-utils": "^0.0.2",
"json-bigint": "^1.0.0"
}
},
"google-auth-library": {
"version": "9.15.1",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
"integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
"requires": {
"base64-js": "^1.3.0",
"ecdsa-sig-formatter": "^1.0.11",
"gaxios": "^6.1.1",
"gcp-metadata": "^6.1.0",
"gtoken": "^7.0.0",
"jws": "^4.0.0"
}
},
"google-logging-utils": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
"integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="
},
"gtoken": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
"integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
"requires": {
"gaxios": "^6.0.0",
"jws": "^4.0.0"
}
}
}
},
"@google/genai": {
@@ -26824,57 +26427,6 @@
"ws": "^8.18.0",
"zod": "^3.22.4",
"zod-to-json-schema": "^3.22.4"
},
"dependencies": {
"gaxios": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
"integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
"requires": {
"extend": "^3.0.2",
"https-proxy-agent": "^7.0.1",
"is-stream": "^2.0.0",
"node-fetch": "^2.6.9",
"uuid": "^9.0.1"
}
},
"gcp-metadata": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
"integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
"requires": {
"gaxios": "^6.1.1",
"google-logging-utils": "^0.0.2",
"json-bigint": "^1.0.0"
}
},
"google-auth-library": {
"version": "9.15.1",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
"integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
"requires": {
"base64-js": "^1.3.0",
"ecdsa-sig-formatter": "^1.0.11",
"gaxios": "^6.1.1",
"gcp-metadata": "^6.1.0",
"gtoken": "^7.0.0",
"jws": "^4.0.0"
}
},
"google-logging-utils": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
"integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="
},
"gtoken": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
"integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
"requires": {
"gaxios": "^6.0.0",
"jws": "^4.0.0"
}
}
}
},
"@grpc/grpc-js": {
@@ -31658,9 +31210,9 @@
"integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg=="
},
"bignumber.js": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz",
"integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA=="
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz",
"integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug=="
},
"binary": {
"version": "0.3.0",
@@ -34012,15 +33564,6 @@
"pend": "~1.2.0"
}
},
"fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"requires": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
}
},
"figures": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
@@ -34256,14 +33799,6 @@
}
}
},
"formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"requires": {
"fetch-blob": "^3.1.2"
}
},
"forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -34449,30 +33984,15 @@
}
},
"gaxios": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.1.tgz",
"integrity": "sha512-Odju3uBUJyVCkW64nLD4wKLhbh93bh6vIg/ZIXkWiLPBrdgtc65+tls/qml+un3pr6JqYVFDZbbmLDQT68rTOQ==",
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
"integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
"requires": {
"extend": "^3.0.2",
"https-proxy-agent": "^7.0.1",
"node-fetch": "^3.3.2"
},
"dependencies": {
"data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="
},
"node-fetch": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"requires": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
}
}
"is-stream": "^2.0.0",
"node-fetch": "^2.6.9",
"uuid": "^9.0.1"
}
},
"gcd": {
@@ -34482,12 +34002,11 @@
"dev": true
},
"gcp-metadata": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-7.0.1.tgz",
"integrity": "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz",
"integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==",
"requires": {
"gaxios": "^7.0.0",
"google-logging-utils": "^1.0.0",
"gaxios": "^6.0.0",
"json-bigint": "^1.0.0"
}
},
@@ -34661,24 +34180,18 @@
}
},
"google-auth-library": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.1.0.tgz",
"integrity": "sha512-GspVjZj1RbyRWpQ9FbAXMKjFGzZwDKnUHi66JJ+tcjcu5/xYAP1pdlWotCuIkMwjfVsxxDvsGZXGLzRt72D0sQ==",
"version": "9.15.1",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
"integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
"requires": {
"base64-js": "^1.3.0",
"ecdsa-sig-formatter": "^1.0.11",
"gaxios": "^7.0.0",
"gcp-metadata": "^7.0.0",
"google-logging-utils": "^1.0.0",
"gtoken": "^8.0.0",
"gaxios": "^6.1.1",
"gcp-metadata": "^6.1.0",
"gtoken": "^7.0.0",
"jws": "^4.0.0"
}
},
"google-logging-utils": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.1.tgz",
"integrity": "sha512-rcX58I7nqpu4mbKztFeOAObbomBbHU2oIb/d3tJfF3dizGSApqtSwYJigGCooHdnMyQBIw8BrWyK96w3YXgr6A=="
},
"gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -34779,11 +34292,11 @@
}
},
"gtoken": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz",
"integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
"integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
"requires": {
"gaxios": "^7.0.0",
"gaxios": "^6.0.0",
"jws": "^4.0.0"
}
},
@@ -35923,11 +35436,11 @@
"dev": true
},
"jwa": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
"integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==",
"requires": {
"buffer-equal-constant-time": "^1.0.1",
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
@@ -41118,11 +40631,6 @@
"integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==",
"dev": true
},
"web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="
},
"web-tree-sitter": {
"version": "0.22.6",
"resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.22.6.tgz",
+3 -4
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.18.0",
"version": "3.18.1",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -332,14 +332,14 @@
"package": "npm run check-types && npm run build:webview && npm run lint && node esbuild.js --production",
"protos": "node proto/build-proto.js && node scripts/generate-server-setup.mjs && node scripts/generate-host-bridge-client.mjs",
"postprotos": "prettier src/shared/proto src/core/controller src/hosts/ webview-ui/src/services src/generated --write --log-level warn",
"clean": "rimraf dist dist-standalone webview-ui/build src/generated",
"clean": "rimraf dist dist-standalone webview-ui/build src/generated out/",
"compile-tests": "node ./scripts/build-tests.js",
"watch-tests": "tsc -p . -w --outDir out",
"pretest": "npm run compile-tests && npm run compile && npm run compile-standalone && npm run lint",
"check-types": "npm run protos && tsc --noEmit",
"lint": "eslint src --ext ts && eslint webview-ui/src --ext ts && buf lint && cd webview-ui && npm run lint",
"format": "prettier . --check",
"format:fix": "prettier . --write",
"pretest": "npm run compile && npm run compile-tests && npm run compile-standalone && npm run lint",
"test": "npm-run-all test:unit test:integration",
"test:ci": "node scripts/test-ci.js",
"test:integration": "vscode-test",
@@ -442,7 +442,6 @@
"fzf": "^0.5.2",
"get-folder-size": "^5.0.0",
"globby": "^14.0.2",
"google-auth-library": "^10.1.0",
"grpc-health-check": "^2.0.2",
"iconv-lite": "^0.6.3",
"ignore": "^7.0.3",
+19 -22
View File
@@ -105,25 +105,24 @@ enum ApiProvider {
OLLAMA = 5;
LMSTUDIO = 6;
GEMINI = 7;
GEMINI_CLI = 8;
OPENAI_NATIVE = 9;
REQUESTY = 10;
TOGETHER = 11;
DEEPSEEK = 12;
QWEN = 13;
DOUBAO = 14;
MISTRAL = 15;
VSCODE_LM = 16;
CLINE = 17;
LITELLM = 18;
NEBIUS = 19;
FIREWORKS = 20;
ASKSAGE = 21;
XAI = 22;
SAMBANOVA = 23;
CEREBRAS = 24;
SAPAICORE = 25;
CLAUDE_CODE = 26;
OPENAI_NATIVE = 8;
REQUESTY = 9;
TOGETHER = 10;
DEEPSEEK = 11;
QWEN = 12;
DOUBAO = 13;
MISTRAL = 14;
VSCODE_LM = 15;
CLINE = 16;
LITELLM = 17;
NEBIUS = 18;
FIREWORKS = 19;
ASKSAGE = 20;
XAI = 21;
SAMBANOVA = 22;
CEREBRAS = 23;
SAPAICORE = 24;
CLAUDE_CODE = 25;
}
// Model info for OpenAI-compatible models
@@ -237,6 +236,4 @@ message ModelsApiConfiguration {
optional string sap_ai_core_token_url = 71;
optional string sap_ai_core_base_url = 72;
optional string claude_code_path = 73;
optional string gemini_cli_oauth_path = 74;
optional string gemini_cli_project_id = 75;
}
}
-3
View File
@@ -8,7 +8,6 @@ import { OpenAiHandler } from "./providers/openai"
import { OllamaHandler } from "./providers/ollama"
import { LmStudioHandler } from "./providers/lmstudio"
import { GeminiHandler } from "./providers/gemini"
import { GeminiCliHandler } from "./providers/gemini-cli"
import { OpenAiNativeHandler } from "./providers/openai-native"
import { ApiStream, ApiStreamUsageChunk } from "./transform/stream"
import { DeepSeekHandler } from "./providers/deepseek"
@@ -57,8 +56,6 @@ function createHandlerForProvider(apiProvider: string | undefined, options: any)
return new LmStudioHandler(options)
case "gemini":
return new GeminiHandler(options)
case "gemini-cli":
return new GeminiCliHandler(options)
case "openai-native":
return new OpenAiNativeHandler(options)
case "deepseek":
-418
View File
@@ -1,418 +0,0 @@
/**
* Gemini CLI Provider - OAuth-based API Handler
*
* This implementation provides access to Google's Gemini models through OAuth authentication,
* leveraging the same authentication mechanism as the official Gemini CLI tool.
*
* Attribution: This implementation is inspired by and uses concepts from the Google Gemini CLI,
* which is licensed under the Apache License 2.0.
* Original project: https://github.com/google-gemini/gemini-cli
*
* Copyright 2025 Google LLC
* Licensed under the Apache License, Version 2.0
*
* Key features:
* - OAuth2 authentication (no API keys required)
* - Auto-discovery of Google Cloud project IDs
* - Real-time streaming via Server-Sent Events
* - Free tier access through Google's Code Assist API
* - Compatible with personal Google accounts only
*/
import type { Anthropic } from "@anthropic-ai/sdk"
import { OAuth2Client } from "google-auth-library"
import fs from "fs/promises"
import path from "path"
import os from "os"
import * as readline from "readline"
import { Readable } from "stream"
import { ApiHandler } from "../"
import { ApiHandlerOptions, GeminiCliModelId, geminiCliModels, ModelInfo, geminiCliDefaultModelId } from "@shared/api"
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
import { ApiStream } from "../transform/stream"
import { withRetry } from "../retry"
const CODE_ASSIST_ENDPOINT = "https://cloudcode-pa.googleapis.com"
const CODE_ASSIST_API_VERSION = "v1internal"
// OAuth configuration
const OAUTH_CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com"
// Change this line in setup.js:
const OAUTH_CLIENT_SECRET = "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"
const OAUTH_REDIRECT_URI = "http://localhost:45289"
interface OAuthCredentials {
access_token: string
refresh_token: string
scope: string
token_type: string
expiry_date: number
}
interface GeminiCliHandlerOptions extends ApiHandlerOptions {
geminiCliOAuthPath?: string
geminiCliProjectId?: string
}
/**
* Handler for Google's Gemini API via OAuth (Gemini CLI style).
*
* This provider uses OAuth authentication instead of API keys, making it suitable
* for users who have already authenticated with the Gemini CLI tool.
* It automatically discovers project IDs and works with the free tier.
*/
export class GeminiCliHandler implements ApiHandler {
private options: GeminiCliHandlerOptions
private authClient: OAuth2Client
private projectId: string | null = null
private authInitialized: boolean = false
constructor(options: GeminiCliHandlerOptions) {
this.options = options
this.authClient = new OAuth2Client(OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, OAUTH_REDIRECT_URI)
}
/**
* Load OAuth credentials from the file system
*/
private async loadOAuthCredentials(): Promise<OAuthCredentials> {
const credPath = this.options.geminiCliOAuthPath || path.join(os.homedir(), ".gemini", "oauth_creds.json")
try {
const data = await fs.readFile(credPath, "utf8")
return JSON.parse(data)
} catch (err) {
throw new Error(`Failed to load OAuth credentials from ${credPath}. Please authenticate with 'gemini auth' first.`)
}
}
/**
* Call a Code Assist API endpoint
*/
private async callEndpoint(method: string, body: any, retryAuth: boolean = true): Promise<any> {
console.log(`[GeminiCLI] Calling endpoint: ${method}`)
console.log(`[GeminiCLI] Request body:`, JSON.stringify(body, null, 2))
try {
const res = await this.authClient.request({
url: `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:${method}`,
method: "POST",
headers: {
"Content-Type": "application/json",
},
responseType: "json",
body: JSON.stringify(body),
})
console.log(`[GeminiCLI] Response status:`, res.status)
console.log(`[GeminiCLI] Response data:`, JSON.stringify(res.data, null, 2))
return res.data
} catch (error: any) {
console.error(`[GeminiCLI] Error calling ${method}:`, error)
console.error(`[GeminiCLI] Error response:`, error.response?.data)
console.error(`[GeminiCLI] Error status:`, error.response?.status)
console.error(`[GeminiCLI] Error message:`, error.message)
// If we get a 401 and haven't retried yet, try refreshing auth
if (error.response?.status === 401 && retryAuth) {
console.log(`[GeminiCLI] Got 401, attempting to refresh authentication...`)
await this.initializeAuth(true) // Force refresh
return this.callEndpoint(method, body, false) // Retry without further auth retries
}
throw error
}
}
/**
* Discover or retrieve the project ID
*/
private async discoverProjectId(): Promise<string> {
// If we already have a project ID, use it
if (this.options.geminiCliProjectId) {
return this.options.geminiCliProjectId
}
// If we've already discovered it, return it
if (this.projectId) {
return this.projectId
}
// Start with a default project ID (can be anything for personal OAuth)
const initialProjectId = "default"
// Prepare client metadata
const clientMetadata = {
ideType: "IDE_UNSPECIFIED",
platform: "PLATFORM_UNSPECIFIED",
pluginType: "GEMINI",
duetProject: initialProjectId,
}
try {
// Call loadCodeAssist to discover the actual project ID
const loadRequest = {
cloudaicompanionProject: initialProjectId,
metadata: clientMetadata,
}
const loadResponse = await this.callEndpoint("loadCodeAssist", loadRequest)
// Check if we already have a project ID from the response
if (loadResponse.cloudaicompanionProject) {
this.projectId = loadResponse.cloudaicompanionProject
return this.projectId as string
}
// If no existing project, we need to onboard
const defaultTier = loadResponse.allowedTiers?.find((tier: any) => tier.isDefault)
const tierId = defaultTier?.id || "free-tier"
const onboardRequest = {
tierId: tierId,
cloudaicompanionProject: initialProjectId,
metadata: clientMetadata,
}
let lroResponse = await this.callEndpoint("onboardUser", onboardRequest)
// Poll until operation is complete
while (!lroResponse.done) {
await new Promise((resolve) => setTimeout(resolve, 2000))
lroResponse = await this.callEndpoint("onboardUser", onboardRequest)
}
const discoveredProjectId = lroResponse.response?.cloudaicompanionProject?.id || initialProjectId
this.projectId = discoveredProjectId
return this.projectId as string
} catch (error: any) {
console.error("Failed to discover project ID:", error.response?.data || error.message)
throw new Error("Could not discover project ID. Make sure you're authenticated with 'gemini auth'.")
}
}
/**
* Initialize the OAuth client with credentials
*/
private async initializeAuth(forceRefresh: boolean = false): Promise<void> {
// Check if we need to initialize or refresh
if (this.authInitialized && !forceRefresh) {
// Check if token is still valid
const credentials = this.authClient.credentials
if (credentials && credentials.expiry_date && Date.now() < credentials.expiry_date) {
console.log(`[GeminiCLI] Auth already initialized and token still valid`)
return
}
}
console.log(`[GeminiCLI] Initializing OAuth authentication...`)
const credentials = await this.loadOAuthCredentials()
const isExpired = credentials.expiry_date ? Date.now() > credentials.expiry_date : false
console.log(`[GeminiCLI] Loaded credentials:`, {
hasAccessToken: !!credentials.access_token,
hasRefreshToken: !!credentials.refresh_token,
tokenType: credentials.token_type,
expiryDate: credentials.expiry_date,
isExpired: isExpired,
})
this.authClient.setCredentials(credentials)
// If token is expired and we have a refresh token, try to refresh
if (isExpired && credentials.refresh_token) {
console.log(`[GeminiCLI] Token expired, attempting to refresh...`)
try {
const { credentials: newCredentials } = await this.authClient.refreshAccessToken()
console.log(`[GeminiCLI] Token refreshed successfully`)
// Note: In a real implementation, you'd want to save the new credentials back to the file
// For now, we'll just use them in memory
} catch (error) {
console.error(`[GeminiCLI] Failed to refresh token:`, error)
// Continue with the expired token - the API might still accept it
}
}
this.authInitialized = true
console.log(`[GeminiCLI] OAuth client configured`)
}
/**
* Parse Server-Sent Events from a stream
*/
private async *parseSSEStream(stream: Readable): AsyncGenerator<any> {
const rl = readline.createInterface({
input: stream,
crlfDelay: Infinity,
})
let bufferedLines: string[] = []
for await (const line of rl) {
// Blank lines separate JSON objects in the stream
if (line === "") {
if (bufferedLines.length === 0) {
continue
}
try {
const jsonData = JSON.parse(bufferedLines.join("\n"))
yield jsonData
} catch (parseError) {
console.error("Error parsing JSON chunk:", parseError)
}
bufferedLines = []
} else if (line.startsWith("data: ")) {
bufferedLines.push(line.slice(6).trim())
}
}
// Process any remaining buffered content
if (bufferedLines.length > 0) {
try {
const jsonData = JSON.parse(bufferedLines.join("\n"))
yield jsonData
} catch (parseError) {
console.error("Error parsing final buffered content:", parseError)
}
}
}
/**
* Create a message using the Gemini CLI OAuth API
*/
@withRetry({
maxRetries: 2,
baseDelay: 2000,
maxDelay: 10000,
})
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
// Initialize auth if not already done
await this.initializeAuth()
// Discover project ID if needed
const projectId = await this.discoverProjectId()
// Convert messages to Gemini format
const contents = messages.map(convertAnthropicMessageToGemini)
// Get the selected model
const { id: modelId, info: modelInfo } = this.getModel()
// Build the request
const streamRequest = {
model: modelId,
project: projectId,
request: {
contents: [
{
role: "user",
parts: [{ text: systemPrompt }],
},
...contents,
],
generationConfig: {
temperature: 0.7,
maxOutputTokens: modelInfo.maxTokens || 8192,
},
},
}
let totalContent = ""
let promptTokens = 0
let outputTokens = 0
let lastUsageMetadata: any = null
try {
// Make the streaming request
const response = await this.authClient.request({
url: `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:streamGenerateContent`,
method: "POST",
params: { alt: "sse" },
headers: {
"Content-Type": "application/json",
},
responseType: "stream",
body: JSON.stringify(streamRequest),
})
// Process the SSE stream
for await (const jsonData of this.parseSSEStream(response.data as Readable)) {
// Extract content from the response
const candidate = jsonData.response?.candidates?.[0]
if (candidate?.content?.parts?.[0]?.text) {
const content = candidate.content.parts[0].text
totalContent += content
// Yield text chunk
yield {
type: "text",
text: content,
}
}
// Store usage metadata for final reporting
if (jsonData.response?.usageMetadata) {
lastUsageMetadata = jsonData.response.usageMetadata
promptTokens = lastUsageMetadata.promptTokenCount || promptTokens
outputTokens = lastUsageMetadata.candidatesTokenCount || outputTokens
}
// Check if this is the final chunk
if (candidate?.finishReason) {
break
}
}
// Yield usage information
if (lastUsageMetadata) {
yield {
type: "usage",
inputTokens: promptTokens,
outputTokens: outputTokens,
totalCost: 0, // Free tier
}
}
} catch (error) {
// Handle rate limit errors similar to the Gemini provider
if (error instanceof Error) {
// Check for rate limit patterns in the error message
const rateLimitPatterns = [
/got status: 429/i,
/429 Too Many Requests/i,
/rate limit exceeded/i,
/too many requests/i,
/quota exceeded/i,
/resource exhausted/i,
/code 429/i,
]
const isRateLimit = rateLimitPatterns.some((pattern) => pattern.test(error.message))
if (isRateLimit) {
const rateLimitError = Object.assign(new Error(error.message), {
...error,
status: 429,
})
throw rateLimitError
}
}
// Re-throw the original error if it's not a rate limit error
throw error
}
}
/**
* Get the model ID and info
*/
getModel(): { id: GeminiCliModelId; info: ModelInfo } {
const modelId = this.options.apiModelId as GeminiCliModelId
if (modelId && modelId in geminiCliModels) {
return { id: modelId, info: geminiCliModels[modelId] }
}
return {
id: geminiCliDefaultModelId,
info: geminiCliModels[geminiCliDefaultModelId],
}
}
}
+12 -2
View File
@@ -122,6 +122,8 @@ export class SapAiCoreHandler implements ApiHandler {
const deploymentId = await this.getDeploymentForModel(model.id)
const anthropicModels = [
"anthropic--claude-4-sonnet",
"anthropic--claude-4-opus",
"anthropic--claude-3.7-sonnet",
"anthropic--claude-3.5-sonnet",
"anthropic--claude-3-sonnet",
@@ -136,7 +138,11 @@ export class SapAiCoreHandler implements ApiHandler {
if (anthropicModels.includes(model.id)) {
url = `${this.options.sapAiCoreBaseUrl}/v2/inference/deployments/${deploymentId}/invoke-with-response-stream`
if (model.id === "anthropic--claude-3.7-sonnet") {
if (
model.id === "anthropic--claude-4-sonnet" ||
model.id === "anthropic--claude-4-opus" ||
model.id === "anthropic--claude-3.7-sonnet"
) {
url = `${this.options.sapAiCoreBaseUrl}/v2/inference/deployments/${deploymentId}/converse-stream`
payload = {
inferenceConfig: {
@@ -221,7 +227,11 @@ export class SapAiCoreHandler implements ApiHandler {
}
} else if (openAIModels.includes(model.id)) {
yield* this.streamCompletionGPT(response.data, model)
} else if (model.id === "anthropic--claude-3.7-sonnet") {
} else if (
model.id === "anthropic--claude-4-sonnet" ||
model.id === "anthropic--claude-4-opus" ||
model.id === "anthropic--claude-3.7-sonnet"
) {
yield* this.streamCompletionSonnet37(response.data, model)
} else {
yield* this.streamCompletion(response.data, model)
@@ -6,6 +6,9 @@ import * as path from "path"
import { FileContextTracker } from "./FileContextTracker"
import * as diskModule from "@core/storage/disk"
import type { TaskMetadata, FileMetadataEntry } from "./ContextTrackerTypes"
import type { WebviewProviderCreator } from "@/hosts/host-providers"
import * as hostProviders from "@hosts/host-providers"
import { vscodeHostBridgeClient } from "@/hosts/vscode/client/host-grpc-client"
describe("FileContextTracker", () => {
let sandbox: sinon.SinonSandbox
@@ -37,7 +40,6 @@ describe("FileContextTracker", () => {
}
// Use a function replacement instead of a direct stub
const originalCreateFileSystemWatcher = vscode.workspace.createFileSystemWatcher
vscode.workspace.createFileSystemWatcher = function () {
return mockFileSystemWatcher
}
@@ -51,6 +53,7 @@ describe("FileContextTracker", () => {
mockTaskMetadata = { files_in_context: [], model_usage: [] }
getTaskMetadataStub = sandbox.stub(diskModule, "getTaskMetadata").resolves(mockTaskMetadata)
saveTaskMetadataStub = sandbox.stub(diskModule, "saveTaskMetadata").resolves()
hostProviders.initializeHostProviders(((_) => {}) as WebviewProviderCreator, vscodeHostBridgeClient)
// Create tracker instance
taskId = "test-task-id"
@@ -5,6 +5,8 @@ import { getWorkspaceState, updateWorkspaceState } from "@core/storage/state"
import { getGlobalState } from "@core/storage/state"
import type { FileMetadataEntry } from "./ContextTrackerTypes"
import type { ClineMessage } from "@shared/ExtensionMessage"
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { getCwd } from "@/utils/path"
// This class is responsible for tracking file operations that may result in stale context.
// If a user modifies a file outside of Cline, the context may become stale and need to be updated.
@@ -37,8 +39,8 @@ export class FileContextTracker {
/**
* Gets the current working directory or returns undefined if it cannot be determined
*/
private getCwd(): string | undefined {
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
private async getCwd(): Promise<string | undefined> {
const cwd = await getCwd(undefined)
if (!cwd) {
console.info("No workspace folder available - cannot determine current working directory")
}
@@ -54,7 +56,7 @@ export class FileContextTracker {
return
}
const cwd = this.getCwd()
const cwd = await this.getCwd()
if (!cwd) {
return
}
@@ -85,7 +87,7 @@ export class FileContextTracker {
*/
async trackFileContext(filePath: string, operation: "read_tool" | "user_edited" | "cline_edited" | "file_mentioned") {
try {
const cwd = this.getCwd()
const cwd = await this.getCwd()
if (!cwd) {
return
}
+1 -1
View File
@@ -13,7 +13,7 @@ import { convertGitCommitsToProtoGitCommits } from "@shared/proto-conversions/fi
* @returns GitCommits containing the matching commits
*/
export const searchCommits: FileMethodHandler = async (controller: Controller, request: StringRequest): Promise<GitCommits> => {
const cwd = getWorkspacePath()
const cwd = await getWorkspacePath()
if (!cwd) {
return GitCommits.create({ commits: [] })
}
+2 -2
View File
@@ -12,10 +12,10 @@ import { convertSearchResultsToProtoFileInfos } from "@shared/proto-conversions/
* @returns Results containing matching files/folders
*/
export const searchFiles: FileMethodHandler = async (
controller: Controller,
_controller: Controller,
request: FileSearchRequest,
): Promise<FileSearchResults> => {
const workspacePath = getWorkspacePath()
const workspacePath = await getWorkspacePath()
if (!workspacePath) {
// Handle case where workspace path is not available
+1 -1
View File
@@ -37,6 +37,7 @@ export class ServiceRegistry {
* @param serviceName The name of the service (used for logging)
*/
constructor(serviceName: string) {
console.log(`Registering Protobus service: ${serviceName}...`)
this.serviceName = serviceName
}
@@ -56,7 +57,6 @@ export class ServiceRegistry {
}
this.methodMetadata[methodName] = { isStreaming, ...metadata }
console.log(`Registered ${this.serviceName} method: ${methodName}${isStreaming ? " (streaming)" : ""}`)
}
/**
+30 -35
View File
@@ -1,31 +1,31 @@
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
import { getCwd } from "@/utils/path"
import { Anthropic } from "@anthropic-ai/sdk"
import { buildApiHandler } from "@api/index"
import { cleanupLegacyCheckpoints } from "@integrations/checkpoints/CheckpointMigration"
import { extractCommitMessage } from "@integrations/git/commit-message-generator"
import { downloadTask } from "@integrations/misc/export-markdown"
import WorkspaceTracker from "@integrations/workspace/WorkspaceTracker"
import { ClineAccountService } from "@services/account/ClineAccountService"
import { McpHub } from "@services/mcp/McpHub"
import { ApiProvider, ModelInfo } from "@shared/api"
import { ChatContent } from "@shared/ChatContent"
import { ChatSettings, StoredChatSettings } from "@shared/ChatSettings"
import { ClineRulesToggles } from "@shared/cline-rules"
import { ExtensionMessage, ExtensionState, Platform } from "@shared/ExtensionMessage"
import { HistoryItem } from "@shared/HistoryItem"
import { McpMarketplaceCatalog } from "@shared/mcp"
import { TelemetrySetting } from "@shared/TelemetrySetting"
import { UserInfo } from "@shared/UserInfo"
import { WebviewMessage } from "@shared/WebviewMessage"
import { fileExistsAtPath } from "@utils/fs"
import { getWorkingState } from "@utils/git"
import axios from "axios"
import { v4 as uuidv4 } from "uuid"
import fs from "fs/promises"
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import pWaitFor from "p-wait-for"
import * as path from "path"
import * as vscode from "vscode"
import { handleGrpcRequest, handleGrpcRequestCancel } from "./grpc-handler"
import { buildApiHandler } from "@api/index"
import { cleanupLegacyCheckpoints } from "@integrations/checkpoints/CheckpointMigration"
import { downloadTask } from "@integrations/misc/export-markdown"
import WorkspaceTracker from "@integrations/workspace/WorkspaceTracker"
import { ClineAccountService } from "@services/account/ClineAccountService"
import { McpHub } from "@services/mcp/McpHub"
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
import { ApiProvider, ModelInfo } from "@shared/api"
import { ChatContent } from "@shared/ChatContent"
import { ChatSettings, StoredChatSettings } from "@shared/ChatSettings"
import { ExtensionMessage, ExtensionState, Platform } from "@shared/ExtensionMessage"
import { HistoryItem } from "@shared/HistoryItem"
import { McpMarketplaceCatalog } from "@shared/mcp"
import { UserInfo } from "@shared/UserInfo"
import { TelemetrySetting } from "@shared/TelemetrySetting"
import { WebviewMessage } from "@shared/WebviewMessage"
import { fileExistsAtPath } from "@utils/fs"
import { getWorkingState } from "@utils/git"
import { extractCommitMessage } from "@integrations/git/commit-message-generator"
import { ensureMcpServersDirectoryExists, ensureSettingsDirectoryExists, GlobalFileNames } from "../storage/disk"
import {
getAllExtensionState,
@@ -33,19 +33,15 @@ import {
getSecret,
getWorkspaceState,
storeSecret,
updateApiConfiguration,
updateGlobalState,
updateWorkspaceState,
} from "../storage/state"
import { Task } from "../task"
import { ClineRulesToggles } from "@shared/cline-rules"
import { sendAuthCallbackEvent } from "./account/subscribeToAuthCallback"
import { handleGrpcRequest, handleGrpcRequestCancel } from "./grpc-handler"
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
import { sendStateUpdate } from "./state/subscribeToState"
import { sendAddToInputEvent } from "./ui/subscribeToAddToInput"
import { sendAuthCallbackEvent } from "./account/subscribeToAuthCallback"
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
import { sendRelinquishControlEvent } from "./ui/subscribeToRelinquishControl"
import { handleTaskServiceRequest } from "./task"
import { BooleanRequest } from "@shared/proto/common"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -175,7 +171,6 @@ export class Controller {
this.workspaceTracker,
(historyItem) => this.updateTaskHistory(historyItem),
() => this.postStateToWebview(),
(message) => this.postMessageToWebview(message),
(taskId) => this.reinitExistingTaskFromId(taskId),
() => this.cancelTask(),
apiConfiguration,
@@ -683,8 +678,8 @@ export class Controller {
// Context menus and code actions
getFileMentionFromPath(filePath: string) {
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
async getFileMentionFromPath(filePath: string) {
const cwd = await getCwd()
if (!cwd) {
return "@/" + filePath
}
@@ -699,7 +694,7 @@ export class Controller {
await setTimeoutPromise(100)
// Post message to webview with the selected code
const fileMention = this.getFileMentionFromPath(filePath)
const fileMention = await this.getFileMentionFromPath(filePath)
let input = `${fileMention}\n\`\`\`\n${code}\n\`\`\``
if (diagnostics) {
@@ -736,7 +731,7 @@ export class Controller {
await vscode.commands.executeCommand("claude-dev.SidebarProvider.focus")
await setTimeoutPromise(100)
const fileMention = this.getFileMentionFromPath(filePath)
const fileMention = await this.getFileMentionFromPath(filePath)
const problemsString = this.convertDiagnosticsToProblemsString(diagnostics)
await this.initTask(`Fix the following code in ${fileMention}\n\`\`\`\n${code}\n\`\`\`\n\nProblems:\n${problemsString}`)
@@ -915,7 +910,7 @@ export class Controller {
if (this.task) {
await telemetryService.sendCollectedEvents(this.task.taskId)
}
this.task?.abortTask()
await this.task?.abortTask()
this.task = undefined // removes reference to it, so once promises end it will be garbage collected
}
@@ -988,7 +983,7 @@ export class Controller {
async generateGitCommitMessage() {
try {
// Check if there's a workspace folder open
const cwd = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
const cwd = await getCwd()
if (!cwd) {
vscode.window.showErrorMessage("No workspace folder open")
return
+1 -1
View File
@@ -15,7 +15,7 @@ export async function getTaskHistory(controller: Controller, request: GetTaskHis
// Get task history from global state
const taskHistory = ((await getGlobalState(controller.context, "taskHistory")) as any[]) || []
const workspacePath = getWorkspacePath()
const workspacePath = await getWorkspacePath()
// Apply filters
let filteredTasks = taskHistory.filter((item) => {
+10 -10
View File
@@ -574,7 +574,7 @@ export class ToolExecutor {
tool: fileExists ? "editedExistingFile" : "newFileCreated",
path: getReadablePath(this.cwd, this.removeClosingTag(block, "path", relPath)),
content: diff || content,
operationIsLocatedInWorkspace: isLocatedInWorkspace(relPath),
operationIsLocatedInWorkspace: await isLocatedInWorkspace(relPath),
}
if (block.partial) {
@@ -645,7 +645,7 @@ export class ToolExecutor {
const completeMessage = JSON.stringify({
...sharedMessageProps,
content: diff || content,
operationIsLocatedInWorkspace: isLocatedInWorkspace(relPath),
operationIsLocatedInWorkspace: await isLocatedInWorkspace(relPath),
// ? formatResponse.createPrettyPatch(
// relPath,
// this.diffViewProvider.originalContent,
@@ -788,7 +788,7 @@ export class ToolExecutor {
const partialMessage = JSON.stringify({
...sharedMessageProps,
content: undefined,
operationIsLocatedInWorkspace: isLocatedInWorkspace(relPath),
operationIsLocatedInWorkspace: await isLocatedInWorkspace(relPath),
} satisfies ClineSayTool)
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
@@ -819,7 +819,7 @@ export class ToolExecutor {
const completeMessage = JSON.stringify({
...sharedMessageProps,
content: absolutePath,
operationIsLocatedInWorkspace: isLocatedInWorkspace(relPath),
operationIsLocatedInWorkspace: await isLocatedInWorkspace(relPath),
} satisfies ClineSayTool)
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
@@ -870,7 +870,7 @@ export class ToolExecutor {
const partialMessage = JSON.stringify({
...sharedMessageProps,
content: "",
operationIsLocatedInWorkspace: isLocatedInWorkspace(block.params.path),
operationIsLocatedInWorkspace: await isLocatedInWorkspace(block.params.path),
} satisfies ClineSayTool)
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
@@ -902,7 +902,7 @@ export class ToolExecutor {
const completeMessage = JSON.stringify({
...sharedMessageProps,
content: result,
operationIsLocatedInWorkspace: isLocatedInWorkspace(block.params.path),
operationIsLocatedInWorkspace: await isLocatedInWorkspace(block.params.path),
} satisfies ClineSayTool)
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
@@ -945,7 +945,7 @@ export class ToolExecutor {
const partialMessage = JSON.stringify({
...sharedMessageProps,
content: "",
operationIsLocatedInWorkspace: isLocatedInWorkspace(block.params.path),
operationIsLocatedInWorkspace: await isLocatedInWorkspace(block.params.path),
} satisfies ClineSayTool)
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
@@ -974,7 +974,7 @@ export class ToolExecutor {
const completeMessage = JSON.stringify({
...sharedMessageProps,
content: result,
operationIsLocatedInWorkspace: isLocatedInWorkspace(block.params.path),
operationIsLocatedInWorkspace: await isLocatedInWorkspace(block.params.path),
} satisfies ClineSayTool)
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
@@ -1021,7 +1021,7 @@ export class ToolExecutor {
const partialMessage = JSON.stringify({
...sharedMessageProps,
content: "",
operationIsLocatedInWorkspace: isLocatedInWorkspace(block.params.path),
operationIsLocatedInWorkspace: await isLocatedInWorkspace(block.params.path),
} satisfies ClineSayTool)
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
@@ -1058,7 +1058,7 @@ export class ToolExecutor {
const completeMessage = JSON.stringify({
...sharedMessageProps,
content: results,
operationIsLocatedInWorkspace: isLocatedInWorkspace(block.params.path),
operationIsLocatedInWorkspace: await isLocatedInWorkspace(block.params.path),
} satisfies ClineSayTool)
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
+37 -53
View File
@@ -1,13 +1,4 @@
import { Anthropic } from "@anthropic-ai/sdk"
import cloneDeep from "clone-deep"
import { execa } from "execa"
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import os from "os"
import pTimeout from "p-timeout"
import pWaitFor from "p-wait-for"
import * as path from "path"
import * as vscode from "vscode"
import { Logger } from "@services/logging/Logger"
import { ApiHandler, buildApiHandler } from "@api/index"
import { AnthropicHandler } from "@api/providers/anthropic"
import { ClineHandler } from "@api/providers/cline"
@@ -21,6 +12,7 @@ import { TerminalManager } from "@integrations/terminal/TerminalManager"
import { BrowserSession } from "@services/browser/BrowserSession"
import { UrlContentFetcher } from "@services/browser/UrlContentFetcher"
import { listFiles } from "@services/glob/list-files"
import { Logger } from "@services/logging/Logger"
import { telemetryService } from "@services/posthog/telemetry/TelemetryService"
import { ApiConfiguration } from "@shared/api"
import { findLast, findLastIndex } from "@shared/array"
@@ -29,42 +21,47 @@ import { BrowserSettings } from "@shared/BrowserSettings"
import { ChatSettings } from "@shared/ChatSettings"
import { combineApiRequests } from "@shared/combineApiRequests"
import { combineCommandSequences } from "@shared/combineCommandSequences"
import {
ClineApiReqCancelReason,
ClineApiReqInfo,
ClineAsk,
ClineMessage,
ClineSay,
ExtensionMessage,
} from "@shared/ExtensionMessage"
import { ClineApiReqCancelReason, ClineApiReqInfo, ClineAsk, ClineMessage, ClineSay } from "@shared/ExtensionMessage"
import { getApiMetrics } from "@shared/getApiMetrics"
import { HistoryItem } from "@shared/HistoryItem"
import { DEFAULT_LANGUAGE_SETTINGS, getLanguageKey, LanguageDisplay } from "@shared/Languages"
import { ClineAskResponse, ClineCheckpointRestore } from "@shared/WebviewMessage"
import { arePathsEqual } from "@utils/path"
import cloneDeep from "clone-deep"
import { execa } from "execa"
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import os from "os"
import pTimeout from "p-timeout"
import pWaitFor from "p-wait-for"
import * as path from "path"
import * as vscode from "vscode"
import {
AssistantMessageContent,
parseAssistantMessageV2,
parseAssistantMessageV3,
ToolParamName,
ToolUseName,
} from "@core/assistant-message"
import { ClineIgnoreController } from "@core/ignore/ClineIgnoreController"
import { parseMentions } from "@core/mentions"
import { formatResponse } from "@core/prompts/responses"
import { addUserInstructions, SYSTEM_PROMPT } from "@core/prompts/system"
import { sendPartialMessageEvent } from "@core/controller/ui/subscribeToPartialMessage"
import { sendRelinquishControlEvent } from "@core/controller/ui/subscribeToRelinquishControl"
import { convertClineMessageToProto } from "@shared/proto-conversions/cline-message"
import { getContextWindowInfo } from "@core/context/context-management/context-window-utils"
import { FileContextTracker } from "@core/context/context-tracking/FileContextTracker"
import { ModelContextTracker } from "@core/context/context-tracking/ModelContextTracker"
import { parseAssistantMessageV2, parseAssistantMessageV3, ToolUseName } from "@core/assistant-message"
import {
checkIsAnthropicContextWindowError,
checkIsOpenRouterContextWindowError,
} from "@core/context/context-management/context-error-handling"
import { getContextWindowInfo } from "@core/context/context-management/context-window-utils"
import { ContextManager } from "@core/context/context-management/ContextManager"
import { FileContextTracker } from "@core/context/context-tracking/FileContextTracker"
import { ModelContextTracker } from "@core/context/context-tracking/ModelContextTracker"
import {
getGlobalClineRules,
getLocalClineRules,
refreshClineRulesToggles,
} from "@core/context/instructions/user-instructions/cline-rules"
import {
getLocalCursorRules,
getLocalWindsurfRules,
refreshExternalRulesToggles,
} from "@core/context/instructions/user-instructions/external-rules"
import { sendPartialMessageEvent } from "@core/controller/ui/subscribeToPartialMessage"
import { sendRelinquishControlEvent } from "@core/controller/ui/subscribeToRelinquishControl"
import { ClineIgnoreController } from "@core/ignore/ClineIgnoreController"
import { parseMentions } from "@core/mentions"
import { formatResponse } from "@core/prompts/responses"
import { addUserInstructions, SYSTEM_PROMPT } from "@core/prompts/system"
import { parseSlashCommands } from "@core/slash-commands"
import {
ensureRulesDirectoryExists,
ensureTaskDirectoryExists,
@@ -72,29 +69,19 @@ import {
getSavedClineMessages,
GlobalFileNames,
} from "@core/storage/disk"
import {
getGlobalClineRules,
getLocalClineRules,
refreshClineRulesToggles,
} from "@core/context/instructions/user-instructions/cline-rules"
import { ensureLocalClineDirExists } from "../context/instructions/user-instructions/rule-helpers"
import {
refreshExternalRulesToggles,
getLocalWindsurfRules,
getLocalCursorRules,
} from "@core/context/instructions/user-instructions/external-rules"
import { refreshWorkflowToggles } from "../context/instructions/user-instructions/workflows"
import { getWorkspaceState } from "@core/storage/state"
import { parseSlashCommands } from "@core/slash-commands"
import { processFilesIntoText } from "@integrations/misc/extract-text"
import WorkspaceTracker from "@integrations/workspace/WorkspaceTracker"
import { McpHub } from "@services/mcp/McpHub"
import { isInTestMode } from "../../services/test/TestMode"
import { processFilesIntoText } from "@integrations/misc/extract-text"
import { convertClineMessageToProto } from "@shared/proto-conversions/cline-message"
import { isClaude4ModelFamily, isGemini2dot5ModelFamily } from "@utils/model-utils"
import { isInTestMode } from "../../services/test/TestMode"
import { ensureLocalClineDirExists } from "../context/instructions/user-instructions/rule-helpers"
import { refreshWorkflowToggles } from "../context/instructions/user-instructions/workflows"
import { MessageStateHandler } from "./message-state"
import { formatErrorWithStatusCode, updateApiReqMsg } from "./utils"
import { TaskState } from "./TaskState"
import { ToolExecutor } from "./ToolExecutor"
import { formatErrorWithStatusCode, updateApiReqMsg } from "./utils"
export const USE_EXPERIMENTAL_CLAUDE4_FEATURES = false
@@ -137,7 +124,6 @@ export class Task {
// Callbacks
private updateTaskHistory: (historyItem: HistoryItem) => Promise<HistoryItem[]>
private postStateToWebview: () => Promise<void>
private postMessageToWebview: (message: ExtensionMessage) => Promise<void>
private reinitExistingTaskFromId: (taskId: string) => Promise<void>
private cancelTask: () => Promise<void>
@@ -154,7 +140,6 @@ export class Task {
workspaceTracker: WorkspaceTracker,
updateTaskHistory: (historyItem: HistoryItem) => Promise<HistoryItem[]>,
postStateToWebview: () => Promise<void>,
postMessageToWebview: (message: ExtensionMessage) => Promise<void>,
reinitExistingTaskFromId: (taskId: string) => Promise<void>,
cancelTask: () => Promise<void>,
apiConfiguration: ApiConfiguration,
@@ -177,7 +162,6 @@ export class Task {
this.workspaceTracker = workspaceTracker
this.updateTaskHistory = updateTaskHistory
this.postStateToWebview = postStateToWebview
this.postMessageToWebview = postMessageToWebview
this.reinitExistingTaskFromId = reinitExistingTaskFromId
this.cancelTask = cancelTask
this.clineIgnoreController = new ClineIgnoreController(cwd)
+16 -11
View File
@@ -2,13 +2,22 @@ import * as vscode from "vscode"
import * as path from "path"
import { listFiles } from "@services/glob/list-files"
import { sendWorkspaceUpdateEvent } from "@core/controller/file/subscribeToWorkspaceUpdates"
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
import { getCwd } from "@/utils/path"
// Note: this is not a drop-in replacement for listFiles at the start of tasks, since that will be done for Desktops when there is no workspace selected
class WorkspaceTracker {
private disposables: vscode.Disposable[] = []
private filePaths: Set<string> = new Set()
private cwd: string = ""
constructor() {
this.initializeCwd()
this.registerListeners()
}
private async initializeCwd() {
this.cwd = await getCwd()
}
private get activeFiles() {
return new Set(
@@ -18,16 +27,12 @@ class WorkspaceTracker {
)
}
constructor() {
this.registerListeners()
}
async populateFilePaths() {
// should not auto get filepaths for desktop since it would immediately show permission popup before cline ever creates a file
if (!cwd) {
if (!this.cwd) {
return
}
const [files, _] = await listFiles(cwd, true, 1_000)
const [files, _] = await listFiles(this.cwd, true, 1_000)
files.forEach((file) => this.filePaths.add(this.normalizeFilePath(file)))
this.workspaceDidUpdate()
}
@@ -91,18 +96,18 @@ class WorkspaceTracker {
}
private async workspaceDidUpdate() {
if (!cwd) {
if (!this.cwd) {
return
}
const filePaths = Array.from(new Set([...this.activeFiles, ...this.filePaths])).map((file) => {
const relativePath = path.relative(cwd, file).toPosix()
const relativePath = path.relative(this.cwd, file).toPosix()
return file.endsWith("/") ? relativePath + "/" : relativePath
})
await sendWorkspaceUpdateEvent(filePaths)
}
private normalizeFilePath(filePath: string): string {
const resolvedPath = cwd ? path.resolve(cwd, filePath) : path.resolve(filePath)
const resolvedPath = this.cwd ? path.resolve(this.cwd, filePath) : path.resolve(filePath)
return filePath.endsWith("/") ? resolvedPath + "/" : resolvedPath
}
+16 -135
View File
@@ -10,7 +10,6 @@ export type ApiProvider =
| "ollama"
| "lmstudio"
| "gemini"
| "gemini-cli"
| "openai-native"
| "requesty"
| "together"
@@ -70,8 +69,6 @@ export interface ApiHandlerOptions {
lmStudioBaseUrl?: string
geminiApiKey?: string
geminiBaseUrl?: string
geminiCliOAuthPath?: string
geminiCliProjectId?: string
openAiNativeApiKey?: string
deepSeekApiKey?: string
requestyApiKey?: string
@@ -878,138 +875,6 @@ export const geminiModels = {
},
} as const satisfies Record<string, ModelInfo>
// Gemini CLI (OAuth-based)
export type GeminiCliModelId = keyof typeof geminiCliModels
export const geminiCliDefaultModelId: GeminiCliModelId = "gemini-2.5-flash"
export const geminiCliModels = {
"gemini-2.5-pro": {
maxTokens: 65536,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0, // Free tier via OAuth
outputPrice: 0, // Free tier via OAuth
description: "Google's Gemini 2.5 Pro model via OAuth (free tier)",
},
"gemini-2.5-flash": {
maxTokens: 65536,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0, // Free tier via OAuth
outputPrice: 0, // Free tier via OAuth
description: "Google's Gemini 2.5 Flash model via OAuth (free tier)",
},
"gemini-2.0-flash-001": {
maxTokens: 8192,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0, // Free tier via OAuth
outputPrice: 0, // Free tier via OAuth
description: "Google's Gemini 2.0 Flash model via OAuth (free tier)",
},
"gemini-2.0-flash-lite-preview-02-05": {
maxTokens: 8192,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Google's Gemini 2.0 Flash Lite Preview model via OAuth",
},
"gemini-2.0-pro-exp-02-05": {
maxTokens: 8192,
contextWindow: 2_097_152,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Google's Gemini 2.0 Pro Experimental model via OAuth",
},
"gemini-2.0-flash-thinking-exp-01-21": {
maxTokens: 65_536,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Google's Gemini 2.0 Flash Thinking Experimental model via OAuth",
},
"gemini-2.0-flash-thinking-exp-1219": {
maxTokens: 8192,
contextWindow: 32_767,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Google's Gemini 2.0 Flash Thinking Experimental (1219) model via OAuth",
},
"gemini-2.0-flash-exp": {
maxTokens: 8192,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Google's Gemini 2.0 Flash Experimental model via OAuth",
},
"gemini-1.5-flash-002": {
maxTokens: 8192,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0, // Free tier via OAuth
outputPrice: 0, // Free tier via OAuth
description: "Google's Gemini 1.5 Flash 002 model via OAuth (free tier)",
},
"gemini-1.5-flash-exp-0827": {
maxTokens: 8192,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Google's Gemini 1.5 Flash Experimental (0827) model via OAuth",
},
"gemini-1.5-flash-8b-exp-0827": {
maxTokens: 8192,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Google's Gemini 1.5 Flash 8B Experimental model via OAuth",
},
"gemini-1.5-pro-002": {
maxTokens: 8192,
contextWindow: 2_097_152,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Google's Gemini 1.5 Pro 002 model via OAuth",
},
"gemini-1.5-pro-exp-0827": {
maxTokens: 8192,
contextWindow: 2_097_152,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Google's Gemini 1.5 Pro Experimental model via OAuth",
},
"gemini-exp-1206": {
maxTokens: 8192,
contextWindow: 2_097_152,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Google's Gemini Experimental (1206) model via OAuth",
},
} as const satisfies Record<string, ModelInfo>
// OpenAI Native
// https://openai.com/api/pricing/
export type OpenAiNativeModelId = keyof typeof openAiNativeModels
@@ -2509,6 +2374,22 @@ export const requestyDefaultModelInfo: ModelInfo = {
export type SapAiCoreModelId = keyof typeof sapAiCoreModels
export const sapAiCoreDefaultModelId: SapAiCoreModelId = "anthropic--claude-3.5-sonnet"
export const sapAiCoreModels = {
"anthropic--claude-4-sonnet": {
maxTokens: 8192,
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 3.0,
outputPrice: 15.0,
},
"anthropic--claude-4-opus": {
maxTokens: 8192,
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 3.0,
outputPrice: 15.0,
},
"anthropic--claude-3.7-sonnet": {
maxTokens: 64_000,
contextWindow: 200_000,
@@ -202,8 +202,6 @@ function convertApiProviderToProto(provider: string | undefined): ProtoApiProvid
return ProtoApiProvider.LMSTUDIO
case "gemini":
return ProtoApiProvider.GEMINI
case "gemini-cli":
return ProtoApiProvider.GEMINI_CLI
case "openai-native":
return ProtoApiProvider.OPENAI_NATIVE
case "requesty":
@@ -264,8 +262,6 @@ function convertProtoToApiProvider(provider: ProtoApiProvider): ApiProvider {
return "lmstudio"
case ProtoApiProvider.GEMINI:
return "gemini"
case ProtoApiProvider.GEMINI_CLI:
return "gemini-cli"
case ProtoApiProvider.OPENAI_NATIVE:
return "openai-native"
case ProtoApiProvider.REQUESTY:
@@ -383,8 +379,6 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
sapAiCoreTokenUrl: config.sapAiCoreTokenUrl,
sapAiCoreBaseUrl: config.sapAiCoreBaseUrl,
claudeCodePath: config.claudeCodePath,
geminiCliOauthPath: config.geminiCliOAuthPath,
geminiCliProjectId: config.geminiCliProjectId,
}
}
@@ -464,7 +458,5 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
sapAiCoreTokenUrl: protoConfig.sapAiCoreTokenUrl,
sapAiCoreBaseUrl: protoConfig.sapAiCoreBaseUrl,
claudeCodePath: protoConfig.claudeCodePath,
geminiCliOAuthPath: protoConfig.geminiCliOauthPath,
geminiCliProjectId: protoConfig.geminiCliProjectId,
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ import { ApiHandler } from "@api/index"
export function isClaude4ModelFamily(api: ApiHandler): boolean {
const model = api.getModel()
const modelId = model.id
const modelId = model.id.toLowerCase()
return (
modelId.includes("sonnet-4") || modelId.includes("opus-4") || modelId.includes("4-sonnet") || modelId.includes("4-opus")
)
+13 -4
View File
@@ -1,6 +1,7 @@
import * as path from "path"
import os from "os"
import * as vscode from "vscode"
import { getHostBridgeProvider } from "@/hosts/host-providers"
/*
The Node.js 'path' module resolves and normalizes paths differently depending on the platform:
@@ -101,9 +102,17 @@ export function getReadablePath(cwd: string, relPath?: string): string {
}
}
export const getWorkspacePath = (defaultCwdPath = "") => {
const cwdPath = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) || defaultCwdPath
// Returns the path of the first workspace directory, or the defaultCwdPath if there is no workspace open.
export const getCwd = async (defaultCwdPath = ""): Promise<string> => {
const workspaceFolders = await getHostBridgeProvider().workspaceClient.getWorkspacePaths({})
return workspaceFolders.paths.shift() || defaultCwdPath
}
// Returns the workspace path of the file in the current editor.
// If there is no path, it returns the top level workspace directory.
export const getWorkspacePath = async (defaultCwdPath = "") => {
const currentFileUri = vscode.window.activeTextEditor?.document.uri
const cwdPath = await getCwd(defaultCwdPath)
if (currentFileUri) {
const workspaceFolder = vscode.workspace.getWorkspaceFolder(currentFileUri)
return workspaceFolder?.uri.fsPath || cwdPath
@@ -111,8 +120,8 @@ export const getWorkspacePath = (defaultCwdPath = "") => {
return cwdPath
}
export const isLocatedInWorkspace = (pathToCheck: string = ""): boolean => {
const workspacePath = getWorkspacePath()
export const isLocatedInWorkspace = async (pathToCheck: string = ""): Promise<boolean> => {
const workspacePath = await getWorkspacePath()
// Handle long paths in Windows
if (pathToCheck.startsWith("\\\\?\\") || workspacePath.startsWith("\\\\?\\")) {
+1 -1
View File
@@ -16,7 +16,7 @@ const tsConfig = JSON.parse(fs.readFileSync(path.join(baseUrl, "tsconfig.json"),
const outPaths = {}
Object.keys(tsConfig.compilerOptions.paths).forEach((key) => {
const value = tsConfig.compilerOptions.paths[key]
outPaths[key] = value.map((path) => path.replace("src", "out"))
outPaths[key] = value.map((path) => path.replace("src", "out/src"))
})
tsConfigPaths.register({
+1 -1
View File
@@ -11,7 +11,7 @@
"types": ["node", "mocha", "should", "vscode", "chai"],
"typeRoots": ["./node_modules/@types", "./src/test/types"],
"outDir": "out",
"rootDir": "src"
"rootDir": "."
},
"include": ["src/**/*.test.ts"],
"exclude": ["src/test/**/*.js", "src/**/__tests__/*"]
@@ -9,13 +9,13 @@ import { BrowserAction, BrowserActionResult, ClineMessage, ClineSayBrowserAction
import { StringRequest } from "@shared/proto/common"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import deepEqual from "fast-deep-equal"
import React, { CSSProperties, memo, useEffect, useMemo, useRef, useState } from "react"
import React, { CSSProperties, memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useSize } from "react-use"
import styled from "styled-components"
interface BrowserSessionRowProps {
messages: ClineMessage[]
isExpanded: (messageTs: number) => boolean
expandedRows: Record<number, boolean>
onToggleExpand: (messageTs: number) => void
lastModifiedMessage?: ClineMessage
isLast: boolean
@@ -294,10 +294,13 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
{currentPage?.nextAction?.messages.map((message) => (
<BrowserSessionRowContent
key={message.ts}
{...props}
message={message}
expandedRows={props.expandedRows}
onToggleExpand={props.onToggleExpand}
lastModifiedMessage={props.lastModifiedMessage}
isLast={props.isLast}
onSetQuote={props.onSetQuote}
setMaxActionHeight={setMaxActionHeight}
onSetQuote={onSetQuote}
/>
))}
{!isBrowsing && messages.some((m) => m.say === "browser_action_result") && currentPageIndex === 0 && (
@@ -498,73 +501,78 @@ interface BrowserSessionRowContentProps extends Omit<BrowserSessionRowProps, "me
onSetQuote: (text: string) => void
}
const BrowserSessionRowContent = ({
message,
isExpanded,
onToggleExpand,
lastModifiedMessage,
isLast,
setMaxActionHeight,
onSetQuote,
}: BrowserSessionRowContentProps) => {
if (message.ask === "browser_action_launch" || message.say === "browser_action_launch") {
return (
<>
<div style={headerStyle}>
<span style={browserSessionStartedTextStyle}>Browser Session Started</span>
</div>
<div style={codeBlockContainerStyle}>
<CodeBlock source={`${"```"}shell\n${message.text}\n${"```"}`} forceWrap={true} />
</div>
</>
)
}
const BrowserSessionRowContent = memo(
({
message,
expandedRows,
onToggleExpand,
lastModifiedMessage,
isLast,
setMaxActionHeight,
onSetQuote,
}: BrowserSessionRowContentProps) => {
const handleToggle = useCallback(() => {
if (message.say === "api_req_started") {
setMaxActionHeight(0)
}
onToggleExpand(message.ts)
}, [onToggleExpand, message.ts, setMaxActionHeight])
switch (message.type) {
case "say":
switch (message.say) {
case "api_req_started":
case "text":
case "reasoning":
return (
<div style={chatRowContentContainerStyle}>
<ChatRowContent
message={message}
isExpanded={isExpanded(message.ts)}
onToggleExpand={() => {
if (message.say === "api_req_started") {
setMaxActionHeight(0)
}
onToggleExpand(message.ts)
}}
lastModifiedMessage={lastModifiedMessage}
isLast={isLast}
onSetQuote={onSetQuote}
if (message.ask === "browser_action_launch" || message.say === "browser_action_launch") {
return (
<>
<div style={headerStyle}>
<span style={browserSessionStartedTextStyle}>Browser Session Started</span>
</div>
<div style={codeBlockContainerStyle}>
<CodeBlock source={`${"```"}shell\n${message.text}\n${"```"}`} forceWrap={true} />
</div>
</>
)
}
switch (message.type) {
case "say":
switch (message.say) {
case "api_req_started":
case "text":
case "reasoning":
return (
<div style={chatRowContentContainerStyle}>
<ChatRowContent
message={message}
isExpanded={expandedRows[message.ts] ?? false}
onToggleExpand={handleToggle}
lastModifiedMessage={lastModifiedMessage}
isLast={isLast}
onSetQuote={onSetQuote}
/>
</div>
)
case "browser_action":
const browserAction = JSON.parse(message.text || "{}") as ClineSayBrowserAction
return (
<BrowserActionBox
action={browserAction.action}
coordinate={browserAction.coordinate}
text={browserAction.text}
/>
</div>
)
)
case "browser_action":
const browserAction = JSON.parse(message.text || "{}") as ClineSayBrowserAction
return (
<BrowserActionBox
action={browserAction.action}
coordinate={browserAction.coordinate}
text={browserAction.text}
/>
)
default:
return null
}
default:
return null
}
case "ask":
switch (message.ask) {
default:
return null
}
}
}
case "ask":
switch (message.ask) {
default:
return null
}
}
},
deepEqual,
)
const BrowserActionBox = ({ action, coordinate, text }: { action: BrowserAction; coordinate?: string; text?: string }) => {
const getBrowserActionText = (action: BrowserAction, coordinate?: string, text?: string) => {
File diff suppressed because it is too large Load Diff
@@ -1,4 +1,6 @@
import { MAX_IMAGES_AND_FILES_PER_MESSAGE } from "@/components/chat/ChatView"
import { CHAT_CONSTANTS } from "@/components/chat/chat-view/constants"
const { MAX_IMAGES_AND_FILES_PER_MESSAGE } = CHAT_CONSTANTS
import ContextMenu from "@/components/chat/ContextMenu"
import SlashCommandMenu from "@/components/chat/SlashCommandMenu"
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
@@ -29,10 +31,8 @@ import {
validateSlashCommand,
} from "@/utils/slash-commands"
import { validateApiConfiguration, validateModelId } from "@/utils/validate"
import { vscode } from "@/utils/vscode"
import { ChatSettings } from "@shared/ChatSettings"
import { mentionRegex, mentionRegexGlobal } from "@shared/context-mentions"
import { ExtensionMessage } from "@shared/ExtensionMessage"
import { EmptyRequest, StringRequest } from "@shared/proto/common"
import { FileSearchRequest, RelativePathsRequest } from "@shared/proto/file"
import { UpdateApiConfigurationRequest } from "@shared/proto/models"
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,100 @@
import React from "react"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import styled from "styled-components"
import { ChatState, MessageHandlers } from "../../types/chatTypes"
interface ActionButtonsProps {
chatState: ChatState
messageHandlers: MessageHandlers
isStreaming: boolean
scrollBehavior: {
scrollToBottomSmooth: () => void
disableAutoScrollRef: React.MutableRefObject<boolean>
showScrollToBottom: boolean
}
}
/**
* Action buttons area including scroll-to-bottom and approve/reject buttons
*/
export const ActionButtons: React.FC<ActionButtonsProps> = ({ chatState, messageHandlers, isStreaming, scrollBehavior }) => {
const { primaryButtonText, secondaryButtonText, enableButtons, didClickCancel, inputValue, selectedImages, selectedFiles } =
chatState
const { showScrollToBottom, scrollToBottomSmooth, disableAutoScrollRef } = scrollBehavior
if (showScrollToBottom) {
return (
<div
style={{
display: "flex",
padding: "10px 15px 0px 15px",
}}>
<ScrollToBottomButton
onClick={() => {
scrollToBottomSmooth()
disableAutoScrollRef.current = false
}}>
<span className="codicon codicon-chevron-down" style={{ fontSize: "18px" }}></span>
</ScrollToBottomButton>
</div>
)
}
const shouldShowButtons = primaryButtonText || secondaryButtonText || isStreaming
const opacity = shouldShowButtons ? (enableButtons || (isStreaming && !didClickCancel) ? 1 : 0.5) : 0
return (
<div
style={{
opacity,
display: "flex",
padding: `${shouldShowButtons ? "10" : "0"}px 15px 0px 15px`,
}}>
{primaryButtonText && !isStreaming && (
<VSCodeButton
appearance="primary"
disabled={!enableButtons}
style={{
flex: secondaryButtonText ? 1 : 2,
marginRight: secondaryButtonText ? "6px" : "0",
}}
onClick={() => messageHandlers.handlePrimaryButtonClick(inputValue, selectedImages, selectedFiles)}>
{primaryButtonText}
</VSCodeButton>
)}
{(secondaryButtonText || isStreaming) && (
<VSCodeButton
appearance="secondary"
disabled={!enableButtons && !(isStreaming && !didClickCancel)}
style={{
flex: isStreaming ? 2 : 1,
marginLeft: isStreaming ? 0 : "6px",
}}
onClick={() => messageHandlers.handleSecondaryButtonClick(inputValue, selectedImages, selectedFiles)}>
{isStreaming ? "Cancel" : secondaryButtonText}
</VSCodeButton>
)}
</div>
)
}
const ScrollToBottomButton = styled.div`
background-color: color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 55%, transparent);
border-radius: 3px;
overflow: hidden;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
flex: 1;
height: 25px;
&:hover {
background-color: color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 90%, transparent);
}
&:active {
background-color: color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 70%, transparent);
}
`
@@ -0,0 +1,26 @@
import React from "react"
import styled from "styled-components"
interface ChatLayoutProps {
isHidden: boolean
children: React.ReactNode
}
/**
* Main layout container for the chat view
* Provides the fixed positioning and flex layout structure
*/
export const ChatLayout: React.FC<ChatLayoutProps> = ({ isHidden, children }) => {
return <ChatLayoutContainer isHidden={isHidden}>{children}</ChatLayoutContainer>
}
const ChatLayoutContainer = styled.div<{ isHidden: boolean }>`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: ${(props) => (props.isHidden ? "none" : "flex")};
flex-direction: column;
overflow: hidden;
`
@@ -0,0 +1,78 @@
import React from "react"
import QuotedMessagePreview from "@/components/chat/QuotedMessagePreview"
import ChatTextArea from "@/components/chat/ChatTextArea"
import { ChatState, MessageHandlers, ScrollBehavior } from "../../types/chatTypes"
interface InputSectionProps {
chatState: ChatState
messageHandlers: MessageHandlers
scrollBehavior: ScrollBehavior
placeholderText: string
shouldDisableFilesAndImages: boolean
selectFilesAndImages: () => Promise<void>
}
/**
* Input section including quoted message preview and chat text area
*/
export const InputSection: React.FC<InputSectionProps> = ({
chatState,
messageHandlers,
scrollBehavior,
placeholderText,
shouldDisableFilesAndImages,
selectFilesAndImages,
}) => {
const {
activeQuote,
setActiveQuote,
isTextAreaFocused,
inputValue,
setInputValue,
sendingDisabled,
selectedImages,
setSelectedImages,
selectedFiles,
setSelectedFiles,
textAreaRef,
handleFocusChange,
} = chatState
const { isAtBottom, scrollToBottomAuto } = scrollBehavior
return (
<>
{activeQuote && (
<div style={{ marginBottom: "-12px", marginTop: "10px" }}>
<QuotedMessagePreview
text={activeQuote}
onDismiss={() => setActiveQuote(null)}
isFocused={isTextAreaFocused}
/>
</div>
)}
<ChatTextArea
ref={textAreaRef}
onFocusChange={handleFocusChange}
activeQuote={activeQuote}
inputValue={inputValue}
setInputValue={setInputValue}
sendingDisabled={sendingDisabled}
placeholderText={placeholderText}
selectedImages={selectedImages}
setSelectedImages={setSelectedImages}
setSelectedFiles={setSelectedFiles}
selectedFiles={selectedFiles}
onSend={() => messageHandlers.handleSendMessage(inputValue, selectedImages, selectedFiles)}
onSelectFilesAndImages={selectFilesAndImages}
shouldDisableFilesAndImages={shouldDisableFilesAndImages}
onHeightChange={() => {
if (isAtBottom) {
scrollToBottomAuto()
}
}}
/>
</>
)
}
@@ -0,0 +1,98 @@
import React, { useCallback } from "react"
import { Virtuoso } from "react-virtuoso"
import AutoApproveBar from "@/components/chat/auto-approve-menu/AutoApproveBar"
import { ClineMessage } from "@shared/ExtensionMessage"
import { ScrollBehavior, ChatState, MessageHandlers } from "../../types/chatTypes"
import { createMessageRenderer } from "../messages/MessageRenderer"
interface MessagesAreaProps {
task: ClineMessage
groupedMessages: (ClineMessage | ClineMessage[])[]
modifiedMessages: ClineMessage[]
scrollBehavior: ScrollBehavior
chatState: ChatState
messageHandlers: MessageHandlers
}
/**
* The scrollable messages area with virtualized list
* Handles rendering of chat rows and browser sessions
*/
export const MessagesArea: React.FC<MessagesAreaProps> = ({
task,
groupedMessages,
modifiedMessages,
scrollBehavior,
chatState,
messageHandlers,
}) => {
const {
virtuosoRef,
scrollContainerRef,
toggleRowExpansion,
handleRowHeightChange,
setIsAtBottom,
setShowScrollToBottom,
disableAutoScrollRef,
} = scrollBehavior
const { expandedRows, inputValue, setActiveQuote } = chatState
const itemContent = useCallback(
createMessageRenderer(
groupedMessages,
modifiedMessages,
expandedRows,
toggleRowExpansion,
handleRowHeightChange,
setActiveQuote,
inputValue,
messageHandlers,
),
[
groupedMessages,
modifiedMessages,
expandedRows,
toggleRowExpansion,
handleRowHeightChange,
setActiveQuote,
inputValue,
messageHandlers,
],
)
return (
<>
<div style={{ flexGrow: 1, display: "flex" }} ref={scrollContainerRef}>
<Virtuoso
ref={virtuosoRef}
key={task.ts} // trick to make sure virtuoso re-renders when task changes
className="scrollable"
style={{
flexGrow: 1,
overflowY: "scroll", // always show scrollbar
}}
components={{
Footer: () => <div style={{ height: 5 }} />, // Add empty padding at the bottom
}}
increaseViewportBy={{
top: 3_000,
bottom: Number.MAX_SAFE_INTEGER,
}}
data={groupedMessages}
itemContent={itemContent}
atBottomStateChange={(isAtBottom) => {
setIsAtBottom(isAtBottom)
if (isAtBottom) {
disableAutoScrollRef.current = false
}
setShowScrollToBottom(disableAutoScrollRef.current && !isAtBottom)
}}
atBottomThreshold={10}
initialTopMostItemIndex={groupedMessages.length - 1}
/>
</div>
<AutoApproveBar />
</>
)
}
@@ -0,0 +1,50 @@
import React from "react"
import TaskHeader from "@/components/chat/task-header/TaskHeader"
import { ClineMessage } from "@shared/ExtensionMessage"
import { MessageHandlers, ScrollBehavior } from "../../types/chatTypes"
interface TaskSectionProps {
task: ClineMessage
apiMetrics: {
totalTokensIn: number
totalTokensOut: number
totalCacheWrites?: number
totalCacheReads?: number
totalCost: number
}
lastApiReqTotalTokens?: number
selectedModelInfo: {
supportsPromptCache: boolean
supportsImages: boolean
}
messageHandlers: MessageHandlers
scrollBehavior: ScrollBehavior
}
/**
* Task section shown when there's an active task
* Includes the task header and manages task-specific UI
*/
export const TaskSection: React.FC<TaskSectionProps> = ({
task,
apiMetrics,
lastApiReqTotalTokens,
selectedModelInfo,
messageHandlers,
scrollBehavior,
}) => {
return (
<TaskHeader
task={task}
tokensIn={apiMetrics.totalTokensIn}
tokensOut={apiMetrics.totalTokensOut}
doesModelSupportPromptCache={selectedModelInfo.supportsPromptCache}
cacheWrites={apiMetrics.totalCacheWrites}
cacheReads={apiMetrics.totalCacheReads}
totalCost={apiMetrics.totalCost}
lastApiReqTotalTokens={lastApiReqTotalTokens}
onClose={messageHandlers.handleTaskCloseButtonClick}
onScrollToMessage={scrollBehavior.scrollToMessage}
/>
)
}
@@ -0,0 +1,43 @@
import React from "react"
import TelemetryBanner from "@/components/common/TelemetryBanner"
import Announcement from "@/components/chat/Announcement"
import HomeHeader from "@/components/welcome/HomeHeader"
import HistoryPreview from "@/components/history/HistoryPreview"
import { SuggestedTasks } from "@/components/welcome/SuggestedTasks"
import AutoApproveBar from "@/components/chat/auto-approve-menu/AutoApproveBar"
import { WelcomeSectionProps } from "../../types/chatTypes"
/**
* Welcome section shown when there's no active task
* Includes telemetry banner, announcements, home header, and history preview
*/
export const WelcomeSection: React.FC<WelcomeSectionProps> = ({
showAnnouncement,
hideAnnouncement,
showHistoryView,
telemetrySetting,
version,
taskHistory,
shouldShowQuickWins,
}) => {
return (
<>
<div
style={{
flex: "1 1 0",
minHeight: 0,
overflowY: "auto",
display: "flex",
flexDirection: "column",
paddingBottom: "10px",
}}>
{telemetrySetting === "unset" && <TelemetryBanner />}
{showAnnouncement && <Announcement version={version} hideAnnouncement={hideAnnouncement} />}
<HomeHeader />
{!shouldShowQuickWins && taskHistory.length > 0 && <HistoryPreview showHistoryView={showHistoryView} />}
</div>
<SuggestedTasks shouldShowQuickWins={shouldShowQuickWins} />
<AutoApproveBar />
</>
)
}
@@ -0,0 +1,10 @@
/**
* Export all layout components
*/
export { ChatLayout } from "./ChatLayout"
export { WelcomeSection } from "./WelcomeSection"
export { TaskSection } from "./TaskSection"
export { MessagesArea } from "./MessagesArea"
export { ActionButtons } from "./ActionButtons"
export { InputSection } from "./InputSection"
@@ -0,0 +1,103 @@
import React, { useCallback } from "react"
import BrowserSessionRow from "@/components/chat/BrowserSessionRow"
import ChatRow from "@/components/chat/ChatRow"
import { ClineMessage } from "@shared/ExtensionMessage"
import { MessageHandlers } from "../../types/chatTypes"
interface MessageRendererProps {
index: number
messageOrGroup: ClineMessage | ClineMessage[]
groupedMessages: (ClineMessage | ClineMessage[])[]
modifiedMessages: ClineMessage[]
expandedRows: Record<number, boolean>
onToggleExpand: (ts: number) => void
onHeightChange: (isTaller: boolean) => void
onSetQuote: (quote: string | null) => void
inputValue: string
messageHandlers: MessageHandlers
}
/**
* Specialized component for rendering different message types
* Handles browser sessions, regular messages, and checkpoint logic
*/
export const MessageRenderer: React.FC<MessageRendererProps> = ({
index,
messageOrGroup,
groupedMessages,
modifiedMessages,
expandedRows,
onToggleExpand,
onHeightChange,
onSetQuote,
inputValue,
messageHandlers,
}) => {
// Browser session group
if (Array.isArray(messageOrGroup)) {
return (
<BrowserSessionRow
key={messageOrGroup[0]?.ts}
messages={messageOrGroup}
isLast={index === groupedMessages.length - 1}
lastModifiedMessage={modifiedMessages.at(-1)}
onHeightChange={onHeightChange}
expandedRows={expandedRows}
onToggleExpand={onToggleExpand}
onSetQuote={onSetQuote}
/>
)
}
// Determine if this is the last message for status display purposes
const nextMessage = index < groupedMessages.length - 1 && groupedMessages[index + 1]
const isNextCheckpoint = !Array.isArray(nextMessage) && nextMessage && nextMessage?.say === "checkpoint_created"
const isLastMessageGroup = isNextCheckpoint && index === groupedMessages.length - 2
const isLast = index === groupedMessages.length - 1 || isLastMessageGroup
// Regular message
return (
<ChatRow
key={messageOrGroup.ts}
message={messageOrGroup}
isExpanded={expandedRows[messageOrGroup.ts] || false}
onToggleExpand={onToggleExpand}
lastModifiedMessage={modifiedMessages.at(-1)}
isLast={isLast}
onHeightChange={onHeightChange}
inputValue={inputValue}
sendMessageFromChatRow={messageHandlers.handleSendMessage}
onSetQuote={onSetQuote}
/>
)
}
/**
* Factory function to create the itemContent callback for Virtuoso
* This allows us to encapsulate the rendering logic while maintaining performance
*/
export const createMessageRenderer = (
groupedMessages: (ClineMessage | ClineMessage[])[],
modifiedMessages: ClineMessage[],
expandedRows: Record<number, boolean>,
onToggleExpand: (ts: number) => void,
onHeightChange: (isTaller: boolean) => void,
onSetQuote: (quote: string | null) => void,
inputValue: string,
messageHandlers: MessageHandlers,
) => {
return (index: number, messageOrGroup: ClineMessage | ClineMessage[]) => (
<MessageRenderer
index={index}
messageOrGroup={messageOrGroup}
groupedMessages={groupedMessages}
modifiedMessages={modifiedMessages}
expandedRows={expandedRows}
onToggleExpand={onToggleExpand}
onHeightChange={onHeightChange}
onSetQuote={onSetQuote}
inputValue={inputValue}
messageHandlers={messageHandlers}
/>
)
}
@@ -0,0 +1,93 @@
import React, { useMemo } from "react"
import { ClineMessage } from "@shared/ExtensionMessage"
import { findLast } from "@shared/array"
/**
* Hook to determine if the chat is currently streaming
* Encapsulates the complex streaming detection logic
*/
export const useIsStreaming = (
modifiedMessages: ClineMessage[],
clineAsk?: string,
enableButtons?: boolean,
primaryButtonText?: string,
): boolean => {
return useMemo(() => {
// Check if the last message is an ask (tool is waiting for user input)
const isLastAsk = !!modifiedMessages.at(-1)?.ask
const isToolCurrentlyAsking = isLastAsk && clineAsk !== undefined && enableButtons && primaryButtonText !== undefined
if (isToolCurrentlyAsking) {
return false
}
// Check if the last message is partial (still being streamed)
const isLastMessagePartial = modifiedMessages.at(-1)?.partial === true
if (isLastMessagePartial) {
return true
}
// Check if there's an ongoing API request
const lastApiReqStarted = findLast(modifiedMessages, (message) => message.say === "api_req_started")
if (lastApiReqStarted && lastApiReqStarted.text != null && lastApiReqStarted.say === "api_req_started") {
const cost = JSON.parse(lastApiReqStarted.text).cost
if (cost === undefined) {
// API request has not finished yet
return true
}
}
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>
)
}
@@ -0,0 +1,6 @@
/**
* Export all message-related components
*/
export { MessageRenderer, createMessageRenderer } from "./MessageRenderer"
export { useIsStreaming, StreamingVisualIndicator } from "./StreamingIndicator"
@@ -0,0 +1,7 @@
/**
* Constants used across the chat view components
*/
export const CHAT_CONSTANTS = {
MAX_IMAGES_AND_FILES_PER_MESSAGE: 20,
QUICK_WINS_HISTORY_THRESHOLD: 300,
} as const
@@ -0,0 +1,8 @@
/**
* Export all custom hooks for the chat view
*/
export { useChatState } from "./useChatState"
export { useButtonState } from "./useButtonState"
export { useScrollBehavior } from "./useScrollBehavior"
export { useMessageHandlers } from "./useMessageHandlers"
@@ -0,0 +1,160 @@
import { useEffect } from "react"
import { useDeepCompareEffect } from "react-use"
import { ClineMessage, ClineSayTool } from "@shared/ExtensionMessage"
import { ChatState } from "../types/chatTypes"
/**
* Custom hook for managing button state based on messages
* Handles button text and enable/disable states based on the current ask type
*/
export function useButtonState(messages: ClineMessage[], chatState: ChatState) {
const {
setSendingDisabled,
setEnableButtons,
setPrimaryButtonText,
setSecondaryButtonText,
setDidClickCancel,
lastMessage,
secondLastMessage,
} = chatState
// Update button state based on last message
useDeepCompareEffect(() => {
if (lastMessage) {
switch (lastMessage.type) {
case "ask":
const isPartial = lastMessage.partial === true
switch (lastMessage.ask) {
case "api_req_failed":
setSendingDisabled(true)
setEnableButtons(true)
setPrimaryButtonText("Retry")
setSecondaryButtonText("Start New Task")
break
case "mistake_limit_reached":
setSendingDisabled(false)
setEnableButtons(true)
setPrimaryButtonText("Proceed Anyways")
setSecondaryButtonText("Start New Task")
break
case "auto_approval_max_req_reached":
setSendingDisabled(true)
setEnableButtons(true)
setPrimaryButtonText("Proceed")
setSecondaryButtonText("Start New Task")
break
case "followup":
setSendingDisabled(isPartial)
setEnableButtons(false)
break
case "plan_mode_respond":
setSendingDisabled(isPartial)
setEnableButtons(false)
break
case "tool":
setSendingDisabled(isPartial)
setEnableButtons(!isPartial)
const tool = JSON.parse(lastMessage.text || "{}") as ClineSayTool
switch (tool.tool) {
case "editedExistingFile":
case "newFileCreated":
setPrimaryButtonText("Save")
setSecondaryButtonText("Reject")
break
default:
setPrimaryButtonText("Approve")
setSecondaryButtonText("Reject")
break
}
break
case "browser_action_launch":
setSendingDisabled(isPartial)
setEnableButtons(!isPartial)
setPrimaryButtonText("Approve")
setSecondaryButtonText("Reject")
break
case "command":
setSendingDisabled(isPartial)
setEnableButtons(!isPartial)
setPrimaryButtonText("Run Command")
setSecondaryButtonText("Reject")
break
case "command_output":
setSendingDisabled(false)
setEnableButtons(true)
setPrimaryButtonText("Proceed While Running")
setSecondaryButtonText(undefined)
break
case "use_mcp_server":
setSendingDisabled(isPartial)
setEnableButtons(!isPartial)
setPrimaryButtonText("Approve")
setSecondaryButtonText("Reject")
break
case "completion_result":
setSendingDisabled(isPartial)
setEnableButtons(!isPartial)
setPrimaryButtonText("Start New Task")
setSecondaryButtonText(undefined)
break
case "resume_task":
setSendingDisabled(false)
setEnableButtons(true)
setPrimaryButtonText("Resume Task")
setSecondaryButtonText(undefined)
setDidClickCancel(false)
break
case "resume_completed_task":
setSendingDisabled(false)
setEnableButtons(true)
setPrimaryButtonText("Start New Task")
setSecondaryButtonText(undefined)
setDidClickCancel(false)
break
case "new_task":
setSendingDisabled(isPartial)
setEnableButtons(!isPartial)
setPrimaryButtonText("Start New Task with Context")
setSecondaryButtonText(undefined)
break
case "condense":
setSendingDisabled(isPartial)
setEnableButtons(!isPartial)
setPrimaryButtonText("Condense Conversation")
setSecondaryButtonText(undefined)
break
case "report_bug":
setSendingDisabled(isPartial)
setEnableButtons(!isPartial)
setPrimaryButtonText("Report GitHub issue")
setSecondaryButtonText(undefined)
break
}
break
case "say":
switch (lastMessage.say) {
case "api_req_started":
if (secondLastMessage?.ask === "command_output") {
chatState.setInputValue("")
setSendingDisabled(true)
chatState.setSelectedImages([])
chatState.setSelectedFiles([])
setEnableButtons(false)
}
break
}
break
}
}
}, [lastMessage, secondLastMessage])
// Reset button state when no messages
useEffect(() => {
if (messages.length === 0) {
setSendingDisabled(false)
setEnableButtons(false)
setPrimaryButtonText("Approve")
setSecondaryButtonText("Reject")
}
}, [messages.length, setSendingDisabled, setEnableButtons, setPrimaryButtonText, setSecondaryButtonText])
}
@@ -0,0 +1,96 @@
import { useState, useMemo, useRef, useCallback } from "react"
import { ClineMessage, ClineAsk } from "@shared/ExtensionMessage"
import { ChatState, MessageHandlers } from "../types/chatTypes"
/**
* Custom hook for managing chat state
* Handles input values, selection states, and UI state
*/
export function useChatState(messages: ClineMessage[]): ChatState {
// Input and selection state
const [inputValue, setInputValue] = useState("")
const [activeQuote, setActiveQuote] = useState<string | null>(null)
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
const [selectedImages, setSelectedImages] = useState<string[]>([])
const [selectedFiles, setSelectedFiles] = useState<string[]>([])
// UI state
const [sendingDisabled, setSendingDisabled] = useState(false)
const [enableButtons, setEnableButtons] = useState<boolean>(false)
const [primaryButtonText, setPrimaryButtonText] = useState<string | undefined>("Approve")
const [secondaryButtonText, setSecondaryButtonText] = useState<string | undefined>("Reject")
const [didClickCancel, setDidClickCancel] = useState(false)
const [expandedRows, setExpandedRows] = useState<Record<number, boolean>>({})
// Refs
const textAreaRef = useRef<HTMLTextAreaElement>(null)
// Derived state
const lastMessage = useMemo(() => messages.at(-1), [messages])
const secondLastMessage = useMemo(() => messages.at(-2), [messages])
const clineAsk = useMemo(() => (lastMessage?.type === "ask" ? lastMessage.ask : undefined), [lastMessage])
// Clear expanded rows when task changes
const task = useMemo(() => messages.at(0), [messages])
const clearExpandedRows = useCallback(() => {
setExpandedRows({})
}, [])
// Reset state when starting new conversation
const resetState = useCallback(() => {
setInputValue("")
setActiveQuote(null)
setSendingDisabled(false)
setSelectedImages([])
setSelectedFiles([])
setEnableButtons(false)
setPrimaryButtonText("Approve")
setSecondaryButtonText("Reject")
setDidClickCancel(false)
}, [])
// Handle focus change
const handleFocusChange = useCallback((isFocused: boolean) => {
setIsTextAreaFocused(isFocused)
}, [])
return {
// State values
inputValue,
setInputValue,
activeQuote,
setActiveQuote,
isTextAreaFocused,
setIsTextAreaFocused,
selectedImages,
setSelectedImages,
selectedFiles,
setSelectedFiles,
sendingDisabled,
setSendingDisabled,
enableButtons,
setEnableButtons,
primaryButtonText,
setPrimaryButtonText,
secondaryButtonText,
setSecondaryButtonText,
didClickCancel,
setDidClickCancel,
expandedRows,
setExpandedRows,
// Refs
textAreaRef,
// Derived values
lastMessage,
secondLastMessage,
clineAsk,
task,
// Handlers
handleFocusChange,
clearExpandedRows,
resetState,
}
}
@@ -0,0 +1,270 @@
import { useCallback } from "react"
import { ClineMessage, ClineAsk } from "@shared/ExtensionMessage"
import { TaskServiceClient, SlashServiceClient } from "@/services/grpc-client"
import { EmptyRequest, StringRequest } from "@shared/proto/common"
import { AskResponseRequest, NewTaskRequest } from "@shared/proto/task"
import { MessageHandlers, ChatState } from "../types/chatTypes"
/**
* Custom hook for managing message handlers
* Handles sending messages, button clicks, and task management
*/
export function useMessageHandlers(messages: ClineMessage[], chatState: ChatState, isStreaming: boolean): MessageHandlers {
const {
inputValue,
setInputValue,
activeQuote,
setActiveQuote,
selectedImages,
setSelectedImages,
selectedFiles,
setSelectedFiles,
setSendingDisabled,
setEnableButtons,
setDidClickCancel,
clineAsk,
lastMessage,
} = chatState
// Handle sending a message
const handleSendMessage = useCallback(
async (text: string, images: string[], files: string[]) => {
let messageToSend = text.trim()
const hasContent = messageToSend || images.length > 0 || files.length > 0
// Prepend the active quote if it exists
if (activeQuote && hasContent) {
const prefix = "[context] \n> "
const formattedQuote = activeQuote
const suffix = "\n[/context] \n\n"
messageToSend = `${prefix} ${formattedQuote} ${suffix} ${messageToSend}`
}
if (hasContent) {
console.log("[ChatView] handleSendMessage - Sending message:", messageToSend)
if (messages.length === 0) {
await TaskServiceClient.newTask(NewTaskRequest.create({ text: messageToSend, images, files }))
} else if (clineAsk) {
switch (clineAsk) {
case "followup":
case "plan_mode_respond":
case "tool":
case "browser_action_launch":
case "command":
case "command_output":
case "use_mcp_server":
case "completion_result":
case "resume_task":
case "resume_completed_task":
case "mistake_limit_reached":
case "new_task":
case "condense":
case "report_bug":
await TaskServiceClient.askResponse(
AskResponseRequest.create({
responseType: "messageResponse",
text: messageToSend,
images,
files,
}),
)
break
}
}
setInputValue("")
setActiveQuote(null)
setSendingDisabled(true)
setSelectedImages([])
setSelectedFiles([])
setEnableButtons(false)
// Reset auto-scroll
if ("disableAutoScrollRef" in chatState) {
;(chatState as any).disableAutoScrollRef.current = false
}
}
},
[
messages.length,
clineAsk,
activeQuote,
setInputValue,
setActiveQuote,
setSendingDisabled,
setSelectedImages,
setSelectedFiles,
setEnableButtons,
chatState,
],
)
// Start a new task
const startNewTask = useCallback(async () => {
setActiveQuote(null)
await TaskServiceClient.clearTask(EmptyRequest.create({}))
}, [setActiveQuote])
// Handle primary button click
const handlePrimaryButtonClick = useCallback(
async (text?: string, images?: string[], files?: string[]) => {
const trimmedInput = text?.trim()
switch (clineAsk) {
case "api_req_failed":
case "command":
case "command_output":
case "tool":
case "browser_action_launch":
case "use_mcp_server":
case "resume_task":
case "mistake_limit_reached":
case "auto_approval_max_req_reached":
if (trimmedInput || (images && images.length > 0) || (files && files.length > 0)) {
await TaskServiceClient.askResponse(
AskResponseRequest.create({
responseType: "yesButtonClicked",
text: trimmedInput,
images: images,
files: files,
}),
)
} else {
await TaskServiceClient.askResponse(
AskResponseRequest.create({
responseType: "yesButtonClicked",
}),
)
}
// Clear input state after sending
setInputValue("")
setActiveQuote(null)
setSelectedImages([])
setSelectedFiles([])
break
case "completion_result":
case "resume_completed_task":
startNewTask()
break
case "new_task":
console.info("new task button clicked!", { lastMessage, messages, clineAsk, text })
await TaskServiceClient.newTask(
NewTaskRequest.create({
text: lastMessage?.text,
images: [],
files: [],
}),
)
break
case "condense":
await SlashServiceClient.condense(StringRequest.create({ value: lastMessage?.text })).catch((err) =>
console.error(err),
)
break
case "report_bug":
await SlashServiceClient.reportBug(StringRequest.create({ value: lastMessage?.text })).catch((err) =>
console.error(err),
)
break
}
setSendingDisabled(true)
setEnableButtons(false)
// Reset auto-scroll
if ("disableAutoScrollRef" in chatState) {
;(chatState as any).disableAutoScrollRef.current = false
}
},
[
clineAsk,
startNewTask,
lastMessage,
messages,
setInputValue,
setActiveQuote,
setSelectedImages,
setSelectedFiles,
setSendingDisabled,
setEnableButtons,
chatState,
],
)
// Handle secondary button click
const handleSecondaryButtonClick = useCallback(
async (text?: string, images?: string[], files?: string[]) => {
const trimmedInput = text?.trim()
if (isStreaming) {
await TaskServiceClient.cancelTask(EmptyRequest.create({}))
setDidClickCancel(true)
return
}
switch (clineAsk) {
case "api_req_failed":
case "mistake_limit_reached":
case "auto_approval_max_req_reached":
startNewTask()
break
case "command":
case "tool":
case "browser_action_launch":
case "use_mcp_server":
if (trimmedInput || (images && images.length > 0) || (files && files.length > 0)) {
await TaskServiceClient.askResponse(
AskResponseRequest.create({
responseType: "noButtonClicked",
text: trimmedInput,
images: images,
files: files,
}),
)
} else {
await TaskServiceClient.askResponse(
AskResponseRequest.create({
responseType: "noButtonClicked",
}),
)
}
// Clear input state after sending
setInputValue("")
setActiveQuote(null)
setSelectedImages([])
setSelectedFiles([])
break
}
setSendingDisabled(true)
setEnableButtons(false)
// Reset auto-scroll
if ("disableAutoScrollRef" in chatState) {
;(chatState as any).disableAutoScrollRef.current = false
}
},
[
isStreaming,
clineAsk,
startNewTask,
setInputValue,
setActiveQuote,
setSelectedImages,
setSelectedFiles,
setSendingDisabled,
setEnableButtons,
setDidClickCancel,
chatState,
],
)
// Handle task close button click
const handleTaskCloseButtonClick = useCallback(() => {
startNewTask()
}, [startNewTask])
return {
handleSendMessage,
handlePrimaryButtonClick,
handleSecondaryButtonClick,
handleTaskCloseButtonClick,
startNewTask,
}
}
@@ -0,0 +1,232 @@
import { useRef, useState, useCallback, useEffect, useMemo } from "react"
import { useEvent } from "react-use"
import debounce from "debounce"
import { VirtuosoHandle } from "react-virtuoso"
import { ClineMessage } from "@shared/ExtensionMessage"
import { ScrollBehavior } from "../types/chatTypes"
/**
* Custom hook for managing scroll behavior
* Handles auto-scrolling, manual scrolling, and scroll-to-message functionality
*/
export function useScrollBehavior(
messages: ClineMessage[],
visibleMessages: ClineMessage[],
groupedMessages: (ClineMessage | ClineMessage[])[],
expandedRows: Record<number, boolean>,
setExpandedRows: React.Dispatch<React.SetStateAction<Record<number, boolean>>>,
): ScrollBehavior & {
showScrollToBottom: boolean
setShowScrollToBottom: React.Dispatch<React.SetStateAction<boolean>>
isAtBottom: boolean
setIsAtBottom: React.Dispatch<React.SetStateAction<boolean>>
pendingScrollToMessage: number | null
setPendingScrollToMessage: React.Dispatch<React.SetStateAction<number | null>>
} {
// Refs
const virtuosoRef = useRef<VirtuosoHandle>(null)
const scrollContainerRef = useRef<HTMLDivElement>(null)
const disableAutoScrollRef = useRef(false)
// State
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(
() => {
virtuosoRef.current?.scrollTo({
top: Number.MAX_SAFE_INTEGER,
behavior: "smooth",
})
},
10,
{ immediate: true },
),
[],
)
// Instant scroll to bottom
const scrollToBottomAuto = useCallback(() => {
virtuosoRef.current?.scrollTo({
top: Number.MAX_SAFE_INTEGER,
behavior: "auto", // instant causes crash
})
}, [])
// Scroll to specific message
const scrollToMessage = useCallback(
(messageIndex: number) => {
setPendingScrollToMessage(messageIndex)
const targetMessage = messages[messageIndex]
if (!targetMessage) {
setPendingScrollToMessage(null)
return
}
const visibleIndex = visibleMessages.findIndex((msg) => msg.ts === targetMessage.ts)
if (visibleIndex === -1) {
setPendingScrollToMessage(null)
return
}
let groupIndex = -1
let currentVisibleIndex = 0
for (let i = 0; i < groupedMessages.length; i++) {
const group = groupedMessages[i]
if (Array.isArray(group)) {
const groupSize = group.length
const messageInGroup = group.some((msg) => msg.ts === targetMessage.ts)
if (messageInGroup) {
groupIndex = i
break
}
currentVisibleIndex += groupSize
} else {
if (group.ts === targetMessage.ts) {
groupIndex = i
break
}
currentVisibleIndex++
}
}
if (groupIndex !== -1) {
setPendingScrollToMessage(null)
disableAutoScrollRef.current = true
requestAnimationFrame(() => {
requestAnimationFrame(() => {
virtuosoRef.current?.scrollToIndex({
index: groupIndex,
align: "start",
behavior: "smooth",
})
})
})
}
},
[messages, visibleMessages, groupedMessages],
)
// Toggle row expansion with scroll handling
const toggleRowExpansion = useCallback(
(ts: number) => {
const isCollapsing = expandedRows[ts] ?? false
const lastGroup = groupedMessages.at(-1)
const isLast = Array.isArray(lastGroup) ? lastGroup[0].ts === ts : lastGroup?.ts === ts
const secondToLastGroup = groupedMessages.at(-2)
const isSecondToLast = Array.isArray(secondToLastGroup)
? secondToLastGroup[0].ts === ts
: secondToLastGroup?.ts === ts
const isLastCollapsedApiReq =
isLast &&
!Array.isArray(lastGroup) && // Make sure it's not a browser session group
lastGroup?.say === "api_req_started" &&
!expandedRows[lastGroup.ts]
setExpandedRows((prev) => ({
...prev,
[ts]: !prev[ts],
}))
// disable auto scroll when user expands row
if (!isCollapsing) {
disableAutoScrollRef.current = true
}
if (isCollapsing && isAtBottom) {
const timer = setTimeout(() => {
scrollToBottomAuto()
}, 0)
return () => clearTimeout(timer)
} else if (isLast || isSecondToLast) {
if (isCollapsing) {
if (isSecondToLast && !isLastCollapsedApiReq) {
return
}
const timer = setTimeout(() => {
scrollToBottomAuto()
}, 0)
return () => clearTimeout(timer)
} else {
const timer = setTimeout(() => {
virtuosoRef.current?.scrollToIndex({
index: groupedMessages.length - (isLast ? 1 : 2),
align: "start",
})
}, 0)
return () => clearTimeout(timer)
}
}
},
[groupedMessages, expandedRows, scrollToBottomAuto, isAtBottom, setExpandedRows],
)
// Handle row height changes
const handleRowHeightChange = useCallback(
(isTaller: boolean) => {
if (!disableAutoScrollRef.current) {
if (isTaller) {
scrollToBottomSmooth()
} else {
setTimeout(() => {
scrollToBottomAuto()
}, 0)
}
}
},
[scrollToBottomSmooth, scrollToBottomAuto],
)
// Auto-scroll when new messages arrive
useEffect(() => {
if (!disableAutoScrollRef.current) {
setTimeout(() => {
scrollToBottomSmooth()
}, 50)
}
}, [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) {
if (scrollContainerRef.current?.contains(wheelEvent.target as Node)) {
// user scrolled up
disableAutoScrollRef.current = true
}
}
}, [])
useEvent("wheel", handleWheel, window, { passive: true })
return {
virtuosoRef,
scrollContainerRef,
disableAutoScrollRef,
scrollToBottomSmooth,
scrollToBottomAuto,
scrollToMessage,
toggleRowExpansion,
handleRowHeightChange,
showScrollToBottom,
setShowScrollToBottom,
isAtBottom,
setIsAtBottom,
pendingScrollToMessage,
setPendingScrollToMessage,
}
}
@@ -0,0 +1,21 @@
/**
* Barrel export for chat-view utilities, hooks, components, and types
*/
// Export utilities
export * from "./utils/markdownUtils"
export * from "./utils/messageUtils"
export * from "./utils/scrollUtils"
// Export hooks
export * from "./hooks"
// Export layout components
export * from "./components/layout"
// Export message components
export * from "./components/messages"
// Export types and constants
export * from "./types/chatTypes"
export * from "./constants"
@@ -0,0 +1,172 @@
/**
* Shared types and interfaces for the chat view components
*/
import { ClineMessage, ClineAsk } from "@shared/ExtensionMessage"
import { VirtuosoHandle } from "react-virtuoso"
/**
* Main ChatView component props
*/
export interface ChatViewProps {
isHidden: boolean
showAnnouncement: boolean
hideAnnouncement: () => void
showHistoryView: () => void
}
/**
* Chat state interface
*/
export interface ChatState {
// State values
inputValue: string
setInputValue: React.Dispatch<React.SetStateAction<string>>
activeQuote: string | null
setActiveQuote: React.Dispatch<React.SetStateAction<string | null>>
isTextAreaFocused: boolean
setIsTextAreaFocused: React.Dispatch<React.SetStateAction<boolean>>
selectedImages: string[]
setSelectedImages: React.Dispatch<React.SetStateAction<string[]>>
selectedFiles: string[]
setSelectedFiles: React.Dispatch<React.SetStateAction<string[]>>
sendingDisabled: boolean
setSendingDisabled: React.Dispatch<React.SetStateAction<boolean>>
enableButtons: boolean
setEnableButtons: React.Dispatch<React.SetStateAction<boolean>>
primaryButtonText: string | undefined
setPrimaryButtonText: React.Dispatch<React.SetStateAction<string | undefined>>
secondaryButtonText: string | undefined
setSecondaryButtonText: React.Dispatch<React.SetStateAction<string | undefined>>
didClickCancel: boolean
setDidClickCancel: React.Dispatch<React.SetStateAction<boolean>>
expandedRows: Record<number, boolean>
setExpandedRows: React.Dispatch<React.SetStateAction<Record<number, boolean>>>
// Refs
textAreaRef: React.RefObject<HTMLTextAreaElement>
// Derived values
lastMessage: ClineMessage | undefined
secondLastMessage: ClineMessage | undefined
clineAsk: ClineAsk | undefined
task: ClineMessage | undefined
// Handlers
handleFocusChange: (isFocused: boolean) => void
clearExpandedRows: () => void
resetState: () => void
// Scroll-related state (will be moved to scroll hook)
showScrollToBottom?: boolean
isAtBottom?: boolean
pendingScrollToMessage?: number | null
}
/**
* Message handlers interface
*/
export interface MessageHandlers {
handleSendMessage: (text: string, images: string[], files: string[]) => Promise<void>
handlePrimaryButtonClick: (text?: string, images?: string[], files?: string[]) => Promise<void>
handleSecondaryButtonClick: (text?: string, images?: string[], files?: string[]) => Promise<void>
handleTaskCloseButtonClick: () => void
startNewTask: () => Promise<void>
}
/**
* Scroll behavior interface
*/
export interface ScrollBehavior {
virtuosoRef: React.RefObject<VirtuosoHandle>
scrollContainerRef: React.RefObject<HTMLDivElement>
disableAutoScrollRef: React.MutableRefObject<boolean>
scrollToBottomSmooth: () => void
scrollToBottomAuto: () => void
scrollToMessage: (messageIndex: number) => void
toggleRowExpansion: (ts: number) => void
handleRowHeightChange: (isTaller: boolean) => void
showScrollToBottom: boolean
setShowScrollToBottom: React.Dispatch<React.SetStateAction<boolean>>
isAtBottom: boolean
setIsAtBottom: React.Dispatch<React.SetStateAction<boolean>>
pendingScrollToMessage: number | null
setPendingScrollToMessage: React.Dispatch<React.SetStateAction<number | null>>
}
/**
* Button state interface
*/
export interface ButtonState {
enableButtons: boolean
primaryButtonText: string | undefined
secondaryButtonText: string | undefined
}
/**
* Input state interface
*/
export interface InputState {
inputValue: string
selectedImages: string[]
selectedFiles: string[]
activeQuote: string | null
isTextAreaFocused: boolean
}
/**
* Task section props
*/
export interface TaskSectionProps {
task: ClineMessage
messages: ClineMessage[]
scrollBehavior: ScrollBehavior
buttonState: ButtonState
messageHandlers: MessageHandlers
chatState: ChatState
apiMetrics: {
totalTokensIn: number
totalTokensOut: number
totalCacheWrites?: number
totalCacheReads?: number
totalCost: number
}
lastApiReqTotalTokens?: number
selectedModelInfo: {
supportsPromptCache: boolean
supportsImages: boolean
}
isStreaming: boolean
clineAsk?: ClineAsk
modifiedMessages: ClineMessage[]
}
/**
* Welcome section props
*/
export interface WelcomeSectionProps {
showAnnouncement: boolean
hideAnnouncement: () => void
showHistoryView: () => void
telemetrySetting: string
version: string
taskHistory: any[]
shouldShowQuickWins: boolean
}
/**
* Input section props
*/
export interface InputSectionProps {
chatState: ChatState
messageHandlers: MessageHandlers
textAreaRef: React.RefObject<HTMLTextAreaElement>
onFocusChange: (isFocused: boolean) => void
onInputChange: (value: string) => void
onQuoteChange: (quote: string | null) => void
onImagesChange: (images: string[]) => void
onFilesChange: (files: string[]) => void
placeholderText: string
shouldDisableFilesAndImages: boolean
selectFilesAndImages: () => Promise<void>
}
@@ -0,0 +1,58 @@
/**
* Utility functions for handling markdown conversions and cleanup
*/
import { unified } from "unified"
import remarkStringify from "remark-stringify"
import rehypeRemark from "rehype-remark"
import rehypeParse from "rehype-parse"
/**
* Clean up markdown escape characters
*/
export function cleanupMarkdownEscapes(markdown: string): string {
return (
markdown
// Handle underscores and asterisks (single or multiple)
.replace(/\\([_*]+)/g, "$1")
// Handle angle brackets (for generics and XML)
.replace(/\\([<>])/g, "$1")
// Handle backticks (for code)
.replace(/\\(`)/g, "$1")
// Handle other common markdown special characters
.replace(/\\([[\]()#.!])/g, "$1")
// Fix multiple consecutive backslashes
.replace(/\\{2,}([_*`<>[\]()#.!])/g, "$1")
)
}
/**
* Convert HTML to Markdown
*/
export async function convertHtmlToMarkdown(html: string): Promise<string> {
// Process the HTML to Markdown
const result = await unified()
.use(rehypeParse as any, { fragment: true }) // Parse HTML fragments
.use(rehypeRemark as any) // Convert HTML to Markdown AST
.use(remarkStringify as any, {
// Convert Markdown AST to text
bullet: "-", // Use - for unordered lists
emphasis: "*", // Use * for emphasis
strong: "_", // Use _ for strong
listItemIndent: "one", // Use one space for list indentation
rule: "-", // Use - for horizontal rules
ruleSpaces: false, // No spaces in horizontal rules
fences: true,
escape: false,
entities: false,
})
.process(html)
const md = String(result)
// Apply comprehensive cleanup of escape characters
return cleanupMarkdownEscapes(md)
}
@@ -0,0 +1,151 @@
/**
* Utility functions for message filtering, grouping, and manipulation
*/
import { ClineMessage, ClineSayBrowserAction } from "@shared/ExtensionMessage"
import { combineApiRequests } from "@shared/combineApiRequests"
import { combineCommandSequences } from "@shared/combineCommandSequences"
/**
* Combine API requests and command sequences in messages
*/
export function processMessages(messages: ClineMessage[]): ClineMessage[] {
return combineApiRequests(combineCommandSequences(messages))
}
/**
* Filter messages that should be visible in the chat
*/
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
if (message.text === "") {
return false
}
break
case "api_req_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":
return false
case "text":
// Sometimes cline returns an empty text message, we don't want to render these
if ((message.text ?? "") === "" && (message.images?.length ?? 0) === 0) {
return false
}
break
case "mcp_server_request_started":
return false
}
return true
})
}
/**
* Check if a message is part of a browser session
*/
export function isBrowserSessionMessage(message: ClineMessage): boolean {
if (message.type === "ask") {
return ["browser_action_launch"].includes(message.ask!)
}
if (message.type === "say") {
return [
"browser_action_launch",
"api_req_started",
"text",
"browser_action",
"browser_action_result",
"checkpoint_created",
"reasoning",
].includes(message.say!)
}
return false
}
/**
* Group messages, combining browser session messages into arrays
*/
export function groupMessages(visibleMessages: ClineMessage[]): (ClineMessage | ClineMessage[])[] {
const result: (ClineMessage | ClineMessage[])[] = []
let currentGroup: ClineMessage[] = []
let isInBrowserSession = false
const endBrowserSession = () => {
if (currentGroup.length > 0) {
result.push([...currentGroup])
currentGroup = []
isInBrowserSession = false
}
}
visibleMessages.forEach((message) => {
if (message.ask === "browser_action_launch" || message.say === "browser_action_launch") {
// complete existing browser session if any
endBrowserSession()
// start new
isInBrowserSession = true
currentGroup.push(message)
} else if (isInBrowserSession) {
// end session if api_req_started is cancelled
if (message.say === "api_req_started") {
// get last api_req_started in currentGroup to check if it's cancelled
const lastApiReqStarted = [...currentGroup].reverse().find((m) => m.say === "api_req_started")
if (lastApiReqStarted?.text != null) {
const info = JSON.parse(lastApiReqStarted.text)
const isCancelled = info.cancelReason != null
if (isCancelled) {
endBrowserSession()
result.push(message)
return
}
}
}
if (isBrowserSessionMessage(message)) {
currentGroup.push(message)
// Check if this is a close action
if (message.say === "browser_action") {
const browserAction = JSON.parse(message.text || "{}") as ClineSayBrowserAction
if (browserAction.action === "close") {
endBrowserSession()
}
}
} else {
// complete existing browser session if any
endBrowserSession()
result.push(message)
}
} else {
result.push(message)
}
})
// Handle case where browser session is the last group
if (currentGroup.length > 0) {
result.push([...currentGroup])
}
return result
}
/**
* Get the task message from the messages array
*/
export function getTaskMessage(messages: ClineMessage[]): ClineMessage | undefined {
return messages.at(0)
}
/**
* Check if we should show the scroll to bottom button
*/
export function shouldShowScrollButton(disableAutoScroll: boolean, isAtBottom: boolean): boolean {
return disableAutoScroll && !isAtBottom
}
@@ -0,0 +1,60 @@
/**
* Utility functions for scroll behavior and management
*/
import debounce from "debounce"
import { VirtuosoHandle } from "react-virtuoso"
/**
* Create a debounced smooth scroll function
*/
export function createSmoothScrollToBottom(virtuosoRef: React.RefObject<VirtuosoHandle>) {
return debounce(
() => {
virtuosoRef.current?.scrollTo({
top: Number.MAX_SAFE_INTEGER,
behavior: "smooth",
})
},
10,
{ immediate: true },
)
}
/**
* Scroll to bottom with auto behavior
*/
export function scrollToBottomAuto(virtuosoRef: React.RefObject<VirtuosoHandle>) {
virtuosoRef.current?.scrollTo({
top: Number.MAX_SAFE_INTEGER,
behavior: "auto", // instant causes crash
})
}
/**
* Handle wheel events to detect user scroll
*/
export function createWheelHandler(
scrollContainerRef: React.RefObject<HTMLDivElement>,
disableAutoScrollRef: React.MutableRefObject<boolean>,
) {
return (event: Event) => {
const wheelEvent = event as WheelEvent
if (wheelEvent.deltaY && wheelEvent.deltaY < 0) {
if (scrollContainerRef.current?.contains(wheelEvent.target as Node)) {
// user scrolled up
disableAutoScrollRef.current = true
}
}
}
}
/**
* Constants for scroll behavior
*/
export const SCROLL_CONSTANTS = {
AT_BOTTOM_THRESHOLD: 10,
VIEWPORT_INCREASE_TOP: 3_000,
VIEWPORT_INCREASE_BOTTOM: Number.MAX_SAFE_INTEGER,
FOOTER_HEIGHT: 5,
} as const
File diff suppressed because it is too large Load Diff
@@ -1,104 +0,0 @@
import { VSCodeCheckbox, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { memo } from "react"
import { OpenAIReasoningEffort } from "@shared/ChatSettings"
const FeatureSettingsSection = () => {
const {
enableCheckpointsSetting,
setEnableCheckpointsSetting,
mcpMarketplaceEnabled,
setMcpMarketplaceEnabled,
mcpRichDisplayEnabled,
setMcpRichDisplayEnabled,
mcpResponsesCollapsed,
setMcpResponsesCollapsed,
chatSettings,
setChatSettings,
} = useExtensionState()
return (
<div style={{ marginBottom: 20 }}>
<div>
<VSCodeCheckbox
checked={enableCheckpointsSetting}
onChange={(e: any) => {
const checked = e.target.checked === true
setEnableCheckpointsSetting(checked)
}}>
Enable Checkpoints
</VSCodeCheckbox>
<p className="text-xs text-[var(--vscode-descriptionForeground)]">
Enables extension to save checkpoints of workspace throughout the task. Uses git under the hood which may not
work well with large workspaces.
</p>
</div>
<div style={{ marginTop: 10 }}>
<VSCodeCheckbox
checked={mcpMarketplaceEnabled}
onChange={(e: any) => {
const checked = e.target.checked === true
setMcpMarketplaceEnabled(checked)
}}>
Enable MCP Marketplace
</VSCodeCheckbox>
<p className="text-xs text-[var(--vscode-descriptionForeground)]">
Enables the MCP Marketplace tab for discovering and installing MCP servers.
</p>
</div>
<div style={{ marginTop: 10 }}>
<VSCodeCheckbox
checked={mcpRichDisplayEnabled}
onChange={(e: any) => {
const checked = e.target.checked === true
setMcpRichDisplayEnabled(checked)
}}>
Enable Rich MCP Display
</VSCodeCheckbox>
<p className="text-xs text-[var(--vscode-descriptionForeground)]">
Enables rich formatting for MCP responses. When disabled, responses will be shown in plain text.
</p>
</div>
<div style={{ marginTop: 10 }}>
<VSCodeCheckbox
checked={mcpResponsesCollapsed}
onChange={(e: any) => {
const checked = e.target.checked === true
setMcpResponsesCollapsed(checked)
}}>
Collapse MCP Responses
</VSCodeCheckbox>
<p className="text-xs text-[var(--vscode-descriptionForeground)]">
Sets the default display mode for MCP response panels
</p>
</div>
<div style={{ marginTop: 10 }}>
<label
htmlFor="openai-reasoning-effort-dropdown"
className="block text-sm font-medium text-[var(--vscode-foreground)] mb-1">
OpenAI Reasoning Effort
</label>
<VSCodeDropdown
id="openai-reasoning-effort-dropdown"
currentValue={chatSettings.openAIReasoningEffort || "medium"}
onChange={(e: any) => {
const newValue = e.target.currentValue as OpenAIReasoningEffort
setChatSettings({
...chatSettings,
openAIReasoningEffort: newValue,
})
}}
className="w-full">
<VSCodeOption value="low">Low</VSCodeOption>
<VSCodeOption value="medium">Medium</VSCodeOption>
<VSCodeOption value="high">High</VSCodeOption>
</VSCodeDropdown>
<p className="text-xs mt-[5px] text-[var(--vscode-descriptionForeground)]">
Reasoning effort for the OpenAI family of models(applies to all OpenAI model providers)
</p>
</div>
</div>
)
}
export default memo(FeatureSettingsSection)
@@ -10,7 +10,7 @@ import { useRemark } from "react-remark"
import { useMount } from "react-use"
import styled from "styled-components"
import { highlight } from "../history/HistoryView"
import { ModelInfoView } from "./ApiOptions"
import { ModelInfoView } from "./common/ModelInfoView"
import { normalizeApiConfiguration } from "./utils/providerUtils"
import FeaturedModelCard from "./FeaturedModelCard"
import ThinkingBudgetSlider from "./ThinkingBudgetSlider"
@@ -66,7 +66,6 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
const [selectedIndex, setSelectedIndex] = useState(-1)
const dropdownRef = useRef<HTMLDivElement>(null)
const itemRefs = useRef<(HTMLDivElement | null)[]>([])
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
const dropdownListRef = useRef<HTMLDivElement>(null)
const handleModelChange = (newModelId: string) => {
@@ -311,13 +310,7 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
<ThinkingBudgetSlider apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
)}
<ModelInfoView
selectedModelId={selectedModelId}
modelInfo={selectedModelInfo}
isDescriptionExpanded={isDescriptionExpanded}
setIsDescriptionExpanded={setIsDescriptionExpanded}
isPopup={isPopup}
/>
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
</>
) : (
<p
@@ -10,7 +10,7 @@ import { useExtensionState } from "../../context/ExtensionStateContext"
import { ModelsServiceClient } from "../../services/grpc-client"
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
import { highlight } from "../history/HistoryView"
import { ModelInfoView } from "./ApiOptions"
import { ModelInfoView } from "./common/ModelInfoView"
import { normalizeApiConfiguration } from "./utils/providerUtils"
import ThinkingBudgetSlider from "./ThinkingBudgetSlider"
@@ -25,7 +25,6 @@ const RequestyModelPicker: React.FC<RequestyModelPickerProps> = ({ isPopup }) =>
const [selectedIndex, setSelectedIndex] = useState(-1)
const dropdownRef = useRef<HTMLDivElement>(null)
const itemRefs = useRef<(HTMLDivElement | null)[]>([])
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
const dropdownListRef = useRef<HTMLDivElement>(null)
const handleModelChange = (newModelId: string) => {
@@ -230,13 +229,7 @@ const RequestyModelPicker: React.FC<RequestyModelPickerProps> = ({ isPopup }) =>
{showBudgetSlider && (
<ThinkingBudgetSlider apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
)}
<ModelInfoView
selectedModelId={selectedModelId}
modelInfo={selectedModelInfo}
isDescriptionExpanded={isDescriptionExpanded}
setIsDescriptionExpanded={setIsDescriptionExpanded}
isPopup={isPopup}
/>
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
</>
) : (
<p
@@ -3,26 +3,27 @@ import HeroTooltip from "@/components/common/HeroTooltip"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { StateServiceClient } from "@/services/grpc-client"
import { validateApiConfiguration, validateModelId } from "@/utils/validate"
import { vscode } from "@/utils/vscode"
import { ExtensionMessage } from "@shared/ExtensionMessage"
import { EmptyRequest, StringRequest } from "@shared/proto/common"
import { StringRequest } from "@shared/proto/common"
import { PlanActMode, ResetStateRequest, TogglePlanActModeRequest, UpdateSettingsRequest } from "@shared/proto/state"
import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react"
import { VSCodeButton, VSCodeLink } 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 { useEvent } from "react-use"
import { Tab, TabContent, TabHeader, TabList, TabTrigger } from "../common/Tab"
import { TabButton } from "../mcp/configuration/McpConfigurationView"
import ApiOptions from "./ApiOptions"
import BrowserSettingsSection from "./BrowserSettingsSection"
import { BrowserSettings } from "@shared/BrowserSettings"
import FeatureSettingsSection from "./FeatureSettingsSection"
import PreferredLanguageSetting from "./PreferredLanguageSetting" // Added import
import FeatureSettingsSection from "./sections/FeatureSettingsSection"
import Section from "./Section"
import SectionHeader from "./SectionHeader"
import TerminalSettingsSection from "./TerminalSettingsSection"
import TerminalSettingsSection from "./sections/TerminalSettingsSection"
import ApiConfigurationSection from "./sections/ApiConfigurationSection"
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
// Styles for the tab system
@@ -761,192 +762,52 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
<TabContent className="flex-1 overflow-auto">
{/* API Configuration Tab */}
{activeTab === "api-config" && (
<div>
{renderSectionHeader("api-config")}
<Section>
{/* Tabs container */}
{planActSeparateModelsSetting ? (
<div className="rounded-md mb-5 bg-[var(--vscode-panel-background)]">
<div className="flex gap-[1px] mb-[10px] -mt-2 border-0 border-b border-solid border-[var(--vscode-panel-border)]">
<TabButton
isActive={chatSettings.mode === "plan"}
onClick={() => handlePlanActModeChange("plan")}
disabled={isSwitchingMode}
style={{
opacity: isSwitchingMode ? 0.6 : 1,
cursor: isSwitchingMode ? "not-allowed" : "pointer",
}}>
{isSwitchingMode && chatSettings.mode === "act"
? "Switching..."
: "Plan Mode"}
</TabButton>
<TabButton
isActive={chatSettings.mode === "act"}
onClick={() => handlePlanActModeChange("act")}
disabled={isSwitchingMode}
style={{
opacity: isSwitchingMode ? 0.6 : 1,
cursor: isSwitchingMode ? "not-allowed" : "pointer",
}}>
{isSwitchingMode && chatSettings.mode === "plan"
? "Switching..."
: "Act Mode"}
</TabButton>
</div>
{/* Content container */}
<div className="-mb-3">
<ApiOptions
key={chatSettings.mode}
showModelOptions={true}
apiErrorMessage={apiErrorMessage}
modelIdErrorMessage={modelIdErrorMessage}
/>
</div>
</div>
) : (
<ApiOptions
key={"single"}
showModelOptions={true}
apiErrorMessage={apiErrorMessage}
modelIdErrorMessage={modelIdErrorMessage}
/>
)}
<div className="mb-[5px]">
<VSCodeCheckbox
className="mb-[5px]"
checked={planActSeparateModelsSetting}
onChange={(e: any) => {
const checked = e.target.checked === true
setPlanActSeparateModelsSetting(checked)
}}>
Use different models for Plan and Act modes
</VSCodeCheckbox>
<p className="text-xs mt-[5px] text-[var(--vscode-descriptionForeground)]">
Switching between Plan and Act mode will persist the API and model used in the
previous mode. This may be helpful e.g. when using a strong reasoning model to
architect a plan for a cheaper coding model to act on.
</p>
</div>
</Section>
</div>
<ApiConfigurationSection
planActSeparateModelsSetting={planActSeparateModelsSetting}
chatSettings={chatSettings}
isSwitchingMode={isSwitchingMode}
apiErrorMessage={apiErrorMessage}
modelIdErrorMessage={modelIdErrorMessage}
handlePlanActModeChange={handlePlanActModeChange}
setPlanActSeparateModelsSetting={setPlanActSeparateModelsSetting}
renderSectionHeader={renderSectionHeader}
/>
)}
{/* General Settings Tab */}
{activeTab === "general" && (
<div>
{renderSectionHeader("general")}
<Section>
{chatSettings && (
<PreferredLanguageSetting
chatSettings={chatSettings}
setChatSettings={setChatSettings}
/>
)}
<div className="mb-[5px]">
<VSCodeCheckbox
className="mb-[5px]"
checked={telemetrySetting !== "disabled"}
onChange={(e: any) => {
const checked = e.target.checked === true
setTelemetrySetting(checked ? "enabled" : "disabled")
}}>
Allow anonymous error and usage reporting
</VSCodeCheckbox>
<p className="text-xs mt-[5px] text-[var(--vscode-descriptionForeground)]">
Help improve Cline by sending anonymous usage data and error reports. No code,
prompts, or personal information are ever sent. See our{" "}
<VSCodeLink
href="https://docs.cline.bot/more-info/telemetry"
className="text-inherit">
telemetry overview
</VSCodeLink>{" "}
and{" "}
<VSCodeLink href="https://cline.bot/privacy" className="text-inherit">
privacy policy
</VSCodeLink>{" "}
for more details.
</p>
</div>
</Section>
</div>
<GeneralSettingsSection
chatSettings={chatSettings}
setChatSettings={setChatSettings}
telemetrySetting={telemetrySetting}
setTelemetrySetting={setTelemetrySetting}
renderSectionHeader={renderSectionHeader}
/>
)}
{/* Feature Settings Tab */}
{activeTab === "features" && (
<div>
{renderSectionHeader("features")}
<Section>
<FeatureSettingsSection />
</Section>
</div>
)}
{activeTab === "features" && <FeatureSettingsSection renderSectionHeader={renderSectionHeader} />}
{/* Browser Settings Tab */}
{activeTab === "browser" && (
<div>
{renderSectionHeader("browser")}
<Section>
<BrowserSettingsSection
localBrowserSettings={localBrowserSettings}
onBrowserSettingsChange={setLocalBrowserSettings}
/>
</Section>
</div>
<BrowserSettingsSection
localBrowserSettings={localBrowserSettings}
onBrowserSettingsChange={setLocalBrowserSettings}
renderSectionHeader={renderSectionHeader}
/>
)}
{/* Terminal Settings Tab */}
{activeTab === "terminal" && (
<div>
{renderSectionHeader("terminal")}
<Section>
<TerminalSettingsSection />
</Section>
</div>
)}
{activeTab === "terminal" && <TerminalSettingsSection renderSectionHeader={renderSectionHeader} />}
{/* Debug Tab (only in dev mode) */}
{IS_DEV && activeTab === "debug" && (
<div>
{renderSectionHeader("debug")}
<Section>
<VSCodeButton
onClick={() => handleResetState()}
className="mt-[5px] w-auto"
style={{ backgroundColor: "var(--vscode-errorForeground)", color: "black" }}>
Reset Workspace State
</VSCodeButton>
<VSCodeButton
onClick={() => handleResetState(true)}
className="mt-[5px] w-auto"
style={{ backgroundColor: "var(--vscode-errorForeground)", color: "black" }}>
Reset Global State
</VSCodeButton>
<p className="text-xs mt-[5px] text-[var(--vscode-descriptionForeground)]">
This will reset all global state and secret storage in the extension.
</p>
</Section>
</div>
<DebugSection onResetState={handleResetState} renderSectionHeader={renderSectionHeader} />
)}
{/* About Tab */}
{activeTab === "about" && (
<div>
{renderSectionHeader("about")}
<Section>
<div className="text-center text-[var(--vscode-descriptionForeground)] text-xs leading-[1.2] px-0 py-0 pr-2 pb-[15px] mt-auto">
<p className="break-words m-0 p-0">
If you have any questions or feedback, feel free to open an issue at{" "}
<VSCodeLink href="https://github.com/cline/cline" className="inline">
https://github.com/cline/cline
</VSCodeLink>
</p>
<p className="italic mt-[10px] mb-0 p-0">v{version}</p>
</div>
</Section>
</div>
<AboutSection version={version} renderSectionHeader={renderSectionHeader} />
)}
</TabContent>
)
@@ -1,142 +0,0 @@
import React, { useState, useEffect } from "react"
import { VSCodeTextField, VSCodeCheckbox, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import TerminalOutputLineLimitSlider from "./TerminalOutputLineLimitSlider"
import { StateServiceClient } from "../../services/grpc-client"
import { Int64, Int64Request } from "@shared/proto/common"
export const TerminalSettingsSection: React.FC = () => {
const {
shellIntegrationTimeout,
setShellIntegrationTimeout,
terminalReuseEnabled,
setTerminalReuseEnabled,
defaultTerminalProfile,
setDefaultTerminalProfile,
availableTerminalProfiles,
platform,
} = useExtensionState()
const [inputValue, setInputValue] = useState((shellIntegrationTimeout / 1000).toString())
const [inputError, setInputError] = useState<string | null>(null)
const handleTimeoutChange = (event: Event) => {
const target = event.target as HTMLInputElement
const value = target.value
setInputValue(value)
const seconds = parseFloat(value)
if (isNaN(seconds) || seconds <= 0) {
setInputError("Please enter a positive number")
return
}
setInputError(null)
const timeout = Math.round(seconds * 1000)
setShellIntegrationTimeout(timeout)
StateServiceClient.updateTerminalConnectionTimeout({
value: timeout,
} as Int64Request)
.then((response: Int64) => {
setShellIntegrationTimeout(response.value)
setInputValue((response.value / 1000).toString())
})
.catch((error) => {
console.error("Failed to update terminal connection timeout:", error)
})
}
const handleInputBlur = () => {
if (inputError) {
setInputValue((shellIntegrationTimeout / 1000).toString())
setInputError(null)
}
}
const handleTerminalReuseChange = (event: Event) => {
const target = event.target as HTMLInputElement
const checked = target.checked
setTerminalReuseEnabled(checked)
StateServiceClient.updateTerminalReuseEnabled({ value: checked } as any).catch((error) => {
console.error("Failed to update terminal reuse enabled:", error)
})
}
// Use any to avoid type conflicts between Event and FormEvent
const handleDefaultTerminalProfileChange = (event: any) => {
const target = event.target as HTMLSelectElement
const profileId = target.value
// Only update the local state, let the Save button handle the backend update
setDefaultTerminalProfile(profileId)
}
const profilesToShow = availableTerminalProfiles
return (
<div id="terminal-settings-section" style={{ marginBottom: 20 }}>
<div style={{ marginBottom: 15 }}>
<label htmlFor="default-terminal-profile" style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>
Default Terminal Profile
</label>
<VSCodeDropdown
id="default-terminal-profile"
value={defaultTerminalProfile || "default"}
onChange={handleDefaultTerminalProfileChange}
style={{ width: "100%" }}>
{profilesToShow.map((profile) => (
<VSCodeOption key={profile.id} value={profile.id} title={profile.description}>
{profile.name}
</VSCodeOption>
))}
</VSCodeDropdown>
<p style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)", margin: "5px 0 0 0" }}>
Select the default terminal Cline will use. 'Default' uses your VSCode global setting.
</p>
</div>
<div style={{ marginBottom: 15 }}>
<div style={{ marginBottom: 8 }}>
<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>
Shell integration timeout (seconds)
</label>
<div style={{ display: "flex", alignItems: "center" }}>
<VSCodeTextField
style={{ width: "100%" }}
value={inputValue}
placeholder="Enter timeout in seconds"
onChange={(event) => handleTimeoutChange(event as Event)}
onBlur={handleInputBlur}
/>
</div>
{inputError && (
<div style={{ color: "var(--vscode-errorForeground)", fontSize: "12px", marginTop: 5 }}>{inputError}</div>
)}
</div>
<p style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)", margin: 0 }}>
Set how long Cline waits for shell integration to activate before executing commands. Increase this value if
you experience terminal connection timeouts.
</p>
</div>
<div style={{ marginBottom: 15 }}>
<div style={{ display: "flex", alignItems: "center", marginBottom: 8 }}>
<VSCodeCheckbox
checked={terminalReuseEnabled ?? true}
onChange={(event) => handleTerminalReuseChange(event as Event)}>
Enable aggressive terminal reuse
</VSCodeCheckbox>
</div>
<p style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)", margin: 0 }}>
When enabled, Cline will reuse existing terminal windows that aren't in the current working directory. Disable
this if you experience issues with task lockout after a terminal command.
</p>
</div>
<TerminalOutputLineLimitSlider />
</div>
)
}
export default TerminalSettingsSection
@@ -99,7 +99,7 @@ export const ModelInfoView = ({ selectedModelId, modelInfo, isPopup }: ModelInfo
// Internal state management for description expansion
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
const isGeminiProvider = Object.keys(geminiModels).includes(selectedModelId)
const isGemini = Object.keys(geminiModels).includes(selectedModelId)
const hasThinkingConfig = hasThinkingBudget(modelInfo)
const hasTiers = !!modelInfo.tiers && modelInfo.tiers.length > 0
@@ -170,7 +170,7 @@ export const ModelInfoView = ({ selectedModelId, modelInfo, isPopup }: ModelInfo
supportsLabel="Supports browser use"
doesNotSupportLabel="Does not support browser use"
/>,
!isGeminiProvider && (
!isGemini && (
<ModelInfoSupportsItem
key="supportsPromptCache"
isSupported={supportsPromptCache(modelInfo)}
@@ -195,13 +195,6 @@ export const ModelInfoView = ({ selectedModelId, modelInfo, isPopup }: ModelInfo
</span>
),
outputPriceElement, // Add the generated output price block
isGeminiProvider && (
<span key="geminiPricing" style={{ fontStyle: "italic" }}>
<VSCodeLink href="https://ai.google.dev/pricing" style={{ display: "inline", fontSize: "inherit" }}>
For more info, see pricing details.
</VSCodeLink>
</span>
),
].filter(Boolean)
return (
@@ -32,6 +32,17 @@ interface ModelSelectorProps {
label?: string
}
/*
OG Saoud Note:
VSCodeDropdown has an open bug where dynamically rendered options don't auto select the provided value prop. You can see this for yourself by comparing it with normal select/option elements, which work as expected.
https://github.com/microsoft/vscode-webview-ui-toolkit/issues/433
In our case, when the user switches between providers, we recalculate the selectedModelId depending on the provider, the default model for that provider, and a modelId that the user may have selected. Unfortunately, the VSCodeDropdown component wouldn't select this calculated value, and would default to the first "Select a model..." option instead, which makes it seem like the model was cleared out when it wasn't.
As a workaround, we create separate instances of the dropdown for each provider, and then conditionally render the one that matches the current provider.
*/
/**
* A reusable component for selecting models from a dropdown
*/
@@ -0,0 +1,311 @@
import { ApiConfiguration, bedrockDefaultModelId, bedrockModels } from "@shared/api"
import {
VSCodeCheckbox,
VSCodeDropdown,
VSCodeOption,
VSCodeRadio,
VSCodeRadioGroup,
VSCodeTextField,
} from "@vscode/webview-ui-toolkit/react"
import { useState } from "react"
import { ModelInfoView } from "../common/ModelInfoView"
import { DropdownContainer } from "../common/ModelSelector"
import ThinkingBudgetSlider from "../ThinkingBudgetSlider"
import { normalizeApiConfiguration } from "../utils/providerUtils"
// 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) => {
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
const [awsEndpointSelected, setAwsEndpointSelected] = useState(!!apiConfiguration?.awsBedrockEndpoint)
return (
<div
style={{
display: "flex",
flexDirection: "column",
gap: 5,
}}>
<VSCodeRadioGroup
value={apiConfiguration?.awsUseProfile ? "profile" : "credentials"}
onChange={(e) => {
const value = (e.target as HTMLInputElement)?.value
const useProfile = value === "profile"
setApiConfiguration({
...apiConfiguration,
awsUseProfile: useProfile,
})
}}>
<VSCodeRadio value="credentials">AWS Credentials</VSCodeRadio>
<VSCodeRadio value="profile">AWS Profile</VSCodeRadio>
</VSCodeRadioGroup>
{apiConfiguration?.awsUseProfile ? (
<VSCodeTextField
value={apiConfiguration?.awsProfile || ""}
style={{ width: "100%" }}
onInput={handleInputChange("awsProfile")}
placeholder="Enter profile name (default if empty)">
<span style={{ fontWeight: 500 }}>AWS Profile Name</span>
</VSCodeTextField>
) : (
<>
<VSCodeTextField
value={apiConfiguration?.awsAccessKey || ""}
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 || ""}
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 || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("awsSessionToken")}
placeholder="Enter Session Token...">
<span style={{ fontWeight: 500 }}>AWS Session Token</span>
</VSCodeTextField>
</>
)}
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 1} className="dropdown-container">
<label htmlFor="aws-region-dropdown">
<span style={{ fontWeight: 500 }}>AWS Region</span>
</label>
<VSCodeDropdown
id="aws-region-dropdown"
value={apiConfiguration?.awsRegion || ""}
style={{ width: "100%" }}
onChange={handleInputChange("awsRegion")}>
<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>
<VSCodeOption value="us-east-2">us-east-2</VSCodeOption>
{/* <VSCodeOption value="us-west-1">us-west-1</VSCodeOption> */}
<VSCodeOption value="us-west-2">us-west-2</VSCodeOption>
{/* <VSCodeOption value="af-south-1">af-south-1</VSCodeOption> */}
{/* <VSCodeOption value="ap-east-1">ap-east-1</VSCodeOption> */}
<VSCodeOption value="ap-south-1">ap-south-1</VSCodeOption>
<VSCodeOption value="ap-northeast-1">ap-northeast-1</VSCodeOption>
<VSCodeOption value="ap-northeast-2">ap-northeast-2</VSCodeOption>
<VSCodeOption value="ap-northeast-3">ap-northeast-3</VSCodeOption>
<VSCodeOption value="ap-southeast-1">ap-southeast-1</VSCodeOption>
<VSCodeOption value="ap-southeast-2">ap-southeast-2</VSCodeOption>
<VSCodeOption value="ca-central-1">ca-central-1</VSCodeOption>
<VSCodeOption value="eu-central-1">eu-central-1</VSCodeOption>
<VSCodeOption value="eu-central-2">eu-central-2</VSCodeOption>
<VSCodeOption value="eu-west-1">eu-west-1</VSCodeOption>
<VSCodeOption value="eu-west-2">eu-west-2</VSCodeOption>
<VSCodeOption value="eu-west-3">eu-west-3</VSCodeOption>
<VSCodeOption value="eu-north-1">eu-north-1</VSCodeOption>
<VSCodeOption value="eu-south-1">eu-south-1</VSCodeOption>
<VSCodeOption value="eu-south-2">eu-south-2</VSCodeOption>
{/* <VSCodeOption value="me-south-1">me-south-1</VSCodeOption> */}
<VSCodeOption value="sa-east-1">sa-east-1</VSCodeOption>
<VSCodeOption value="us-gov-east-1">us-gov-east-1</VSCodeOption>
<VSCodeOption value="us-gov-west-1">us-gov-west-1</VSCodeOption>
{/* <VSCodeOption value="us-gov-east-1">us-gov-east-1</VSCodeOption> */}
</VSCodeDropdown>
</DropdownContainer>
<div style={{ display: "flex", flexDirection: "column" }}>
<VSCodeCheckbox
checked={awsEndpointSelected}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setAwsEndpointSelected(isChecked)
if (!isChecked) {
setApiConfiguration({
...apiConfiguration,
awsBedrockEndpoint: "",
})
}
}}>
Use custom VPC endpoint
</VSCodeCheckbox>
{awsEndpointSelected && (
<VSCodeTextField
value={apiConfiguration?.awsBedrockEndpoint || ""}
style={{ width: "100%", marginTop: 3, marginBottom: 5 }}
type="url"
onInput={handleInputChange("awsBedrockEndpoint")}
placeholder="Enter VPC Endpoint URL (optional)"
/>
)}
<VSCodeCheckbox
checked={apiConfiguration?.awsUseCrossRegionInference || false}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setApiConfiguration({
...apiConfiguration,
awsUseCrossRegionInference: isChecked,
})
}}>
Use cross-region inference
</VSCodeCheckbox>
{selectedModelInfo.supportsPromptCache && (
<>
<VSCodeCheckbox
checked={apiConfiguration?.awsBedrockUsePromptCache || false}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setApiConfiguration({
...apiConfiguration,
awsBedrockUsePromptCache: isChecked,
})
}}>
Use prompt caching
</VSCodeCheckbox>
</>
)}
</div>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
{apiConfiguration?.awsUseProfile ? (
<>
Using AWS Profile credentials from ~/.aws/credentials. Leave profile name empty to use the default
profile. These credentials are only used locally to make API requests from this extension.
</>
) : (
<>
Authenticate by either providing the keys above or use the default AWS credential providers, i.e.
~/.aws/credentials or environment variables. These credentials are only used locally to make API requests
from this extension.
</>
)}
</p>
{showModelOptions && (
<>
<label htmlFor="bedrock-model-dropdown">
<span style={{ fontWeight: 500 }}>Model</span>
</label>
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 2} className="dropdown-container">
<VSCodeDropdown
id="bedrock-model-dropdown"
value={apiConfiguration?.awsBedrockCustomSelected ? "custom" : selectedModelId}
onChange={(e: any) => {
const isCustom = e.target.value === "custom"
setApiConfiguration({
...apiConfiguration,
apiModelId: isCustom ? "" : e.target.value,
awsBedrockCustomSelected: isCustom,
awsBedrockCustomModelBaseId: bedrockDefaultModelId,
})
}}
style={{ width: "100%" }}>
<VSCodeOption value="">Select a model...</VSCodeOption>
{Object.keys(bedrockModels).map((modelId) => (
<VSCodeOption
key={modelId}
value={modelId}
style={{
whiteSpace: "normal",
wordWrap: "break-word",
maxWidth: "100%",
}}>
{modelId}
</VSCodeOption>
))}
<VSCodeOption value="custom">Custom</VSCodeOption>
</VSCodeDropdown>
</DropdownContainer>
{apiConfiguration?.awsBedrockCustomSelected && (
<div>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
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
id="bedrock-model-input"
value={apiConfiguration?.apiModelId || ""}
style={{ width: "100%", marginTop: 3 }}
onInput={handleInputChange("apiModelId")}
placeholder="Enter custom model ID..."
/>
<label htmlFor="bedrock-base-model-dropdown">
<span style={{ fontWeight: 500 }}>Base Inference Model</span>
</label>
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 3} className="dropdown-container">
<VSCodeDropdown
id="bedrock-base-model-dropdown"
value={apiConfiguration?.awsBedrockCustomModelBaseId || bedrockDefaultModelId}
onChange={handleInputChange("awsBedrockCustomModelBaseId")}
style={{ width: "100%" }}>
<VSCodeOption value="">Select a model...</VSCodeOption>
{Object.keys(bedrockModels).map((modelId) => (
<VSCodeOption
key={modelId}
value={modelId}
style={{
whiteSpace: "normal",
wordWrap: "break-word",
maxWidth: "100%",
}}>
{modelId}
</VSCodeOption>
))}
</VSCodeDropdown>
</DropdownContainer>
</div>
)}
{(selectedModelId === "anthropic.claude-3-7-sonnet-20250219-v1:0" ||
selectedModelId === "anthropic.claude-sonnet-4-20250514-v1:0" ||
selectedModelId === "anthropic.claude-opus-4-20250514-v1:0" ||
(apiConfiguration?.awsBedrockCustomSelected &&
apiConfiguration?.awsBedrockCustomModelBaseId === "anthropic.claude-3-7-sonnet-20250219-v1:0") ||
(apiConfiguration?.awsBedrockCustomSelected &&
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} />
)}
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
</>
)}
</div>
)
}
@@ -0,0 +1,86 @@
import { ApiConfiguration } from "@shared/api"
import { VSCodeCheckbox, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { useState } from "react"
import { ClineAccountInfoCard } from "../ClineAccountInfoCard"
import OpenRouterModelPicker, { OPENROUTER_MODEL_PICKER_Z_INDEX } from "../OpenRouterModelPicker"
import { DropdownContainer } from "../common/ModelSelector"
/**
* Props for the ClineProvider component
*/
interface ClineProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
/**
* The Cline provider configuration component
*/
export const ClineProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: ClineProviderProps) => {
const [providerSortingSelected, setProviderSortingSelected] = useState(!!apiConfiguration?.openRouterProviderSorting)
// Create a wrapper for handling field changes more directly
const handleFieldChange = (field: keyof ApiConfiguration) => (value: any) => {
handleInputChange(field)({ target: { value } })
}
return (
<div>
{/* Cline Account Info Card */}
<div style={{ marginBottom: 14, marginTop: 4 }}>
<ClineAccountInfoCard />
</div>
{showModelOptions && (
<>
{/* Provider Sorting Options */}
<VSCodeCheckbox
style={{ marginTop: -10 }}
checked={providerSortingSelected}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setProviderSortingSelected(isChecked)
if (!isChecked) {
handleFieldChange("openRouterProviderSorting")("")
}
}}>
Sort underlying provider routing
</VSCodeCheckbox>
{providerSortingSelected && (
<div style={{ marginBottom: -6 }}>
<DropdownContainer className="dropdown-container" zIndex={OPENROUTER_MODEL_PICKER_Z_INDEX + 1}>
<VSCodeDropdown
style={{ width: "100%", marginTop: 3 }}
value={apiConfiguration?.openRouterProviderSorting}
onChange={(e: any) => {
handleFieldChange("openRouterProviderSorting")(e.target.value)
}}>
<VSCodeOption value="">Default</VSCodeOption>
<VSCodeOption value="price">Price</VSCodeOption>
<VSCodeOption value="throughput">Throughput</VSCodeOption>
<VSCodeOption value="latency">Latency</VSCodeOption>
</VSCodeDropdown>
</DropdownContainer>
<p style={{ fontSize: "12px", marginTop: 3, color: "var(--vscode-descriptionForeground)" }}>
{!apiConfiguration?.openRouterProviderSorting &&
"Default behavior is to load balance requests across providers (like AWS, Google Vertex, Anthropic), prioritizing price while considering provider uptime"}
{apiConfiguration?.openRouterProviderSorting === "price" &&
"Sort providers by price, prioritizing the lowest cost provider"}
{apiConfiguration?.openRouterProviderSorting === "throughput" &&
"Sort providers by throughput, prioritizing the provider with the highest throughput (may increase cost)"}
{apiConfiguration?.openRouterProviderSorting === "latency" &&
"Sort providers by response time, prioritizing the provider with the lowest latency"}
</p>
</div>
)}
{/* OpenRouter Model Picker */}
<OpenRouterModelPicker isPopup={isPopup} />
</>
)}
</div>
)
}
@@ -1,169 +0,0 @@
/**
* Gemini CLI Provider Component
*
* This component integrates with Google's Gemini CLI tool for OAuth authentication.
*
* Attribution: This implementation is inspired by and uses concepts from the Google Gemini CLI,
* which is licensed under the Apache License 2.0.
* Original project: https://github.com/google-gemini/gemini-cli
*
* Copyright 2025 Google LLC
* Licensed under the Apache License, Version 2.0
*/
import { ApiConfiguration, geminiCliModels } from "@shared/api"
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { memo } from "react"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
interface GeminiCliProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
const GeminiCliProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: GeminiCliProviderProps) => {
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
return (
<div>
<VSCodeTextField
value={apiConfiguration?.geminiCliOAuthPath || ""}
style={{ width: "100%", marginTop: 3 }}
type="text"
onInput={handleInputChange("geminiCliOAuthPath")}
placeholder="Default: ~/.gemini/oauth_creds.json">
<span style={{ fontWeight: 500 }}>OAuth Credentials Path (optional)</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
Path to the OAuth credentials file. Leave empty to use the default location (~/.gemini/oauth_creds.json).
</p>
{apiConfiguration?.geminiCliProjectId && (
<>
<VSCodeTextField
value={apiConfiguration.geminiCliProjectId}
style={{ width: "100%", marginTop: 3 }}
type="text"
disabled>
<span style={{ fontWeight: 500 }}>Discovered Project ID</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
This project ID was automatically discovered from your OAuth credentials.
</p>
</>
)}
<p
style={{
fontSize: "12px",
marginTop: 5,
color: "var(--vscode-descriptionForeground)",
}}>
This provider uses OAuth authentication from the Gemini CLI tool and does not require API keys. If you haven't
authenticated yet, please run{" "}
<code
style={{
backgroundColor: "var(--vscode-textCodeBlock-background)",
padding: "2px 4px",
borderRadius: "3px",
}}>
gemini
</code>{" "}
in your terminal first.
<br />
<VSCodeLink
href="https://github.com/google-gemini/gemini-cli?tab=readme-ov-file#quickstart"
style={{ display: "inline", fontSize: "inherit" }}>
Gemini CLI Setup Instructions
</VSCodeLink>
</p>
{showModelOptions && (
<>
<ModelSelector
models={geminiCliModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
label="Model"
/>
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
</>
)}
<div
style={{
backgroundColor: "var(--vscode-editorWarning-background, rgba(255, 191, 0, 0.1))",
padding: "8px",
borderRadius: "4px",
border: "1px solid var(--vscode-editorWarning-border, rgba(255, 191, 0, 0.3))",
marginTop: "8px",
marginBottom: "16px",
}}>
<div
style={{
display: "flex",
alignItems: "center",
marginBottom: "4px",
}}>
<i
className="codicon codicon-info"
style={{
marginRight: "6px",
fontSize: "14px",
color: "#FFA500",
}}></i>
<span
style={{
fontWeight: "bold",
color: "#FFA500",
fontSize: "12px",
}}>
Important Requirements
</span>
</div>
<p
style={{
margin: 0,
fontSize: "11px",
lineHeight: "1.4",
color: "var(--vscode-foreground)",
}}>
First, you need to install the <strong>Gemini CLI tool</strong>
<br /> Then, run <strong>gemini</strong> in your terminal and make sure you{" "}
<strong>Log in with Google</strong>
<br /> Only works with <strong>personal Google accounts</strong> (not Google Workspace accounts)
<br />
Does not use API keys - authentication is handled via OAuth
<br /> Requires the Gemini CLI tool to be installed and authenticated first
</p>
</div>
<p
style={{
fontSize: "12px",
marginTop: 5,
color: "var(--vscode-charts-green)",
fontWeight: 500,
}}>
Free tier access via OAuth authentication
</p>
</div>
)
}
export default memo(GeminiCliProvider)
@@ -0,0 +1,109 @@
import { ApiConfiguration } from "@shared/api"
import { VSCodeTextField, VSCodeRadioGroup, VSCodeRadio, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { useState, useCallback, useEffect } from "react"
import { useInterval } from "react-use"
import { ModelsServiceClient } from "@/services/grpc-client"
import { StringRequest } from "@shared/proto/common"
import { BaseUrlField } from "../common/BaseUrlField"
/**
* Props for the LMStudioProvider component
*/
interface LMStudioProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
/**
* The LM Studio provider configuration component
*/
export const LMStudioProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: LMStudioProviderProps) => {
const [lmStudioModels, setLmStudioModels] = useState<string[]>([])
// Poll LM Studio models
const requestLmStudioModels = useCallback(async () => {
try {
const response = await ModelsServiceClient.getLmStudioModels(
StringRequest.create({
value: apiConfiguration?.lmStudioBaseUrl || "",
}),
)
if (response && response.values) {
setLmStudioModels(response.values)
}
} catch (error) {
console.error("Failed to fetch LM Studio models:", error)
setLmStudioModels([])
}
}, [apiConfiguration?.lmStudioBaseUrl])
useEffect(() => {
requestLmStudioModels()
}, [requestLmStudioModels])
useInterval(requestLmStudioModels, 2000)
return (
<div>
<BaseUrlField
value={apiConfiguration?.lmStudioBaseUrl}
onChange={(value) => handleInputChange("lmStudioBaseUrl")({ target: { value } })}
placeholder="Default: http://localhost:1234"
label="Use custom base URL"
/>
<VSCodeTextField
value={apiConfiguration?.lmStudioModelId || ""}
style={{ width: "100%" }}
onInput={handleInputChange("lmStudioModelId")}
placeholder={"e.g. meta-llama-3.1-8b-instruct"}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
{lmStudioModels.length > 0 && (
<VSCodeRadioGroup
value={
lmStudioModels.includes(apiConfiguration?.lmStudioModelId || "") ? apiConfiguration?.lmStudioModelId : ""
}
onChange={(e) => {
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 },
})
}
}}>
{lmStudioModels.map((model) => (
<VSCodeRadio key={model} value={model} checked={apiConfiguration?.lmStudioModelId === model}>
{model}
</VSCodeRadio>
))}
</VSCodeRadioGroup>
)}
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
LM Studio allows you to run models locally on your computer. For instructions on how to get started, see their
<VSCodeLink href="https://lmstudio.ai/docs" style={{ display: "inline", fontSize: "inherit" }}>
quickstart guide.
</VSCodeLink>
You will also need to start LM Studio's{" "}
<VSCodeLink href="https://lmstudio.ai/docs/basics/server" style={{ display: "inline", fontSize: "inherit" }}>
local server
</VSCodeLink>{" "}
feature to use it with this extension.{" "}
<span style={{ color: "var(--vscode-errorForeground)" }}>
(<span style={{ fontWeight: 500 }}>Note:</span> Cline uses complex prompts and works best with Claude models.
Less capable models may not work as expected.)
</span>
</p>
</div>
)
}
@@ -0,0 +1,231 @@
import { useState } from "react"
import { ApiConfiguration, liteLlmModelInfoSaneDefaults } from "@shared/api"
import { VSCodeTextField, VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { getAsVar, VSC_DESCRIPTION_FOREGROUND } from "@/utils/vscStyles"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { ModelInfoView } from "../common/ModelInfoView"
import ThinkingBudgetSlider from "../ThinkingBudgetSlider"
/**
* 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) => {
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
// Local state for collapsible model configuration section
const [modelConfigurationSelected, setModelConfigurationSelected] = useState(false)
return (
<div>
<VSCodeTextField
value={apiConfiguration?.liteLlmBaseUrl || ""}
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 || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("liteLlmApiKey")}
placeholder="Default: noop">
<span style={{ fontWeight: 500 }}>API Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.liteLlmModelId || ""}
style={{ width: "100%" }}
onInput={handleInputChange("liteLlmModelId")}
placeholder={"e.g. anthropic/claude-sonnet-4-20250514"}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
<div style={{ display: "flex", flexDirection: "column", marginTop: 10, marginBottom: 10 }}>
{selectedModelInfo.supportsPromptCache && (
<>
<VSCodeCheckbox
checked={apiConfiguration?.liteLlmUsePromptCache || false}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setApiConfiguration({
...apiConfiguration,
liteLlmUsePromptCache: isChecked,
})
}}
style={{ fontWeight: 500, color: "var(--vscode-charts-green)" }}>
Use prompt caching (GA)
</VSCodeCheckbox>
<p style={{ fontSize: "12px", marginTop: 3, color: "var(--vscode-charts-green)" }}>
Prompt caching requires a supported provider and model
</p>
</>
)}
</div>
<>
<ThinkingBudgetSlider apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Extended thinking is available for models such as Sonnet-4, o3-mini, Deepseek R1, etc. More info on{" "}
<VSCodeLink
href="https://docs.litellm.ai/docs/reasoning_content"
style={{ display: "inline", fontSize: "inherit" }}>
thinking mode configuration
</VSCodeLink>
</p>
</>
<div
style={{
color: getAsVar(VSC_DESCRIPTION_FOREGROUND),
display: "flex",
margin: "10px 0",
cursor: "pointer",
alignItems: "center",
}}
onClick={() => setModelConfigurationSelected((val) => !val)}>
<span
className={`codicon ${modelConfigurationSelected ? "codicon-chevron-down" : "codicon-chevron-right"}`}
style={{
marginRight: "4px",
}}></span>
<span
style={{
fontWeight: 700,
textTransform: "uppercase",
}}>
Model Configuration
</span>
</div>
{modelConfigurationSelected && (
<>
<VSCodeCheckbox
checked={!!apiConfiguration?.liteLlmModelInfo?.supportsImages}
onChange={(e: any) => {
const isChecked = e.target.checked === true
const modelInfo = apiConfiguration?.liteLlmModelInfo
? apiConfiguration.liteLlmModelInfo
: { ...liteLlmModelInfoSaneDefaults }
modelInfo.supportsImages = isChecked
setApiConfiguration({
...apiConfiguration,
liteLlmModelInfo: modelInfo,
})
}}>
Supports Images
</VSCodeCheckbox>
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
<VSCodeTextField
value={
apiConfiguration?.liteLlmModelInfo?.contextWindow
? apiConfiguration.liteLlmModelInfo.contextWindow.toString()
: liteLlmModelInfoSaneDefaults.contextWindow?.toString()
}
style={{ flex: 1 }}
onInput={(input: any) => {
const modelInfo = apiConfiguration?.liteLlmModelInfo
? apiConfiguration.liteLlmModelInfo
: { ...liteLlmModelInfoSaneDefaults }
modelInfo.contextWindow = Number(input.target.value)
setApiConfiguration({
...apiConfiguration,
liteLlmModelInfo: modelInfo,
})
}}>
<span style={{ fontWeight: 500 }}>Context Window Size</span>
</VSCodeTextField>
<VSCodeTextField
value={
apiConfiguration?.liteLlmModelInfo?.maxTokens
? apiConfiguration.liteLlmModelInfo.maxTokens.toString()
: liteLlmModelInfoSaneDefaults.maxTokens?.toString()
}
style={{ flex: 1 }}
onInput={(input: any) => {
const modelInfo = apiConfiguration?.liteLlmModelInfo
? apiConfiguration.liteLlmModelInfo
: { ...liteLlmModelInfoSaneDefaults }
modelInfo.maxTokens = input.target.value
setApiConfiguration({
...apiConfiguration,
liteLlmModelInfo: modelInfo,
})
}}>
<span style={{ fontWeight: 500 }}>Max Output Tokens</span>
</VSCodeTextField>
</div>
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
<VSCodeTextField
value={
apiConfiguration?.liteLlmModelInfo?.temperature !== undefined
? apiConfiguration.liteLlmModelInfo.temperature.toString()
: liteLlmModelInfoSaneDefaults.temperature?.toString()
}
onInput={(input: any) => {
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)
setApiConfiguration({
...apiConfiguration,
liteLlmModelInfo: modelInfo,
})
}}>
<span style={{ fontWeight: 500 }}>Temperature</span>
</VSCodeTextField>
</div>
</>
)}
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
LiteLLM provides a unified interface to access various LLM providers' models. See their{" "}
<VSCodeLink href="https://docs.litellm.ai/docs/" style={{ display: "inline", fontSize: "inherit" }}>
quickstart guide
</VSCodeLink>{" "}
for more information.
</p>
{showModelOptions && (
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
)}
</div>
)
}
@@ -0,0 +1,48 @@
import { ApiConfiguration, nebiusModels } from "@shared/api"
import { ApiKeyField } from "../common/ApiKeyField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
/**
* Props for the NebiusProvider component
*/
interface NebiusProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
/**
* The Nebius AI Studio provider configuration component
*/
export const NebiusProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: NebiusProviderProps) => {
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
return (
<div>
<ApiKeyField
value={apiConfiguration?.nebiusApiKey || ""}
onChange={handleInputChange("nebiusApiKey")}
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.)"
/>
{showModelOptions && (
<>
<ModelSelector
models={nebiusModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
label="Model"
/>
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
</>
)}
</div>
)
}
@@ -0,0 +1,108 @@
import { ApiConfiguration, internationalQwenModels, mainlandQwenModels } from "@shared/api"
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { ApiKeyField } from "../common/ApiKeyField"
import { ModelSelector, DropdownContainer } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import ThinkingBudgetSlider from "../ThinkingBudgetSlider"
import { DROPDOWN_Z_INDEX } from "../ApiOptions"
const SUPPORTED_THINKING_MODELS = [
"qwen3-235b-a22b",
"qwen3-32b",
"qwen3-30b-a3b",
"qwen3-14b",
"qwen3-8b",
"qwen3-4b",
"qwen3-1.7b",
"qwen3-0.6b",
"qwen-plus-latest",
"qwen-turbo-latest",
]
/**
* 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) => {
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
// Determine which models to use based on API line selection
const qwenModels = apiConfiguration?.qwenApiLine === "china" ? mainlandQwenModels : internationalQwenModels
return (
<div>
<DropdownContainer className="dropdown-container" style={{ position: "inherit" }}>
<label htmlFor="qwen-line-provider">
<span style={{ fontWeight: 500, marginTop: 5 }}>Alibaba API Line</span>
</label>
<VSCodeDropdown
id="qwen-line-provider"
value={apiConfiguration?.qwenApiLine || "china"}
onChange={handleInputChange("qwenApiLine")}
style={{
minWidth: 130,
position: "relative",
}}>
<VSCodeOption value="china">China API</VSCodeOption>
<VSCodeOption value="international">International API</VSCodeOption>
</VSCodeDropdown>
</DropdownContainer>
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
Please select the appropriate API interface based on your location. If you are in China, choose the China API
interface. Otherwise, choose the International API interface.
</p>
<ApiKeyField
value={apiConfiguration?.qwenApiKey || ""}
onChange={handleInputChange("qwenApiKey")}
providerName="Qwen"
signupUrl="https://bailian.console.aliyun.com/"
/>
{showModelOptions && (
<>
<ModelSelector
models={qwenModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
label="Model"
zIndex={DROPDOWN_Z_INDEX - 2}
/>
{SUPPORTED_THINKING_MODELS.includes(selectedModelId) && (
<ThinkingBudgetSlider
apiConfiguration={apiConfiguration}
setApiConfiguration={setApiConfiguration}
maxBudget={selectedModelInfo.thinkingConfig?.maxBudget}
/>
)}
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
</>
)}
</div>
)
}
@@ -0,0 +1,96 @@
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"
import { useState, useCallback, useEffect } from "react"
import { useInterval } from "react-use"
import * as vscodemodels from "vscode"
import { DropdownContainer, DROPDOWN_Z_INDEX } from "../ApiOptions"
interface VSCodeLmProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
}
export const VSCodeLmProvider = ({ apiConfiguration, handleInputChange }: VSCodeLmProviderProps) => {
const [vsCodeLmModels, setVsCodeLmModels] = useState<vscodemodels.LanguageModelChatSelector[]>([])
// Poll VS Code LM models
const requestVsCodeLmModels = useCallback(async () => {
try {
const response = await ModelsServiceClient.getVsCodeLmModels(EmptyRequest.create({}))
if (response && response.models) {
setVsCodeLmModels(response.models)
}
} catch (error) {
console.error("Failed to fetch VS Code LM models:", error)
setVsCodeLmModels([])
}
}, [])
useEffect(() => {
requestVsCodeLmModels()
}, [requestVsCodeLmModels])
useInterval(requestVsCodeLmModels, 2000)
return (
<div>
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 2} className="dropdown-container">
<label htmlFor="vscode-lm-model">
<span style={{ fontWeight: 500 }}>Language Model</span>
</label>
{vsCodeLmModels.length > 0 ? (
<VSCodeDropdown
id="vscode-lm-model"
value={
apiConfiguration?.vsCodeLmModelSelector
? `${apiConfiguration.vsCodeLmModelSelector.vendor ?? ""}/${apiConfiguration.vsCodeLmModelSelector.family ?? ""}`
: ""
}
onChange={(e) => {
const value = (e.target as HTMLInputElement).value
if (!value) {
return
}
const [vendor, family] = value.split("/")
handleInputChange("vsCodeLmModelSelector")({
target: {
value: { vendor, family },
},
})
}}
style={{ width: "100%" }}>
<VSCodeOption value="">Select a model...</VSCodeOption>
{vsCodeLmModels.map((model) => (
<VSCodeOption key={`${model.vendor}/${model.family}`} value={`${model.vendor}/${model.family}`}>
{model.vendor} - {model.family}
</VSCodeOption>
))}
</VSCodeDropdown>
) : (
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
The VS Code Language Model API allows you to run models provided by other VS Code extensions (including
but not limited to GitHub Copilot). The easiest way to get started is to install the Copilot extension
from the VS Marketplace and enabling Claude 4 Sonnet.
</p>
)}
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-errorForeground)",
fontWeight: 500,
}}>
Note: This is a very experimental integration and may not work as expected.
</p>
</DropdownContainer>
</div>
)
}
@@ -0,0 +1,28 @@
import Section from "../Section"
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
interface AboutSectionProps {
version: string
renderSectionHeader: (tabId: string) => JSX.Element | null
}
const AboutSection = ({ version, renderSectionHeader }: AboutSectionProps) => {
return (
<div>
{renderSectionHeader("about")}
<Section>
<div className="text-center text-[var(--vscode-descriptionForeground)] text-xs leading-[1.2] px-0 py-0 pr-2 pb-[15px] mt-auto">
<p className="break-words m-0 p-0">
If you have any questions or feedback, feel free to open an issue at{" "}
<VSCodeLink href="https://github.com/cline/cline" className="inline">
https://github.com/cline/cline
</VSCodeLink>
</p>
<p className="italic mt-[10px] mb-0 p-0">v{version}</p>
</div>
</Section>
</div>
)
}
export default AboutSection
@@ -0,0 +1,98 @@
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { TabButton } from "../../mcp/configuration/McpConfigurationView"
import ApiOptions from "../ApiOptions"
import Section from "../Section"
import { ChatSettings } from "@shared/ChatSettings"
interface ApiConfigurationSectionProps {
planActSeparateModelsSetting: boolean
chatSettings: ChatSettings
isSwitchingMode: boolean
apiErrorMessage?: string
modelIdErrorMessage?: string
handlePlanActModeChange: (mode: "plan" | "act") => Promise<void>
setPlanActSeparateModelsSetting: (value: boolean) => void
renderSectionHeader: (tabId: string) => JSX.Element | null
}
const ApiConfigurationSection = ({
planActSeparateModelsSetting,
chatSettings,
isSwitchingMode,
apiErrorMessage,
modelIdErrorMessage,
handlePlanActModeChange,
setPlanActSeparateModelsSetting,
renderSectionHeader,
}: ApiConfigurationSectionProps) => {
return (
<div>
{renderSectionHeader("api-config")}
<Section>
{/* Tabs container */}
{planActSeparateModelsSetting ? (
<div className="rounded-md mb-5 bg-[var(--vscode-panel-background)]">
<div className="flex gap-[1px] mb-[10px] -mt-2 border-0 border-b border-solid border-[var(--vscode-panel-border)]">
<TabButton
isActive={chatSettings.mode === "plan"}
onClick={() => handlePlanActModeChange("plan")}
disabled={isSwitchingMode}
style={{
opacity: isSwitchingMode ? 0.6 : 1,
cursor: isSwitchingMode ? "not-allowed" : "pointer",
}}>
{isSwitchingMode && chatSettings.mode === "act" ? "Switching..." : "Plan Mode"}
</TabButton>
<TabButton
isActive={chatSettings.mode === "act"}
onClick={() => handlePlanActModeChange("act")}
disabled={isSwitchingMode}
style={{
opacity: isSwitchingMode ? 0.6 : 1,
cursor: isSwitchingMode ? "not-allowed" : "pointer",
}}>
{isSwitchingMode && chatSettings.mode === "plan" ? "Switching..." : "Act Mode"}
</TabButton>
</div>
{/* Content container */}
<div className="-mb-3">
<ApiOptions
key={chatSettings.mode}
showModelOptions={true}
apiErrorMessage={apiErrorMessage}
modelIdErrorMessage={modelIdErrorMessage}
/>
</div>
</div>
) : (
<ApiOptions
key={"single"}
showModelOptions={true}
apiErrorMessage={apiErrorMessage}
modelIdErrorMessage={modelIdErrorMessage}
/>
)}
<div className="mb-[5px]">
<VSCodeCheckbox
className="mb-[5px]"
checked={planActSeparateModelsSetting}
onChange={(e: any) => {
const checked = e.target.checked === true
setPlanActSeparateModelsSetting(checked)
}}>
Use different models for Plan and Act modes
</VSCodeCheckbox>
<p className="text-xs mt-[5px] text-[var(--vscode-descriptionForeground)]">
Switching between Plan and Act mode will persist the API and model used in the previous mode. This may be
helpful e.g. when using a strong reasoning model to architect a plan for a cheaper coding model to act on.
</p>
</div>
</Section>
</div>
)
}
export default ApiConfigurationSection
@@ -2,15 +2,17 @@ import { VSCodeButton, VSCodeCheckbox, VSCodeDropdown, VSCodeOption, VSCodeTextF
import debounce from "debounce"
import React, { useCallback, useEffect, useState } from "react"
import styled from "styled-components"
import { BROWSER_VIEWPORT_PRESETS } from "../../../../src/shared/BrowserSettings"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { BrowserServiceClient } from "../../services/grpc-client"
import { BROWSER_VIEWPORT_PRESETS } from "../../../../../src/shared/BrowserSettings"
import { useExtensionState } from "../../../context/ExtensionStateContext"
import { BrowserServiceClient } from "../../../services/grpc-client"
import { EmptyRequest, StringRequest } from "@shared/proto/common"
import { BrowserSettings } from "@shared/BrowserSettings"
import Section from "../Section"
interface BrowserSettingsSectionProps {
localBrowserSettings: BrowserSettings
onBrowserSettingsChange: (settings: BrowserSettings) => void
renderSectionHeader: (tabId: string) => JSX.Element | null
}
const ConnectionStatusIndicator = ({
@@ -59,6 +61,7 @@ const CollapsibleContent = styled.div<{ isOpen: boolean }>`
export const BrowserSettingsSection: React.FC<BrowserSettingsSectionProps> = ({
localBrowserSettings,
onBrowserSettingsChange,
renderSectionHeader,
}) => {
const { browserSettings } = useExtensionState()
const [localChromePath, setLocalChromePath] = useState(localBrowserSettings.chromeExecutablePath || "")
@@ -275,161 +278,184 @@ export const BrowserSettingsSection: React.FC<BrowserSettingsSectionProps> = ({
const isSubSettingsOpen = !(localBrowserSettings.disableToolUse || false)
return (
<div id="browser-settings-section" style={{ marginBottom: 20 }}>
{/* Master Toggle */}
<div style={{ marginBottom: isSubSettingsOpen ? 0 : 10 }}>
<VSCodeCheckbox
checked={localBrowserSettings.disableToolUse || false}
onChange={(e) => updateDisableToolUse((e.target as HTMLInputElement).checked)}>
Disable browser tool usage
</VSCodeCheckbox>
<p
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
margin: "4px 0 0 0px",
}}>
Prevent Cline from using browser actions (e.g. launch, click, type).
</p>
</div>
<CollapsibleContent isOpen={isSubSettingsOpen}>
<div style={{ marginBottom: 15 }}>
<div style={{ marginBottom: 8 }}>
<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>Viewport size</label>
<VSCodeDropdown
style={{ width: "100%" }}
value={
Object.entries(BROWSER_VIEWPORT_PRESETS).find(([_, size]) => {
const typedSize = size as { width: number; height: number }
return (
typedSize.width === localBrowserSettings.viewport.width &&
typedSize.height === localBrowserSettings.viewport.height
)
})?.[0]
}
onChange={(event) => handleViewportChange(event as Event)}>
{Object.entries(BROWSER_VIEWPORT_PRESETS).map(([name]) => (
<VSCodeOption key={name} value={name}>
{name}
</VSCodeOption>
))}
</VSCodeDropdown>
</div>
<p
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
margin: 0,
}}>
Set the size of the browser viewport for screenshots and interactions.
</p>
</div>
<div style={{ marginBottom: 0 }}>
{" "}
{/* This div now contains Remote Connection & Chrome Path */}
<div style={{ marginBottom: 4, display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<div>
{renderSectionHeader("browser")}
<Section>
<div id="browser-settings-section" style={{ marginBottom: 20 }}>
{/* Master Toggle */}
<div style={{ marginBottom: isSubSettingsOpen ? 0 : 10 }}>
<VSCodeCheckbox
checked={localBrowserSettings.remoteBrowserEnabled}
onChange={(e) => updateRemoteBrowserEnabled((e.target as HTMLInputElement).checked)}>
Use remote browser connection
checked={localBrowserSettings.disableToolUse || false}
onChange={(e) => updateDisableToolUse((e.target as HTMLInputElement).checked)}>
Disable browser tool usage
</VSCodeCheckbox>
<ConnectionStatusIndicator
isChecking={isCheckingConnection}
isConnected={connectionStatus}
remoteBrowserEnabled={localBrowserSettings.remoteBrowserEnabled}
/>
<p
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
margin: "4px 0 0 0px",
}}>
Prevent Cline from using browser actions (e.g. launch, click, type).
</p>
</div>
<p
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
margin: "0 0 6px 0px",
}}>
Enable Cline to use your Chrome
{isBundled ? "(not detected on your machine)" : detectedChromePath ? ` (${detectedChromePath})` : ""}. You
can specify a custom path below. Using a remote browser connection requires starting Chrome in debug mode
{localBrowserSettings.remoteBrowserEnabled ? (
<>
{" "}
manually (<code>--remote-debugging-port=9222</code>) or using the button below. Enter the host
address or leave it blank for automatic discovery.
</>
) : (
"."
)}
</p>
{/* Moved remote-specific settings to appear directly after enabling remote connection */}
{localBrowserSettings.remoteBrowserEnabled && (
<div style={{ marginLeft: 0, marginTop: 8 }}>
<VSCodeTextField
value={localBrowserSettings.remoteBrowserHost || ""}
placeholder="http://localhost:9222"
style={{ width: "100%", marginBottom: 8 }}
onChange={(e: any) => updateRemoteBrowserHost(e.target.value || undefined)}
/>
{shouldShowRelaunchButton && (
<div style={{ display: "flex", gap: "10px", marginBottom: 8, justifyContent: "center" }}>
<VSCodeButton style={{ flex: 1 }} disabled={debugMode} onClick={relaunchChromeDebugMode}>
{debugMode ? "Launching Browser..." : "Launch Browser with Debug Mode"}
</VSCodeButton>
</div>
)}
{relaunchResult && (
<div
style={{
padding: "8px",
marginBottom: "8px",
backgroundColor: relaunchResult.success ? "rgba(0, 128, 0, 0.1)" : "rgba(255, 0, 0, 0.1)",
color: relaunchResult.success
? "var(--vscode-terminal-ansiGreen)"
: "var(--vscode-terminal-ansiRed)",
borderRadius: "3px",
fontSize: "11px",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
}}>
{relaunchResult.message}
</div>
)}
<CollapsibleContent isOpen={isSubSettingsOpen}>
<div style={{ marginBottom: 15 }}>
<div style={{ marginBottom: 8 }}>
<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>Viewport size</label>
<VSCodeDropdown
style={{ width: "100%" }}
value={
Object.entries(BROWSER_VIEWPORT_PRESETS).find(([_, size]) => {
const typedSize = size as { width: number; height: number }
return (
typedSize.width === localBrowserSettings.viewport.width &&
typedSize.height === localBrowserSettings.viewport.height
)
})?.[0]
}
onChange={(event) => handleViewportChange(event as Event)}>
{Object.entries(BROWSER_VIEWPORT_PRESETS).map(([name]) => (
<VSCodeOption key={name} value={name}>
{name}
</VSCodeOption>
))}
</VSCodeDropdown>
</div>
<p
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
margin: 0,
}}></p>
}}>
Set the size of the browser viewport for screenshots and interactions.
</p>
</div>
)}
{/* Chrome Executable Path section now follows remote-specific settings */}
<div style={{ marginBottom: 8, marginTop: 8 }}>
<label htmlFor="chrome-executable-path" style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>
Chrome Executable Path (Optional)
</label>
<VSCodeTextField
id="chrome-executable-path"
value={localChromePath}
placeholder="e.g., /usr/bin/google-chrome or C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
style={{ width: "100%" }}
onChange={(e: any) => {
const newValue = e.target.value || ""
updateChromeExecutablePath(newValue)
}}
/>
<p
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
margin: "4px 0 0 0",
}}>
Leave blank to auto-detect.
</p>
</div>
<div style={{ marginBottom: 0 }}>
{" "}
{/* This div now contains Remote Connection & Chrome Path */}
<div
style={{
marginBottom: 4,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}>
<VSCodeCheckbox
checked={localBrowserSettings.remoteBrowserEnabled}
onChange={(e) => updateRemoteBrowserEnabled((e.target as HTMLInputElement).checked)}>
Use remote browser connection
</VSCodeCheckbox>
<ConnectionStatusIndicator
isChecking={isCheckingConnection}
isConnected={connectionStatus}
remoteBrowserEnabled={localBrowserSettings.remoteBrowserEnabled}
/>
</div>
<p
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
margin: "0 0 6px 0px",
}}>
Enable Cline to use your Chrome
{isBundled
? "(not detected on your machine)"
: detectedChromePath
? ` (${detectedChromePath})`
: ""}
. You can specify a custom path below. Using a remote browser connection requires starting Chrome
in debug mode
{localBrowserSettings.remoteBrowserEnabled ? (
<>
{" "}
manually (<code>--remote-debugging-port=9222</code>) or using the button below. Enter the
host address or leave it blank for automatic discovery.
</>
) : (
"."
)}
</p>
{/* Moved remote-specific settings to appear directly after enabling remote connection */}
{localBrowserSettings.remoteBrowserEnabled && (
<div style={{ marginLeft: 0, marginTop: 8 }}>
<VSCodeTextField
value={localBrowserSettings.remoteBrowserHost || ""}
placeholder="http://localhost:9222"
style={{ width: "100%", marginBottom: 8 }}
onChange={(e: any) => updateRemoteBrowserHost(e.target.value || undefined)}
/>
{shouldShowRelaunchButton && (
<div style={{ display: "flex", gap: "10px", marginBottom: 8, justifyContent: "center" }}>
<VSCodeButton
style={{ flex: 1 }}
disabled={debugMode}
onClick={relaunchChromeDebugMode}>
{debugMode ? "Launching Browser..." : "Launch Browser with Debug Mode"}
</VSCodeButton>
</div>
)}
{relaunchResult && (
<div
style={{
padding: "8px",
marginBottom: "8px",
backgroundColor: relaunchResult.success
? "rgba(0, 128, 0, 0.1)"
: "rgba(255, 0, 0, 0.1)",
color: relaunchResult.success
? "var(--vscode-terminal-ansiGreen)"
: "var(--vscode-terminal-ansiRed)",
borderRadius: "3px",
fontSize: "11px",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
}}>
{relaunchResult.message}
</div>
)}
<p
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
margin: 0,
}}></p>
</div>
)}
{/* Chrome Executable Path section now follows remote-specific settings */}
<div style={{ marginBottom: 8, marginTop: 8 }}>
<label
htmlFor="chrome-executable-path"
style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>
Chrome Executable Path (Optional)
</label>
<VSCodeTextField
id="chrome-executable-path"
value={localChromePath}
placeholder="e.g., /usr/bin/google-chrome or C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
style={{ width: "100%" }}
onChange={(e: any) => {
const newValue = e.target.value || ""
updateChromeExecutablePath(newValue)
}}
/>
<p
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
margin: "4px 0 0 0",
}}>
Leave blank to auto-detect.
</p>
</div>
</div>
</CollapsibleContent>
</div>
</CollapsibleContent>
</Section>
</div>
)
}
@@ -0,0 +1,34 @@
import Section from "../Section"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
interface DebugSectionProps {
onResetState: (resetGlobalState?: boolean) => Promise<void>
renderSectionHeader: (tabId: string) => JSX.Element | null
}
const DebugSection = ({ onResetState, renderSectionHeader }: DebugSectionProps) => {
return (
<div>
{renderSectionHeader("debug")}
<Section>
<VSCodeButton
onClick={() => onResetState()}
className="mt-[5px] w-auto"
style={{ backgroundColor: "var(--vscode-errorForeground)", color: "black" }}>
Reset Workspace State
</VSCodeButton>
<VSCodeButton
onClick={() => onResetState(true)}
className="mt-[5px] w-auto"
style={{ backgroundColor: "var(--vscode-errorForeground)", color: "black" }}>
Reset Global State
</VSCodeButton>
<p className="text-xs mt-[5px] text-[var(--vscode-descriptionForeground)]">
This will reset all global state and secret storage in the extension.
</p>
</Section>
</div>
)
}
export default DebugSection
@@ -0,0 +1,114 @@
import { VSCodeCheckbox, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { memo } from "react"
import { OpenAIReasoningEffort } from "@shared/ChatSettings"
import Section from "../Section"
interface FeatureSettingsSectionProps {
renderSectionHeader: (tabId: string) => JSX.Element | null
}
const FeatureSettingsSection = ({ renderSectionHeader }: FeatureSettingsSectionProps) => {
const {
enableCheckpointsSetting,
setEnableCheckpointsSetting,
mcpMarketplaceEnabled,
setMcpMarketplaceEnabled,
mcpRichDisplayEnabled,
setMcpRichDisplayEnabled,
mcpResponsesCollapsed,
setMcpResponsesCollapsed,
chatSettings,
setChatSettings,
} = useExtensionState()
return (
<div>
{renderSectionHeader("features")}
<Section>
<div style={{ marginBottom: 20 }}>
<div>
<VSCodeCheckbox
checked={enableCheckpointsSetting}
onChange={(e: any) => {
const checked = e.target.checked === true
setEnableCheckpointsSetting(checked)
}}>
Enable Checkpoints
</VSCodeCheckbox>
<p className="text-xs text-[var(--vscode-descriptionForeground)]">
Enables extension to save checkpoints of workspace throughout the task. Uses git under the hood which
may not work well with large workspaces.
</p>
</div>
<div style={{ marginTop: 10 }}>
<VSCodeCheckbox
checked={mcpMarketplaceEnabled}
onChange={(e: any) => {
const checked = e.target.checked === true
setMcpMarketplaceEnabled(checked)
}}>
Enable MCP Marketplace
</VSCodeCheckbox>
<p className="text-xs text-[var(--vscode-descriptionForeground)]">
Enables the MCP Marketplace tab for discovering and installing MCP servers.
</p>
</div>
<div style={{ marginTop: 10 }}>
<VSCodeCheckbox
checked={mcpRichDisplayEnabled}
onChange={(e: any) => {
const checked = e.target.checked === true
setMcpRichDisplayEnabled(checked)
}}>
Enable Rich MCP Display
</VSCodeCheckbox>
<p className="text-xs text-[var(--vscode-descriptionForeground)]">
Enables rich formatting for MCP responses. When disabled, responses will be shown in plain text.
</p>
</div>
<div style={{ marginTop: 10 }}>
<VSCodeCheckbox
checked={mcpResponsesCollapsed}
onChange={(e: any) => {
const checked = e.target.checked === true
setMcpResponsesCollapsed(checked)
}}>
Collapse MCP Responses
</VSCodeCheckbox>
<p className="text-xs text-[var(--vscode-descriptionForeground)]">
Sets the default display mode for MCP response panels
</p>
</div>
<div style={{ marginTop: 10 }}>
<label
htmlFor="openai-reasoning-effort-dropdown"
className="block text-sm font-medium text-[var(--vscode-foreground)] mb-1">
OpenAI Reasoning Effort
</label>
<VSCodeDropdown
id="openai-reasoning-effort-dropdown"
currentValue={chatSettings.openAIReasoningEffort || "medium"}
onChange={(e: any) => {
const newValue = e.target.currentValue as OpenAIReasoningEffort
setChatSettings({
...chatSettings,
openAIReasoningEffort: newValue,
})
}}
className="w-full">
<VSCodeOption value="low">Low</VSCodeOption>
<VSCodeOption value="medium">Medium</VSCodeOption>
<VSCodeOption value="high">High</VSCodeOption>
</VSCodeDropdown>
<p className="text-xs mt-[5px] text-[var(--vscode-descriptionForeground)]">
Reasoning effort for the OpenAI family of models(applies to all OpenAI model providers)
</p>
</div>
</div>
</Section>
</div>
)
}
export default memo(FeatureSettingsSection)
@@ -0,0 +1,56 @@
import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { ChatSettings } from "@shared/ChatSettings"
import { TelemetrySetting } from "@shared/TelemetrySetting"
import PreferredLanguageSetting from "../PreferredLanguageSetting"
import Section from "../Section"
interface GeneralSettingsSectionProps {
chatSettings: ChatSettings
setChatSettings: (settings: ChatSettings) => void
telemetrySetting: string
setTelemetrySetting: (value: TelemetrySetting) => void
renderSectionHeader: (tabId: string) => JSX.Element | null
}
const GeneralSettingsSection = ({
chatSettings,
setChatSettings,
telemetrySetting,
setTelemetrySetting,
renderSectionHeader,
}: GeneralSettingsSectionProps) => {
return (
<div>
{renderSectionHeader("general")}
<Section>
{chatSettings && <PreferredLanguageSetting chatSettings={chatSettings} setChatSettings={setChatSettings} />}
<div className="mb-[5px]">
<VSCodeCheckbox
className="mb-[5px]"
checked={telemetrySetting !== "disabled"}
onChange={(e: any) => {
const checked = e.target.checked === true
setTelemetrySetting(checked ? "enabled" : "disabled")
}}>
Allow anonymous error and usage reporting
</VSCodeCheckbox>
<p className="text-xs mt-[5px] text-[var(--vscode-descriptionForeground)]">
Help improve Cline by sending anonymous usage data and error reports. No code, prompts, or personal
information are ever sent. See our{" "}
<VSCodeLink href="https://docs.cline.bot/more-info/telemetry" className="text-inherit">
telemetry overview
</VSCodeLink>{" "}
and{" "}
<VSCodeLink href="https://cline.bot/privacy" className="text-inherit">
privacy policy
</VSCodeLink>{" "}
for more details.
</p>
</div>
</Section>
</div>
)
}
export default GeneralSettingsSection
@@ -0,0 +1,169 @@
import React, { useState, useEffect } from "react"
import { VSCodeTextField, VSCodeCheckbox, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import TerminalOutputLineLimitSlider from "../TerminalOutputLineLimitSlider"
import { StateServiceClient } from "../../../services/grpc-client"
import { Int64, Int64Request } from "@shared/proto/common"
import Section from "../Section"
interface TerminalSettingsSectionProps {
renderSectionHeader: (tabId: string) => JSX.Element | null
}
export const TerminalSettingsSection: React.FC<TerminalSettingsSectionProps> = ({ renderSectionHeader }) => {
const {
shellIntegrationTimeout,
setShellIntegrationTimeout,
terminalReuseEnabled,
setTerminalReuseEnabled,
defaultTerminalProfile,
setDefaultTerminalProfile,
availableTerminalProfiles,
platform,
} = useExtensionState()
const [inputValue, setInputValue] = useState((shellIntegrationTimeout / 1000).toString())
const [inputError, setInputError] = useState<string | null>(null)
const handleTimeoutChange = (event: Event) => {
const target = event.target as HTMLInputElement
const value = target.value
setInputValue(value)
const seconds = parseFloat(value)
if (isNaN(seconds) || seconds <= 0) {
setInputError("Please enter a positive number")
return
}
setInputError(null)
const timeout = Math.round(seconds * 1000)
setShellIntegrationTimeout(timeout)
StateServiceClient.updateTerminalConnectionTimeout({
value: timeout,
} as Int64Request)
.then((response: Int64) => {
setShellIntegrationTimeout(response.value)
setInputValue((response.value / 1000).toString())
})
.catch((error) => {
console.error("Failed to update terminal connection timeout:", error)
})
}
const handleInputBlur = () => {
if (inputError) {
setInputValue((shellIntegrationTimeout / 1000).toString())
setInputError(null)
}
}
const handleTerminalReuseChange = (event: Event) => {
const target = event.target as HTMLInputElement
const checked = target.checked
setTerminalReuseEnabled(checked)
StateServiceClient.updateTerminalReuseEnabled({ value: checked } as any).catch((error) => {
console.error("Failed to update terminal reuse enabled:", error)
})
}
// Use any to avoid type conflicts between Event and FormEvent
const handleDefaultTerminalProfileChange = (event: any) => {
const target = event.target as HTMLSelectElement
const profileId = target.value
// Only update the local state, let the Save button handle the backend update
setDefaultTerminalProfile(profileId)
}
const profilesToShow = availableTerminalProfiles
return (
<div>
{renderSectionHeader("terminal")}
<Section>
<div id="terminal-settings-section" className="mb-5">
<div className="mb-4">
<label htmlFor="default-terminal-profile" className="font-medium block mb-1">
Default Terminal Profile
</label>
<VSCodeDropdown
id="default-terminal-profile"
value={defaultTerminalProfile || "default"}
onChange={handleDefaultTerminalProfileChange}
className="w-full">
{profilesToShow.map((profile) => (
<VSCodeOption key={profile.id} value={profile.id} title={profile.description}>
{profile.name}
</VSCodeOption>
))}
</VSCodeDropdown>
<p className="text-xs text-[var(--vscode-descriptionForeground)] mt-1">
Select the default terminal Cline will use. 'Default' uses your VSCode global setting.
</p>
</div>
<div className="mb-4">
<div className="mb-2">
<label className="font-medium block mb-1">Shell integration timeout (seconds)</label>
<div className="flex items-center">
<VSCodeTextField
className="w-full"
value={inputValue}
placeholder="Enter timeout in seconds"
onChange={(event) => handleTimeoutChange(event as Event)}
onBlur={handleInputBlur}
/>
</div>
{inputError && <div className="text-[var(--vscode-errorForeground)] text-xs mt-1">{inputError}</div>}
</div>
<p className="text-xs text-[var(--vscode-descriptionForeground)]">
Set how long Cline waits for shell integration to activate before executing commands. Increase this
value if you experience terminal connection timeouts.
</p>
</div>
<div className="mb-4">
<div className="flex items-center mb-2">
<VSCodeCheckbox
checked={terminalReuseEnabled ?? true}
onChange={(event) => handleTerminalReuseChange(event as Event)}>
Enable aggressive terminal reuse
</VSCodeCheckbox>
</div>
<p className="text-xs text-[var(--vscode-descriptionForeground)]">
When enabled, Cline will reuse existing terminal windows that aren't in the current working directory.
Disable this if you experience issues with task lockout after a terminal command.
</p>
</div>
<TerminalOutputLineLimitSlider />
<div className="mt-5 p-3 bg-[var(--vscode-textBlockQuote-background)] rounded border border-[var(--vscode-textBlockQuote-border)]">
<p className="text-[13px] m-0">
<strong>Having terminal issues?</strong> Check our{" "}
<a
href="https://docs.cline.bot/troubleshooting/terminal-quick-fixes"
className="text-[var(--vscode-textLink-foreground)] underline hover:no-underline"
target="_blank"
rel="noopener noreferrer">
Terminal Quick Fixes
</a>{" "}
or the{" "}
<a
href="https://docs.cline.bot/troubleshooting/terminal-integration-guide"
className="text-[var(--vscode-textLink-foreground)] underline hover:no-underline"
target="_blank"
rel="noopener noreferrer">
Complete Troubleshooting Guide
</a>
.
</p>
</div>
</div>
</Section>
</div>
)
}
export default TerminalSettingsSection
@@ -10,8 +10,6 @@ import {
deepSeekModels,
geminiDefaultModelId,
geminiModels,
geminiCliDefaultModelId,
geminiCliModels,
mistralDefaultModelId,
mistralModels,
openAiModelInfoSaneDefaults,
@@ -98,8 +96,6 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
return getProviderData(vertexModels, vertexDefaultModelId)
case "gemini":
return getProviderData(geminiModels, geminiDefaultModelId)
case "gemini-cli":
return getProviderData(geminiCliModels, geminiCliDefaultModelId)
case "openai-native":
return getProviderData(openAiNativeModels, openAiNativeDefaultModelId)
case "deepseek":