mirror of
https://github.com/cline/cline.git
synced 2026-09-04 20:02:30 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dfcd61667e |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix ENAMETOOLONG when calling Claude Code
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
clearAllTaskHistory protobus migration
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Removed deleteNonFavoritedTasks, moved popup to extension, cleaned up deletion logic
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
host bridge migration - clipboard
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Refactor task class, moving auto approve
|
||||
@@ -1,14 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [3.18.0]
|
||||
|
||||
- Optimized Cline to work with the Claude 4 family of models, resulting in improved performance, reliability, and new capabilities
|
||||
- Added a new Gemini CLI provider that allows you to use your local Gemini CLI authentication to access Gemini models for free (Thanks @google-gemini!)
|
||||
- Optimized Cline to work with the Gemini 2.5 family of models
|
||||
- Updated the default and recommended model to Claude 4 Sonnet for the best performance
|
||||
- Fix race condition in Plan/Act mode switching
|
||||
- Improve robustness of search and replace parsing
|
||||
|
||||
## [3.17.16]
|
||||
|
||||
- Fix Claude Code provider error handling for incomplete messages during long-running tasks (Thanks @BarreiroT!)
|
||||
|
||||
@@ -30,7 +30,7 @@ English | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md
|
||||
</table>
|
||||
</div>
|
||||
|
||||
Meet Cline (pronounced /klaɪn/, like "Klein"), an AI assistant that can use your **CLI** a**N**d **E**ditor.
|
||||
Meet Cline, an AI assistant that can use your **CLI** a**N**d **E**ditor.
|
||||
|
||||
Thanks to [Claude 3.7 Sonnet's agentic coding capabilities](https://www.anthropic.com/claude/sonnet), Cline can handle complex software development tasks step-by-step. With tools that let him create & edit files, explore large projects, use the browser, and execute terminal commands (after you grant permission), he can assist you in ways that go beyond code completion or tech support. Cline can even use the Model Context Protocol (MCP) to create new tools and extend his own capabilities. While autonomous AI scripts traditionally run in sandboxed environments, this extension provides a human-in-the-loop GUI to approve every file change and terminal command, providing a safe and accessible way to explore the potential of agentic AI.
|
||||
|
||||
|
||||
@@ -146,7 +146,6 @@
|
||||
"group": "Provider Configuration",
|
||||
"pages": [
|
||||
"provider-config/anthropic",
|
||||
"provider-config/claude-code",
|
||||
"provider-config/aws-bedrock-with-credentials-authentication",
|
||||
"provider-config/aws-bedrock-with-profile-authentication",
|
||||
"provider-config/gcp-vertex-ai",
|
||||
|
||||
@@ -120,7 +120,7 @@ Example of context window usage over 50% with a 200K context window:
|
||||
# Context Window Usage
|
||||
|
||||
105,000 / 200,000 tokens (53%)
|
||||
Model: anthropic/claude-sonnet-4 (200K context window)
|
||||
Model: anthropic/claude-3.7-sonnet (200K context window)
|
||||
\`\`\`
|
||||
|
||||
**IMPORTANT**: When you see context window usage at or above 50%, you MUST:
|
||||
|
||||
@@ -6,7 +6,7 @@ title: "Telemetry"
|
||||
|
||||
To help make Cline better for everyone, we collect anonymous usage data that helps us understand how developers are using our open-source AI coding agent. This feedback loop is crucial for improving Cline's capabilities and user experience.
|
||||
|
||||
We use PostHog, an open-source analytics platform, for data collection and analysis. Our telemetry implementation is fully transparent - you can review the [source code](https://github.com/cline/cline/blob/main/src/services/posthog/telemetry/TelemetryService.ts) to see exactly what we track.
|
||||
We use PostHog, an open-source analytics platform, for data collection and analysis. Our telemetry implementation is fully transparent - you can review the [source code](https://github.com/cline/cline/blob/main/src/services/telemetry/TelemetryService.ts) to see exactly what we track.
|
||||
|
||||
### Tracking Policy
|
||||
|
||||
@@ -22,7 +22,7 @@ We collect basic anonymous usage data including:
|
||||
**System Context:** OS type and VS Code environment details\
|
||||
**UI Activity:** Navigation patterns and feature usage
|
||||
|
||||
For complete transparency, you can inspect our [telemetry implementation](https://github.com/cline/cline/blob/main/src/services/posthog/telemetry/TelemetryService.ts) to see the exact events we track.
|
||||
For complete transparency, you can inspect our [telemetry implementation](https://github.com/cline/cline/blob/main/src/services/telemetry/TelemetryService.ts) to see the exact events we track.
|
||||
|
||||
### How to Opt Out
|
||||
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
---
|
||||
title: "Claude Code"
|
||||
description: "Use your Claude Max subscription with Cline instead of paying per token. Learn how to set up and configure the Claude Code provider."
|
||||
---
|
||||
|
||||
**Website:** [https://docs.anthropic.com/en/docs/claude-code/setup](https://docs.anthropic.com/en/docs/claude-code/setup)
|
||||
|
||||
The Claude Code provider lets you use your existing Claude subscription with Cline. If you have Claude Max, this means you can use Claude in Cline without paying extra API costs.
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/claude-code-use-opus.gif"
|
||||
alt="Using the Claude Code provider in Cline with Opus model"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
## Setup
|
||||
|
||||
First, you'll need to install and authenticate Claude Code on your system:
|
||||
|
||||
1. **Install Claude Code**: Follow Anthropic's [official setup guide](https://docs.anthropic.com/en/docs/claude-code/setup) to install and authenticate the Claude CLI.
|
||||
|
||||
2. **Configure in Cline**:
|
||||
- Open Cline settings (⚙️ icon)
|
||||
- Select **Claude Code** from the **API Provider** dropdown
|
||||
- Set the path to your Claude CLI executable (usually just `claude` if it's in your PATH)
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/claude-code-setup.gif"
|
||||
alt="Setting up the Claude Code provider in Cline"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
### Finding your Claude Code path
|
||||
|
||||
If you're not sure where Claude Code is installed:
|
||||
|
||||
- **macOS / Linux**: Run `which claude` in your terminal
|
||||
- **Windows (Command Prompt)**: Run `where claude`
|
||||
- **Windows (PowerShell)**: Run `Get-Command claude`
|
||||
|
||||
## Supported Models
|
||||
|
||||
The Claude Code provider supports these models:
|
||||
|
||||
- `claude-sonnet-4-20250514` (Recommended)
|
||||
- `claude-opus-4-20250514`
|
||||
- `claude-3-7-sonnet-20250219`
|
||||
- `claude-3-5-sonnet-20241022`
|
||||
- `claude-3-5-haiku-20241022`
|
||||
|
||||
## How it works
|
||||
|
||||
When you use Claude Code with Cline, here's what happens behind the scenes:
|
||||
|
||||
Cline wraps the Claude Code CLI to handle your requests. Each time you send a message, Cline starts a new `claude` process, sends your conversation, and streams the response back. The AI reasoning comes from Claude Code, but all the actual file editing, terminal commands, and other tools are handled by Cline.
|
||||
|
||||
The main difference you'll notice is that responses don't stream character-by-character like other providers. Instead, Claude Code processes your full request before sending back the complete response.
|
||||
|
||||
## Limitations
|
||||
|
||||
There are a few things to keep in mind with Claude Code:
|
||||
|
||||
- Images in your messages get converted to text placeholders since Claude Code doesn't support image uploads through the CLI
|
||||
- Prompt caching isn't available with this provider
|
||||
- Responses don't stream in real-time like other providers
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you run into issues:
|
||||
|
||||
**Authentication problems**: Make sure you're logged into Claude Code with your subscription account. Run `claude auth status` to check.
|
||||
|
||||
**Path issues**: Double-check that the Claude CLI path in Cline's settings is correct. Try running `claude --version` in your terminal to verify it's working.
|
||||
|
||||
**Still having trouble?** We're actively improving this integration. Report issues on our [GitHub](https://github.com/cline/cline/issues) or ask for help in our [Discord](https://discord.gg/cline).
|
||||
|
||||
## Usage with subscriptions
|
||||
|
||||
If you have a Claude Max subscription, your usage in Cline shows up as $0.00 in the billing interface since you're not paying additional API costs. Your usage still counts against your subscription limits, but you won't see per-token charges.
|
||||
|
||||
For more details about using Claude Code with your subscription, check out Anthropic's documentation:
|
||||
|
||||
- [Claude Code Setup Guide](https://docs.anthropic.com/en/docs/claude-code/setup)
|
||||
- [Using Claude Code with Pro/Max Plans](https://support.anthropic.com/en/articles/11145838-using-claude-code-with-your-pro-or-max-plan)
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
parseAssistantMessageV3,
|
||||
AssistantMessageContent,
|
||||
} from "./parsing/parse-assistant-message-06-06-25" // "../../src/core/assistant-message"
|
||||
import { constructNewFileContent as constructNewFileContent_06_06_25 } from "./diff-apply/diff-06-06-25"
|
||||
import { constructNewFileContent as constructNewFileContent_06_23_25 } from "./diff-apply/diff-06-23-25"
|
||||
import { constructNewFileContent as constructNewFileContent_06_25_25 } from "./diff-apply/diff-06-25-25"
|
||||
import { constructNewFileContent as constructNewFileContentV1, constructNewFileContentV2 } from "./diff-apply/diff-06-06-25"
|
||||
import { constructNewFileContent as constructNewFileContentV2_1 } from "./diff-apply/diff-06-23-25"
|
||||
import { constructNewFileContent as constructNewFileContentV3 } from "../../src/core/assistant-message/diff" // this defaults to the new v1 when called
|
||||
|
||||
type ParseAssistantMessageFn = (message: string) => AssistantMessageContent[]
|
||||
type ConstructNewFileContentFn = (diff: string, original: string, strict: boolean) => Promise<string>
|
||||
@@ -22,9 +22,11 @@ const parsingFunctions: Record<string, ParseAssistantMessageFn> = {
|
||||
}
|
||||
|
||||
const diffEditingFunctions: Record<string, ConstructNewFileContentFn> = {
|
||||
"diff-06-06-25": constructNewFileContent_06_06_25,
|
||||
"diff-06-23-25": constructNewFileContent_06_23_25,
|
||||
"diff-06-25-25": constructNewFileContent_06_25_25,
|
||||
"diff-06-06-25": constructNewFileContentV2,
|
||||
"diff-06-23-25": constructNewFileContentV2_1,
|
||||
constructNewFileContentV1: constructNewFileContentV1,
|
||||
constructNewFileContentV2: constructNewFileContentV2,
|
||||
constructNewFileContentV3: constructNewFileContentV3, // position invariant diff
|
||||
}
|
||||
|
||||
import { TestInput, TestResult, ExtractedToolCall } from "./types"
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { runSingleEvaluation, TestInput, TestResult } from "./ClineWrapper"
|
||||
import { parseAssistantMessageV2, AssistantMessageContent } from "./parsing/parse-assistant-message-06-06-25"
|
||||
import { constructNewFileContent as constructNewFileContent_06_06_25 } from "./diff-apply/diff-06-06-25"
|
||||
import { constructNewFileContent as constructNewFileContent_06_23_25 } from "./diff-apply/diff-06-23-25"
|
||||
import { constructNewFileContent as constructNewFileContent_06_25_25 } from "./diff-apply/diff-06-25-25"
|
||||
import { constructNewFileContent as constructNewFileContentV2 } from "./diff-apply/diff-06-06-25"
|
||||
import { constructNewFileContent as constructNewFileContentV2_1 } from "./diff-apply/diff-06-23-25"
|
||||
import { constructNewFileContent as constructNewFileContentV3 } from "../../src/core/assistant-message/diff"
|
||||
import { basicSystemPrompt } from "./prompts/basicSystemPrompt-06-06-25"
|
||||
import { claude4SystemPrompt } from "./prompts/claude4SystemPrompt-06-06-25"
|
||||
@@ -481,9 +480,8 @@ class NodeTestRunner {
|
||||
|
||||
// 1. Get the correct diffing function
|
||||
const diffEditingFunctions: Record<string, any> = {
|
||||
"diff-06-06-25": constructNewFileContent_06_06_25,
|
||||
"diff-06-23-25": constructNewFileContent_06_23_25,
|
||||
"diff-06-25-25": constructNewFileContent_06_25_25,
|
||||
"diff-06-06-25": constructNewFileContentV2,
|
||||
"diff-06-23-25": constructNewFileContentV2_1,
|
||||
constructNewFileContentV3: constructNewFileContentV3,
|
||||
}
|
||||
const constructNewFileContent = diffEditingFunctions[diffApplyFile]
|
||||
|
||||
@@ -444,9 +444,6 @@ def render_model_comparison_cards(model_performance):
|
||||
st.write("") # Add some spacing
|
||||
if st.button(f"Drill Down", key=f"drill_{model['model_id']}", use_container_width=True):
|
||||
st.session_state.drill_down_model = model['model_id']
|
||||
# Update URL with model_id for drill down
|
||||
st.query_params["model_id"] = model['model_id']
|
||||
st.rerun()
|
||||
|
||||
st.divider() # Add a divider between models
|
||||
|
||||
@@ -847,11 +844,6 @@ def main():
|
||||
if 'selected_run_id' not in st.session_state:
|
||||
st.session_state.selected_run_id = None
|
||||
|
||||
# Handle URL parameters for direct linking
|
||||
query_params = st.query_params
|
||||
url_run_id = query_params.get("run_id")
|
||||
url_model_id = query_params.get("model_id")
|
||||
|
||||
# Load all runs for sidebar
|
||||
all_runs = load_all_runs()
|
||||
|
||||
@@ -859,18 +851,6 @@ def main():
|
||||
st.error("No evaluation runs found in the database.")
|
||||
st.stop()
|
||||
|
||||
# Set initial run selection from URL or default to latest
|
||||
if url_run_id and url_run_id in all_runs['run_id'].values:
|
||||
if st.session_state.selected_run_id != url_run_id:
|
||||
st.session_state.selected_run_id = url_run_id
|
||||
st.session_state.drill_down_model = None # Reset drill down when changing runs via URL
|
||||
elif st.session_state.selected_run_id is None:
|
||||
st.session_state.selected_run_id = all_runs.iloc[0]['run_id'] # Default to latest
|
||||
|
||||
# Set drill down model from URL
|
||||
if url_model_id and st.session_state.selected_run_id == url_run_id:
|
||||
st.session_state.drill_down_model = url_model_id
|
||||
|
||||
# Sidebar for run selection
|
||||
with st.sidebar:
|
||||
st.markdown("## 📊 Evaluation Runs")
|
||||
@@ -916,10 +896,6 @@ def main():
|
||||
if run_ids[selected_run_idx] != st.session_state.selected_run_id:
|
||||
st.session_state.selected_run_id = run_ids[selected_run_idx]
|
||||
st.session_state.drill_down_model = None # Reset drill down when changing runs
|
||||
# Update URL with new run_id
|
||||
st.query_params["run_id"] = st.session_state.selected_run_id
|
||||
if "model_id" in st.query_params:
|
||||
del st.query_params["model_id"] # Clear model_id when changing runs
|
||||
st.rerun()
|
||||
|
||||
# Show run details in sidebar
|
||||
@@ -930,57 +906,6 @@ def main():
|
||||
st.markdown(f"**Created:** {selected_run['created_at']}")
|
||||
if selected_run['description']:
|
||||
st.markdown(f"**Description:** {selected_run['description']}")
|
||||
|
||||
# Show shareable URL
|
||||
st.markdown("---")
|
||||
st.markdown("### 🔗 Share This View")
|
||||
|
||||
# Build current URL
|
||||
# Dynamically derive the base URL
|
||||
server_address = st.server.server_address if hasattr(st.server, 'server_address') else "localhost"
|
||||
server_port = st.server.server_port if hasattr(st.server, 'server_port') else "8501"
|
||||
base_url = f"http://{server_address}:{server_port}"
|
||||
current_url = f"{base_url}/?run_id={st.session_state.selected_run_id}"
|
||||
if st.session_state.drill_down_model:
|
||||
current_url += f"&model_id={st.session_state.drill_down_model}"
|
||||
|
||||
st.markdown("**Current URL:**")
|
||||
st.code(current_url, language=None)
|
||||
|
||||
# Copy button using HTML/JS
|
||||
copy_button_html = f"""
|
||||
<button onclick="copyToClipboard('{current_url}')" style="
|
||||
padding: 8px 16px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ccc;
|
||||
background: #f0f2f6;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
margin-top: 5px;
|
||||
">📋 Copy Link</button>
|
||||
<script>
|
||||
function copyToClipboard(text) {{
|
||||
navigator.clipboard.writeText(text).then(function() {{
|
||||
// Success feedback
|
||||
event.target.innerText = '✅ Copied!';
|
||||
event.target.style.backgroundColor = '#d4edda';
|
||||
setTimeout(() => {{
|
||||
event.target.innerText = '📋 Copy Link';
|
||||
event.target.style.backgroundColor = '#f0f2f6';
|
||||
}}, 2000);
|
||||
}}, function(err) {{
|
||||
// Error feedback
|
||||
event.target.innerText = '❌ Failed';
|
||||
event.target.style.backgroundColor = '#f8d7da';
|
||||
setTimeout(() => {{
|
||||
event.target.innerText = '📋 Copy Link';
|
||||
event.target.style.backgroundColor = '#f0f2f6';
|
||||
}}, 2000);
|
||||
}});
|
||||
}}
|
||||
</script>
|
||||
"""
|
||||
st.components.v1.html(copy_button_html, height=50)
|
||||
|
||||
# Load data for selected run
|
||||
current_run, model_performance = load_run_comparison(st.session_state.selected_run_id)
|
||||
@@ -998,9 +923,6 @@ def main():
|
||||
with col1:
|
||||
if st.button("Back to Overview", use_container_width=True):
|
||||
st.session_state.drill_down_model = None
|
||||
# Clear model_id from URL when going back to overview
|
||||
if "model_id" in st.query_params:
|
||||
del st.query_params["model_id"]
|
||||
st.rerun()
|
||||
|
||||
render_detailed_analysis(current_run['run_id'], st.session_state.drill_down_model)
|
||||
|
||||
@@ -1,829 +0,0 @@
|
||||
const SEARCH_BLOCK_START = "------- SEARCH"
|
||||
const SEARCH_BLOCK_END = "======="
|
||||
const REPLACE_BLOCK_END = "+++++++ REPLACE"
|
||||
|
||||
const SEARCH_BLOCK_CHAR = "-"
|
||||
const REPLACE_BLOCK_CHAR = "+"
|
||||
const LEGACY_SEARCH_BLOCK_CHAR = "<"
|
||||
const LEGACY_REPLACE_BLOCK_CHAR = ">"
|
||||
|
||||
// Replace the exact string constants with flexible regex patterns
|
||||
const SEARCH_BLOCK_START_REGEX = /^[-]{3,} SEARCH>?$/
|
||||
const LEGACY_SEARCH_BLOCK_START_REGEX = /^[<]{3,} SEARCH>?$/
|
||||
|
||||
const SEARCH_BLOCK_END_REGEX = /^[=]{3,}$/
|
||||
|
||||
const REPLACE_BLOCK_END_REGEX = /^[+]{3,} REPLACE>?$/
|
||||
const LEGACY_REPLACE_BLOCK_END_REGEX = /^[>]{3,} REPLACE>?$/
|
||||
|
||||
// Helper functions to check if a line matches the flexible patterns
|
||||
function isSearchBlockStart(line: string): boolean {
|
||||
return SEARCH_BLOCK_START_REGEX.test(line) || LEGACY_SEARCH_BLOCK_START_REGEX.test(line)
|
||||
}
|
||||
|
||||
function isSearchBlockEnd(line: string): boolean {
|
||||
return SEARCH_BLOCK_END_REGEX.test(line)
|
||||
}
|
||||
|
||||
function isReplaceBlockEnd(line: string): boolean {
|
||||
return REPLACE_BLOCK_END_REGEX.test(line) || LEGACY_REPLACE_BLOCK_END_REGEX.test(line)
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts a line-trimmed fallback match for the given search content in the original content.
|
||||
* It tries to match `searchContent` lines against a block of lines in `originalContent` starting
|
||||
* from `lastProcessedIndex`. Lines are matched by trimming leading/trailing whitespace and ensuring
|
||||
* they are identical afterwards.
|
||||
*
|
||||
* Returns [matchIndexStart, matchIndexEnd] if found, or false if not found.
|
||||
*/
|
||||
function lineTrimmedFallbackMatch(originalContent: string, searchContent: string, startIndex: number): [number, number] | false {
|
||||
// Split both contents into lines
|
||||
const originalLines = originalContent.split("\n")
|
||||
const searchLines = searchContent.split("\n")
|
||||
|
||||
// Trim trailing empty line if exists (from the trailing \n in searchContent)
|
||||
if (searchLines[searchLines.length - 1] === "") {
|
||||
searchLines.pop()
|
||||
}
|
||||
|
||||
// Find the line number where startIndex falls
|
||||
let startLineNum = 0
|
||||
let currentIndex = 0
|
||||
while (currentIndex < startIndex && startLineNum < originalLines.length) {
|
||||
currentIndex += originalLines[startLineNum].length + 1 // +1 for \n
|
||||
startLineNum++
|
||||
}
|
||||
|
||||
// For each possible starting position in original content
|
||||
for (let i = startLineNum; i <= originalLines.length - searchLines.length; i++) {
|
||||
let matches = true
|
||||
|
||||
// Try to match all search lines from this position
|
||||
for (let j = 0; j < searchLines.length; j++) {
|
||||
const originalTrimmed = originalLines[i + j].trim()
|
||||
const searchTrimmed = searchLines[j].trim()
|
||||
|
||||
if (originalTrimmed !== searchTrimmed) {
|
||||
matches = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If we found a match, calculate the exact character positions
|
||||
if (matches) {
|
||||
// Find start character index
|
||||
let matchStartIndex = 0
|
||||
for (let k = 0; k < i; k++) {
|
||||
matchStartIndex += originalLines[k].length + 1 // +1 for \n
|
||||
}
|
||||
|
||||
// Find end character index
|
||||
let matchEndIndex = matchStartIndex
|
||||
for (let k = 0; k < searchLines.length; k++) {
|
||||
matchEndIndex += originalLines[i + k].length + 1 // +1 for \n
|
||||
}
|
||||
|
||||
return [matchStartIndex, matchEndIndex]
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to match blocks of code by using the first and last lines as anchors.
|
||||
* This is a third-tier fallback strategy that helps match blocks where we can identify
|
||||
* the correct location by matching the beginning and end, even if the exact content
|
||||
* differs slightly.
|
||||
*
|
||||
* The matching strategy:
|
||||
* 1. Only attempts to match blocks of 3 or more lines to avoid false positives
|
||||
* 2. Extracts from the search content:
|
||||
* - First line as the "start anchor"
|
||||
* - Last line as the "end anchor"
|
||||
* 3. For each position in the original content:
|
||||
* - Checks if the next line matches the start anchor
|
||||
* - If it does, jumps ahead by the search block size
|
||||
* - Checks if that line matches the end anchor
|
||||
* - All comparisons are done after trimming whitespace
|
||||
*
|
||||
* This approach is particularly useful for matching blocks of code where:
|
||||
* - The exact content might have minor differences
|
||||
* - The beginning and end of the block are distinctive enough to serve as anchors
|
||||
* - The overall structure (number of lines) remains the same
|
||||
*
|
||||
* @param originalContent - The full content of the original file
|
||||
* @param searchContent - The content we're trying to find in the original file
|
||||
* @param startIndex - The character index in originalContent where to start searching
|
||||
* @returns A tuple of [startIndex, endIndex] if a match is found, false otherwise
|
||||
*/
|
||||
function blockAnchorFallbackMatch(originalContent: string, searchContent: string, startIndex: number): [number, number] | false {
|
||||
const originalLines = originalContent.split("\n")
|
||||
const searchLines = searchContent.split("\n")
|
||||
|
||||
// Only use this approach for blocks of 3+ lines
|
||||
if (searchLines.length < 3) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Trim trailing empty line if exists
|
||||
if (searchLines[searchLines.length - 1] === "") {
|
||||
searchLines.pop()
|
||||
}
|
||||
|
||||
const firstLineSearch = searchLines[0].trim()
|
||||
const lastLineSearch = searchLines[searchLines.length - 1].trim()
|
||||
const searchBlockSize = searchLines.length
|
||||
|
||||
// Find the line number where startIndex falls
|
||||
let startLineNum = 0
|
||||
let currentIndex = 0
|
||||
while (currentIndex < startIndex && startLineNum < originalLines.length) {
|
||||
currentIndex += originalLines[startLineNum].length + 1
|
||||
startLineNum++
|
||||
}
|
||||
|
||||
// Look for matching start and end anchors
|
||||
for (let i = startLineNum; i <= originalLines.length - searchBlockSize; i++) {
|
||||
// Check if first line matches
|
||||
if (originalLines[i].trim() !== firstLineSearch) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if last line matches at the expected position
|
||||
if (originalLines[i + searchBlockSize - 1].trim() !== lastLineSearch) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Calculate exact character positions
|
||||
let matchStartIndex = 0
|
||||
for (let k = 0; k < i; k++) {
|
||||
matchStartIndex += originalLines[k].length + 1
|
||||
}
|
||||
|
||||
let matchEndIndex = matchStartIndex
|
||||
for (let k = 0; k < searchBlockSize; k++) {
|
||||
matchEndIndex += originalLines[i + k].length + 1
|
||||
}
|
||||
|
||||
return [matchStartIndex, matchEndIndex]
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* This function reconstructs the file content by applying a streamed diff (in a
|
||||
* specialized SEARCH/REPLACE block format) to the original file content. It is designed
|
||||
* to handle both incremental updates and the final resulting file after all chunks have
|
||||
* been processed.
|
||||
*
|
||||
* The diff format is a custom structure that uses three markers to define changes:
|
||||
*
|
||||
* ------- SEARCH
|
||||
* [Exact content to find in the original file]
|
||||
* =======
|
||||
* [Content to replace with]
|
||||
* +++++++ REPLACE
|
||||
*
|
||||
* Behavior and Assumptions:
|
||||
* 1. The file is processed chunk-by-chunk. Each chunk of `diffContent` may contain
|
||||
* partial or complete SEARCH/REPLACE blocks. By calling this function with each
|
||||
* incremental chunk (with `isFinal` indicating the last chunk), the final reconstructed
|
||||
* file content is produced.
|
||||
*
|
||||
* 2. Matching Strategy (in order of attempt):
|
||||
* a. Exact Match: First attempts to find the exact SEARCH block text in the original file
|
||||
* b. Line-Trimmed Match: Falls back to line-by-line comparison ignoring leading/trailing whitespace
|
||||
* c. Block Anchor Match: For blocks of 3+ lines, tries to match using first/last lines as anchors
|
||||
* If all matching strategies fail, an error is thrown.
|
||||
*
|
||||
* 3. Empty SEARCH Section:
|
||||
* - If SEARCH is empty and the original file is empty, this indicates creating a new file
|
||||
* (pure insertion).
|
||||
* - If SEARCH is empty and the original file is not empty, this indicates a complete
|
||||
* file replacement (the entire original content is considered matched and replaced).
|
||||
*
|
||||
* 4. Applying Changes:
|
||||
* - Before encountering the "=======" marker, lines are accumulated as search content.
|
||||
* - After "=======" and before ">>>>>>> REPLACE", lines are accumulated as replacement content.
|
||||
* - Once the block is complete (">>>>>>> REPLACE"), the matched section in the original
|
||||
* file is replaced with the accumulated replacement lines, and the position in the original
|
||||
* file is advanced.
|
||||
*
|
||||
* 5. Incremental Output:
|
||||
* - As soon as the match location is found and we are in the REPLACE section, each new
|
||||
* replacement line is appended to the result so that partial updates can be viewed
|
||||
* incrementally.
|
||||
*
|
||||
* 6. Partial Markers:
|
||||
* - If the final line of the chunk looks like it might be part of a marker but is not one
|
||||
* of the known markers, it is removed. This prevents incomplete or partial markers
|
||||
* from corrupting the output.
|
||||
*
|
||||
* 7. Finalization:
|
||||
* - Once all chunks have been processed (when `isFinal` is true), any remaining original
|
||||
* content after the last replaced section is appended to the result.
|
||||
* - Trailing newlines are not forcibly added. The code tries to output exactly what is specified.
|
||||
*
|
||||
* Errors:
|
||||
* - If the search block cannot be matched using any of the available matching strategies,
|
||||
* an error is thrown.
|
||||
*/
|
||||
export async function constructNewFileContent(
|
||||
diffContent: string,
|
||||
originalContent: string,
|
||||
isFinal: boolean,
|
||||
version: "v1" | "v2" = "v1",
|
||||
): Promise<string> {
|
||||
const constructor = constructNewFileContentVersionMapping[version]
|
||||
if (!constructor) {
|
||||
throw new Error(`Invalid version '${version}' for file content constructor`)
|
||||
}
|
||||
return constructor(diffContent, originalContent, isFinal)
|
||||
}
|
||||
|
||||
const constructNewFileContentVersionMapping: Record<
|
||||
string,
|
||||
(diffContent: string, originalContent: string, isFinal: boolean) => Promise<string>
|
||||
> = {
|
||||
v1: constructNewFileContentV1,
|
||||
v2: constructNewFileContentV2,
|
||||
} as const
|
||||
|
||||
async function constructNewFileContentV1(diffContent: string, originalContent: string, isFinal: boolean): Promise<string> {
|
||||
let result = ""
|
||||
let lastProcessedIndex = 0
|
||||
|
||||
let currentSearchContent = ""
|
||||
let currentReplaceContent = ""
|
||||
let inSearch = false
|
||||
let inReplace = false
|
||||
|
||||
let searchMatchIndex = -1
|
||||
let searchEndIndex = -1
|
||||
|
||||
// Track all replacements to handle out-of-order edits
|
||||
let replacements: Array<{ start: number; end: number; content: string }> = []
|
||||
let pendingOutOfOrderReplacement = false
|
||||
|
||||
let lines = diffContent.split("\n")
|
||||
|
||||
// If the last line looks like a partial marker but isn't recognized,
|
||||
// remove it because it might be incomplete.
|
||||
const lastLine = lines[lines.length - 1]
|
||||
if (
|
||||
lines.length > 0 &&
|
||||
(lastLine.startsWith(SEARCH_BLOCK_CHAR) ||
|
||||
lastLine.startsWith(LEGACY_SEARCH_BLOCK_CHAR) ||
|
||||
lastLine.startsWith("=") ||
|
||||
lastLine.startsWith(REPLACE_BLOCK_CHAR) ||
|
||||
lastLine.startsWith(LEGACY_REPLACE_BLOCK_CHAR)) &&
|
||||
!isSearchBlockStart(lastLine) &&
|
||||
!isSearchBlockEnd(lastLine) &&
|
||||
!isReplaceBlockEnd(lastLine)
|
||||
) {
|
||||
lines.pop()
|
||||
}
|
||||
|
||||
for (const line of lines) {
|
||||
if (isSearchBlockStart(line)) {
|
||||
inSearch = true
|
||||
currentSearchContent = ""
|
||||
currentReplaceContent = ""
|
||||
continue
|
||||
}
|
||||
|
||||
if (isSearchBlockEnd(line)) {
|
||||
inSearch = false
|
||||
inReplace = true
|
||||
|
||||
// Remove trailing linebreak for adding the === marker
|
||||
// if (currentSearchContent.endsWith("\r\n")) {
|
||||
// currentSearchContent = currentSearchContent.slice(0, -2)
|
||||
// } else if (currentSearchContent.endsWith("\n")) {
|
||||
// currentSearchContent = currentSearchContent.slice(0, -1)
|
||||
// }
|
||||
|
||||
if (!currentSearchContent) {
|
||||
// Empty search block
|
||||
if (originalContent.length === 0) {
|
||||
// New file scenario: nothing to match, just start inserting
|
||||
searchMatchIndex = 0
|
||||
searchEndIndex = 0
|
||||
} else {
|
||||
// ERROR: Empty search block with non-empty file indicates malformed SEARCH marker
|
||||
throw new Error(
|
||||
"Empty SEARCH block detected with non-empty file. This usually indicates a malformed SEARCH marker.\n" +
|
||||
"Please ensure your SEARCH marker follows the correct format:\n" +
|
||||
"- Use '------- SEARCH' (7+ dashes + space + SEARCH)\n",
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Add check for inefficient full-file search
|
||||
// if (currentSearchContent.trim() === originalContent.trim()) {
|
||||
// throw new Error(
|
||||
// "The SEARCH block contains the entire file content. Please either:\n" +
|
||||
// "1. Use an empty SEARCH block to replace the entire file, or\n" +
|
||||
// "2. Make focused changes to specific parts of the file that need modification.",
|
||||
// )
|
||||
// }
|
||||
|
||||
// Exact search match scenario
|
||||
const exactIndex = originalContent.indexOf(currentSearchContent, lastProcessedIndex)
|
||||
if (exactIndex !== -1) {
|
||||
searchMatchIndex = exactIndex
|
||||
searchEndIndex = exactIndex + currentSearchContent.length
|
||||
} else {
|
||||
// Attempt fallback line-trimmed matching
|
||||
const lineMatch = lineTrimmedFallbackMatch(originalContent, currentSearchContent, lastProcessedIndex)
|
||||
if (lineMatch) {
|
||||
;[searchMatchIndex, searchEndIndex] = lineMatch
|
||||
} else {
|
||||
// Try block anchor fallback for larger blocks
|
||||
const blockMatch = blockAnchorFallbackMatch(originalContent, currentSearchContent, lastProcessedIndex)
|
||||
if (blockMatch) {
|
||||
;[searchMatchIndex, searchEndIndex] = blockMatch
|
||||
} else {
|
||||
// Last resort: search the entire file from the beginning
|
||||
const fullFileIndex = originalContent.indexOf(currentSearchContent, 0)
|
||||
if (fullFileIndex !== -1) {
|
||||
// Found in the file - could be out of order
|
||||
searchMatchIndex = fullFileIndex
|
||||
searchEndIndex = fullFileIndex + currentSearchContent.length
|
||||
if (searchMatchIndex < lastProcessedIndex) {
|
||||
pendingOutOfOrderReplacement = true
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
`The SEARCH block:\n${currentSearchContent.trimEnd()}\n...does not match anything in the file.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is an out-of-order replacement
|
||||
if (searchMatchIndex < lastProcessedIndex) {
|
||||
pendingOutOfOrderReplacement = true
|
||||
}
|
||||
|
||||
// For in-order replacements, output everything up to the match location
|
||||
if (!pendingOutOfOrderReplacement) {
|
||||
result += originalContent.slice(lastProcessedIndex, searchMatchIndex)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (isReplaceBlockEnd(line)) {
|
||||
// Finished one replace block
|
||||
|
||||
// Store this replacement
|
||||
replacements.push({
|
||||
start: searchMatchIndex,
|
||||
end: searchEndIndex,
|
||||
content: currentReplaceContent,
|
||||
})
|
||||
|
||||
// If this was an in-order replacement, advance lastProcessedIndex
|
||||
if (!pendingOutOfOrderReplacement) {
|
||||
lastProcessedIndex = searchEndIndex
|
||||
}
|
||||
|
||||
// Reset for next block
|
||||
inSearch = false
|
||||
inReplace = false
|
||||
currentSearchContent = ""
|
||||
currentReplaceContent = ""
|
||||
searchMatchIndex = -1
|
||||
searchEndIndex = -1
|
||||
pendingOutOfOrderReplacement = false
|
||||
continue
|
||||
}
|
||||
|
||||
// Accumulate content for search or replace
|
||||
// (currentReplaceContent is not being used for anything right now since we directly append to result.)
|
||||
// (We artificially add a linebreak since we split on \n at the beginning. In order to not include a trailing linebreak in the final search/result blocks we need to remove it before using them. This allows for partial line matches to be correctly identified.)
|
||||
// NOTE: search/replace blocks must be arranged in the order they appear in the file due to how we build the content using lastProcessedIndex. We also cannot strip the trailing newline since for non-partial lines it would remove the linebreak from the original content. (If we remove end linebreak from search, then we'd also have to remove it from replace but we can't know if it's a partial line or not since the model may be using the line break to indicate the end of the block rather than as part of the search content.) We require the model to output full lines in order for our fallbacks to work as well.
|
||||
if (inSearch) {
|
||||
currentSearchContent += line + "\n"
|
||||
} else if (inReplace) {
|
||||
currentReplaceContent += line + "\n"
|
||||
// Only output replacement lines immediately for in-order replacements
|
||||
if (searchMatchIndex !== -1 && !pendingOutOfOrderReplacement) {
|
||||
result += line + "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If this is the final chunk, we need to apply all replacements and build the final result
|
||||
if (isFinal) {
|
||||
// Handle the case where we're still in replace mode when processing ends
|
||||
// and this is the final chunk - treat it as if we encountered the REPLACE marker
|
||||
if (inReplace && searchMatchIndex !== -1) {
|
||||
// Store this replacement
|
||||
replacements.push({
|
||||
start: searchMatchIndex,
|
||||
end: searchEndIndex,
|
||||
content: currentReplaceContent,
|
||||
})
|
||||
|
||||
// If this was an in-order replacement, advance lastProcessedIndex
|
||||
if (!pendingOutOfOrderReplacement) {
|
||||
lastProcessedIndex = searchEndIndex
|
||||
}
|
||||
|
||||
// Reset state
|
||||
inSearch = false
|
||||
inReplace = false
|
||||
currentSearchContent = ""
|
||||
currentReplaceContent = ""
|
||||
searchMatchIndex = -1
|
||||
searchEndIndex = -1
|
||||
pendingOutOfOrderReplacement = false
|
||||
}
|
||||
// end of handling missing replace marker
|
||||
|
||||
// Sort replacements by start position
|
||||
replacements.sort((a, b) => a.start - b.start)
|
||||
|
||||
// Rebuild the entire result by applying all replacements
|
||||
result = ""
|
||||
let currentPos = 0
|
||||
|
||||
for (const replacement of replacements) {
|
||||
// Add original content up to this replacement
|
||||
result += originalContent.slice(currentPos, replacement.start)
|
||||
// Add the replacement content
|
||||
result += replacement.content
|
||||
// Move position to after the replaced section
|
||||
currentPos = replacement.end
|
||||
}
|
||||
|
||||
// Add any remaining original content
|
||||
result += originalContent.slice(currentPos)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
enum ProcessingState {
|
||||
Idle = 0,
|
||||
StateSearch = 1 << 0,
|
||||
StateReplace = 1 << 1,
|
||||
}
|
||||
|
||||
class NewFileContentConstructor {
|
||||
private originalContent: string
|
||||
private isFinal: boolean
|
||||
private state: number
|
||||
private pendingNonStandardLines: string[]
|
||||
private result: string
|
||||
private lastProcessedIndex: number
|
||||
private currentSearchContent: string
|
||||
private currentReplaceContent: string
|
||||
private searchMatchIndex: number
|
||||
private searchEndIndex: number
|
||||
|
||||
constructor(originalContent: string, isFinal: boolean) {
|
||||
this.originalContent = originalContent
|
||||
this.isFinal = isFinal
|
||||
this.pendingNonStandardLines = []
|
||||
this.result = ""
|
||||
this.lastProcessedIndex = 0
|
||||
this.state = ProcessingState.Idle
|
||||
this.currentSearchContent = ""
|
||||
this.currentReplaceContent = ""
|
||||
this.searchMatchIndex = -1
|
||||
this.searchEndIndex = -1
|
||||
}
|
||||
|
||||
private resetForNextBlock() {
|
||||
// Reset for next block
|
||||
this.state = ProcessingState.Idle
|
||||
this.currentSearchContent = ""
|
||||
this.currentReplaceContent = ""
|
||||
this.searchMatchIndex = -1
|
||||
this.searchEndIndex = -1
|
||||
}
|
||||
|
||||
private findLastMatchingLineIndex(regx: RegExp, lineLimit: number) {
|
||||
for (let i = lineLimit; i > 0; ) {
|
||||
i--
|
||||
if (this.pendingNonStandardLines[i].match(regx)) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
private updateProcessingState(newState: ProcessingState) {
|
||||
const isValidTransition =
|
||||
(this.state === ProcessingState.Idle && newState === ProcessingState.StateSearch) ||
|
||||
(this.state === ProcessingState.StateSearch && newState === ProcessingState.StateReplace)
|
||||
|
||||
if (!isValidTransition) {
|
||||
throw new Error(
|
||||
`Invalid state transition.\n` +
|
||||
"Valid transitions are:\n" +
|
||||
"- Idle → StateSearch\n" +
|
||||
"- StateSearch → StateReplace",
|
||||
)
|
||||
}
|
||||
|
||||
this.state |= newState
|
||||
}
|
||||
|
||||
private isStateActive(state: ProcessingState): boolean {
|
||||
return (this.state & state) === state
|
||||
}
|
||||
|
||||
private activateReplaceState() {
|
||||
this.updateProcessingState(ProcessingState.StateReplace)
|
||||
}
|
||||
|
||||
private activateSearchState() {
|
||||
this.updateProcessingState(ProcessingState.StateSearch)
|
||||
this.currentSearchContent = ""
|
||||
this.currentReplaceContent = ""
|
||||
}
|
||||
|
||||
private isSearchingActive(): boolean {
|
||||
return this.isStateActive(ProcessingState.StateSearch)
|
||||
}
|
||||
|
||||
private isReplacingActive(): boolean {
|
||||
return this.isStateActive(ProcessingState.StateReplace)
|
||||
}
|
||||
|
||||
private hasPendingNonStandardLines(pendingNonStandardLineLimit: number): boolean {
|
||||
return this.pendingNonStandardLines.length - pendingNonStandardLineLimit < this.pendingNonStandardLines.length
|
||||
}
|
||||
|
||||
public processLine(line: string) {
|
||||
this.internalProcessLine(line, true, this.pendingNonStandardLines.length)
|
||||
}
|
||||
|
||||
public getResult() {
|
||||
// If this is the final chunk, append any remaining original content
|
||||
if (this.isFinal && this.lastProcessedIndex < this.originalContent.length) {
|
||||
this.result += this.originalContent.slice(this.lastProcessedIndex)
|
||||
}
|
||||
if (this.isFinal && this.state !== ProcessingState.Idle) {
|
||||
throw new Error("File processing incomplete - SEARCH/REPLACE operations still active during finalization")
|
||||
}
|
||||
return this.result
|
||||
}
|
||||
|
||||
private internalProcessLine(
|
||||
line: string,
|
||||
canWritependingNonStandardLines: boolean,
|
||||
pendingNonStandardLineLimit: number,
|
||||
): number {
|
||||
let removeLineCount = 0
|
||||
if (isSearchBlockStart(line)) {
|
||||
removeLineCount = this.trimPendingNonStandardTrailingEmptyLines(pendingNonStandardLineLimit)
|
||||
if (removeLineCount > 0) {
|
||||
pendingNonStandardLineLimit = pendingNonStandardLineLimit - removeLineCount
|
||||
}
|
||||
if (this.hasPendingNonStandardLines(pendingNonStandardLineLimit)) {
|
||||
this.tryFixSearchReplaceBlock(pendingNonStandardLineLimit)
|
||||
canWritependingNonStandardLines && (this.pendingNonStandardLines.length = 0)
|
||||
}
|
||||
this.activateSearchState()
|
||||
} else if (isSearchBlockEnd(line)) {
|
||||
// 校验非标内容
|
||||
if (!this.isSearchingActive()) {
|
||||
this.tryFixSearchBlock(pendingNonStandardLineLimit)
|
||||
canWritependingNonStandardLines && (this.pendingNonStandardLines.length = 0)
|
||||
}
|
||||
this.activateReplaceState()
|
||||
this.beforeReplace()
|
||||
} else if (isReplaceBlockEnd(line)) {
|
||||
if (!this.isReplacingActive()) {
|
||||
this.tryFixReplaceBlock(pendingNonStandardLineLimit)
|
||||
canWritependingNonStandardLines && (this.pendingNonStandardLines.length = 0)
|
||||
}
|
||||
this.lastProcessedIndex = this.searchEndIndex
|
||||
this.resetForNextBlock()
|
||||
} else {
|
||||
// Accumulate content for search or replace
|
||||
// (currentReplaceContent is not being used for anything right now since we directly append to result.)
|
||||
// (We artificially add a linebreak since we split on \n at the beginning. In order to not include a trailing linebreak in the final search/result blocks we need to remove it before using them. This allows for partial line matches to be correctly identified.)
|
||||
// NOTE: search/replace blocks must be arranged in the order they appear in the file due to how we build the content using lastProcessedIndex. We also cannot strip the trailing newline since for non-partial lines it would remove the linebreak from the original content. (If we remove end linebreak from search, then we'd also have to remove it from replace but we can't know if it's a partial line or not since the model may be using the line break to indicate the end of the block rather than as part of the search content.) We require the model to output full lines in order for our fallbacks to work as well.
|
||||
if (this.isReplacingActive()) {
|
||||
this.currentReplaceContent += line + "\n"
|
||||
// Output replacement lines immediately if we know the insertion point
|
||||
if (this.searchMatchIndex !== -1) {
|
||||
this.result += line + "\n"
|
||||
}
|
||||
} else if (this.isSearchingActive()) {
|
||||
this.currentSearchContent += line + "\n"
|
||||
} else {
|
||||
let appendToPendingNonStandardLines = canWritependingNonStandardLines
|
||||
if (appendToPendingNonStandardLines) {
|
||||
// 处理非标内容
|
||||
this.pendingNonStandardLines.push(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
return removeLineCount
|
||||
}
|
||||
|
||||
private beforeReplace() {
|
||||
// Remove trailing linebreak for adding the === marker
|
||||
// if (currentSearchContent.endsWith("\r\n")) {
|
||||
// currentSearchContent = currentSearchContent.slice(0, -2)
|
||||
// } else if (currentSearchContent.endsWith("\n")) {
|
||||
// currentSearchContent = currentSearchContent.slice(0, -1)
|
||||
// }
|
||||
|
||||
if (!this.currentSearchContent) {
|
||||
// Empty search block
|
||||
if (this.originalContent.length === 0) {
|
||||
// New file scenario: nothing to match, just start inserting
|
||||
this.searchMatchIndex = 0
|
||||
this.searchEndIndex = 0
|
||||
} else {
|
||||
// Complete file replacement scenario: treat the entire file as matched
|
||||
this.searchMatchIndex = 0
|
||||
this.searchEndIndex = this.originalContent.length
|
||||
}
|
||||
} else {
|
||||
// Add check for inefficient full-file search
|
||||
// if (currentSearchContent.trim() === originalContent.trim()) {
|
||||
// throw new Error(
|
||||
// "The SEARCH block contains the entire file content. Please either:\n" +
|
||||
// "1. Use an empty SEARCH block to replace the entire file, or\n" +
|
||||
// "2. Make focused changes to specific parts of the file that need modification.",
|
||||
// )
|
||||
// }
|
||||
// Exact search match scenario
|
||||
const exactIndex = this.originalContent.indexOf(this.currentSearchContent, this.lastProcessedIndex)
|
||||
if (exactIndex !== -1) {
|
||||
this.searchMatchIndex = exactIndex
|
||||
this.searchEndIndex = exactIndex + this.currentSearchContent.length
|
||||
} else {
|
||||
// Attempt fallback line-trimmed matching
|
||||
const lineMatch = lineTrimmedFallbackMatch(
|
||||
this.originalContent,
|
||||
this.currentSearchContent,
|
||||
this.lastProcessedIndex,
|
||||
)
|
||||
if (lineMatch) {
|
||||
;[this.searchMatchIndex, this.searchEndIndex] = lineMatch
|
||||
} else {
|
||||
// Try block anchor fallback for larger blocks
|
||||
const blockMatch = blockAnchorFallbackMatch(
|
||||
this.originalContent,
|
||||
this.currentSearchContent,
|
||||
this.lastProcessedIndex,
|
||||
)
|
||||
if (blockMatch) {
|
||||
;[this.searchMatchIndex, this.searchEndIndex] = blockMatch
|
||||
} else {
|
||||
throw new Error(
|
||||
`The SEARCH block:\n${this.currentSearchContent.trimEnd()}\n...does not match anything in the file.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.searchMatchIndex < this.lastProcessedIndex) {
|
||||
throw new Error(
|
||||
`The SEARCH block:\n${this.currentSearchContent.trimEnd()}\n...matched an incorrect content in the file.`,
|
||||
)
|
||||
}
|
||||
// Output everything up to the match location
|
||||
this.result += this.originalContent.slice(this.lastProcessedIndex, this.searchMatchIndex)
|
||||
}
|
||||
|
||||
private tryFixSearchBlock(lineLimit: number): number {
|
||||
let removeLineCount = 0
|
||||
if (lineLimit < 0) {
|
||||
lineLimit = this.pendingNonStandardLines.length
|
||||
}
|
||||
if (!lineLimit) {
|
||||
throw new Error("Invalid SEARCH/REPLACE block structure - no lines available to process")
|
||||
}
|
||||
let searchTagRegexp = /^([-]{3,}|[<]{3,}) SEARCH$/
|
||||
const searchTagIndex = this.findLastMatchingLineIndex(searchTagRegexp, lineLimit)
|
||||
if (searchTagIndex !== -1) {
|
||||
let fixLines = this.pendingNonStandardLines.slice(searchTagIndex, lineLimit)
|
||||
fixLines[0] = SEARCH_BLOCK_START
|
||||
for (const line of fixLines) {
|
||||
removeLineCount += this.internalProcessLine(line, false, searchTagIndex)
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
`Invalid REPLACE marker detected - could not find matching SEARCH block starting from line ${searchTagIndex + 1}`,
|
||||
)
|
||||
}
|
||||
return removeLineCount
|
||||
}
|
||||
|
||||
private tryFixReplaceBlock(lineLimit: number): number {
|
||||
let removeLineCount = 0
|
||||
if (lineLimit < 0) {
|
||||
lineLimit = this.pendingNonStandardLines.length
|
||||
}
|
||||
if (!lineLimit) {
|
||||
throw new Error()
|
||||
}
|
||||
let replaceBeginTagRegexp = /^[=]{3,}$/
|
||||
const replaceBeginTagIndex = this.findLastMatchingLineIndex(replaceBeginTagRegexp, lineLimit)
|
||||
if (replaceBeginTagIndex !== -1) {
|
||||
// // 校验非标内容
|
||||
// if (!this.isSearchingActive()) {
|
||||
// removeLineCount += this.tryFixSearchBlock(replaceBeginTagIndex)
|
||||
// }
|
||||
let fixLines = this.pendingNonStandardLines.slice(replaceBeginTagIndex - removeLineCount, lineLimit - removeLineCount)
|
||||
fixLines[0] = SEARCH_BLOCK_END
|
||||
for (const line of fixLines) {
|
||||
removeLineCount += this.internalProcessLine(line, false, replaceBeginTagIndex - removeLineCount)
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Malformed REPLACE block - missing valid separator after line ${replaceBeginTagIndex + 1}`)
|
||||
}
|
||||
return removeLineCount
|
||||
}
|
||||
|
||||
private tryFixSearchReplaceBlock(lineLimit: number): number {
|
||||
let removeLineCount = 0
|
||||
if (lineLimit < 0) {
|
||||
lineLimit = this.pendingNonStandardLines.length
|
||||
}
|
||||
if (!lineLimit) {
|
||||
throw new Error()
|
||||
}
|
||||
|
||||
let replaceEndTagRegexp = /^([+]{3,}|[>]{3,}) REPLACE$/
|
||||
const replaceEndTagIndex = this.findLastMatchingLineIndex(replaceEndTagRegexp, lineLimit)
|
||||
const likeReplaceEndTag = replaceEndTagIndex === lineLimit - 1
|
||||
if (likeReplaceEndTag) {
|
||||
// // 校验非标内容
|
||||
// if (!this.isReplacingActive()) {
|
||||
// removeLineCount += this.tryFixReplaceBlock(replaceEndTagIndex)
|
||||
// }
|
||||
let fixLines = this.pendingNonStandardLines.slice(replaceEndTagIndex - removeLineCount, lineLimit - removeLineCount)
|
||||
fixLines[fixLines.length - 1] = REPLACE_BLOCK_END
|
||||
for (const line of fixLines) {
|
||||
removeLineCount += this.internalProcessLine(line, false, replaceEndTagIndex - removeLineCount)
|
||||
}
|
||||
} else {
|
||||
throw new Error("Malformed SEARCH/REPLACE block structure: Missing valid closing REPLACE marker")
|
||||
}
|
||||
return removeLineCount
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes trailing empty lines from the pendingNonStandardLines array
|
||||
* @param lineLimit - The index to start checking from (exclusive).
|
||||
* Removes empty lines from lineLimit-1 backwards.
|
||||
* @returns The number of empty lines removed
|
||||
*/
|
||||
private trimPendingNonStandardTrailingEmptyLines(lineLimit: number): number {
|
||||
let removedCount = 0
|
||||
let i = Math.min(lineLimit, this.pendingNonStandardLines.length) - 1
|
||||
|
||||
while (i >= 0 && this.pendingNonStandardLines[i].trim() === "") {
|
||||
this.pendingNonStandardLines.pop()
|
||||
removedCount++
|
||||
i--
|
||||
}
|
||||
|
||||
return removedCount
|
||||
}
|
||||
}
|
||||
|
||||
export async function constructNewFileContentV2(diffContent: string, originalContent: string, isFinal: boolean): Promise<string> {
|
||||
let newFileContentConstructor = new NewFileContentConstructor(originalContent, isFinal)
|
||||
|
||||
let lines = diffContent.split("\n")
|
||||
|
||||
// If the last line looks like a partial marker but isn't recognized,
|
||||
// remove it because it might be incomplete.
|
||||
const lastLine = lines[lines.length - 1]
|
||||
if (
|
||||
lines.length > 0 &&
|
||||
(lastLine.startsWith(SEARCH_BLOCK_CHAR) ||
|
||||
lastLine.startsWith(LEGACY_SEARCH_BLOCK_CHAR) ||
|
||||
lastLine.startsWith("=") ||
|
||||
lastLine.startsWith(REPLACE_BLOCK_CHAR) ||
|
||||
lastLine.startsWith(LEGACY_REPLACE_BLOCK_CHAR)) &&
|
||||
lastLine !== SEARCH_BLOCK_START &&
|
||||
lastLine !== SEARCH_BLOCK_END &&
|
||||
lastLine !== REPLACE_BLOCK_END
|
||||
) {
|
||||
lines.pop()
|
||||
}
|
||||
|
||||
for (const line of lines) {
|
||||
newFileContentConstructor.processLine(line)
|
||||
}
|
||||
|
||||
let result = newFileContentConstructor.getResult()
|
||||
return result
|
||||
}
|
||||
Generated
+60
-552
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.18.0",
|
||||
"version": "3.17.16",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.18.0",
|
||||
"version": "3.17.16",
|
||||
"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",
|
||||
|
||||
+1
-2
@@ -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.17.16",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -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",
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
// Configuration file for protocol buffer build scripts
|
||||
// Contains service name mappings used by both build-proto.js and build-go-proto.js
|
||||
|
||||
// List of gRPC services
|
||||
// To add a new service, simply add it to this map and run the build scripts
|
||||
// The service handler will be automatically discovered and used by grpc-handler.ts
|
||||
export const serviceNameMap = {
|
||||
account: "cline.AccountService",
|
||||
browser: "cline.BrowserService",
|
||||
checkpoints: "cline.CheckpointsService",
|
||||
file: "cline.FileService",
|
||||
mcp: "cline.McpService",
|
||||
state: "cline.StateService",
|
||||
task: "cline.TaskService",
|
||||
web: "cline.WebService",
|
||||
models: "cline.ModelsService",
|
||||
slash: "cline.SlashService",
|
||||
ui: "cline.UiService",
|
||||
// Add new services here - no other code changes needed!
|
||||
}
|
||||
|
||||
// List of host gRPC services (IDE API bridge)
|
||||
// These services are implemented in the IDE extension and called by the standalone Cline Core
|
||||
export const hostServiceNameMap = {
|
||||
uri: "host.UriService",
|
||||
watch: "host.WatchService",
|
||||
workspace: "host.WorkspaceService",
|
||||
env: "host.EnvService",
|
||||
// Add new host services here
|
||||
}
|
||||
+44
-18
@@ -9,18 +9,16 @@ import chalk from "chalk"
|
||||
import os from "os"
|
||||
|
||||
import { createRequire } from "module"
|
||||
import { serviceNameMap, hostServiceNameMap } from "./build-proto-config.js"
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const PROTOC = path.join(require.resolve("grpc-tools"), "../bin/protoc")
|
||||
|
||||
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url))
|
||||
const ROOT_DIR = path.resolve(SCRIPT_DIR, "..")
|
||||
|
||||
const TS_OUT_DIR = path.join(ROOT_DIR, "src", "shared", "proto")
|
||||
const GRPC_JS_OUT_DIR = path.join(ROOT_DIR, "src", "generated", "grpc-js")
|
||||
const NICE_JS_OUT_DIR = path.join(ROOT_DIR, "src", "generated", "nice-grpc")
|
||||
const DESCRIPTOR_OUT_DIR = path.join(ROOT_DIR, "dist-standalone", "proto")
|
||||
const TS_OUT_DIR = path.join(ROOT_DIR, "src/shared/proto")
|
||||
const GRPC_JS_OUT_DIR = path.join(ROOT_DIR, "src/generated/grpc-js")
|
||||
const NICE_JS_OUT_DIR = path.join(ROOT_DIR, "src/generated/nice-grpc")
|
||||
const DESCRIPTOR_OUT_DIR = path.join(ROOT_DIR, "dist-standalone/proto")
|
||||
|
||||
const isWindows = process.platform === "win32"
|
||||
const TS_PROTO_PLUGIN = isWindows
|
||||
@@ -36,13 +34,33 @@ const TS_PROTO_OPTIONS = [
|
||||
"useDate=false", // Timestamp fields will not be automatically converted to Date.
|
||||
]
|
||||
|
||||
// Service directories derived from imported serviceNameMap
|
||||
const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src", "core", "controller", serviceKey))
|
||||
// List of gRPC services
|
||||
// To add a new service, simply add it to this map and run this script
|
||||
// The service handler will be automatically discovered and used by grpc-handler.ts
|
||||
const serviceNameMap = {
|
||||
account: "cline.AccountService",
|
||||
browser: "cline.BrowserService",
|
||||
checkpoints: "cline.CheckpointsService",
|
||||
file: "cline.FileService",
|
||||
mcp: "cline.McpService",
|
||||
state: "cline.StateService",
|
||||
task: "cline.TaskService",
|
||||
web: "cline.WebService",
|
||||
models: "cline.ModelsService",
|
||||
slash: "cline.SlashService",
|
||||
ui: "cline.UiService",
|
||||
// Add new services here - no other code changes needed!
|
||||
}
|
||||
const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src/core/controller", serviceKey))
|
||||
|
||||
// Host service directories derived from imported hostServiceNameMap
|
||||
const hostServiceDirs = Object.keys(hostServiceNameMap).map((serviceKey) =>
|
||||
path.join(ROOT_DIR, "src", "hosts", "vscode", serviceKey),
|
||||
)
|
||||
// List of host gRPC services (IDE API bridge)
|
||||
// These services are implemented in the IDE extension and called by the standalone Cline Core
|
||||
const hostServiceNameMap = {
|
||||
uri: "host.UriService",
|
||||
watch: "host.WatchService",
|
||||
// Add new host services here
|
||||
}
|
||||
const hostServiceDirs = Object.keys(hostServiceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src/hosts/vscode", serviceKey))
|
||||
|
||||
async function main() {
|
||||
console.log(chalk.bold.blue("Starting Protocol Buffer code generation..."))
|
||||
@@ -158,7 +176,7 @@ export {
|
||||
${serviceExports.join(",\n\t")}
|
||||
}`
|
||||
|
||||
const filePath = path.join(ROOT_DIR, "webview-ui", "src", "services", "grpc-client.ts")
|
||||
const filePath = path.join(ROOT_DIR, "webview-ui/src/services/grpc-client.ts")
|
||||
await writeFileWithMkdirs(filePath, content)
|
||||
log_verbose(chalk.green(`Generated gRPC client at ${filePath}`))
|
||||
}
|
||||
@@ -367,7 +385,7 @@ export interface ServiceHandlerConfig {
|
||||
export const serviceHandlers: Record<string, ServiceHandlerConfig> = {${serviceConfigs.join(",")}
|
||||
};`
|
||||
|
||||
const configPath = path.join(ROOT_DIR, "src", "core", "controller", "grpc-service-config.ts")
|
||||
const configPath = path.join(ROOT_DIR, "src/core/controller/grpc-service-config.ts")
|
||||
await writeFileWithMkdirs(configPath, content)
|
||||
log_verbose(chalk.green(`Generated service configuration at ${configPath}`))
|
||||
}
|
||||
@@ -583,12 +601,13 @@ async function cleanup() {
|
||||
for (const file of existingFiles) {
|
||||
await fs.unlink(path.join(TS_OUT_DIR, file))
|
||||
}
|
||||
await rmdir(path.join(ROOT_DIR, "src", "generated"))
|
||||
await rmdir(path.join(ROOT_DIR, "src/generated"))
|
||||
|
||||
// Clean up generated files that were moved.
|
||||
await fs.rm(path.join(ROOT_DIR, "src", "standalone", "services", "host-grpc-client.ts"), { force: true })
|
||||
await rmdir(path.join(ROOT_DIR, "src", "standalone", "services"))
|
||||
await fs.rm(path.join(ROOT_DIR, "hosts", "vscode"), { force: true, recursive: true })
|
||||
await fs.rm(path.join(ROOT_DIR, "src/standalone/services/host-grpc-client.ts"), { force: true })
|
||||
await rmdir(path.join(ROOT_DIR, "src/standalone/services"))
|
||||
|
||||
await fs.rm(path.join(ROOT_DIR, "hosts/vscode"), { force: true, recursive: true })
|
||||
await rmdir(path.join(ROOT_DIR, "hosts"))
|
||||
|
||||
await fs.rm(path.join(ROOT_DIR, "src/standalone/server-setup.ts"), { force: true })
|
||||
@@ -616,6 +635,13 @@ async function rmdir(path) {
|
||||
}
|
||||
}
|
||||
|
||||
function serviceNameWithoutPackage(fullServiceName) {
|
||||
return fullServiceName.replace(/.*\./, "")
|
||||
}
|
||||
function lowercaseFirstChar(str) {
|
||||
return str.charAt(0).toLowerCase() + str.slice(1)
|
||||
}
|
||||
|
||||
// Check for Apple Silicon compatibility
|
||||
function checkAppleSiliconCompatibility() {
|
||||
// Only run check on macOS
|
||||
|
||||
@@ -29,6 +29,9 @@ service FileService {
|
||||
// Search git commits in the workspace
|
||||
rpc searchCommits(StringRequest) returns (GitCommits);
|
||||
|
||||
// Select images from the file system and return as data URLs
|
||||
rpc selectImages(EmptyRequest) returns (StringArray);
|
||||
|
||||
// Select images and other files from the file system and returns as data URLs & paths respectively
|
||||
rpc selectFiles(BooleanRequest) returns (StringArrays);
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package host;
|
||||
option java_package = "bot.cline.host.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
// Provides methods for working with the user's environment.
|
||||
service EnvService {
|
||||
// Writes text to the system clipboard.
|
||||
rpc clipboardWriteText(cline.StringRequest) returns (cline.Empty);
|
||||
|
||||
// Reads text from the system clipboard.
|
||||
rpc clipboardReadText(cline.EmptyRequest) returns (cline.String);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package host;
|
||||
option java_package = "bot.cline.host.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
// Provides methods for working with workspaces/projects.
|
||||
service WorkspaceService {
|
||||
// Returns a list of the top level directories of the workspace.
|
||||
rpc getWorkspacePaths(GetWorkspacePathsRequest) returns (GetWorkspacePathsResponse);
|
||||
}
|
||||
|
||||
message GetWorkspacePathsRequest {
|
||||
// The unique ID for the workspace/project.
|
||||
// This is currently optional in vscode. It is required in other environments where cline is running at
|
||||
// the application level, and the user can open multiple projects.
|
||||
optional string id = 1;
|
||||
}
|
||||
|
||||
message GetWorkspacePathsResponse {
|
||||
// The unique ID for the workspace/project.
|
||||
optional string id = 1;
|
||||
repeated string paths = 2;
|
||||
}
|
||||
+19
-22
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ service StateService {
|
||||
rpc togglePlanActMode(TogglePlanActModeRequest) returns (Boolean);
|
||||
rpc updateAutoApprovalSettings(AutoApprovalSettingsRequest) returns (Empty);
|
||||
rpc updateSettings(UpdateSettingsRequest) returns (Empty);
|
||||
rpc updateTelemetrySetting(TelemetrySettingRequest) returns (Empty);
|
||||
}
|
||||
|
||||
message State {
|
||||
@@ -89,17 +88,6 @@ message AutoApprovalSettingsRequest {
|
||||
repeated string favorites = 7;
|
||||
}
|
||||
|
||||
enum TelemetrySettingEnum {
|
||||
UNSET = 0;
|
||||
ENABLED = 1;
|
||||
DISABLED = 2;
|
||||
}
|
||||
|
||||
message TelemetrySettingRequest {
|
||||
Metadata metadata = 1;
|
||||
TelemetrySettingEnum setting = 2;
|
||||
}
|
||||
|
||||
// Message for updating settings
|
||||
message UpdateSettingsRequest {
|
||||
Metadata metadata = 1;
|
||||
|
||||
+9
-1
@@ -23,6 +23,8 @@ service TaskService {
|
||||
rpc exportTaskWithId(StringRequest) returns (Empty);
|
||||
// Toggles the favorite status of a task
|
||||
rpc toggleTaskFavorite(TaskFavoriteRequest) returns (Empty);
|
||||
// Deletes all non-favorited tasks
|
||||
rpc deleteNonFavoritedTasks(EmptyRequest) returns (DeleteNonFavoritedTasksResults);
|
||||
// Gets filtered task history
|
||||
rpc getTaskHistory(GetTaskHistoryRequest) returns (TaskHistoryArray);
|
||||
// Sends a response to a previous ask operation
|
||||
@@ -34,7 +36,7 @@ service TaskService {
|
||||
// Executes a quick win task with command and title
|
||||
rpc executeQuickWin(ExecuteQuickWinRequest) returns (Empty);
|
||||
// Deletes all task history
|
||||
rpc deleteAllTaskHistory(EmptyRequest) returns (DeleteAllTaskHistoryCount);
|
||||
rpc deleteAllTaskHistory(BooleanRequest) returns (DeleteAllTaskHistoryCount);
|
||||
}
|
||||
|
||||
// Request message for creating a new task
|
||||
@@ -66,6 +68,12 @@ message TaskResponse {
|
||||
int32 cache_reads = 10;
|
||||
}
|
||||
|
||||
// Results returned when deleting non-favorited tasks
|
||||
message DeleteNonFavoritedTasksResults {
|
||||
int32 tasks_preserved = 1;
|
||||
int32 tasks_deleted = 2;
|
||||
}
|
||||
|
||||
// Request for getting task history with filtering
|
||||
message GetTaskHistoryRequest {
|
||||
Metadata metadata = 1;
|
||||
|
||||
@@ -265,7 +265,4 @@ service UiService {
|
||||
|
||||
// Returns the HTML for the webview index page. This is only used by external clients, not by the vscode webview.
|
||||
rpc getWebviewHtml(EmptyRequest) returns (String);
|
||||
|
||||
// Opens a URL in the default browser
|
||||
rpc openUrl(StringRequest) returns (Empty);
|
||||
}
|
||||
|
||||
+2
-28
@@ -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"
|
||||
@@ -39,7 +38,8 @@ export interface SingleCompletionHandler {
|
||||
completePrompt(prompt: string): Promise<string>
|
||||
}
|
||||
|
||||
function createHandlerForProvider(apiProvider: string | undefined, options: any): ApiHandler {
|
||||
export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
|
||||
const { apiProvider, ...options } = configuration
|
||||
switch (apiProvider) {
|
||||
case "anthropic":
|
||||
return new AnthropicHandler(options)
|
||||
@@ -57,8 +57,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":
|
||||
@@ -99,27 +97,3 @@ function createHandlerForProvider(apiProvider: string | undefined, options: any)
|
||||
return new AnthropicHandler(options)
|
||||
}
|
||||
}
|
||||
|
||||
export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
|
||||
const { apiProvider, ...options } = configuration
|
||||
|
||||
// Validate thinking budget tokens against model's maxTokens to prevent API errors
|
||||
// wrapped in a try-catch for safety, but this should never throw
|
||||
try {
|
||||
if (options.thinkingBudgetTokens && options.thinkingBudgetTokens > 0) {
|
||||
const handler = createHandlerForProvider(apiProvider, options)
|
||||
|
||||
const modelInfo = handler.getModel().info
|
||||
if (modelInfo.maxTokens && options.thinkingBudgetTokens > modelInfo.maxTokens) {
|
||||
const clippedValue = modelInfo.maxTokens - 1
|
||||
options.thinkingBudgetTokens = clippedValue
|
||||
} else {
|
||||
return handler // don't rebuild unless its necessary
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("buildApiHandler error:", error)
|
||||
}
|
||||
|
||||
return createHandlerForProvider(apiProvider, options)
|
||||
}
|
||||
|
||||
+17
-57
@@ -56,21 +56,7 @@ export class ClineHandler implements ApiHandler {
|
||||
this.lastGenerationId = chunk.id
|
||||
}
|
||||
|
||||
// Check for mid-stream error via finish_reason
|
||||
const choice = chunk.choices?.[0]
|
||||
// OpenRouter may return finish_reason = "error" with error details
|
||||
if ((choice?.finish_reason as string) === "error") {
|
||||
const choiceWithError = choice as any
|
||||
if (choiceWithError.error) {
|
||||
const error = choiceWithError.error
|
||||
console.error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
|
||||
throw new Error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
|
||||
} else {
|
||||
throw new Error("Cline Mid-Stream Error: Stream terminated with error status but no error details provided")
|
||||
}
|
||||
}
|
||||
|
||||
const delta = choice?.delta
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
@@ -89,7 +75,7 @@ export class ClineHandler implements ApiHandler {
|
||||
|
||||
if (!didOutputUsage && chunk.usage) {
|
||||
// @ts-ignore-next-line
|
||||
let totalCost = (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0)
|
||||
let totalCost = chunk.usage.cost || 0
|
||||
const modelId = this.getModel().id
|
||||
const provider = modelId.split("/")[0]
|
||||
|
||||
@@ -98,26 +84,14 @@ export class ClineHandler implements ApiHandler {
|
||||
totalCost = 0
|
||||
}
|
||||
|
||||
if (modelId.includes("gemini")) {
|
||||
yield {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
inputTokens: (chunk.usage.prompt_tokens || 0) - (chunk.usage.prompt_tokens_details?.cached_tokens || 0),
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
totalCost,
|
||||
}
|
||||
} else {
|
||||
yield {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
totalCost,
|
||||
}
|
||||
yield {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
totalCost,
|
||||
}
|
||||
didOutputUsage = true
|
||||
}
|
||||
@@ -143,27 +117,13 @@ export class ClineHandler implements ApiHandler {
|
||||
})
|
||||
|
||||
const generation = response.data
|
||||
let modelId = this.options.openRouterModelId
|
||||
if (modelId && modelId.includes("gemini")) {
|
||||
return {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: generation?.native_tokens_cached || 0,
|
||||
// openrouter generation endpoint fails often
|
||||
inputTokens: (generation?.native_tokens_prompt || 0) - (generation?.native_tokens_cached || 0),
|
||||
outputTokens: generation?.native_tokens_completion || 0,
|
||||
totalCost: generation?.total_cost || 0,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: generation?.native_tokens_cached || 0,
|
||||
// openrouter generation endpoint fails often
|
||||
inputTokens: generation?.native_tokens_prompt || 0,
|
||||
outputTokens: generation?.native_tokens_completion || 0,
|
||||
totalCost: generation?.total_cost || 0,
|
||||
}
|
||||
return {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: generation?.native_tokens_cached || 0,
|
||||
inputTokens: generation?.native_tokens_prompt || 0,
|
||||
outputTokens: generation?.native_tokens_completion || 0,
|
||||
totalCost: generation?.total_cost || 0,
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore if fails
|
||||
|
||||
@@ -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],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,6 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
|
||||
for await (const chunk of stream) {
|
||||
// openrouter returns an error object instead of the openai sdk throwing an error
|
||||
// Check for error field directly on chunk
|
||||
if ("error" in chunk) {
|
||||
const error = chunk.error as OpenRouterErrorResponse["error"]
|
||||
console.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`)
|
||||
@@ -53,29 +52,6 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
throw new Error(`OpenRouter API Error ${error.code}: ${error.message}${metadataStr}`)
|
||||
}
|
||||
|
||||
// Check for error in choices[0].finish_reason
|
||||
// OpenRouter may return errors in a non-standard way within choices
|
||||
const choice = chunk.choices?.[0]
|
||||
// Use type assertion since OpenRouter uses non-standard "error" finish_reason
|
||||
if ((choice?.finish_reason as string) === "error") {
|
||||
// Use type assertion since OpenRouter adds non-standard error property
|
||||
const choiceWithError = choice as any
|
||||
if (choiceWithError.error) {
|
||||
const error = choiceWithError.error
|
||||
console.error(
|
||||
`OpenRouter Mid-Stream Error: ${error?.code || "Unknown"} - ${error?.message || "Unknown error"}`,
|
||||
)
|
||||
// Format error details
|
||||
const errorDetails = typeof error === "object" ? JSON.stringify(error, null, 2) : String(error)
|
||||
throw new Error(`OpenRouter Mid-Stream Error: ${errorDetails}`)
|
||||
} else {
|
||||
// Fallback if error details are not available
|
||||
throw new Error(
|
||||
`OpenRouter Mid-Stream Error: Stream terminated with error status but no error details provided`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.lastGenerationId && chunk.id) {
|
||||
this.lastGenerationId = chunk.id
|
||||
}
|
||||
@@ -98,27 +74,14 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
if (!didOutputUsage && chunk.usage) {
|
||||
let modelId = this.options.openRouterModelId
|
||||
if (modelId && modelId.includes("gemini")) {
|
||||
yield {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
inputTokens: (chunk.usage.prompt_tokens || 0) - (chunk.usage.prompt_tokens_details?.cached_tokens || 0),
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
totalCost: (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0),
|
||||
}
|
||||
} else {
|
||||
yield {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
totalCost: (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0),
|
||||
}
|
||||
yield {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
totalCost: chunk.usage.cost || 0,
|
||||
}
|
||||
didOutputUsage = true
|
||||
}
|
||||
@@ -140,27 +103,14 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
const generationIterator = this.fetchGenerationDetails(this.lastGenerationId)
|
||||
const generation = (await generationIterator.next()).value
|
||||
// console.log("OpenRouter generation details:", generation)
|
||||
let modelId = this.options.openRouterModelId
|
||||
if (modelId && modelId.includes("gemini")) {
|
||||
return {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: generation?.native_tokens_cached || 0,
|
||||
// openrouter generation endpoint fails often
|
||||
inputTokens: (generation?.native_tokens_prompt || 0) - (generation?.native_tokens_cached || 0),
|
||||
outputTokens: generation?.native_tokens_completion || 0,
|
||||
totalCost: generation?.total_cost || 0,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: generation?.native_tokens_cached || 0,
|
||||
// openrouter generation endpoint fails often
|
||||
inputTokens: generation?.native_tokens_prompt || 0,
|
||||
outputTokens: generation?.native_tokens_completion || 0,
|
||||
totalCost: generation?.total_cost || 0,
|
||||
}
|
||||
return {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: generation?.native_tokens_cached || 0,
|
||||
// openrouter generation endpoint fails often
|
||||
inputTokens: generation?.native_tokens_prompt || 0,
|
||||
outputTokens: generation?.native_tokens_completion || 0,
|
||||
totalCost: generation?.total_cost || 0,
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore if fails
|
||||
|
||||
@@ -8,13 +8,11 @@ const LEGACY_SEARCH_BLOCK_CHAR = "<"
|
||||
const LEGACY_REPLACE_BLOCK_CHAR = ">"
|
||||
|
||||
// Replace the exact string constants with flexible regex patterns
|
||||
const SEARCH_BLOCK_START_REGEX = /^[-]{3,} SEARCH>?$/
|
||||
const LEGACY_SEARCH_BLOCK_START_REGEX = /^[<]{3,} SEARCH>?$/
|
||||
|
||||
const SEARCH_BLOCK_START_REGEX = /^[-]{3,} SEARCH$/
|
||||
const SEARCH_BLOCK_END_REGEX = /^[=]{3,}$/
|
||||
|
||||
const REPLACE_BLOCK_END_REGEX = /^[+]{3,} REPLACE>?$/
|
||||
const LEGACY_REPLACE_BLOCK_END_REGEX = /^[>]{3,} REPLACE>?$/
|
||||
const REPLACE_BLOCK_END_REGEX = /^[+]{3,} REPLACE$/
|
||||
const LEGACY_SEARCH_BLOCK_START_REGEX = /^[<]{3,} SEARCH$/
|
||||
const LEGACY_REPLACE_BLOCK_END_REGEX = /^[>]{3,} REPLACE$/
|
||||
|
||||
// Helper functions to check if a line matches the flexible patterns
|
||||
function isSearchBlockStart(line: string): boolean {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as vscode from "vscode"
|
||||
import { Controller } from ".."
|
||||
import { Empty, StringRequest } from "../../../shared/proto/common"
|
||||
import { writeTextToClipboard } from "@/utils/env"
|
||||
|
||||
/**
|
||||
* Copies text to the system clipboard
|
||||
@@ -12,7 +11,7 @@ import { writeTextToClipboard } from "@/utils/env"
|
||||
export async function copyToClipboard(controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
try {
|
||||
if (request.value) {
|
||||
await writeTextToClipboard(request.value)
|
||||
await vscode.env.clipboard.writeText(request.value)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error copying to clipboard:", error)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Controller } from ".."
|
||||
import { EmptyRequest, StringArray } from "@shared/proto/common"
|
||||
import { selectImages as selectImagesIntegration } from "@integrations/misc/process-images"
|
||||
import { FileMethodHandler } from "./index"
|
||||
|
||||
/**
|
||||
* Prompts the user to select images from the file system and returns them as data URLs
|
||||
* @param controller The controller instance
|
||||
* @param request Empty request, no parameters needed
|
||||
* @returns Array of image data URLs
|
||||
*/
|
||||
export const selectImages: FileMethodHandler = async (controller: Controller, request: EmptyRequest): Promise<StringArray> => {
|
||||
try {
|
||||
const images = await selectImagesIntegration()
|
||||
return StringArray.create({ values: images })
|
||||
} catch (error) {
|
||||
console.error("Error selecting images:", error)
|
||||
// Return empty array on error
|
||||
return StringArray.create({ values: [] })
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,7 @@ export class Controller {
|
||||
workspaceTracker: WorkspaceTracker
|
||||
mcpHub: McpHub
|
||||
accountService: ClineAccountService
|
||||
latestAnnouncementId = "june-25-2025_16:11:00" // update to some unique identifier when we add a new announcement
|
||||
latestAnnouncementId = "may-22-2025_16:11:00" // update to some unique identifier when we add a new announcement
|
||||
|
||||
constructor(
|
||||
readonly context: vscode.ExtensionContext,
|
||||
@@ -218,6 +218,15 @@ export class Controller {
|
||||
await this.fetchMcpMarketplace(message.bool)
|
||||
break
|
||||
}
|
||||
|
||||
// telemetry
|
||||
case "telemetrySetting": {
|
||||
if (message.telemetrySetting) {
|
||||
await this.updateTelemetrySetting(message.telemetrySetting)
|
||||
}
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
}
|
||||
case "grpc_request": {
|
||||
if (message.grpc_request) {
|
||||
await handleGrpcRequest(this, message.grpc_request)
|
||||
|
||||
@@ -22,7 +22,7 @@ export async function updateAutoApprovalSettings(controller: Controller, request
|
||||
await updateGlobalState(controller.context, "autoApprovalSettings", settings)
|
||||
|
||||
if (controller.task) {
|
||||
controller.task.updateAutoApprovalSettings(settings)
|
||||
controller.task.autoApprovalSettings = settings
|
||||
}
|
||||
|
||||
await controller.postStateToWebview()
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Controller } from ".."
|
||||
import { Empty } from "../../../shared/proto/common"
|
||||
import { TelemetrySettingRequest } from "../../../shared/proto/state"
|
||||
import { convertProtoTelemetrySettingToDomain } from "../../../shared/proto-conversions/state/telemetry-setting-conversion"
|
||||
|
||||
/**
|
||||
* Updates the telemetry setting
|
||||
* @param controller The controller instance
|
||||
* @param request The telemetry setting request
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function updateTelemetrySetting(controller: Controller, request: TelemetrySettingRequest): Promise<Empty> {
|
||||
const telemetrySetting = convertProtoTelemetrySettingToDomain(request.setting)
|
||||
await controller.updateTelemetrySetting(telemetrySetting)
|
||||
return Empty.create()
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { Controller } from ".."
|
||||
import { DeleteAllTaskHistoryCount } from "../../../shared/proto/task"
|
||||
import { BooleanRequest } from "../../../shared/proto/common"
|
||||
import { getGlobalState, updateGlobalState } from "../../storage/state"
|
||||
import { fileExistsAtPath } from "../../../utils/fs"
|
||||
import vscode from "vscode"
|
||||
@@ -12,7 +13,7 @@ import vscode from "vscode"
|
||||
* @param request Request with option to preserve favorites
|
||||
* @returns Results with count of deleted tasks
|
||||
*/
|
||||
export async function deleteAllTaskHistory(controller: Controller): Promise<DeleteAllTaskHistoryCount> {
|
||||
export async function deleteAllTaskHistory(controller: Controller, request: BooleanRequest): Promise<DeleteAllTaskHistoryCount> {
|
||||
try {
|
||||
// Clear current task first
|
||||
await controller.clearTask()
|
||||
@@ -21,22 +22,8 @@ export async function deleteAllTaskHistory(controller: Controller): Promise<Dele
|
||||
const taskHistory = ((await getGlobalState(controller.context, "taskHistory")) as any[]) || []
|
||||
const totalTasks = taskHistory.length
|
||||
|
||||
const userChoice = await vscode.window.showWarningMessage(
|
||||
"What would you like to delete?",
|
||||
{ modal: true },
|
||||
"Delete All Except Favorites",
|
||||
"Delete Everything",
|
||||
)
|
||||
|
||||
// Default VS Code Cancel button returns `undefined` - don't delete anything
|
||||
if (userChoice === undefined) {
|
||||
return DeleteAllTaskHistoryCount.create({
|
||||
tasksDeleted: 0,
|
||||
})
|
||||
}
|
||||
|
||||
// If preserving favorites, filter out non-favorites
|
||||
if (userChoice === "Delete All Except Favorites") {
|
||||
if (request.value) {
|
||||
const favoritedTasks = taskHistory.filter((task) => task.isFavorited === true)
|
||||
|
||||
// If there are favorited tasks, update state
|
||||
@@ -58,20 +45,9 @@ export async function deleteAllTaskHistory(controller: Controller): Promise<Dele
|
||||
tasksDeleted: totalTasks - favoritedTasks.length,
|
||||
})
|
||||
} else {
|
||||
// No favorited tasks found - show warning and ask user what to do
|
||||
const answer = await vscode.window.showWarningMessage(
|
||||
"No favorited tasks found. Would you like to delete all tasks anyway?",
|
||||
{ modal: true },
|
||||
"Delete All Tasks",
|
||||
)
|
||||
|
||||
// User cancelled - don't delete anything
|
||||
if (answer === undefined) {
|
||||
return DeleteAllTaskHistoryCount.create({
|
||||
tasksDeleted: 0,
|
||||
})
|
||||
}
|
||||
// If user chose "Delete All Tasks", fall through to the `delete everything` section below
|
||||
return DeleteAllTaskHistoryCount.create({
|
||||
tasksDeleted: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { Controller } from ".."
|
||||
import { EmptyRequest } from "../../../shared/proto/common"
|
||||
import { DeleteNonFavoritedTasksResults } from "../../../shared/proto/task"
|
||||
import { getGlobalState, updateGlobalState } from "../../storage/state"
|
||||
import { fileExistsAtPath } from "../../../utils/fs"
|
||||
|
||||
/**
|
||||
* Deletes all non-favorited tasks, preserving only favorited ones
|
||||
* @param controller The controller instance
|
||||
* @param request Empty request
|
||||
* @returns DeleteNonFavoritedTasksResults with counts of preserved and deleted tasks
|
||||
*/
|
||||
export async function deleteNonFavoritedTasks(
|
||||
controller: Controller,
|
||||
_request: EmptyRequest,
|
||||
): Promise<DeleteNonFavoritedTasksResults> {
|
||||
try {
|
||||
// Clear current task first
|
||||
await controller.clearTask()
|
||||
|
||||
// Get existing task history
|
||||
const taskHistory = ((await getGlobalState(controller.context, "taskHistory")) as any[]) || []
|
||||
|
||||
// Filter out non-favorited tasks
|
||||
const favoritedTasks = taskHistory.filter((task) => task.isFavorited === true)
|
||||
const deletedCount = taskHistory.length - favoritedTasks.length
|
||||
|
||||
console.log(`[deleteNonFavoritedTasks] Found ${favoritedTasks.length} favorited tasks to preserve`)
|
||||
|
||||
// Update global state
|
||||
if (favoritedTasks.length > 0) {
|
||||
await updateGlobalState(controller.context, "taskHistory", favoritedTasks)
|
||||
} else {
|
||||
await updateGlobalState(controller.context, "taskHistory", undefined)
|
||||
}
|
||||
|
||||
// Handle file system cleanup for deleted tasks
|
||||
const preserveTaskIds = favoritedTasks.map((task) => task.id)
|
||||
await cleanupTaskFiles(controller, preserveTaskIds)
|
||||
|
||||
// Update webview
|
||||
try {
|
||||
await controller.postStateToWebview()
|
||||
} catch (webviewErr) {
|
||||
console.error("Error posting to webview:", webviewErr)
|
||||
}
|
||||
|
||||
return DeleteNonFavoritedTasksResults.create({
|
||||
tasksPreserved: favoritedTasks.length,
|
||||
tasksDeleted: deletedCount,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error in deleteNonFavoritedTasks:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to cleanup task files while preserving specified tasks
|
||||
*/
|
||||
async function cleanupTaskFiles(controller: Controller, preserveTaskIds: string[]) {
|
||||
const taskDirPath = path.join(controller.context.globalStorageUri.fsPath, "tasks")
|
||||
|
||||
try {
|
||||
if (await fileExistsAtPath(taskDirPath)) {
|
||||
if (preserveTaskIds.length > 0) {
|
||||
const taskDirs = await fs.readdir(taskDirPath)
|
||||
console.debug(`[cleanupTaskFiles] Found ${taskDirs.length} task directories`)
|
||||
|
||||
// Delete only non-preserved task directories
|
||||
for (const dir of taskDirs) {
|
||||
if (!preserveTaskIds.includes(dir)) {
|
||||
await fs.rm(path.join(taskDirPath, dir), { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No tasks to preserve, delete everything
|
||||
await fs.rm(taskDirPath, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error cleaning up task files:", error)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { Controller } from "../index"
|
||||
import type { StringRequest } from "../../../shared/proto/common"
|
||||
import { Empty } from "../../../shared/proto/common"
|
||||
import { openUrlInBrowser } from "../../../utils/github-url-utils"
|
||||
|
||||
/**
|
||||
* Opens a URL in the default browser
|
||||
* @param controller The controller instance
|
||||
* @param request The URL to open
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function openUrl(controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
try {
|
||||
await openUrlInBrowser(request.value)
|
||||
return Empty.create({})
|
||||
} catch (error) {
|
||||
console.error(`Failed to open URL: ${error}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -268,9 +268,7 @@ Usage:
|
||||
</new_task>
|
||||
|
||||
## plan_mode_respond
|
||||
Description: Respond to the user's inquiry in an effort to plan a solution to the user's task. This tool should ONLY be used when you have already explored the relevant files and are ready to present a concrete plan. DO NOT use this tool to announce what files you're going to read - just read them first. This tool is only available in PLAN MODE. The environment_details will specify the current mode, if it is not PLAN_MODE then you should not use this tool. For example, if the user's task is to create a website, you may start by asking some clarifying questions with the ask_followup_question tool if their message was vague, explore the codebase, read files, then present a detailed plan for how you will accomplish the task given the context, and perhaps engage in a back and forth to finalize the details before the user switches you to ACT_MODE to implement the solution.
|
||||
CRITICAL: You must complete your information gathering (reading files, exploring the codebase) BEFORE using this tool. The user expects to see a well thought-out plan based on actual analysis, not intentions.
|
||||
|
||||
Description: Respond to the user's inquiry in an effort to plan a solution to the user's task. This tool should be used when you need to provide a response to a question or statement from the user about how you plan to accomplish the task. This tool is only available in PLAN MODE. The environment_details will specify the current mode, if it is not PLAN MODE then you should not use this tool. Depending on the user's message, you may ask questions to get clarification about the user's request, architect a solution to the task, and to brainstorm ideas with the user. For example, if the user's task is to create a website, you may start by asking some clarifying questions, then present a detailed plan for how you will accomplish the task given the context, and perhaps engage in a back and forth to finalize the details before the user switches you to ACT MODE to implement the solution. IMPORTANT NOTE: You should NOT ask for permission to read files or explore the repo. Just do that proactively. This tool should only be used when you've already gathered enough information to make a plan, or if you have a question for the user.
|
||||
Parameters:
|
||||
- response: (required) The response to provide to the user. Do not try to use tools in this parameter, this is simply a chat response. (You MUST use the response parameter, do not simply place the response text directly within <plan_mode_respond> tags.)
|
||||
Usage:
|
||||
@@ -574,8 +572,8 @@ In each user message, the environment_details will specify the current mode. The
|
||||
## What is PLAN MODE?
|
||||
|
||||
- While you are usually in ACT MODE, the user may switch to PLAN MODE in order to have a back and forth with you to plan how to best accomplish the task.
|
||||
- When starting in PLAN MODE, depending on the user's request, you may need to do some information gathering e.g. using read_file or search_files to get more context about the task. You may also ask the user clarifying questions with ask_followup_question to get a better understanding of the task. You may return mermaid diagrams to visually display your understanding.
|
||||
- Once you've gained more context about the user's request, you should architect a detailed plan for how you will accomplish the task. Returning mermaid diagrams may be helpful here as well. Present the plan to the user using the plan_mode_respond tool.
|
||||
- When starting in PLAN MODE, depending on the user's request, you may need to do some information gathering e.g. using read_file or search_files to get more context about the task. You may also ask the user clarifying questions to get a better understanding of the task. You may return mermaid diagrams to visually display your understanding.
|
||||
- Once you've gained more context about the user's request, you should architect a detailed plan for how you will accomplish the task. Returning mermaid diagrams may be helpful here as well.
|
||||
- Then you might ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and plan the best way to accomplish it.
|
||||
- If at any point a mermaid diagram would make your plan clearer to help the user quickly see the structure, you are encouraged to include a Mermaid code block in the response. (Note: if you use colors in your mermaid diagrams, be sure to use high contrast colors so the text is readable.)
|
||||
- Finally once it seems like you've reached a good plan, ask the user to switch you back to ACT MODE to implement the solution.
|
||||
@@ -623,7 +621,6 @@ RULES
|
||||
- When you want to modify a file, use the replace_in_file or write_to_file tool directly with the desired changes. You do not need to display the changes before using the tool.
|
||||
- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively. When you've completed your task, you must use the attempt_completion tool to present the result to the user. The user may provide feedback, which you can use to make improvements and try again.
|
||||
- You are only allowed to ask the user questions using the ask_followup_question tool. Use this tool only when you need additional details to complete a task, and be sure to use a clear and concise question that will help you move forward with the task. However if you can use the available tools to avoid having to ask the user questions, you should do so. For example, if the user mentions a file that may be in an outside directory like the Desktop, you should use the list_files tool to list the files in the Desktop and check if the file they are talking about is there, rather than asking the user to provide the file path themselves.
|
||||
- When the user is being vague, you should be proactive about asking clarifying questions using the ask_followup_question tool to ensure you understand their request. However, if you can infer the user's intent based on the context and available tools, you should proceed without asking unnecessary questions
|
||||
- When executing commands, if you don't see the expected output, assume the terminal executed the command successfully and proceed with the task. The user's terminal may be unable to stream the output back properly. If you absolutely need to see the actual terminal output, use the ask_followup_question tool to request the user to copy and paste it back to you.
|
||||
- The user may provide a file's contents directly in their message, in which case you shouldn't use the read_file tool to get the file contents again since you already have it.
|
||||
- Your goal is to try to accomplish the user's task, NOT engage in a back and forth conversation.${
|
||||
|
||||
@@ -12,14 +12,14 @@ export const SYSTEM_PROMPT = async (
|
||||
supportsBrowserUse: boolean,
|
||||
mcpHub: McpHub,
|
||||
browserSettings: BrowserSettings,
|
||||
isNextGenModel: boolean = false,
|
||||
isClaude4ModelFamily: boolean = false,
|
||||
) => {
|
||||
|
||||
if (isNextGenModel && USE_EXPERIMENTAL_CLAUDE4_FEATURES) {
|
||||
if (isClaude4ModelFamily && USE_EXPERIMENTAL_CLAUDE4_FEATURES) {
|
||||
return SYSTEM_PROMPT_CLAUDE4_EXPERIMENTAL(cwd, supportsBrowserUse, mcpHub, browserSettings)
|
||||
}
|
||||
|
||||
if (isNextGenModel) {
|
||||
if (isClaude4ModelFamily) {
|
||||
return SYSTEM_PROMPT_CLAUDE4(cwd, supportsBrowserUse, mcpHub, browserSettings)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
import * as vscode from "vscode"
|
||||
import { ensureRulesDirectoryExists } from "./disk"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from "./state"
|
||||
import { GlobalStateKey } from "./state-keys"
|
||||
|
||||
export async function migratePlanActGlobalToWorkspaceStorage(context: vscode.ExtensionContext) {
|
||||
// Keys that were migrated from global storage to workspace storage
|
||||
const keysToMigrate = [
|
||||
// Core settings
|
||||
"apiProvider",
|
||||
"apiModelId",
|
||||
"thinkingBudgetTokens",
|
||||
"reasoningEffort",
|
||||
"chatSettings",
|
||||
"vsCodeLmModelSelector",
|
||||
|
||||
// Provider-specific model keys
|
||||
"awsBedrockCustomSelected",
|
||||
"awsBedrockCustomModelBaseId",
|
||||
"openRouterModelId",
|
||||
"openRouterModelInfo",
|
||||
"openAiModelId",
|
||||
"openAiModelInfo",
|
||||
"ollamaModelId",
|
||||
"lmStudioModelId",
|
||||
"liteLlmModelId",
|
||||
"liteLlmModelInfo",
|
||||
"requestyModelId",
|
||||
"requestyModelInfo",
|
||||
"togetherModelId",
|
||||
"fireworksModelId",
|
||||
|
||||
// Previous mode settings
|
||||
"previousModeApiProvider",
|
||||
"previousModeModelId",
|
||||
"previousModeModelInfo",
|
||||
"previousModeVsCodeLmModelSelector",
|
||||
"previousModeThinkingBudgetTokens",
|
||||
"previousModeReasoningEffort",
|
||||
"previousModeAwsBedrockCustomSelected",
|
||||
"previousModeAwsBedrockCustomModelBaseId",
|
||||
]
|
||||
|
||||
for (const key of keysToMigrate) {
|
||||
const globalValue = await getGlobalState(context, key as GlobalStateKey)
|
||||
if (globalValue !== undefined) {
|
||||
const workspaceValue = await getWorkspaceState(context, key)
|
||||
if (workspaceValue === undefined) {
|
||||
await updateWorkspaceState(context, key, globalValue)
|
||||
}
|
||||
// Delete from global storage regardless of whether we copied it
|
||||
await updateGlobalState(context, key as GlobalStateKey, undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function migrateMcpMarketplaceEnableSetting(mcpMarketplaceEnabledRaw: boolean | undefined): Promise<boolean> {
|
||||
const config = vscode.workspace.getConfiguration("cline")
|
||||
const mcpMarketplaceEnabled = config.get<boolean>("mcpMarketplace.enabled")
|
||||
if (mcpMarketplaceEnabled !== undefined) {
|
||||
// Remove from VSCode configuration
|
||||
await config.update("mcpMarketplace.enabled", undefined, true)
|
||||
|
||||
return !mcpMarketplaceEnabled
|
||||
}
|
||||
return mcpMarketplaceEnabledRaw ?? true
|
||||
}
|
||||
|
||||
export async function migrateEnableCheckpointsSetting(enableCheckpointsSettingRaw: boolean | undefined): Promise<boolean> {
|
||||
const config = vscode.workspace.getConfiguration("cline")
|
||||
const enableCheckpoints = config.get<boolean>("enableCheckpoints")
|
||||
if (enableCheckpoints !== undefined) {
|
||||
// Remove from VSCode configuration
|
||||
await config.update("enableCheckpoints", undefined, true)
|
||||
return enableCheckpoints
|
||||
}
|
||||
return enableCheckpointsSettingRaw ?? true
|
||||
}
|
||||
|
||||
export async function migrateCustomInstructionsToGlobalRules(context: vscode.ExtensionContext) {
|
||||
try {
|
||||
const customInstructions = (await context.globalState.get("customInstructions")) as string | undefined
|
||||
|
||||
if (customInstructions?.trim()) {
|
||||
console.log("Migrating custom instructions to global Cline rules...")
|
||||
|
||||
// Create global .clinerules directory if it doesn't exist
|
||||
const globalRulesDir = await ensureRulesDirectoryExists()
|
||||
|
||||
// Use a fixed filename for custom instructions
|
||||
const migrationFileName = "custom_instructions.md"
|
||||
const migrationFilePath = path.join(globalRulesDir, migrationFileName)
|
||||
|
||||
try {
|
||||
// Check if file already exists to determine if we should append
|
||||
let existingContent = ""
|
||||
try {
|
||||
existingContent = await fs.readFile(migrationFilePath, "utf8")
|
||||
} catch (readError) {
|
||||
// File doesn't exist, which is fine
|
||||
}
|
||||
|
||||
// Append or create the file with custom instructions
|
||||
const contentToWrite = existingContent
|
||||
? `${existingContent}\n\n---\n\n${customInstructions.trim()}`
|
||||
: customInstructions.trim()
|
||||
|
||||
await fs.writeFile(migrationFilePath, contentToWrite)
|
||||
console.log(`Successfully ${existingContent ? "appended to" : "created"} migration file: ${migrationFilePath}`)
|
||||
} catch (fileError) {
|
||||
console.error("Failed to write migration file:", fileError)
|
||||
return
|
||||
}
|
||||
|
||||
// Remove customInstructions from global state only after successful file creation
|
||||
await context.globalState.update("customInstructions", undefined)
|
||||
console.log("Successfully migrated custom instructions to global Cline rules")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to migrate custom instructions to global rules:", error)
|
||||
// Continue execution - migration failure shouldn't break extension startup
|
||||
}
|
||||
}
|
||||
|
||||
export async function migrateModeFromWorkspaceStorageToControllerState(context: vscode.ExtensionContext) {
|
||||
try {
|
||||
// Get current chatSettings from workspace storage
|
||||
const chatSettings = (await getWorkspaceState(context, "chatSettings")) as any
|
||||
|
||||
if (chatSettings && typeof chatSettings === "object" && "mode" in chatSettings) {
|
||||
console.log("Cleaning up mode from workspace storage...")
|
||||
|
||||
// Remove mode property from chatSettings
|
||||
const { mode, ...cleanedChatSettings } = chatSettings
|
||||
|
||||
// Save cleaned chatSettings back to workspace storage
|
||||
await updateWorkspaceState(context, "chatSettings", cleanedChatSettings)
|
||||
|
||||
console.log("Successfully removed mode from workspace storage chatSettings")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to cleanup mode from workspace storage:", error)
|
||||
// Continue execution - migration failure shouldn't break extension startup
|
||||
}
|
||||
}
|
||||
+146
-3
@@ -1,5 +1,5 @@
|
||||
import * as vscode from "vscode"
|
||||
import { DEFAULT_CHAT_SETTINGS } from "@shared/ChatSettings"
|
||||
import { DEFAULT_CHAT_SETTINGS, OpenAIReasoningEffort } from "@shared/ChatSettings"
|
||||
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
|
||||
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
|
||||
import { GlobalStateKey, SecretKey } from "./state-keys"
|
||||
@@ -7,11 +7,13 @@ import { ApiConfiguration, ApiProvider, BedrockModelId, ModelInfo } from "@share
|
||||
import { HistoryItem } from "@shared/HistoryItem"
|
||||
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
|
||||
import { BrowserSettings } from "@shared/BrowserSettings"
|
||||
import { StoredChatSettings } from "@shared/ChatSettings"
|
||||
import { ChatSettings, StoredChatSettings } from "@shared/ChatSettings"
|
||||
import { TelemetrySetting } from "@shared/TelemetrySetting"
|
||||
import { UserInfo } from "@shared/UserInfo"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import { migrateEnableCheckpointsSetting, migrateMcpMarketplaceEnableSetting } from "./state-migrations"
|
||||
import { ensureRulesDirectoryExists } from "./disk"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
/*
|
||||
Storage
|
||||
https://dev.to/kompotkot/how-to-use-secretstorage-in-your-vscode-extensions-2hco
|
||||
@@ -52,6 +54,147 @@ export async function getWorkspaceState(context: vscode.ExtensionContext, key: s
|
||||
return await context.workspaceState.get(key)
|
||||
}
|
||||
|
||||
export async function migratePlanActGlobalToWorkspaceStorage(context: vscode.ExtensionContext) {
|
||||
// Keys that were migrated from global storage to workspace storage
|
||||
const keysToMigrate = [
|
||||
// Core settings
|
||||
"apiProvider",
|
||||
"apiModelId",
|
||||
"thinkingBudgetTokens",
|
||||
"reasoningEffort",
|
||||
"chatSettings",
|
||||
"vsCodeLmModelSelector",
|
||||
|
||||
// Provider-specific model keys
|
||||
"awsBedrockCustomSelected",
|
||||
"awsBedrockCustomModelBaseId",
|
||||
"openRouterModelId",
|
||||
"openRouterModelInfo",
|
||||
"openAiModelId",
|
||||
"openAiModelInfo",
|
||||
"ollamaModelId",
|
||||
"lmStudioModelId",
|
||||
"liteLlmModelId",
|
||||
"liteLlmModelInfo",
|
||||
"requestyModelId",
|
||||
"requestyModelInfo",
|
||||
"togetherModelId",
|
||||
"fireworksModelId",
|
||||
|
||||
// Previous mode settings
|
||||
"previousModeApiProvider",
|
||||
"previousModeModelId",
|
||||
"previousModeModelInfo",
|
||||
"previousModeVsCodeLmModelSelector",
|
||||
"previousModeThinkingBudgetTokens",
|
||||
"previousModeReasoningEffort",
|
||||
"previousModeAwsBedrockCustomSelected",
|
||||
"previousModeAwsBedrockCustomModelBaseId",
|
||||
]
|
||||
|
||||
for (const key of keysToMigrate) {
|
||||
const globalValue = await getGlobalState(context, key as GlobalStateKey)
|
||||
if (globalValue !== undefined) {
|
||||
const workspaceValue = await getWorkspaceState(context, key)
|
||||
if (workspaceValue === undefined) {
|
||||
await updateWorkspaceState(context, key, globalValue)
|
||||
}
|
||||
// Delete from global storage regardless of whether we copied it
|
||||
await updateGlobalState(context, key as GlobalStateKey, undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function migrateMcpMarketplaceEnableSetting(mcpMarketplaceEnabledRaw: boolean | undefined): Promise<boolean> {
|
||||
const config = vscode.workspace.getConfiguration("cline")
|
||||
const mcpMarketplaceEnabled = config.get<boolean>("mcpMarketplace.enabled")
|
||||
if (mcpMarketplaceEnabled !== undefined) {
|
||||
// Remove from VSCode configuration
|
||||
await config.update("mcpMarketplace.enabled", undefined, true)
|
||||
|
||||
return !mcpMarketplaceEnabled
|
||||
}
|
||||
return mcpMarketplaceEnabledRaw ?? true
|
||||
}
|
||||
|
||||
async function migrateEnableCheckpointsSetting(enableCheckpointsSettingRaw: boolean | undefined): Promise<boolean> {
|
||||
const config = vscode.workspace.getConfiguration("cline")
|
||||
const enableCheckpoints = config.get<boolean>("enableCheckpoints")
|
||||
if (enableCheckpoints !== undefined) {
|
||||
// Remove from VSCode configuration
|
||||
await config.update("enableCheckpoints", undefined, true)
|
||||
return enableCheckpoints
|
||||
}
|
||||
return enableCheckpointsSettingRaw ?? true
|
||||
}
|
||||
|
||||
export async function migrateCustomInstructionsToGlobalRules(context: vscode.ExtensionContext) {
|
||||
try {
|
||||
const customInstructions = (await context.globalState.get("customInstructions")) as string | undefined
|
||||
|
||||
if (customInstructions?.trim()) {
|
||||
console.log("Migrating custom instructions to global Cline rules...")
|
||||
|
||||
// Create global .clinerules directory if it doesn't exist
|
||||
const globalRulesDir = await ensureRulesDirectoryExists()
|
||||
|
||||
// Use a fixed filename for custom instructions
|
||||
const migrationFileName = "custom_instructions.md"
|
||||
const migrationFilePath = path.join(globalRulesDir, migrationFileName)
|
||||
|
||||
try {
|
||||
// Check if file already exists to determine if we should append
|
||||
let existingContent = ""
|
||||
try {
|
||||
existingContent = await fs.readFile(migrationFilePath, "utf8")
|
||||
} catch (readError) {
|
||||
// File doesn't exist, which is fine
|
||||
}
|
||||
|
||||
// Append or create the file with custom instructions
|
||||
const contentToWrite = existingContent
|
||||
? `${existingContent}\n\n---\n\n${customInstructions.trim()}`
|
||||
: customInstructions.trim()
|
||||
|
||||
await fs.writeFile(migrationFilePath, contentToWrite)
|
||||
console.log(`Successfully ${existingContent ? "appended to" : "created"} migration file: ${migrationFilePath}`)
|
||||
} catch (fileError) {
|
||||
console.error("Failed to write migration file:", fileError)
|
||||
return
|
||||
}
|
||||
|
||||
// Remove customInstructions from global state only after successful file creation
|
||||
await context.globalState.update("customInstructions", undefined)
|
||||
console.log("Successfully migrated custom instructions to global Cline rules")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to migrate custom instructions to global rules:", error)
|
||||
// Continue execution - migration failure shouldn't break extension startup
|
||||
}
|
||||
}
|
||||
|
||||
export async function cleanupModeFromWorkspaceStorage(context: vscode.ExtensionContext) {
|
||||
try {
|
||||
// Get current chatSettings from workspace storage
|
||||
const chatSettings = (await getWorkspaceState(context, "chatSettings")) as any
|
||||
|
||||
if (chatSettings && typeof chatSettings === "object" && "mode" in chatSettings) {
|
||||
console.log("Cleaning up mode from workspace storage...")
|
||||
|
||||
// Remove mode property from chatSettings
|
||||
const { mode, ...cleanedChatSettings } = chatSettings
|
||||
|
||||
// Save cleaned chatSettings back to workspace storage
|
||||
await updateWorkspaceState(context, "chatSettings", cleanedChatSettings)
|
||||
|
||||
console.log("Successfully removed mode from workspace storage chatSettings")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to cleanup mode from workspace storage:", error)
|
||||
// Continue execution - migration failure shouldn't break extension startup
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
const [
|
||||
isNewUser,
|
||||
|
||||
@@ -11,7 +11,6 @@ import { ClineIgnoreController } from "@core/ignore/ClineIgnoreController"
|
||||
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
|
||||
import { ChatSettings } from "@shared/ChatSettings"
|
||||
import { ToolParamName, ToolUse, ToolUseName } from "../assistant-message"
|
||||
import { AutoApprove } from "./tools/autoApprove"
|
||||
import {
|
||||
BrowserAction,
|
||||
BrowserActionResult,
|
||||
@@ -31,7 +30,7 @@ import { ClineAskResponse } from "@shared/WebviewMessage"
|
||||
import { fixModelHtmlEscaping, removeInvalidChars } from "@utils/string"
|
||||
import { fileExistsAtPath } from "@utils/fs"
|
||||
import { formatResponse } from "../prompts/responses"
|
||||
import { isClaude4ModelFamily, isGemini2dot5ModelFamily } from "@utils/model-utils"
|
||||
import { isClaude4ModelFamily } from "@utils/model-utils"
|
||||
import { ToolResponse, USE_EXPERIMENTAL_CLAUDE4_FEATURES } from "."
|
||||
import { serializeError } from "serialize-error"
|
||||
import * as path from "path"
|
||||
@@ -58,17 +57,6 @@ import { setTimeout as setTimeoutPromise } from "node:timers/promises"
|
||||
import { ChangeLocation, StreamingJsonReplacer } from "../assistant-message/diff-json"
|
||||
|
||||
export class ToolExecutor {
|
||||
private autoApprover: AutoApprove
|
||||
|
||||
// Auto-approval methods using the AutoApprove class
|
||||
private shouldAutoApproveTool(toolName: ToolUseName): boolean | [boolean, boolean] {
|
||||
return this.autoApprover.shouldAutoApproveTool(toolName)
|
||||
}
|
||||
|
||||
private shouldAutoApproveToolWithPath(blockname: ToolUseName, autoApproveActionpath: string | undefined): boolean {
|
||||
return this.autoApprover.shouldAutoApproveToolWithPath(blockname, autoApproveActionpath)
|
||||
}
|
||||
|
||||
constructor(
|
||||
// Core Services & Managers
|
||||
private context: vscode.ExtensionContext,
|
||||
@@ -108,28 +96,21 @@ export class ToolExecutor {
|
||||
private saveCheckpoint: (isAttemptCompletionMessage?: boolean) => Promise<void>,
|
||||
private reinitExistingTaskFromId: (taskId: string) => Promise<void>,
|
||||
private cancelTask: () => Promise<void>,
|
||||
private shouldAutoApproveTool: (toolName: ToolUseName) => boolean | [boolean, boolean],
|
||||
private shouldAutoApproveToolWithPath: (blockname: ToolUseName, autoApproveActionpath: string | undefined) => boolean,
|
||||
private sayAndCreateMissingParamError: (toolName: ToolUseName, paramName: string, relPath?: string) => Promise<any>,
|
||||
private removeLastPartialMessageIfExistsWithType: (type: "ask" | "say", askOrSay: ClineAsk | ClineSay) => Promise<void>,
|
||||
private executeCommandTool: (command: string) => Promise<[boolean, any]>,
|
||||
private doesLatestTaskCompletionHaveNewChanges: () => Promise<boolean>,
|
||||
) {
|
||||
this.autoApprover = new AutoApprove(autoApprovalSettings)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the auto approval settings
|
||||
*/
|
||||
public updateAutoApprovalSettings(settings: AutoApprovalSettings): void {
|
||||
this.autoApprover.updateSettings(settings)
|
||||
}
|
||||
) {}
|
||||
|
||||
private pushToolResult = (content: ToolResponse, block: ToolUse) => {
|
||||
const isNextGenModel = isClaude4ModelFamily(this.api) || isGemini2dot5ModelFamily(this.api)
|
||||
const isClaude4Model = isClaude4ModelFamily(this.api)
|
||||
|
||||
if (typeof content === "string") {
|
||||
const resultText = content || "(tool did not return anything)"
|
||||
|
||||
if (isNextGenModel && USE_EXPERIMENTAL_CLAUDE4_FEATURES) {
|
||||
if (isClaude4Model && USE_EXPERIMENTAL_CLAUDE4_FEATURES) {
|
||||
// Claude 4 family: Use function_results format
|
||||
this.taskState.userMessageContent.push({
|
||||
type: "text",
|
||||
@@ -491,9 +472,9 @@ export class ToolExecutor {
|
||||
|
||||
const currentFullJson = block.params.diff
|
||||
// Check if we should use streaming (e.g., for specific models)
|
||||
const isNextGenModel = isClaude4ModelFamily(this.api) || isGemini2dot5ModelFamily(this.api)
|
||||
const isClaude4Model = isClaude4ModelFamily(this.api)
|
||||
// Going through claude family of models
|
||||
if (isNextGenModel && USE_EXPERIMENTAL_CLAUDE4_FEATURES && currentFullJson) {
|
||||
if (isClaude4Model && USE_EXPERIMENTAL_CLAUDE4_FEATURES && currentFullJson) {
|
||||
const streamingResult = await this.handleStreamingJsonReplacement(block, relPath, currentFullJson)
|
||||
|
||||
if (streamingResult.error) {
|
||||
@@ -858,6 +839,7 @@ export class ToolExecutor {
|
||||
}
|
||||
}
|
||||
case "list_files": {
|
||||
const isClaude4Model = isClaude4ModelFamily(this.api)
|
||||
const relDirPath: string | undefined = block.params.path
|
||||
const recursiveRaw: string | undefined = block.params.recursive
|
||||
const recursive = recursiveRaw?.toLowerCase() === "true"
|
||||
@@ -1007,6 +989,7 @@ export class ToolExecutor {
|
||||
}
|
||||
}
|
||||
case "search_files": {
|
||||
const isClaude4Model = isClaude4ModelFamily(this.api)
|
||||
const relDirPath: string | undefined = block.params.path
|
||||
const regex: string | undefined = block.params.regex
|
||||
const filePattern: string | undefined = block.params.file_pattern
|
||||
|
||||
+71
-13
@@ -90,7 +90,7 @@ 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 { isClaude4ModelFamily, isGemini2dot5ModelFamily } from "@utils/model-utils"
|
||||
import { isClaude4ModelFamily } from "@utils/model-utils"
|
||||
import { MessageStateHandler } from "./message-state"
|
||||
import { formatErrorWithStatusCode, updateApiReqMsg } from "./utils"
|
||||
import { TaskState } from "./TaskState"
|
||||
@@ -314,6 +314,8 @@ export class Task {
|
||||
this.saveCheckpoint.bind(this),
|
||||
this.reinitExistingTaskFromId.bind(this),
|
||||
this.cancelTask.bind(this),
|
||||
this.shouldAutoApproveTool.bind(this),
|
||||
this.shouldAutoApproveToolWithPath.bind(this),
|
||||
this.sayAndCreateMissingParamError.bind(this),
|
||||
this.removeLastPartialMessageIfExistsWithType.bind(this),
|
||||
this.executeCommandTool.bind(this),
|
||||
@@ -331,13 +333,6 @@ export class Task {
|
||||
return context
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the auto approval settings for this task
|
||||
*/
|
||||
public updateAutoApprovalSettings(settings: AutoApprovalSettings): void {
|
||||
this.toolExecutor.updateAutoApprovalSettings(settings)
|
||||
}
|
||||
|
||||
async restoreCheckpoint(messageTs: number, restoreType: ClineCheckpointRestore, offset?: number) {
|
||||
const clineMessages = this.messageStateHandler.getClineMessages()
|
||||
const messageIndex = clineMessages.findIndex((m) => m.ts === messageTs) - (offset || 0)
|
||||
@@ -1559,6 +1554,69 @@ export class Task {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the tool should be auto-approved based on the settings
|
||||
// Returns bool for most tools, and tuple for tools with nested settings
|
||||
shouldAutoApproveTool(toolName: ToolUseName): boolean | [boolean, boolean] {
|
||||
if (this.autoApprovalSettings.enabled) {
|
||||
switch (toolName) {
|
||||
case "read_file":
|
||||
case "list_files":
|
||||
case "list_code_definition_names":
|
||||
case "search_files":
|
||||
return [
|
||||
this.autoApprovalSettings.actions.readFiles,
|
||||
this.autoApprovalSettings.actions.readFilesExternally ?? false,
|
||||
]
|
||||
case "new_rule":
|
||||
case "write_to_file":
|
||||
case "replace_in_file":
|
||||
return [
|
||||
this.autoApprovalSettings.actions.editFiles,
|
||||
this.autoApprovalSettings.actions.editFilesExternally ?? false,
|
||||
]
|
||||
case "execute_command":
|
||||
return [
|
||||
this.autoApprovalSettings.actions.executeSafeCommands ?? false,
|
||||
this.autoApprovalSettings.actions.executeAllCommands ?? false,
|
||||
]
|
||||
case "browser_action":
|
||||
return this.autoApprovalSettings.actions.useBrowser
|
||||
case "web_fetch":
|
||||
return this.autoApprovalSettings.actions.useBrowser
|
||||
case "access_mcp_resource":
|
||||
case "use_mcp_tool":
|
||||
return this.autoApprovalSettings.actions.useMcp
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if the tool should be auto-approved based on the settings
|
||||
// and the path of the action. Returns true if the tool should be auto-approved
|
||||
// based on the user's settings and the path of the action.
|
||||
shouldAutoApproveToolWithPath(blockname: ToolUseName, autoApproveActionpath: string | undefined): boolean {
|
||||
let isLocalRead: boolean = false
|
||||
if (autoApproveActionpath) {
|
||||
const absolutePath = path.resolve(cwd, autoApproveActionpath)
|
||||
isLocalRead = absolutePath.startsWith(cwd)
|
||||
} else {
|
||||
// If we do not get a path for some reason, default to a (safer) false return
|
||||
isLocalRead = false
|
||||
}
|
||||
|
||||
// Get auto-approve settings for local and external edits
|
||||
const autoApproveResult = this.shouldAutoApproveTool(blockname)
|
||||
const [autoApproveLocal, autoApproveExternal] = Array.isArray(autoApproveResult)
|
||||
? autoApproveResult
|
||||
: [autoApproveResult, false]
|
||||
|
||||
if ((isLocalRead && autoApproveLocal) || (!isLocalRead && autoApproveLocal && autoApproveExternal)) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates the disableBrowserTool setting from VSCode configuration to browserSettings
|
||||
*/
|
||||
@@ -1596,8 +1654,8 @@ export class Task {
|
||||
|
||||
const supportsBrowserUse = modelSupportsBrowserUse && !disableBrowserTool // only enable browser use if the model supports it and the user hasn't disabled it
|
||||
|
||||
const isNextGenModel = isClaude4ModelFamily(this.api) || isGemini2dot5ModelFamily(this.api)
|
||||
let systemPrompt = await SYSTEM_PROMPT(cwd, supportsBrowserUse, this.mcpHub, this.browserSettings, isNextGenModel)
|
||||
const isClaude4Model = isClaude4ModelFamily(this.api)
|
||||
let systemPrompt = await SYSTEM_PROMPT(cwd, supportsBrowserUse, this.mcpHub, this.browserSettings, isClaude4Model)
|
||||
|
||||
await this.migratePreferredLanguageToolSetting()
|
||||
const preferredLanguage = getLanguageKey(this.chatSettings.preferredLanguage as LanguageDisplay)
|
||||
@@ -1907,7 +1965,7 @@ export class Task {
|
||||
"mistake_limit_reached",
|
||||
this.api.getModel().id.includes("claude")
|
||||
? `This may indicate a failure in his thought process or inability to use a tool properly, which can be mitigated with some user guidance (e.g. "Try breaking down the task into smaller steps").`
|
||||
: "Cline uses complex prompts and iterative task execution that may be challenging for less capable models. For best results, it's recommended to use Claude 4 Sonnet for its advanced agentic coding capabilities.",
|
||||
: "Cline uses complex prompts and iterative task execution that may be challenging for less capable models. For best results, it's recommended to use Claude 3.7 Sonnet for its advanced agentic coding capabilities.",
|
||||
)
|
||||
if (response === "messageResponse") {
|
||||
// This userContent is for the *next* API call.
|
||||
@@ -2165,8 +2223,8 @@ export class Task {
|
||||
assistantMessage += chunk.text
|
||||
// parse raw assistant message into content blocks
|
||||
const prevLength = this.taskState.assistantMessageContent.length
|
||||
const isNextGenModel = isClaude4ModelFamily(this.api) || isGemini2dot5ModelFamily(this.api)
|
||||
if (isNextGenModel && USE_EXPERIMENTAL_CLAUDE4_FEATURES) {
|
||||
const isClaude4Model = isClaude4ModelFamily(this.api)
|
||||
if (isClaude4Model && USE_EXPERIMENTAL_CLAUDE4_FEATURES) {
|
||||
this.taskState.assistantMessageContent = parseAssistantMessageV3(assistantMessage)
|
||||
} else {
|
||||
this.taskState.assistantMessageContent = parseAssistantMessageV2(assistantMessage)
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
|
||||
import { ToolUseName } from "@core/assistant-message"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import os from "os"
|
||||
|
||||
export const cwd =
|
||||
vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop")
|
||||
|
||||
export class AutoApprove {
|
||||
autoApprovalSettings: AutoApprovalSettings
|
||||
|
||||
constructor(autoApprovalSettings: AutoApprovalSettings) {
|
||||
this.autoApprovalSettings = autoApprovalSettings
|
||||
}
|
||||
|
||||
// Check if the tool should be auto-approved based on the settings
|
||||
// Returns bool for most tools, and tuple for tools with nested settings
|
||||
shouldAutoApproveTool(toolName: ToolUseName): boolean | [boolean, boolean] {
|
||||
if (this.autoApprovalSettings.enabled) {
|
||||
switch (toolName) {
|
||||
case "read_file":
|
||||
case "list_files":
|
||||
case "list_code_definition_names":
|
||||
case "search_files":
|
||||
return [
|
||||
this.autoApprovalSettings.actions.readFiles,
|
||||
this.autoApprovalSettings.actions.readFilesExternally ?? false,
|
||||
]
|
||||
case "new_rule":
|
||||
case "write_to_file":
|
||||
case "replace_in_file":
|
||||
return [
|
||||
this.autoApprovalSettings.actions.editFiles,
|
||||
this.autoApprovalSettings.actions.editFilesExternally ?? false,
|
||||
]
|
||||
case "execute_command":
|
||||
return [
|
||||
this.autoApprovalSettings.actions.executeSafeCommands ?? false,
|
||||
this.autoApprovalSettings.actions.executeAllCommands ?? false,
|
||||
]
|
||||
case "browser_action":
|
||||
return this.autoApprovalSettings.actions.useBrowser
|
||||
case "web_fetch":
|
||||
return this.autoApprovalSettings.actions.useBrowser
|
||||
case "access_mcp_resource":
|
||||
case "use_mcp_tool":
|
||||
return this.autoApprovalSettings.actions.useMcp
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if the tool should be auto-approved based on the settings
|
||||
// and the path of the action. Returns true if the tool should be auto-approved
|
||||
// based on the user's settings and the path of the action.
|
||||
shouldAutoApproveToolWithPath(blockname: ToolUseName, autoApproveActionpath: string | undefined): boolean {
|
||||
let isLocalRead: boolean = false
|
||||
if (autoApproveActionpath) {
|
||||
const absolutePath = path.resolve(cwd, autoApproveActionpath)
|
||||
isLocalRead = absolutePath.startsWith(cwd)
|
||||
} else {
|
||||
// If we do not get a path for some reason, default to a (safer) false return
|
||||
isLocalRead = false
|
||||
}
|
||||
|
||||
// Get auto-approve settings for local and external edits
|
||||
const autoApproveResult = this.shouldAutoApproveTool(blockname)
|
||||
const [autoApproveLocal, autoApproveExternal] = Array.isArray(autoApproveResult)
|
||||
? autoApproveResult
|
||||
: [autoApproveResult, false]
|
||||
|
||||
if ((isLocalRead && autoApproveLocal) || (!isLocalRead && autoApproveLocal && autoApproveExternal)) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
updateSettings(settings: AutoApprovalSettings): void {
|
||||
this.autoApprovalSettings = settings
|
||||
}
|
||||
}
|
||||
+8
-9
@@ -25,8 +25,8 @@ import { sendAccountButtonClickedEvent } from "./core/controller/ui/subscribeToA
|
||||
import {
|
||||
migratePlanActGlobalToWorkspaceStorage,
|
||||
migrateCustomInstructionsToGlobalRules,
|
||||
migrateModeFromWorkspaceStorageToControllerState,
|
||||
} from "./core/storage/state-migrations"
|
||||
cleanupModeFromWorkspaceStorage,
|
||||
} from "./core/storage/state"
|
||||
|
||||
import { sendFocusChatInputEvent } from "./core/controller/ui/subscribeToFocusChatInput"
|
||||
import { FileContextTracker } from "./core/context/context-tracking/FileContextTracker"
|
||||
@@ -34,7 +34,6 @@ import * as hostProviders from "@hosts/host-providers"
|
||||
import { vscodeHostBridgeClient } from "@/hosts/vscode/client/host-grpc-client"
|
||||
import { VscodeWebviewProvider } from "./core/webview/VscodeWebviewProvider"
|
||||
import { ExtensionContext } from "vscode"
|
||||
import { writeTextToClipboard, readTextFromClipboard } from "@/utils/env"
|
||||
|
||||
/*
|
||||
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
|
||||
@@ -65,8 +64,8 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
// Migrate custom instructions to global Cline rules (one-time cleanup)
|
||||
await migrateCustomInstructionsToGlobalRules(context)
|
||||
|
||||
// Migrate mode from workspace storage to controller state (one-time cleanup)
|
||||
await migrateModeFromWorkspaceStorageToControllerState(context)
|
||||
// Clean up mode from workspace storage (one-time cleanup)
|
||||
await cleanupModeFromWorkspaceStorage(context)
|
||||
|
||||
// Clean up orphaned file context warnings (startup cleanup)
|
||||
await FileContextTracker.cleanupOrphanedWarnings(context)
|
||||
@@ -370,17 +369,17 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
}
|
||||
|
||||
// Save current clipboard content
|
||||
const tempCopyBuffer = await readTextFromClipboard()
|
||||
const tempCopyBuffer = await vscode.env.clipboard.readText()
|
||||
|
||||
try {
|
||||
// Copy the *existing* terminal selection (without selecting all)
|
||||
await vscode.commands.executeCommand("workbench.action.terminal.copySelection")
|
||||
|
||||
// Get copied content
|
||||
let terminalContents = (await readTextFromClipboard()).trim()
|
||||
let terminalContents = (await vscode.env.clipboard.readText()).trim()
|
||||
|
||||
// Restore original clipboard content
|
||||
await writeTextToClipboard(tempCopyBuffer)
|
||||
await vscode.env.clipboard.writeText(tempCopyBuffer)
|
||||
|
||||
if (!terminalContents) {
|
||||
// No terminal content was copied (either nothing selected or some error)
|
||||
@@ -406,7 +405,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
await visibleWebview?.controller.addSelectedTerminalOutputToChat(terminalContents, terminal.name)
|
||||
} catch (error) {
|
||||
// Ensure clipboard is restored even if an error occurs
|
||||
await writeTextToClipboard(tempCopyBuffer)
|
||||
await vscode.env.clipboard.writeText(tempCopyBuffer)
|
||||
console.error("Error getting terminal contents:", error)
|
||||
vscode.window.showErrorMessage("Failed to get terminal contents")
|
||||
}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
UriServiceClientInterface,
|
||||
WatchServiceClientInterface,
|
||||
WorkspaceServiceClientInterface,
|
||||
EnvServiceClientInterface,
|
||||
} from "@generated/hosts/host-bridge-client-types"
|
||||
import { UriServiceClientInterface, WatchServiceClientInterface } from "@generated/hosts/host-bridge-client-types"
|
||||
|
||||
/**
|
||||
* Interface for host bridge client providers
|
||||
@@ -11,8 +6,6 @@ import {
|
||||
export interface HostBridgeClientProvider {
|
||||
uriServiceClient: UriServiceClientInterface
|
||||
watchServiceClient: WatchServiceClientInterface
|
||||
workspaceClient: WorkspaceServiceClientInterface
|
||||
envClient: EnvServiceClientInterface
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,4 @@ import * as host from "@shared/proto/index.host"
|
||||
export const vscodeHostBridgeClient: HostBridgeClientProvider = {
|
||||
uriServiceClient: createGrpcClient(host.UriServiceDefinition),
|
||||
watchServiceClient: createGrpcClient(host.WatchServiceDefinition),
|
||||
workspaceClient: createGrpcClient(host.WorkspaceServiceDefinition),
|
||||
envClient: createGrpcClient(host.EnvServiceDefinition),
|
||||
}
|
||||
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
import { EmptyRequest, String } from "@/shared/proto/common"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
export async function clipboardReadText(_: EmptyRequest): Promise<String> {
|
||||
const text = await vscode.env.clipboard.readText()
|
||||
return String.create({ value: text })
|
||||
}
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
import { StringRequest, Empty } from "@/shared/proto/common"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
export async function clipboardWriteText(request: StringRequest): Promise<Empty> {
|
||||
await vscode.env.clipboard.writeText(request.value)
|
||||
return Empty.create({})
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { GetWorkspacePathsRequest, GetWorkspacePathsResponse } from "@/shared/proto/index.host"
|
||||
import * as vscode from "vscode"
|
||||
export async function getWorkspacePaths(_: GetWorkspacePathsRequest): Promise<GetWorkspacePathsResponse> {
|
||||
const paths = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath) ?? []
|
||||
return GetWorkspacePathsResponse.create({ paths: paths })
|
||||
}
|
||||
@@ -112,6 +112,7 @@ function runProcess({ systemPrompt, messages, path, modelId }: ClaudeCodeOptions
|
||||
|
||||
const args = [
|
||||
"-p",
|
||||
JSON.stringify(messages),
|
||||
"--system-prompt",
|
||||
systemPrompt,
|
||||
"--verbose",
|
||||
@@ -128,8 +129,8 @@ function runProcess({ systemPrompt, messages, path, modelId }: ClaudeCodeOptions
|
||||
args.push("--model", modelId)
|
||||
}
|
||||
|
||||
const claudeCodeProcess = execa(claudePath, args, {
|
||||
stdin: "pipe",
|
||||
return execa(claudePath, args, {
|
||||
stdin: "ignore",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
env: {
|
||||
@@ -141,11 +142,6 @@ function runProcess({ systemPrompt, messages, path, modelId }: ClaudeCodeOptions
|
||||
maxBuffer: 1024 * 1024 * 1000,
|
||||
timeout: CLAUDE_CODE_TIMEOUT,
|
||||
})
|
||||
|
||||
claudeCodeProcess.stdin.write(JSON.stringify(messages))
|
||||
claudeCodeProcess.stdin.end()
|
||||
|
||||
return claudeCodeProcess
|
||||
}
|
||||
|
||||
function parseChunk(data: string, processState: ProcessState) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as vscode from "vscode"
|
||||
import { getWorkingState } from "@utils/git"
|
||||
import { writeTextToClipboard } from "@utils/env"
|
||||
|
||||
/**
|
||||
* Formats the git diff into a prompt for the AI
|
||||
@@ -58,7 +57,7 @@ export function extractCommitMessage(aiResponse: string): string {
|
||||
* @param message The commit message to copy
|
||||
*/
|
||||
export async function copyCommitMessageToClipboard(message: string): Promise<void> {
|
||||
await writeTextToClipboard(message)
|
||||
await vscode.env.clipboard.writeText(message)
|
||||
vscode.window.showInformationMessage("Commit message copied to clipboard")
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import * as vscode from "vscode"
|
||||
import fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import sizeOf from "image-size"
|
||||
|
||||
export async function selectImages(): Promise<string[]> {
|
||||
const options: vscode.OpenDialogOptions = {
|
||||
canSelectMany: true,
|
||||
openLabel: "Select",
|
||||
filters: {
|
||||
Images: ["png", "jpg", "jpeg", "webp"], // supported by anthropic and openrouter
|
||||
},
|
||||
}
|
||||
|
||||
const fileUris = await vscode.window.showOpenDialog(options)
|
||||
|
||||
if (!fileUris || fileUris.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const processedImagePromises = fileUris.map(async (uri) => {
|
||||
const imagePath = uri.fsPath
|
||||
let buffer: Buffer
|
||||
try {
|
||||
// Read the file into a buffer first
|
||||
buffer = await fs.readFile(imagePath)
|
||||
// Convert Node.js Buffer to Uint8Array
|
||||
const uint8Array = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength)
|
||||
const dimensions = sizeOf(uint8Array) // Get dimensions from Uint8Array
|
||||
if (dimensions.width! > 7500 || dimensions.height! > 7500) {
|
||||
console.warn(`Image dimensions exceed 7500px, skipping: ${imagePath}`)
|
||||
vscode.window.showErrorMessage(
|
||||
`Image too large: ${path.basename(imagePath)} was skipped (dimensions exceed 7500px).`,
|
||||
)
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error reading file or getting dimensions for ${imagePath}:`, error)
|
||||
vscode.window.showErrorMessage(`Could not read dimensions for ${path.basename(imagePath)}, skipping.`)
|
||||
return null
|
||||
}
|
||||
|
||||
// If dimensions are valid, proceed to convert the existing buffer to base64
|
||||
const base64 = buffer.toString("base64")
|
||||
const mimeType = getMimeType(imagePath)
|
||||
return `data:${mimeType};base64,${base64}`
|
||||
})
|
||||
|
||||
const dataUrlsWithNulls = await Promise.all(processedImagePromises)
|
||||
return dataUrlsWithNulls.filter((url) => url !== null) as string[] // Filter out skipped images
|
||||
}
|
||||
|
||||
function getMimeType(filePath: string): string {
|
||||
const ext = path.extname(filePath).toLowerCase()
|
||||
switch (ext) {
|
||||
case ".png":
|
||||
return "image/png"
|
||||
case ".jpeg":
|
||||
case ".jpg":
|
||||
return "image/jpeg"
|
||||
case ".webp":
|
||||
return "image/webp"
|
||||
default:
|
||||
throw new Error(`Unsupported file type: ${ext}`)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as vscode from "vscode"
|
||||
import { readTextFromClipboard, writeTextToClipboard } from "@utils/env"
|
||||
|
||||
/**
|
||||
* Gets the contents of the active terminal
|
||||
@@ -7,7 +6,7 @@ import { readTextFromClipboard, writeTextToClipboard } from "@utils/env"
|
||||
*/
|
||||
export async function getLatestTerminalOutput(): Promise<string> {
|
||||
// Store original clipboard content to restore later
|
||||
const originalClipboard = await readTextFromClipboard()
|
||||
const originalClipboard = await vscode.env.clipboard.readText()
|
||||
|
||||
try {
|
||||
// Select terminal content
|
||||
@@ -20,7 +19,7 @@ export async function getLatestTerminalOutput(): Promise<string> {
|
||||
await vscode.commands.executeCommand("workbench.action.terminal.clearSelection")
|
||||
|
||||
// Get terminal contents from clipboard
|
||||
let terminalContents = (await readTextFromClipboard()).trim()
|
||||
let terminalContents = (await vscode.env.clipboard.readText()).trim()
|
||||
|
||||
// Check if there's actually a terminal open
|
||||
if (terminalContents === originalClipboard) {
|
||||
@@ -41,6 +40,6 @@ export async function getLatestTerminalOutput(): Promise<string> {
|
||||
return terminalContents
|
||||
} finally {
|
||||
// Restore original clipboard content
|
||||
await writeTextToClipboard(originalClipboard)
|
||||
await vscode.env.clipboard.writeText(originalClipboard)
|
||||
}
|
||||
}
|
||||
|
||||
+4
-139
@@ -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
|
||||
@@ -403,7 +400,7 @@ export const bedrockModels = {
|
||||
|
||||
// OpenRouter
|
||||
// https://openrouter.ai/models?order=newest&supported_parameters=tools
|
||||
export const openRouterDefaultModelId = "anthropic/claude-sonnet-4" // will always exist in openRouterModels
|
||||
export const openRouterDefaultModelId = "anthropic/claude-3.7-sonnet" // will always exist in openRouterModels
|
||||
export const openRouterDefaultModelInfo: ModelInfo = {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
@@ -415,7 +412,7 @@ export const openRouterDefaultModelInfo: ModelInfo = {
|
||||
cacheWritesPrice: 3.75,
|
||||
cacheReadsPrice: 0.3,
|
||||
description:
|
||||
"Claude Sonnet 4 delivers superior intelligence across coding, agentic search, and AI agent capabilities. It's a powerful choice for agentic coding, and can complete tasks across the entire software development lifecycle—from initial planning to bug fixes, maintenance to large refactors. It offers strong performance in both planning and solving for complex coding tasks, making it an ideal choice to power end-to-end software development processes.\n\nRead more in the [blog post here](https://www.anthropic.com/claude/sonnet)",
|
||||
"Claude 3.7 Sonnet is an advanced large language model with improved reasoning, coding, and problem-solving capabilities. It introduces a hybrid reasoning approach, allowing users to choose between rapid responses and extended, step-by-step processing for complex tasks. The model demonstrates notable improvements in coding, particularly in front-end development and full-stack updates, and excels in agentic workflows, where it can autonomously navigate multi-step processes. \n\nClaude 3.7 Sonnet maintains performance parity with its predecessor in standard mode while offering an extended reasoning mode for enhanced accuracy in math, coding, and instruction-following tasks.\n\nRead more at the [blog post here](https://www.anthropic.com/news/claude-3-7-sonnet)",
|
||||
}
|
||||
// Vertex AI
|
||||
// https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude
|
||||
@@ -724,7 +721,7 @@ export const openAiModelInfoSaneDefaults: OpenAiCompatibleModelInfo = {
|
||||
// Gemini
|
||||
// https://ai.google.dev/gemini-api/docs/models/gemini
|
||||
export type GeminiModelId = keyof typeof geminiModels
|
||||
export const geminiDefaultModelId: GeminiModelId = "gemini-2.5-pro"
|
||||
export const geminiDefaultModelId: GeminiModelId = "gemini-2.0-flash-001"
|
||||
export const geminiModels = {
|
||||
"gemini-2.5-pro": {
|
||||
maxTokens: 65536,
|
||||
@@ -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
|
||||
@@ -2011,7 +1876,7 @@ export const liteLlmModelInfoSaneDefaults: LiteLLMModelInfo = {
|
||||
// AskSage Models
|
||||
// https://docs.asksage.ai/
|
||||
export type AskSageModelId = keyof typeof askSageModels
|
||||
export const askSageDefaultModelId: AskSageModelId = "claude-4-sonnet"
|
||||
export const askSageDefaultModelId: AskSageModelId = "claude-35-sonnet"
|
||||
export const askSageDefaultURL: string = "https://api.asksage.ai/server"
|
||||
export const askSageModels = {
|
||||
"gpt-4o": {
|
||||
|
||||
@@ -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,34 +0,0 @@
|
||||
import { TelemetrySettingEnum } from "../../proto/state"
|
||||
import { TelemetrySetting } from "../../TelemetrySetting"
|
||||
|
||||
/**
|
||||
* Converts a domain TelemetrySetting string to a proto TelemetrySettingEnum
|
||||
*/
|
||||
export function convertDomainTelemetrySettingToProto(setting: TelemetrySetting): TelemetrySettingEnum {
|
||||
switch (setting) {
|
||||
case "unset":
|
||||
return TelemetrySettingEnum.UNSET
|
||||
case "enabled":
|
||||
return TelemetrySettingEnum.ENABLED
|
||||
case "disabled":
|
||||
return TelemetrySettingEnum.DISABLED
|
||||
default:
|
||||
return TelemetrySettingEnum.UNSET
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a proto TelemetrySettingEnum to a domain TelemetrySetting string
|
||||
*/
|
||||
export function convertProtoTelemetrySettingToDomain(setting: TelemetrySettingEnum): TelemetrySetting {
|
||||
switch (setting) {
|
||||
case TelemetrySettingEnum.UNSET:
|
||||
return "unset"
|
||||
case TelemetrySettingEnum.ENABLED:
|
||||
return "enabled"
|
||||
case TelemetrySettingEnum.DISABLED:
|
||||
return "disabled"
|
||||
default:
|
||||
return "unset"
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,16 @@
|
||||
import { Channel, createChannel } from "nice-grpc"
|
||||
import {
|
||||
UriServiceClientImpl,
|
||||
WatchServiceClientImpl,
|
||||
WorkspaceServiceClientImpl,
|
||||
EnvServiceClientImpl,
|
||||
} from "@generated/standalone/host-bridge-clients"
|
||||
import {
|
||||
UriServiceClientInterface,
|
||||
WatchServiceClientInterface,
|
||||
WorkspaceServiceClientInterface,
|
||||
EnvServiceClientInterface,
|
||||
} from "@generated/hosts/host-bridge-client-types"
|
||||
import { UriServiceClientImpl, WatchServiceClientImpl } from "@generated/standalone/host-bridge-clients"
|
||||
import { UriServiceClientInterface, WatchServiceClientInterface } from "@generated/hosts/host-bridge-client-types"
|
||||
import { HostBridgeClientProvider } from "@/hosts/host-provider-types"
|
||||
|
||||
/**
|
||||
* Manager to hold the gRPC clients for the host bridge. The clients should be re-used to avoid
|
||||
* Singleton class to hold the gRPC clients for the host bridge. The clients should be re-used to avoid
|
||||
* creating a new TCP connection every time a rpc is made.
|
||||
*/
|
||||
export class ExternalHostBridgeClientManager implements HostBridgeClientProvider {
|
||||
private channel: Channel
|
||||
uriServiceClient: UriServiceClientInterface
|
||||
watchServiceClient: WatchServiceClientInterface
|
||||
workspaceClient: WorkspaceServiceClientInterface
|
||||
envClient: EnvServiceClientInterface
|
||||
|
||||
constructor() {
|
||||
const address = process.env.HOST_BRIDGE_ADDRESS || "localhost:50052"
|
||||
@@ -30,8 +18,6 @@ export class ExternalHostBridgeClientManager implements HostBridgeClientProvider
|
||||
|
||||
this.uriServiceClient = new UriServiceClientImpl(this.channel)
|
||||
this.watchServiceClient = new WatchServiceClientImpl(this.channel)
|
||||
this.workspaceClient = new WorkspaceServiceClientImpl(this.channel)
|
||||
this.envClient = new EnvServiceClientImpl(this.channel)
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { StringRequest, EmptyRequest } from "@/shared/proto/common"
|
||||
|
||||
/**
|
||||
* Writes text to the system clipboard
|
||||
* @param text The text to write to the clipboard
|
||||
* @returns Promise that resolves when the operation is complete
|
||||
* @throws Error if the operation fails
|
||||
*/
|
||||
export async function writeTextToClipboard(text: string): Promise<void> {
|
||||
try {
|
||||
await getHostBridgeProvider().envClient.clipboardWriteText(StringRequest.create({ value: text }))
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
throw new Error(`Failed to write to clipboard: ${errorMessage}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads text from the system clipboard
|
||||
* @returns Promise that resolves to the clipboard text
|
||||
* @throws Error if the operation fails
|
||||
*/
|
||||
export async function readTextFromClipboard(): Promise<string> {
|
||||
try {
|
||||
const response = await getHostBridgeProvider().envClient.clipboardReadText(EmptyRequest.create({}))
|
||||
return response.value
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
throw new Error(`Failed to read from clipboard: ${errorMessage}`)
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ import * as vscode from "vscode"
|
||||
import * as cp from "child_process"
|
||||
import * as os from "os"
|
||||
import * as util from "util"
|
||||
import { writeTextToClipboard } from "@/utils/env"
|
||||
|
||||
/**
|
||||
* Creates a properly encoded GitHub issue URL.
|
||||
@@ -82,7 +81,7 @@ export async function openUrlInBrowser(url: string): Promise<void> {
|
||||
|
||||
// Always copy to clipboard as a fallback
|
||||
try {
|
||||
await writeTextToClipboard(url)
|
||||
await vscode.env.clipboard.writeText(url)
|
||||
console.log("URL copied to clipboard as backup")
|
||||
} catch (error) {
|
||||
console.error(`Failed to copy URL to clipboard: ${error}`)
|
||||
@@ -160,7 +159,7 @@ export async function openUrlInBrowser(url: string): Promise<void> {
|
||||
)
|
||||
.then((selection) => {
|
||||
if (selection === "Copy URL Again") {
|
||||
writeTextToClipboard(url)
|
||||
vscode.env.clipboard.writeText(url)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,13 +3,5 @@ import { ApiHandler } from "@api/index"
|
||||
export function isClaude4ModelFamily(api: ApiHandler): boolean {
|
||||
const model = api.getModel()
|
||||
const modelId = model.id
|
||||
return (
|
||||
modelId.includes("sonnet-4") || modelId.includes("opus-4") || modelId.includes("4-sonnet") || modelId.includes("4-opus")
|
||||
)
|
||||
}
|
||||
|
||||
export function isGemini2dot5ModelFamily(api: ApiHandler): boolean {
|
||||
const model = api.getModel()
|
||||
const modelId = model.id
|
||||
return modelId.includes("gemini-2.5")
|
||||
return modelId.includes("sonnet-4") || modelId.includes("opus-4")
|
||||
}
|
||||
|
||||
@@ -1,514 +0,0 @@
|
||||
const { spawn } = require("child_process")
|
||||
const { EventEmitter } = require("events")
|
||||
const path = require("path")
|
||||
const os = require("os")
|
||||
|
||||
// Enhanced terminal management for standalone Cline
|
||||
// This replaces VSCode's terminal integration with real subprocess management
|
||||
|
||||
class StandaloneTerminalProcess extends EventEmitter {
|
||||
constructor() {
|
||||
super()
|
||||
this.waitForShellIntegration = false // We don't need to wait since we control the process
|
||||
this.isListening = true
|
||||
this.buffer = ""
|
||||
this.fullOutput = ""
|
||||
this.lastRetrievedIndex = 0
|
||||
this.isHot = false
|
||||
this.hotTimer = null
|
||||
this.childProcess = null
|
||||
this.exitCode = null
|
||||
this.isCompleted = false
|
||||
}
|
||||
|
||||
async run(terminal, command) {
|
||||
console.log(`[StandaloneTerminal] Running command: ${command}`)
|
||||
|
||||
// Get shell and working directory from terminal
|
||||
const shell = terminal._shellPath || this.getDefaultShell()
|
||||
const cwd = terminal._cwd || process.cwd()
|
||||
|
||||
// Prepare command for execution
|
||||
const shellArgs = this.getShellArgs(shell, command)
|
||||
|
||||
try {
|
||||
// Spawn the process
|
||||
this.childProcess = spawn(shell, shellArgs, {
|
||||
cwd: cwd,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
env: { ...process.env, TERM: "xterm-256color" },
|
||||
})
|
||||
|
||||
// Track process state
|
||||
let didEmitEmptyLine = false
|
||||
|
||||
// Handle stdout
|
||||
this.childProcess.stdout.on("data", (data) => {
|
||||
const output = data.toString()
|
||||
this.handleOutput(output, didEmitEmptyLine)
|
||||
if (!didEmitEmptyLine && output) {
|
||||
this.emit("line", "") // Signal start of output
|
||||
didEmitEmptyLine = true
|
||||
}
|
||||
})
|
||||
|
||||
// Handle stderr
|
||||
this.childProcess.stderr.on("data", (data) => {
|
||||
const output = data.toString()
|
||||
this.handleOutput(output, didEmitEmptyLine)
|
||||
if (!didEmitEmptyLine && output) {
|
||||
this.emit("line", "")
|
||||
didEmitEmptyLine = true
|
||||
}
|
||||
})
|
||||
|
||||
// Handle process completion
|
||||
this.childProcess.on("close", (code, signal) => {
|
||||
console.log(`[StandaloneTerminal] Process closed with code ${code}, signal ${signal}`)
|
||||
this.exitCode = code
|
||||
this.isCompleted = true
|
||||
this.emitRemainingBuffer()
|
||||
|
||||
// Clear hot timer
|
||||
if (this.hotTimer) {
|
||||
clearTimeout(this.hotTimer)
|
||||
this.isHot = false
|
||||
}
|
||||
|
||||
this.emit("completed")
|
||||
this.emit("continue")
|
||||
})
|
||||
|
||||
// Handle process errors
|
||||
this.childProcess.on("error", (error) => {
|
||||
console.error(`[StandaloneTerminal] Process error:`, error)
|
||||
this.emit("error", error)
|
||||
})
|
||||
|
||||
// Update terminal's process reference
|
||||
terminal._process = this.childProcess
|
||||
terminal._processId = this.childProcess.pid
|
||||
} catch (error) {
|
||||
console.error(`[StandaloneTerminal] Failed to spawn process:`, error)
|
||||
this.emit("error", error)
|
||||
}
|
||||
}
|
||||
|
||||
handleOutput(data, didEmitEmptyLine) {
|
||||
// Set process as hot (actively outputting)
|
||||
this.isHot = true
|
||||
if (this.hotTimer) {
|
||||
clearTimeout(this.hotTimer)
|
||||
}
|
||||
|
||||
// Check for compilation markers to adjust hot timeout
|
||||
const compilingMarkers = ["compiling", "building", "bundling", "transpiling", "generating", "starting"]
|
||||
const markerNullifiers = [
|
||||
"compiled",
|
||||
"success",
|
||||
"finish",
|
||||
"complete",
|
||||
"succeed",
|
||||
"done",
|
||||
"end",
|
||||
"stop",
|
||||
"exit",
|
||||
"terminate",
|
||||
"error",
|
||||
"fail",
|
||||
]
|
||||
|
||||
const isCompiling =
|
||||
compilingMarkers.some((marker) => data.toLowerCase().includes(marker.toLowerCase())) &&
|
||||
!markerNullifiers.some((nullifier) => data.toLowerCase().includes(nullifier.toLowerCase()))
|
||||
|
||||
const hotTimeout = isCompiling ? 15000 : 2000
|
||||
this.hotTimer = setTimeout(() => {
|
||||
this.isHot = false
|
||||
}, hotTimeout)
|
||||
|
||||
// Store full output
|
||||
this.fullOutput += data
|
||||
|
||||
if (this.isListening) {
|
||||
this.emitLines(data)
|
||||
this.lastRetrievedIndex = this.fullOutput.length - this.buffer.length
|
||||
}
|
||||
}
|
||||
|
||||
emitLines(chunk) {
|
||||
this.buffer += chunk
|
||||
let lineEndIndex
|
||||
while ((lineEndIndex = this.buffer.indexOf("\n")) !== -1) {
|
||||
let line = this.buffer.slice(0, lineEndIndex).trimEnd()
|
||||
this.emit("line", line)
|
||||
this.buffer = this.buffer.slice(lineEndIndex + 1)
|
||||
}
|
||||
}
|
||||
|
||||
emitRemainingBuffer() {
|
||||
if (this.buffer && this.isListening) {
|
||||
const remainingBuffer = this.removeLastLineArtifacts(this.buffer)
|
||||
if (remainingBuffer) {
|
||||
this.emit("line", remainingBuffer)
|
||||
}
|
||||
this.buffer = ""
|
||||
this.lastRetrievedIndex = this.fullOutput.length
|
||||
}
|
||||
}
|
||||
|
||||
continue() {
|
||||
this.emitRemainingBuffer()
|
||||
this.isListening = false
|
||||
this.removeAllListeners("line")
|
||||
this.emit("continue")
|
||||
}
|
||||
|
||||
getUnretrievedOutput() {
|
||||
const unretrieved = this.fullOutput.slice(this.lastRetrievedIndex)
|
||||
this.lastRetrievedIndex = this.fullOutput.length
|
||||
return this.removeLastLineArtifacts(unretrieved)
|
||||
}
|
||||
|
||||
removeLastLineArtifacts(output) {
|
||||
const lines = output.trimEnd().split("\n")
|
||||
if (lines.length > 0) {
|
||||
const lastLine = lines[lines.length - 1]
|
||||
lines[lines.length - 1] = lastLine.replace(/[%$#>]\s*$/, "")
|
||||
}
|
||||
return lines.join("\n").trimEnd()
|
||||
}
|
||||
|
||||
getDefaultShell() {
|
||||
if (process.platform === "win32") {
|
||||
return process.env.COMSPEC || "cmd.exe"
|
||||
} else {
|
||||
return process.env.SHELL || "/bin/bash"
|
||||
}
|
||||
}
|
||||
|
||||
getShellArgs(shell, command) {
|
||||
if (process.platform === "win32") {
|
||||
if (shell.toLowerCase().includes("powershell") || shell.toLowerCase().includes("pwsh")) {
|
||||
return ["-Command", command]
|
||||
} else {
|
||||
return ["/c", command]
|
||||
}
|
||||
} else {
|
||||
return ["-c", command]
|
||||
}
|
||||
}
|
||||
|
||||
// Terminate the process if it's still running
|
||||
terminate() {
|
||||
if (this.childProcess && !this.isCompleted) {
|
||||
console.log(`[StandaloneTerminal] Terminating process ${this.childProcess.pid}`)
|
||||
this.childProcess.kill("SIGTERM")
|
||||
|
||||
// Force kill after timeout
|
||||
setTimeout(() => {
|
||||
if (!this.isCompleted) {
|
||||
console.log(`[StandaloneTerminal] Force killing process ${this.childProcess.pid}`)
|
||||
this.childProcess.kill("SIGKILL")
|
||||
}
|
||||
}, 5000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class StandaloneTerminal {
|
||||
constructor(options = {}) {
|
||||
this.name = options.name || `Terminal ${Math.floor(Math.random() * 10000)}`
|
||||
this.processId = Promise.resolve(Math.floor(Math.random() * 100000))
|
||||
this.creationOptions = options
|
||||
this.exitStatus = undefined
|
||||
this.state = { isInteractedWith: false }
|
||||
this._cwd = options.cwd || process.cwd()
|
||||
this._shellPath = options.shellPath
|
||||
this._process = null
|
||||
this._processId = null
|
||||
|
||||
// Mock shell integration for compatibility
|
||||
this.shellIntegration = {
|
||||
cwd: { fsPath: this._cwd },
|
||||
executeCommand: (command) => {
|
||||
// Return a mock execution object that the TerminalProcess expects
|
||||
return {
|
||||
read: async function* () {
|
||||
// This will be handled by our StandaloneTerminalProcess
|
||||
yield ""
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
console.log(`[StandaloneTerminal] Created terminal: ${this.name} in ${this._cwd}`)
|
||||
}
|
||||
|
||||
sendText(text, addNewLine = true) {
|
||||
console.log(`[StandaloneTerminal] sendText: ${text}`)
|
||||
|
||||
// If we have an active process, send input to it
|
||||
if (this._process && !this._process.killed) {
|
||||
try {
|
||||
this._process.stdin.write(text + (addNewLine ? "\n" : ""))
|
||||
} catch (error) {
|
||||
console.error(`[StandaloneTerminal] Error sending text to process:`, error)
|
||||
}
|
||||
} else {
|
||||
// For compatibility with old behavior, we could spawn a new process
|
||||
console.log(`[StandaloneTerminal] No active process to send text to`)
|
||||
}
|
||||
}
|
||||
|
||||
show() {
|
||||
console.log(`[StandaloneTerminal] show: ${this.name}`)
|
||||
this.state.isInteractedWith = true
|
||||
}
|
||||
|
||||
hide() {
|
||||
console.log(`[StandaloneTerminal] hide: ${this.name}`)
|
||||
}
|
||||
|
||||
dispose() {
|
||||
console.log(`[StandaloneTerminal] dispose: ${this.name}`)
|
||||
if (this._process && !this._process.killed) {
|
||||
this._process.kill("SIGTERM")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Terminal registry for tracking terminals
|
||||
class StandaloneTerminalRegistry {
|
||||
constructor() {
|
||||
this.terminals = new Map()
|
||||
this.nextId = 1
|
||||
}
|
||||
|
||||
createTerminal(options = {}) {
|
||||
const terminal = new StandaloneTerminal(options)
|
||||
const id = this.nextId++
|
||||
|
||||
const terminalInfo = {
|
||||
id: id,
|
||||
terminal: terminal,
|
||||
busy: false,
|
||||
lastCommand: "",
|
||||
shellPath: options.shellPath,
|
||||
lastActive: Date.now(),
|
||||
pendingCwdChange: undefined,
|
||||
cwdResolved: undefined,
|
||||
}
|
||||
|
||||
this.terminals.set(id, terminalInfo)
|
||||
console.log(`[StandaloneTerminalRegistry] Created terminal ${id}`)
|
||||
return terminalInfo
|
||||
}
|
||||
|
||||
getTerminal(id) {
|
||||
return this.terminals.get(id)
|
||||
}
|
||||
|
||||
getAllTerminals() {
|
||||
return Array.from(this.terminals.values())
|
||||
}
|
||||
|
||||
removeTerminal(id) {
|
||||
const terminalInfo = this.terminals.get(id)
|
||||
if (terminalInfo) {
|
||||
terminalInfo.terminal.dispose()
|
||||
this.terminals.delete(id)
|
||||
console.log(`[StandaloneTerminalRegistry] Removed terminal ${id}`)
|
||||
}
|
||||
}
|
||||
|
||||
updateTerminal(id, updates) {
|
||||
const terminalInfo = this.terminals.get(id)
|
||||
if (terminalInfo) {
|
||||
Object.assign(terminalInfo, updates)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced terminal manager
|
||||
class StandaloneTerminalManager {
|
||||
constructor() {
|
||||
this.registry = new StandaloneTerminalRegistry()
|
||||
this.processes = new Map()
|
||||
this.terminalIds = new Set()
|
||||
this.shellIntegrationTimeout = 4000
|
||||
this.terminalReuseEnabled = true
|
||||
this.terminalOutputLineLimit = 500
|
||||
this.defaultTerminalProfile = "default"
|
||||
}
|
||||
|
||||
runCommand(terminalInfo, command) {
|
||||
console.log(`[StandaloneTerminalManager] Running command on terminal ${terminalInfo.id}: ${command}`)
|
||||
|
||||
terminalInfo.busy = true
|
||||
terminalInfo.lastCommand = command
|
||||
|
||||
const process = new StandaloneTerminalProcess()
|
||||
this.processes.set(terminalInfo.id, process)
|
||||
|
||||
process.once("completed", () => {
|
||||
terminalInfo.busy = false
|
||||
console.log(`[StandaloneTerminalManager] Command completed on terminal ${terminalInfo.id}`)
|
||||
})
|
||||
|
||||
process.once("error", (error) => {
|
||||
terminalInfo.busy = false
|
||||
console.error(`[StandaloneTerminalManager] Command error on terminal ${terminalInfo.id}:`, error)
|
||||
})
|
||||
|
||||
// Create promise for the process
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
process.once("continue", () => resolve())
|
||||
process.once("error", (error) => reject(error))
|
||||
})
|
||||
|
||||
// Run the command immediately (no shell integration wait needed)
|
||||
process.run(terminalInfo.terminal, command)
|
||||
|
||||
// Return merged promise/process object
|
||||
return this.mergePromise(process, promise)
|
||||
}
|
||||
|
||||
async getOrCreateTerminal(cwd) {
|
||||
const terminals = this.registry.getAllTerminals()
|
||||
|
||||
// Find available terminal with matching CWD
|
||||
const matchingTerminal = terminals.find((t) => {
|
||||
if (t.busy) return false
|
||||
return t.terminal._cwd === cwd
|
||||
})
|
||||
|
||||
if (matchingTerminal) {
|
||||
this.terminalIds.add(matchingTerminal.id)
|
||||
console.log(`[StandaloneTerminalManager] Reusing terminal ${matchingTerminal.id}`)
|
||||
return matchingTerminal
|
||||
}
|
||||
|
||||
// Find any available terminal if reuse is enabled
|
||||
if (this.terminalReuseEnabled) {
|
||||
const availableTerminal = terminals.find((t) => !t.busy)
|
||||
if (availableTerminal) {
|
||||
// Change directory
|
||||
await this.runCommand(availableTerminal, `cd "${cwd}"`)
|
||||
availableTerminal.terminal._cwd = cwd
|
||||
availableTerminal.terminal.shellIntegration.cwd.fsPath = cwd
|
||||
this.terminalIds.add(availableTerminal.id)
|
||||
console.log(`[StandaloneTerminalManager] Reused terminal ${availableTerminal.id} with cd`)
|
||||
return availableTerminal
|
||||
}
|
||||
}
|
||||
|
||||
// Create new terminal
|
||||
const newTerminalInfo = this.registry.createTerminal({
|
||||
cwd: cwd,
|
||||
name: `Cline Terminal ${this.registry.nextId}`,
|
||||
})
|
||||
this.terminalIds.add(newTerminalInfo.id)
|
||||
console.log(`[StandaloneTerminalManager] Created new terminal ${newTerminalInfo.id}`)
|
||||
return newTerminalInfo
|
||||
}
|
||||
|
||||
getTerminals(busy) {
|
||||
return Array.from(this.terminalIds)
|
||||
.map((id) => this.registry.getTerminal(id))
|
||||
.filter((t) => t && t.busy === busy)
|
||||
.map((t) => ({ id: t.id, lastCommand: t.lastCommand }))
|
||||
}
|
||||
|
||||
getUnretrievedOutput(terminalId) {
|
||||
if (!this.terminalIds.has(terminalId)) {
|
||||
return ""
|
||||
}
|
||||
const process = this.processes.get(terminalId)
|
||||
return process ? process.getUnretrievedOutput() : ""
|
||||
}
|
||||
|
||||
isProcessHot(terminalId) {
|
||||
const process = this.processes.get(terminalId)
|
||||
return process ? process.isHot : false
|
||||
}
|
||||
|
||||
processOutput(outputLines) {
|
||||
if (outputLines.length > this.terminalOutputLineLimit) {
|
||||
const halfLimit = Math.floor(this.terminalOutputLineLimit / 2)
|
||||
const start = outputLines.slice(0, halfLimit)
|
||||
const end = outputLines.slice(outputLines.length - halfLimit)
|
||||
return `${start.join("\n")}\n... (output truncated) ...\n${end.join("\n")}`.trim()
|
||||
}
|
||||
return outputLines.join("\n").trim()
|
||||
}
|
||||
|
||||
disposeAll() {
|
||||
// Terminate all processes
|
||||
for (const [terminalId, process] of this.processes) {
|
||||
if (process && process.terminate) {
|
||||
process.terminate()
|
||||
}
|
||||
}
|
||||
|
||||
// Clear all tracking
|
||||
this.terminalIds.clear()
|
||||
this.processes.clear()
|
||||
|
||||
// Dispose all terminals
|
||||
for (const terminalInfo of this.registry.getAllTerminals()) {
|
||||
terminalInfo.terminal.dispose()
|
||||
}
|
||||
|
||||
console.log(`[StandaloneTerminalManager] Disposed all terminals`)
|
||||
}
|
||||
|
||||
// Set shell integration timeout (compatibility method)
|
||||
setShellIntegrationTimeout(timeout) {
|
||||
this.shellIntegrationTimeout = timeout
|
||||
console.log(`[StandaloneTerminalManager] Set shell integration timeout to ${timeout}ms`)
|
||||
}
|
||||
|
||||
// Set terminal reuse enabled (compatibility method)
|
||||
setTerminalReuseEnabled(enabled) {
|
||||
this.terminalReuseEnabled = enabled
|
||||
console.log(`[StandaloneTerminalManager] Set terminal reuse enabled to ${enabled}`)
|
||||
}
|
||||
|
||||
// Set terminal output line limit (compatibility method)
|
||||
setTerminalOutputLineLimit(limit) {
|
||||
this.terminalOutputLineLimit = limit
|
||||
console.log(`[StandaloneTerminalManager] Set terminal output line limit to ${limit}`)
|
||||
}
|
||||
|
||||
// Set default terminal profile (compatibility method)
|
||||
setDefaultTerminalProfile(profile) {
|
||||
this.defaultTerminalProfile = profile
|
||||
console.log(`[StandaloneTerminalManager] Set default terminal profile to ${profile}`)
|
||||
}
|
||||
|
||||
// Helper to merge process and promise (similar to execa)
|
||||
mergePromise(process, promise) {
|
||||
const nativePromisePrototype = (async () => {})().constructor.prototype
|
||||
const descriptors = ["then", "catch", "finally"].map((property) => [
|
||||
property,
|
||||
Reflect.getOwnPropertyDescriptor(nativePromisePrototype, property),
|
||||
])
|
||||
|
||||
for (const [property, descriptor] of descriptors) {
|
||||
if (descriptor) {
|
||||
const value = descriptor.value.bind(promise)
|
||||
Reflect.defineProperty(process, property, { ...descriptor, value })
|
||||
}
|
||||
}
|
||||
|
||||
return process
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
StandaloneTerminal,
|
||||
StandaloneTerminalProcess,
|
||||
StandaloneTerminalRegistry,
|
||||
StandaloneTerminalManager,
|
||||
}
|
||||
@@ -2,19 +2,8 @@ console.log("Loading stub impls...")
|
||||
|
||||
const { createStub } = require("./stub-utils")
|
||||
const open = require("open").default
|
||||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
const { StandaloneTerminalManager } = require("./enhanced-terminal")
|
||||
|
||||
// Import the base vscode object from stubs
|
||||
const vscode = require("./vscode-stubs.js")
|
||||
|
||||
// Create global terminal manager instance
|
||||
const globalTerminalManager = new StandaloneTerminalManager()
|
||||
|
||||
// Extend the existing window object from stubs rather than overwriting it
|
||||
vscode.window = {
|
||||
...vscode.window, // Keep existing properties from stubs
|
||||
showInformationMessage: (...args) => {
|
||||
console.log("Stubbed showInformationMessage:", ...args)
|
||||
return Promise.resolve(undefined)
|
||||
@@ -39,210 +28,9 @@ vscode.window = {
|
||||
console.log("Stubbed showSaveDialog:", options)
|
||||
return undefined
|
||||
},
|
||||
showTextDocument: async (uri, options) => {
|
||||
console.log("Stubbed showTextDocument:", uri, options)
|
||||
|
||||
// Extract file path from URI
|
||||
let filePath = uri.path || uri.fsPath || uri
|
||||
if (typeof filePath !== "string") {
|
||||
filePath = uri.toString()
|
||||
}
|
||||
|
||||
// Remove file:// prefix if present
|
||||
if (filePath.startsWith("file://")) {
|
||||
filePath = filePath.substring(7)
|
||||
}
|
||||
|
||||
// Create a function that always reads the current file content
|
||||
const getCurrentFileContent = async () => {
|
||||
try {
|
||||
const content = await fs.promises.readFile(filePath, "utf8")
|
||||
console.log(`getCurrentFileContent: Read file ${filePath} (${content.length} chars)`)
|
||||
return content
|
||||
} catch (error) {
|
||||
console.log(`getCurrentFileContent: Could not read file ${filePath}:`, error.message)
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Try to read the initial file content
|
||||
let fileContent = await getCurrentFileContent()
|
||||
let lineCount = fileContent.split("\n").length
|
||||
|
||||
// Check if we already have an active editor for this file path
|
||||
const existingEditor = vscode.window._documentEditors && vscode.window._documentEditors[filePath]
|
||||
if (existingEditor) {
|
||||
console.log(`showTextDocument: Updating existing editor for ${filePath}`)
|
||||
// Update the existing editor's content
|
||||
fileContent = await getCurrentFileContent()
|
||||
lineCount = fileContent.split("\n").length
|
||||
|
||||
// Update the document's getText method to return current content
|
||||
existingEditor.document.getText = (range) => {
|
||||
// Always read fresh content for getText calls
|
||||
const currentContent = require("fs").readFileSync(filePath, "utf8")
|
||||
if (!range) {
|
||||
return currentContent
|
||||
}
|
||||
// Handle range-based getText with current content
|
||||
const lines = currentContent.split("\n")
|
||||
const startLine = Math.max(0, range.start.line)
|
||||
const endLine = Math.min(lines.length - 1, range.end.line)
|
||||
|
||||
if (startLine === endLine) {
|
||||
// Single line
|
||||
const line = lines[startLine] || ""
|
||||
const startChar = Math.max(0, range.start.character)
|
||||
const endChar = Math.min(line.length, range.end.character)
|
||||
return line.substring(startChar, endChar)
|
||||
} else {
|
||||
// Multiple lines
|
||||
const result = []
|
||||
for (let i = startLine; i <= endLine; i++) {
|
||||
const line = lines[i] || ""
|
||||
if (i === startLine) {
|
||||
result.push(line.substring(range.start.character))
|
||||
} else if (i === endLine) {
|
||||
result.push(line.substring(0, range.end.character))
|
||||
} else {
|
||||
result.push(line)
|
||||
}
|
||||
}
|
||||
return result.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Update other properties
|
||||
existingEditor.document.lineCount = lineCount
|
||||
existingEditor.document.fileName = filePath
|
||||
|
||||
// Update the active text editor reference
|
||||
vscode.window.activeTextEditor = existingEditor
|
||||
|
||||
return existingEditor
|
||||
}
|
||||
|
||||
// Create a new mock text editor that always reads current file content
|
||||
const mockEditor = {
|
||||
document: {
|
||||
uri: uri,
|
||||
fileName: filePath,
|
||||
isDirty: false,
|
||||
lineCount: lineCount,
|
||||
getText: (range) => {
|
||||
// Always read fresh content for getText calls
|
||||
try {
|
||||
const currentContent = require("fs").readFileSync(filePath, "utf8")
|
||||
console.log(`document.getText: Read fresh content (${currentContent.length} chars)`)
|
||||
if (!range) {
|
||||
return currentContent
|
||||
}
|
||||
// Handle range-based getText with current content
|
||||
const lines = currentContent.split("\n")
|
||||
const startLine = Math.max(0, range.start.line)
|
||||
const endLine = Math.min(lines.length - 1, range.end.line)
|
||||
|
||||
if (startLine === endLine) {
|
||||
// Single line
|
||||
const line = lines[startLine] || ""
|
||||
const startChar = Math.max(0, range.start.character)
|
||||
const endChar = Math.min(line.length, range.end.character)
|
||||
return line.substring(startChar, endChar)
|
||||
} else {
|
||||
// Multiple lines
|
||||
const result = []
|
||||
for (let i = startLine; i <= endLine; i++) {
|
||||
const line = lines[i] || ""
|
||||
if (i === startLine) {
|
||||
result.push(line.substring(range.start.character))
|
||||
} else if (i === endLine) {
|
||||
result.push(line.substring(0, range.end.character))
|
||||
} else {
|
||||
result.push(line)
|
||||
}
|
||||
}
|
||||
return result.join("\n")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error reading file in getText: ${error.message}`)
|
||||
return ""
|
||||
}
|
||||
},
|
||||
save: async () => {
|
||||
console.log("Called mock textDocument.save")
|
||||
return true
|
||||
},
|
||||
positionAt: (offset) => {
|
||||
try {
|
||||
const currentContent = require("fs").readFileSync(filePath, "utf8")
|
||||
const lines = currentContent.split("\n")
|
||||
let currentOffset = 0
|
||||
for (let line = 0; line < lines.length; line++) {
|
||||
const lineLength = lines[line].length + 1 // +1 for newline
|
||||
if (currentOffset + lineLength > offset) {
|
||||
return { line: line, character: offset - currentOffset }
|
||||
}
|
||||
currentOffset += lineLength
|
||||
}
|
||||
return { line: lines.length - 1, character: lines[lines.length - 1]?.length || 0 }
|
||||
} catch (error) {
|
||||
return { line: 0, character: 0 }
|
||||
}
|
||||
},
|
||||
offsetAt: (position) => {
|
||||
try {
|
||||
const currentContent = require("fs").readFileSync(filePath, "utf8")
|
||||
const lines = currentContent.split("\n")
|
||||
let offset = 0
|
||||
for (let i = 0; i < position.line && i < lines.length; i++) {
|
||||
offset += lines[i].length + 1 // +1 for newline
|
||||
}
|
||||
offset += Math.min(position.character, lines[position.line]?.length || 0)
|
||||
return offset
|
||||
} catch (error) {
|
||||
return 0
|
||||
}
|
||||
},
|
||||
},
|
||||
selection: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
|
||||
selections: [],
|
||||
visibleRanges: [{ start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }],
|
||||
options: {},
|
||||
viewColumn: 1,
|
||||
edit: async (callback) => {
|
||||
console.log("Called mock textEditor.edit")
|
||||
return true
|
||||
},
|
||||
insertSnippet: async () => true,
|
||||
setDecorations: () => {},
|
||||
revealRange: () => {},
|
||||
show: () => {},
|
||||
hide: () => {},
|
||||
}
|
||||
|
||||
// Store the editor by file path for future reference
|
||||
if (!vscode.window._documentEditors) {
|
||||
vscode.window._documentEditors = {}
|
||||
}
|
||||
vscode.window._documentEditors[filePath] = mockEditor
|
||||
|
||||
// Update the active text editor
|
||||
vscode.window.activeTextEditor = mockEditor
|
||||
|
||||
// Trigger onDidChangeActiveTextEditor listeners
|
||||
if (vscode.window._activeTextEditorListeners) {
|
||||
setTimeout(() => {
|
||||
vscode.window._activeTextEditorListeners.forEach((listener) => {
|
||||
try {
|
||||
listener(mockEditor)
|
||||
} catch (error) {
|
||||
console.error("Error calling onDidChangeActiveTextEditor listener:", error)
|
||||
}
|
||||
})
|
||||
}, 10) // Small delay to simulate async behavior
|
||||
}
|
||||
|
||||
return mockEditor
|
||||
showTextDocument: async (...args) => {
|
||||
console.log("Stubbed showTextDocument:", ...args)
|
||||
return {}
|
||||
},
|
||||
createOutputChannel: (name) => {
|
||||
console.log("Stubbed createOutputChannel:", name)
|
||||
@@ -253,60 +41,19 @@ vscode.window = {
|
||||
}
|
||||
},
|
||||
createTerminal: (...args) => {
|
||||
console.log("Enhanced createTerminal:", ...args)
|
||||
|
||||
// Extract options from arguments
|
||||
let options = {}
|
||||
if (args.length > 0) {
|
||||
if (typeof args[0] === "string") {
|
||||
// Called with (name, shellPath, shellArgs)
|
||||
options = {
|
||||
name: args[0],
|
||||
shellPath: args[1],
|
||||
shellArgs: args[2],
|
||||
}
|
||||
} else if (typeof args[0] === "object") {
|
||||
// Called with options object
|
||||
options = args[0]
|
||||
}
|
||||
console.log("Stubbed createTerminal:", ...args)
|
||||
return {
|
||||
sendText: console.log,
|
||||
show: () => {},
|
||||
dispose: () => {},
|
||||
}
|
||||
|
||||
// Use our enhanced terminal manager to create a terminal
|
||||
const terminalInfo = globalTerminalManager.registry.createTerminal({
|
||||
name: options.name || `Terminal ${Date.now()}`,
|
||||
cwd: options.cwd || process.cwd(),
|
||||
shellPath: options.shellPath,
|
||||
})
|
||||
|
||||
// Store reference for tracking
|
||||
vscode.window.terminals.push(terminalInfo.terminal)
|
||||
if (!vscode.window.activeTerminal) {
|
||||
vscode.window.activeTerminal = terminalInfo.terminal
|
||||
}
|
||||
|
||||
console.log(`Enhanced terminal created: ${terminalInfo.id}`)
|
||||
return terminalInfo.terminal
|
||||
},
|
||||
activeTextEditor: undefined,
|
||||
visibleTextEditors: [],
|
||||
tabGroups: {
|
||||
all: [
|
||||
{
|
||||
tabs: [],
|
||||
isActive: true,
|
||||
viewColumn: 1,
|
||||
},
|
||||
],
|
||||
activeTabGroup: {
|
||||
tabs: [],
|
||||
isActive: true,
|
||||
viewColumn: 1,
|
||||
},
|
||||
close: async (tab) => {
|
||||
console.log("Stubbed tabGroups.close:", tab)
|
||||
return true
|
||||
},
|
||||
onDidChangeTabs: createStub("vscode.window.tabGroups.onDidChangeTabs"),
|
||||
all: [],
|
||||
close: async () => {},
|
||||
onDidChangeTabs: createStub("vscode.env.tabGroups.onDidChangeTabs"),
|
||||
},
|
||||
withProgress: async (_options, task) => {
|
||||
console.log("Stubbed withProgress")
|
||||
@@ -314,56 +61,14 @@ vscode.window = {
|
||||
},
|
||||
registerUriHandler: () => ({ dispose: () => {} }),
|
||||
registerWebviewViewProvider: () => ({ dispose: () => {} }),
|
||||
onDidChangeActiveTextEditor: (listener) => {
|
||||
console.log("Called vscode.window.onDidChangeActiveTextEditor")
|
||||
// Store the listener so we can call it when showTextDocument is called
|
||||
vscode.window._activeTextEditorListeners = vscode.window._activeTextEditorListeners || []
|
||||
vscode.window._activeTextEditorListeners.push(listener)
|
||||
return {
|
||||
dispose: () => {
|
||||
console.log("Disposed onDidChangeActiveTextEditor listener")
|
||||
const index = vscode.window._activeTextEditorListeners.indexOf(listener)
|
||||
if (index > -1) {
|
||||
vscode.window._activeTextEditorListeners.splice(index, 1)
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
onDidChangeActiveTextEditor: () => ({ dispose: () => {} }),
|
||||
createTextEditorDecorationType: () => ({ dispose: () => {} }),
|
||||
createWebviewPanel: (...args) => {
|
||||
console.log("Stubbed createWebviewPanel:", ...args)
|
||||
return {
|
||||
webview: {},
|
||||
reveal: () => {},
|
||||
dispose: () => {},
|
||||
}
|
||||
createWebviewPanel: (..._args) => {
|
||||
throw new Error("WebviewPanel is not supported in standalone app.")
|
||||
},
|
||||
onDidChangeTerminalState: (listener) => {
|
||||
console.log("Called vscode.window.onDidChangeTerminalState")
|
||||
return {
|
||||
dispose: () => {
|
||||
console.log("Disposed onDidChangeTerminalState listener")
|
||||
},
|
||||
}
|
||||
},
|
||||
onDidChangeTextEditorVisibleRanges: (listener) => {
|
||||
console.log("Called vscode.window.onDidChangeTextEditorVisibleRanges")
|
||||
return {
|
||||
dispose: () => {
|
||||
console.log("Disposed onDidChangeTextEditorVisibleRanges listener")
|
||||
},
|
||||
}
|
||||
},
|
||||
terminals: [],
|
||||
activeTerminal: null,
|
||||
}
|
||||
|
||||
// Initialize env object if it doesn't exist, then extend it
|
||||
if (!vscode.env) {
|
||||
vscode.env = {}
|
||||
}
|
||||
|
||||
Object.assign(vscode.env, {
|
||||
vscode.env = {
|
||||
uriScheme: "vscode",
|
||||
appName: "Visual Studio Code",
|
||||
appRoot: "/tmp/vscode/appRoot",
|
||||
@@ -373,26 +78,17 @@ Object.assign(vscode.env, {
|
||||
sessionId: "stub-session-id",
|
||||
shell: "/bin/bash",
|
||||
|
||||
// Add the stub functions that were missing
|
||||
clipboard: createStub("vscode.env.clipboard"),
|
||||
openExternal: createStub("vscode.env.openExternal"),
|
||||
getQueryParameter: createStub("vscode.env.getQueryParameter"),
|
||||
onDidChangeTelemetryEnabled: createStub("vscode.env.onDidChangeTelemetryEnabled"),
|
||||
isTelemetryEnabled: createStub("vscode.env.isTelemetryEnabled"),
|
||||
telemetryConfiguration: createStub("vscode.env.telemetryConfiguration"),
|
||||
onDidChangeTelemetryConfiguration: createStub("vscode.env.onDidChangeTelemetryConfiguration"),
|
||||
createTelemetryLogger: createStub("vscode.env.createTelemetryLogger"),
|
||||
})
|
||||
|
||||
// Override the openExternal function with actual implementation
|
||||
vscode.env.openExternal = async (uri) => {
|
||||
const url = typeof uri === "string" ? uri : (uri.toString?.() ?? "")
|
||||
console.log("Opening browser:", url)
|
||||
await open(url)
|
||||
return true
|
||||
}
|
||||
|
||||
// Extend Uri object with improved implementations
|
||||
Object.assign(vscode.Uri, {
|
||||
vscode.Uri = {
|
||||
parse: (uriString) => {
|
||||
const url = new URL(uriString)
|
||||
return {
|
||||
@@ -437,405 +133,13 @@ Object.assign(vscode.Uri, {
|
||||
const joined = segments.map((s) => (typeof s === "string" ? s : s.path)).join("/")
|
||||
return vscode.Uri.file("/" + joined.replace(/\/+/g, "/"))
|
||||
},
|
||||
})
|
||||
|
||||
// Extend workspace object with file system operations
|
||||
Object.assign(vscode.workspace, {
|
||||
fs: {
|
||||
readFile: async function (uri) {
|
||||
console.log(`Called vscode.workspace.fs.readFile with uri:`, uri)
|
||||
try {
|
||||
// Extract file path from URI
|
||||
let filePath = uri.path || uri.fsPath || uri
|
||||
if (typeof filePath !== "string") {
|
||||
filePath = uri.toString()
|
||||
}
|
||||
|
||||
// Remove file:// prefix if present
|
||||
if (filePath.startsWith("file://")) {
|
||||
filePath = filePath.substring(7)
|
||||
}
|
||||
|
||||
console.log(`Reading file: ${filePath}`)
|
||||
const content = await fs.promises.readFile(filePath, "utf8")
|
||||
console.log(
|
||||
`File content read (${content.length} chars):`,
|
||||
content.substring(0, 100) + (content.length > 100 ? "..." : ""),
|
||||
)
|
||||
return new Uint8Array(Buffer.from(content, "utf8"))
|
||||
} catch (error) {
|
||||
console.error(`Error reading file:`, error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
writeFile: async function (uri, content) {
|
||||
console.log(`Called vscode.workspace.fs.writeFile with uri:`, uri)
|
||||
try {
|
||||
// Extract file path from URI
|
||||
let filePath = uri.path || uri.fsPath || uri
|
||||
if (typeof filePath !== "string") {
|
||||
filePath = uri.toString()
|
||||
}
|
||||
|
||||
// Remove file:// prefix if present
|
||||
if (filePath.startsWith("file://")) {
|
||||
filePath = filePath.substring(7)
|
||||
}
|
||||
|
||||
console.log(`Writing file: ${filePath}`)
|
||||
|
||||
// Ensure directory exists
|
||||
const dir = path.dirname(filePath)
|
||||
await fs.promises.mkdir(dir, { recursive: true })
|
||||
|
||||
// Write the file
|
||||
await fs.promises.writeFile(filePath, content)
|
||||
} catch (error) {
|
||||
console.error(`Error writing file:`, error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// Add workspace folder configuration
|
||||
rootPath: process.cwd(),
|
||||
workspaceFolders: [
|
||||
{
|
||||
uri: vscode.Uri.file(process.cwd()),
|
||||
name: path.basename(process.cwd()),
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
name: path.basename(process.cwd()),
|
||||
workspaceFile: vscode.Uri.file(path.join(process.cwd(), ".vscode", "workspace.json")),
|
||||
|
||||
// Add other workspace methods as stubs
|
||||
getConfiguration: () => ({
|
||||
get: () => undefined,
|
||||
update: () => Promise.resolve(),
|
||||
has: () => false,
|
||||
}),
|
||||
getWorkspaceFolder: (uri) => {
|
||||
console.log("Called vscode.workspace.getWorkspaceFolder with:", uri)
|
||||
// Return the first workspace folder for any URI in standalone mode
|
||||
if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) {
|
||||
return vscode.workspace.workspaceFolders[0]
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
createFileSystemWatcher: () => ({
|
||||
onDidChange: () => ({ dispose: () => {} }),
|
||||
onDidCreate: () => ({ dispose: () => {} }),
|
||||
onDidDelete: () => ({ dispose: () => {} }),
|
||||
dispose: () => {},
|
||||
}),
|
||||
onDidChangeConfiguration: () => ({ dispose: () => {} }),
|
||||
onDidChangeWorkspaceFolders: () => ({ dispose: () => {} }),
|
||||
onDidCreateFiles: createStub("vscode.workspace.onDidCreateFiles"),
|
||||
onDidDeleteFiles: createStub("vscode.workspace.onDidDeleteFiles"),
|
||||
onDidRenameFiles: createStub("vscode.workspace.onDidRenameFiles"),
|
||||
onWillCreateFiles: createStub("vscode.workspace.onWillCreateFiles"),
|
||||
onWillDeleteFiles: createStub("vscode.workspace.onWillDeleteFiles"),
|
||||
onWillRenameFiles: createStub("vscode.workspace.onWillRenameFiles"),
|
||||
textDocuments: {
|
||||
find: (predicate) => {
|
||||
console.log("Called vscode.workspace.textDocuments.find")
|
||||
// Return a mock text document that behaves like VSCode expects
|
||||
return {
|
||||
uri: { fsPath: "/tmp/mock-document" },
|
||||
fileName: "/tmp/mock-document",
|
||||
isDirty: false,
|
||||
save: async () => {
|
||||
console.log("Called mock textDocument.save")
|
||||
return true
|
||||
},
|
||||
getText: () => "",
|
||||
lineCount: 0,
|
||||
}
|
||||
},
|
||||
forEach: (callback) => {
|
||||
console.log("Called vscode.workspace.textDocuments.forEach")
|
||||
// No documents to iterate over in standalone mode
|
||||
},
|
||||
length: 0,
|
||||
[Symbol.iterator]: function* () {
|
||||
// Empty iterator for standalone mode
|
||||
},
|
||||
},
|
||||
|
||||
// Add the crucial applyEdit method
|
||||
applyEdit: async (workspaceEdit) => {
|
||||
console.log("Called vscode.workspace.applyEdit", workspaceEdit)
|
||||
|
||||
// For standalone mode, we'll simulate applying the edit by actually writing to files
|
||||
try {
|
||||
// WorkspaceEdit can contain multiple types of edits
|
||||
if (workspaceEdit._edits) {
|
||||
for (const edit of workspaceEdit._edits) {
|
||||
if (edit._type === 1) {
|
||||
// TextEdit
|
||||
const uri = edit._uri
|
||||
const edits = edit._edits
|
||||
|
||||
let filePath = uri.path || uri.fsPath
|
||||
if (filePath.startsWith("file://")) {
|
||||
filePath = filePath.substring(7)
|
||||
}
|
||||
|
||||
console.log(`Applying text edits to: ${filePath}`)
|
||||
|
||||
// Read current content if file exists
|
||||
let currentContent = ""
|
||||
try {
|
||||
currentContent = await fs.promises.readFile(filePath, "utf8")
|
||||
} catch (e) {
|
||||
// File doesn't exist, start with empty content
|
||||
console.log(`File ${filePath} doesn't exist, starting with empty content`)
|
||||
}
|
||||
|
||||
// Apply edits in reverse order (from end to beginning) to maintain positions
|
||||
const sortedEdits = edits.sort((a, b) => {
|
||||
const aStart = a.range.start.line * 1000000 + a.range.start.character
|
||||
const bStart = b.range.start.line * 1000000 + b.range.start.character
|
||||
return bStart - aStart
|
||||
})
|
||||
|
||||
let lines = currentContent.split("\n")
|
||||
|
||||
for (const edit of sortedEdits) {
|
||||
const startLine = edit.range.start.line
|
||||
const startChar = edit.range.start.character
|
||||
const endLine = edit.range.end.line
|
||||
const endChar = edit.range.end.character
|
||||
const newText = edit.newText
|
||||
|
||||
console.log(`Applying edit: ${startLine}:${startChar} - ${endLine}:${endChar} -> "${newText}"`)
|
||||
|
||||
// Handle the edit
|
||||
if (startLine === endLine) {
|
||||
// Single line edit
|
||||
const line = lines[startLine] || ""
|
||||
lines[startLine] = line.substring(0, startChar) + newText + line.substring(endChar)
|
||||
} else {
|
||||
// Multi-line edit
|
||||
const firstLine = lines[startLine] || ""
|
||||
const lastLine = lines[endLine] || ""
|
||||
const newFirstLine = firstLine.substring(0, startChar) + newText + lastLine.substring(endChar)
|
||||
|
||||
// Replace the range with the new content
|
||||
lines.splice(startLine, endLine - startLine + 1, newFirstLine)
|
||||
}
|
||||
}
|
||||
|
||||
const newContent = lines.join("\n")
|
||||
|
||||
// Ensure directory exists
|
||||
const dir = path.dirname(filePath)
|
||||
await fs.promises.mkdir(dir, { recursive: true })
|
||||
|
||||
// Write the updated content
|
||||
await fs.promises.writeFile(filePath, newContent, "utf8")
|
||||
console.log(`Successfully applied edits to: ${filePath}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error("Error applying workspace edit:", error)
|
||||
return false
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Fix CodeActionKind to have static properties instead of being a class
|
||||
vscode.CodeActionKind = {
|
||||
Empty: "",
|
||||
QuickFix: "quickfix",
|
||||
Refactor: "refactor",
|
||||
RefactorExtract: "refactor.extract",
|
||||
RefactorInline: "refactor.inline",
|
||||
RefactorRewrite: "refactor.rewrite",
|
||||
Source: "source",
|
||||
SourceOrganizeImports: "source.organizeImports",
|
||||
SourceFixAll: "source.fixAll",
|
||||
}
|
||||
|
||||
// Add missing commands implementation
|
||||
if (!vscode.commands) {
|
||||
vscode.commands = {}
|
||||
vscode.env.openExternal = async (uri) => {
|
||||
const url = typeof uri === "string" ? uri : (uri.toString?.() ?? "")
|
||||
console.log("Opening browser:", url)
|
||||
await open(url)
|
||||
return true
|
||||
}
|
||||
|
||||
Object.assign(vscode.commands, {
|
||||
executeCommand: async (command, ...args) => {
|
||||
console.log(`Called vscode.commands.executeCommand: ${command}`, args)
|
||||
|
||||
// Handle the vscode.diff command specifically
|
||||
if (command === "vscode.diff") {
|
||||
const [originalUri, modifiedUri, title, options] = args
|
||||
console.log("Opening diff view:", { originalUri, modifiedUri, title })
|
||||
|
||||
// For standalone mode, just open the modified file directly
|
||||
// since we can't show a proper diff view
|
||||
const editor = await vscode.window.showTextDocument(modifiedUri, {
|
||||
preserveFocus: options?.preserveFocus || false,
|
||||
preview: false,
|
||||
})
|
||||
|
||||
// Ensure the onDidChangeActiveTextEditor event fires with a slight delay
|
||||
// This is crucial for DiffViewProvider.openDiffEditor() to work properly
|
||||
setTimeout(() => {
|
||||
if (vscode.window._activeTextEditorListeners) {
|
||||
vscode.window._activeTextEditorListeners.forEach((listener) => {
|
||||
try {
|
||||
listener(editor)
|
||||
} catch (error) {
|
||||
console.error("Error calling onDidChangeActiveTextEditor listener in vscode.diff:", error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}, 50) // Slightly longer delay to ensure proper event ordering
|
||||
|
||||
return editor
|
||||
}
|
||||
|
||||
// For other commands, just return a resolved promise
|
||||
return Promise.resolve()
|
||||
},
|
||||
registerCommand: (command, callback) => {
|
||||
console.log(`Registered command: ${command}`)
|
||||
return { dispose: () => {} }
|
||||
},
|
||||
getCommands: async () => {
|
||||
return []
|
||||
},
|
||||
})
|
||||
|
||||
// Add missing TabInput classes
|
||||
vscode.TabInputText = class TabInputText {
|
||||
constructor(uri) {
|
||||
this.uri = uri
|
||||
}
|
||||
}
|
||||
|
||||
vscode.TabInputTextDiff = class TabInputTextDiff {
|
||||
constructor(original, modified) {
|
||||
this.original = original
|
||||
this.modified = modified
|
||||
}
|
||||
}
|
||||
|
||||
// Add missing WorkspaceEdit and related classes
|
||||
vscode.WorkspaceEdit = class WorkspaceEdit {
|
||||
constructor() {
|
||||
this._edits = []
|
||||
}
|
||||
|
||||
replace(uri, range, newText) {
|
||||
console.log("WorkspaceEdit.replace:", uri, range, newText)
|
||||
this._edits.push({
|
||||
_type: 1, // TextEdit
|
||||
_uri: uri,
|
||||
_edits: [
|
||||
{
|
||||
range: range,
|
||||
newText: newText,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
insert(uri, position, newText) {
|
||||
console.log("WorkspaceEdit.insert:", uri, position, newText)
|
||||
this.replace(uri, new vscode.Range(position, position), newText)
|
||||
}
|
||||
|
||||
delete(uri, range) {
|
||||
console.log("WorkspaceEdit.delete:", uri, range)
|
||||
this.replace(uri, range, "")
|
||||
}
|
||||
}
|
||||
|
||||
vscode.Range = class Range {
|
||||
constructor(startLine, startCharacter, endLine, endCharacter) {
|
||||
if (typeof startLine === "object") {
|
||||
// Called with Position objects
|
||||
this.start = startLine
|
||||
this.end = startCharacter
|
||||
} else {
|
||||
// Called with line/character numbers
|
||||
this.start = new vscode.Position(startLine, startCharacter)
|
||||
this.end = new vscode.Position(endLine, endCharacter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vscode.Position = class Position {
|
||||
constructor(line, character) {
|
||||
this.line = line
|
||||
this.character = character
|
||||
}
|
||||
}
|
||||
|
||||
vscode.Selection = class Selection extends vscode.Range {
|
||||
constructor(anchorLine, anchorCharacter, activeLine, activeCharacter) {
|
||||
if (typeof anchorLine === "object") {
|
||||
// Called with Position objects
|
||||
super(anchorLine, anchorCharacter)
|
||||
this.anchor = anchorLine
|
||||
this.active = anchorCharacter
|
||||
} else {
|
||||
// Called with line/character numbers
|
||||
super(anchorLine, anchorCharacter, activeLine, activeCharacter)
|
||||
this.anchor = new vscode.Position(anchorLine, anchorCharacter)
|
||||
this.active = new vscode.Position(activeLine, activeCharacter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add TextEditorRevealType enum
|
||||
vscode.TextEditorRevealType = {
|
||||
Default: 0,
|
||||
InCenter: 1,
|
||||
InCenterIfOutsideViewport: 2,
|
||||
AtTop: 3,
|
||||
}
|
||||
|
||||
// Add missing languages API
|
||||
if (!vscode.languages) {
|
||||
vscode.languages = {}
|
||||
}
|
||||
|
||||
Object.assign(vscode.languages, {
|
||||
getDiagnostics: (uri) => {
|
||||
console.log("Called vscode.languages.getDiagnostics")
|
||||
// Return empty diagnostics for standalone mode
|
||||
if (uri) {
|
||||
return []
|
||||
} else {
|
||||
// Return all diagnostics as empty array
|
||||
return []
|
||||
}
|
||||
},
|
||||
registerCodeActionsProvider: () => ({ dispose: () => {} }),
|
||||
createDiagnosticCollection: () => ({
|
||||
set: () => {},
|
||||
delete: () => {},
|
||||
clear: () => {},
|
||||
dispose: () => {},
|
||||
}),
|
||||
})
|
||||
|
||||
console.log("Finished loading stub impls...")
|
||||
|
||||
// Export the terminal manager globally for Cline core to use
|
||||
global.standaloneTerminalManager = globalTerminalManager
|
||||
|
||||
// Override the TerminalManager to use our standalone implementation
|
||||
if (typeof global !== "undefined") {
|
||||
// Replace the TerminalManager class with our standalone implementation
|
||||
global.StandaloneTerminalManagerClass = require("./enhanced-terminal").StandaloneTerminalManager
|
||||
}
|
||||
|
||||
module.exports = vscode
|
||||
|
||||
@@ -44,23 +44,15 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
</h3>
|
||||
<ul style={ulStyle}>
|
||||
<li>
|
||||
<b>Optimized for Claude 4:</b> Cline is now optimized to work with the Claude 4 family of models, resulting in
|
||||
improved performance, reliability, and new capabilities.
|
||||
<b>Claude 4 Models:</b> Now with support for Anthropic Claude Sonnet 4 and Claude Opus 4 in both Anthropic and
|
||||
Vertex providers.
|
||||
</li>
|
||||
<li>
|
||||
<b>Gemini CLI Provider:</b> Added a new Gemini CLI provider that allows you to use your local Gemini CLI
|
||||
authentication to access Gemini models for free.
|
||||
<b>New Settings Page:</b> Redesigned settings, now split into tabs for easier navigation and a cleaner
|
||||
experience.
|
||||
</li>
|
||||
<li>
|
||||
<b>WebFetch Tool:</b> Gemini 2.5 Pro and Claude 4 models now support the WebFetch tool, allowing Cline to
|
||||
retrieve and summarize web content directly in conversations.
|
||||
</li>
|
||||
<li>
|
||||
<b>Self Knowledge:</b> When using frontier models, Cline is self-aware about his capabilities and featureset.
|
||||
</li>
|
||||
<li>
|
||||
<b>Improved Diff Editing:</b> Improved diff editing to achieve record lows in diff edit failures for frontier
|
||||
models
|
||||
<b>Nebius AI Studio:</b> Added Nebius AI Studio as a new provider. (Thanks @Aktsvigun!)
|
||||
</li>
|
||||
</ul>
|
||||
<Accordion isCompact className="pl-0">
|
||||
@@ -75,17 +67,6 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
"text-[var(--vscode-foreground)] mb-0.5 -rotate-180 data-[open=true]:-rotate-90 rtl:rotate-0 rtl:data-[open=true]:-rotate-90",
|
||||
}}>
|
||||
<ul style={ulStyle}>
|
||||
<li>
|
||||
<b>Claude 4 Models:</b> Now with support for Anthropic Claude Sonnet 4 and Claude Opus 4 in both
|
||||
Anthropic and Vertex providers.
|
||||
</li>
|
||||
<li>
|
||||
<b>New Settings Page:</b> Redesigned settings, now split into tabs for easier navigation and a cleaner
|
||||
experience.
|
||||
</li>
|
||||
<li>
|
||||
<b>Nebius AI Studio:</b> Added Nebius AI Studio as a new provider. (Thanks @Aktsvigun!)
|
||||
</li>
|
||||
<li>
|
||||
<b>Workflows:</b> Create and manage workflow files that can be injected into conversations via slash
|
||||
commands, making it easy to automate repetitive tasks.
|
||||
@@ -98,6 +79,29 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
<b>Global Endpoint for Vertex AI:</b> Improved availability and reduced rate limiting errors for
|
||||
Vertex AI users.
|
||||
</li>
|
||||
<li>
|
||||
<b>New User Experience:</b> Special components and guidance for new users to help them get started
|
||||
with Cline.
|
||||
</li>
|
||||
<li>
|
||||
<b>UI Improvements:</b> Fixed loading states and improved settings organization for a smoother
|
||||
experience.
|
||||
</li>
|
||||
<li>
|
||||
<b>Task Timeline:</b> See the history of your coding journey with a visual timeline of checkpoints.
|
||||
</li>
|
||||
<li>
|
||||
<b>UX Improvements:</b> Type while Cline works, smarter auto-scrolling, and copy buttons for task
|
||||
headers and messages.
|
||||
</li>
|
||||
<li>
|
||||
<b>Gemini prompt caching:</b> Gemini and Vertex providers now support prompt caching and price
|
||||
tracking.
|
||||
</li>
|
||||
<li>
|
||||
<b>Global Cline Rules:</b> Store multiple rules files in Documents/Cline/Rules to share between
|
||||
projects.
|
||||
</li>
|
||||
</ul>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
@@ -17,7 +17,7 @@ import McpResponseDisplay from "@/components/mcp/chat-display/McpResponseDisplay
|
||||
import McpResourceRow from "@/components/mcp/configuration/tabs/installed/server-row/McpResourceRow"
|
||||
import McpToolRow from "@/components/mcp/configuration/tabs/installed/server-row/McpToolRow"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { FileServiceClient, TaskServiceClient, UiServiceClient } from "@/services/grpc-client"
|
||||
import { FileServiceClient, TaskServiceClient } from "@/services/grpc-client"
|
||||
import { findMatchingResourceOrTemplate, getMcpServerDisplayName } from "@/utils/mcp"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import {
|
||||
@@ -184,7 +184,7 @@ export const ChatRowContent = ({
|
||||
sendMessageFromChatRow,
|
||||
onSetQuote,
|
||||
}: ChatRowContentProps) => {
|
||||
const { mcpServers, mcpMarketplaceCatalog, onRelinquishControl, apiConfiguration } = useExtensionState()
|
||||
const { mcpServers, mcpMarketplaceCatalog, onRelinquishControl } = useExtensionState()
|
||||
const [seeNewChangesDisabled, setSeeNewChangesDisabled] = useState(false)
|
||||
const [quoteButtonState, setQuoteButtonState] = useState<QuoteButtonState>({
|
||||
visible: false,
|
||||
@@ -697,13 +697,15 @@ export const ChatRowContent = ({
|
||||
msUserSelect: "none",
|
||||
}}
|
||||
onClick={() => {
|
||||
// Open the URL in the default browser using gRPC
|
||||
// Attempt to open the URL in the default browser
|
||||
if (tool.path) {
|
||||
UiServiceClient.openUrl(StringRequest.create({ value: tool.path }))
|
||||
|
||||
.catch((err) => {
|
||||
console.error("Failed to open URL:", err)
|
||||
})
|
||||
// Assuming 'openUrl' is a valid action the extension can handle.
|
||||
// If not, this might need adjustment based on how other external link openings are handled.
|
||||
vscode.postMessage({
|
||||
type: "action", // This should be a valid MessageType from WebviewMessage
|
||||
action: "openUrl", // This should be a valid WebviewAction from WebviewMessage
|
||||
url: tool.path,
|
||||
} as any) // Using 'as any' for now if 'openUrl' isn't strictly typed yet
|
||||
}
|
||||
}}>
|
||||
<span
|
||||
@@ -953,88 +955,6 @@ export const ChatRowContent = ({
|
||||
}
|
||||
}
|
||||
|
||||
// Check for rate limit errors (status code 429)
|
||||
const isRateLimitError =
|
||||
apiRequestFailedMessage?.includes("status code 429") ||
|
||||
apiRequestFailedMessage?.toLowerCase().includes("rate limit") ||
|
||||
apiRequestFailedMessage?.toLowerCase().includes("too many requests") ||
|
||||
apiRequestFailedMessage?.toLowerCase().includes("quota exceeded") ||
|
||||
apiRequestFailedMessage?.toLowerCase().includes("resource exhausted")
|
||||
|
||||
if (isRateLimitError) {
|
||||
// Check if current provider is Gemini CLI to show specific message
|
||||
const isGeminiCliProvider = apiConfiguration?.apiProvider === "gemini-cli"
|
||||
|
||||
if (isGeminiCliProvider) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "rgba(255, 191, 0, 0.1)",
|
||||
padding: "12px",
|
||||
borderRadius: "4px",
|
||||
border: "1px solid rgba(255, 191, 0, 0.3)",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginBottom: "8px",
|
||||
}}>
|
||||
<i
|
||||
className="codicon codicon-warning"
|
||||
style={{
|
||||
marginRight: "8px",
|
||||
fontSize: "16px",
|
||||
color: "#FFA500",
|
||||
}}></i>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: "bold",
|
||||
color: "#FFA500",
|
||||
}}>
|
||||
Rate Limit Exceeded
|
||||
</span>
|
||||
</div>
|
||||
<p style={{ margin: 0, fontSize: "14px", lineHeight: "1.4" }}>
|
||||
You've hit the API rate limit. This is likely due to free tier limits.
|
||||
</p>
|
||||
<p style={{ margin: "8px 0 0 0", fontSize: "12px", lineHeight: "1.4" }}>
|
||||
You can read about the tier limits{" "}
|
||||
<a
|
||||
href="https://codeassist.google/"
|
||||
style={{
|
||||
color: "inherit",
|
||||
textDecoration: "underline",
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
UiServiceClient.openUrl(
|
||||
StringRequest.create({
|
||||
value: "https://codeassist.google/",
|
||||
}),
|
||||
).catch((err) => console.error("Failed to open URL:", err))
|
||||
}}>
|
||||
here
|
||||
</a>
|
||||
, or alternatively, you can use the Gemini Flash Model that will give
|
||||
you better limits.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
// Generic rate limit error for other providers
|
||||
return (
|
||||
<p
|
||||
style={{
|
||||
...pStyle,
|
||||
color: "var(--vscode-errorForeground)",
|
||||
}}>
|
||||
{apiRequestFailedMessage || apiReqStreamingFailedMessage}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Default error display
|
||||
return (
|
||||
<p
|
||||
|
||||
@@ -414,28 +414,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
}, [task?.ts])
|
||||
|
||||
const isStreaming = useMemo(() => {
|
||||
const isLastAsk = !!modifiedMessages.at(-1)?.ask // checking clineAsk isn't enough since messages effect may be called again for a tool for example, set clineAsk to its value, and if the next message is not an ask then it doesn't reset. This is likely due to how much more often we're updating messages as compared to before, and should be resolved with optimizations as it's likely a rendering bug. but as a final guard for now, the cancel button will show if the last message is not an ask
|
||||
const isToolCurrentlyAsking = isLastAsk && clineAsk !== undefined && enableButtons && primaryButtonText !== undefined
|
||||
if (isToolCurrentlyAsking) {
|
||||
return false
|
||||
}
|
||||
|
||||
const isLastMessagePartial = modifiedMessages.at(-1)?.partial === true
|
||||
if (isLastMessagePartial) {
|
||||
return true
|
||||
} else {
|
||||
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])
|
||||
return modifiedMessages.at(-1)?.partial === true
|
||||
}, [modifiedMessages])
|
||||
|
||||
const handleSendMessage = useCallback(
|
||||
async (text: string, images: string[], files: string[]) => {
|
||||
|
||||
@@ -74,22 +74,6 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages, onBlockClick }) =
|
||||
// Render a timeline block
|
||||
const TimelineBlock = useCallback(
|
||||
(index: number) => {
|
||||
// Show placeholder block when no items exist
|
||||
if (taskTimelinePropsMessages.length === 0 || index >= taskTimelinePropsMessages.length) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: BLOCK_WIDTH,
|
||||
height: "100%",
|
||||
backgroundColor: "#e5e5e5", // Light gray placeholder
|
||||
flexShrink: 0,
|
||||
marginRight: BLOCK_GAP,
|
||||
opacity: 0.5,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const message = taskTimelinePropsMessages[index]
|
||||
const originalMessageIndex = messageIndexMap[index]
|
||||
|
||||
@@ -128,6 +112,10 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages, onBlockClick }) =
|
||||
}
|
||||
}, [taskTimelinePropsMessages])
|
||||
|
||||
if (taskTimelinePropsMessages.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
@@ -158,7 +146,7 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages, onBlockClick }) =
|
||||
height: TIMELINE_HEIGHT,
|
||||
width: "100%",
|
||||
}}
|
||||
totalCount={Math.max(1, taskTimelinePropsMessages.length)}
|
||||
totalCount={taskTimelinePropsMessages.length}
|
||||
itemContent={TimelineBlock}
|
||||
horizontalDirection={true}
|
||||
increaseViewportBy={12}
|
||||
|
||||
@@ -35,7 +35,7 @@ export function AlertDialogContent({ className, children, ...props }: React.HTML
|
||||
className={`fixed top-[50%] left-[50%] grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] ${className}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
{...props}>
|
||||
<div className="bg-[var(--vscode-editor-background)] rounded-sm gap-3 border border-[var(--vscode-panel-border)] p-6 shadow-lg sm:max-w-lg">
|
||||
<div className="bg-[var(--vscode-editor-background)] rounded-sm gap-3 border border-[var(--vscode-panel-border)] p-4 shadow-lg sm:max-w-md">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
@@ -47,7 +47,7 @@ export function AlertDialogHeader({ className, ...props }: React.HTMLAttributes<
|
||||
}
|
||||
|
||||
export function AlertDialogFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return <div className={`flex flex-row justify-end gap-3 mt-6 ${className}`} {...props} />
|
||||
return <div className={`flex flex-row justify-end gap-2 mt-4 ${className}`} {...props} />
|
||||
}
|
||||
|
||||
export function AlertDialogTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
|
||||
@@ -76,23 +76,11 @@ export function UnsavedChangesDialog({
|
||||
onOpenChange,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
onSave,
|
||||
title = "Unsaved Changes",
|
||||
description = "You have unsaved changes. Are you sure you want to discard them?",
|
||||
confirmText = "Discard Changes",
|
||||
saveText = "Save & Continue",
|
||||
showSaveOption = false,
|
||||
}: {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onConfirm: () => void
|
||||
onCancel: () => void
|
||||
onSave?: () => void
|
||||
title?: string
|
||||
description?: string
|
||||
confirmText?: string
|
||||
saveText?: string
|
||||
showSaveOption?: boolean
|
||||
}) {
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
@@ -100,16 +88,15 @@ export function UnsavedChangesDialog({
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<AlertTriangle className="w-5 h-5 text-[var(--vscode-errorForeground)]" />
|
||||
{title}
|
||||
Unsaved Changes
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>{description}</AlertDialogDescription>
|
||||
<AlertDialogDescription>
|
||||
You have unsaved changes. Are you sure you want to discard them?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={onCancel}>Cancel</AlertDialogCancel>
|
||||
{showSaveOption && onSave && <AlertDialogAction onClick={onSave}>{saveText}</AlertDialogAction>}
|
||||
<AlertDialogAction onClick={onConfirm} appearance={showSaveOption ? "secondary" : "primary"}>
|
||||
{confirmText}
|
||||
</AlertDialogAction>
|
||||
<AlertDialogAction onClick={onConfirm}>Discard Changes</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
@@ -15,36 +15,25 @@ import { StateServiceClient } from "@/services/grpc-client"
|
||||
import { PlanActMode, TogglePlanActModeRequest } from "@shared/proto/state"
|
||||
|
||||
// Styled component for Act Mode text with more specific styling
|
||||
const ActModeHighlight: React.FC = () => {
|
||||
const { chatSettings } = useExtensionState()
|
||||
|
||||
return (
|
||||
<span
|
||||
onClick={() => {
|
||||
// Only toggle to Act mode if we're currently in Plan mode
|
||||
if (chatSettings.mode === "plan") {
|
||||
StateServiceClient.togglePlanActMode(
|
||||
TogglePlanActModeRequest.create({
|
||||
chatSettings: {
|
||||
mode: PlanActMode.ACT,
|
||||
preferredLanguage: chatSettings.preferredLanguage,
|
||||
openAiReasoningEffort: chatSettings.openAIReasoningEffort,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
}}
|
||||
title={chatSettings.mode === "plan" ? "Click to toggle to Act Mode" : "Already in Act Mode"}
|
||||
className={`text-[var(--vscode-textLink-foreground)] inline-flex items-center gap-1 ${
|
||||
chatSettings.mode === "plan" ? "hover:opacity-90 cursor-pointer" : "cursor-default opacity-60"
|
||||
}`}>
|
||||
<div className="p-1 rounded-[12px] bg-[var(--vscode-editor-background)] flex items-center justify-end w-4 border-[1px] border-[var(--vscode-input-border)]">
|
||||
<div className="rounded-full bg-[var(--vscode-textLink-foreground)] w-2 h-2" />
|
||||
</div>
|
||||
Act Mode (⌘⇧A)
|
||||
</span>
|
||||
)
|
||||
}
|
||||
const ActModeHighlight: React.FC = () => (
|
||||
<span
|
||||
onClick={() => {
|
||||
StateServiceClient.togglePlanActMode(
|
||||
TogglePlanActModeRequest.create({
|
||||
chatSettings: {
|
||||
mode: PlanActMode.ACT,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}}
|
||||
title="Click to toggle to Act Mode"
|
||||
className="text-[var(--vscode-textLink-foreground)] hover:opacity-90 cursor-pointer inline-flex items-center gap-1">
|
||||
<div className="p-1 rounded-[12px] bg-[var(--vscode-editor-background)] flex items-center justify-end w-4 border-[1px] border-[var(--vscode-input-border)]">
|
||||
<div className="rounded-full bg-[var(--vscode-textLink-foreground)] w-2 h-2" />
|
||||
</div>
|
||||
Act Mode (⌘⇧A)
|
||||
</span>
|
||||
)
|
||||
|
||||
interface MarkdownBlockProps {
|
||||
markdown?: string
|
||||
|
||||
@@ -2,8 +2,8 @@ import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
import { memo, useState } from "react"
|
||||
import styled from "styled-components"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { StateServiceClient } from "@/services/grpc-client"
|
||||
import { TelemetrySettingEnum, TelemetrySettingRequest } from "@shared/proto/state"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { TelemetrySetting } from "@shared/TelemetrySetting"
|
||||
|
||||
const BannerContainer = styled.div`
|
||||
background-color: var(--vscode-banner-background);
|
||||
@@ -53,16 +53,8 @@ const TelemetryBanner = () => {
|
||||
navigateToSettings()
|
||||
}
|
||||
|
||||
const handleClose = async () => {
|
||||
try {
|
||||
await StateServiceClient.updateTelemetrySetting(
|
||||
TelemetrySettingRequest.create({
|
||||
setting: TelemetrySettingEnum.ENABLED,
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Error updating telemetry setting:", error)
|
||||
}
|
||||
const handleClose = () => {
|
||||
vscode.postMessage({ type: "telemetrySetting", telemetrySetting: "enabled" satisfies TelemetrySetting })
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -727,9 +727,17 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
disabled={deleteAllDisabled || taskHistory.length === 0}
|
||||
onClick={() => {
|
||||
setDeleteAllDisabled(true)
|
||||
TaskServiceClient.deleteAllTaskHistory(BooleanRequest.create({}))
|
||||
.catch((error) => console.error("Error deleting task history:", error))
|
||||
.finally(() => setDeleteAllDisabled(false))
|
||||
const confirmDelete = window.confirm("Are you sure you want to delete all task history?")
|
||||
if (confirmDelete) {
|
||||
const preserveFavorites = window.confirm(
|
||||
"Would you like to preserve favorited tasks?\n\nClick 'OK' to preserve favorites, or 'Cancel' to delete everything.",
|
||||
)
|
||||
TaskServiceClient.deleteAllTaskHistory(BooleanRequest.create({ value: preserveFavorites }))
|
||||
.catch((error) => console.error("Error deleting task history:", error))
|
||||
.finally(() => setDeleteAllDisabled(false))
|
||||
} else {
|
||||
setDeleteAllDisabled(false)
|
||||
}
|
||||
}}>
|
||||
Delete All History{totalTasksSize !== null ? ` (${formatSize(totalTasksSize)})` : ""}
|
||||
</DangerButton>
|
||||
|
||||
@@ -113,21 +113,19 @@ const McpConfigurationView = ({ onDone, initialTab }: McpViewProps) => {
|
||||
)
|
||||
}
|
||||
|
||||
const StyledTabButton = styled.button<{ isActive: boolean; disabled?: boolean }>`
|
||||
const StyledTabButton = styled.button<{ isActive: boolean }>`
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid ${(props) => (props.isActive ? "var(--vscode-foreground)" : "transparent")};
|
||||
color: ${(props) => (props.isActive ? "var(--vscode-foreground)" : "var(--vscode-descriptionForeground)")};
|
||||
padding: 8px 16px;
|
||||
cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
margin-bottom: -1px;
|
||||
font-family: inherit;
|
||||
opacity: ${(props) => (props.disabled ? 0.6 : 1)};
|
||||
pointer-events: ${(props) => (props.disabled ? "none" : "auto")};
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => (props.disabled ? "var(--vscode-descriptionForeground)" : "var(--vscode-foreground)")};
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
`
|
||||
|
||||
@@ -135,16 +133,12 @@ export const TabButton = ({
|
||||
children,
|
||||
isActive,
|
||||
onClick,
|
||||
disabled,
|
||||
style,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
isActive: boolean
|
||||
onClick: () => void
|
||||
disabled?: boolean
|
||||
style?: React.CSSProperties
|
||||
}) => (
|
||||
<StyledTabButton isActive={isActive} onClick={onClick} disabled={disabled} style={style}>
|
||||
<StyledTabButton isActive={isActive} onClick={onClick}>
|
||||
{children}
|
||||
</StyledTabButton>
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
||||
import { UpdateBrowserSettingsRequest } from "@shared/proto/browser"
|
||||
import { EmptyRequest, StringRequest } from "@shared/proto/common"
|
||||
import { VSCodeButton, VSCodeCheckbox, VSCodeDropdown, VSCodeOption, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import debounce from "debounce"
|
||||
import React, { useCallback, useEffect, useState } from "react"
|
||||
@@ -5,13 +7,6 @@ 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 { EmptyRequest, StringRequest } from "@shared/proto/common"
|
||||
import { BrowserSettings } from "@shared/BrowserSettings"
|
||||
|
||||
interface BrowserSettingsSectionProps {
|
||||
localBrowserSettings: BrowserSettings
|
||||
onBrowserSettingsChange: (settings: BrowserSettings) => void
|
||||
}
|
||||
|
||||
const ConnectionStatusIndicator = ({
|
||||
isChecking,
|
||||
@@ -56,12 +51,9 @@ const CollapsibleContent = styled.div<{ isOpen: boolean }>`
|
||||
visibility: ${({ isOpen }) => (isOpen ? "visible" : "hidden")};
|
||||
`
|
||||
|
||||
export const BrowserSettingsSection: React.FC<BrowserSettingsSectionProps> = ({
|
||||
localBrowserSettings,
|
||||
onBrowserSettingsChange,
|
||||
}) => {
|
||||
export const BrowserSettingsSection: React.FC = () => {
|
||||
const { browserSettings } = useExtensionState()
|
||||
const [localChromePath, setLocalChromePath] = useState(localBrowserSettings.chromeExecutablePath || "")
|
||||
const [localChromePath, setLocalChromePath] = useState(browserSettings.chromeExecutablePath || "")
|
||||
const [isCheckingConnection, setIsCheckingConnection] = useState(false)
|
||||
const [connectionStatus, setConnectionStatus] = useState<boolean | null>(null)
|
||||
const [relaunchResult, setRelaunchResult] = useState<{ success: boolean; message: string } | null>(null)
|
||||
@@ -94,24 +86,23 @@ export const BrowserSettingsSection: React.FC<BrowserSettingsSectionProps> = ({
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Sync localChromePath with prop changes
|
||||
// Sync localChromePath with global state
|
||||
useEffect(() => {
|
||||
if (localBrowserSettings.chromeExecutablePath !== localChromePath) {
|
||||
setLocalChromePath(localBrowserSettings.chromeExecutablePath || "")
|
||||
if (browserSettings.chromeExecutablePath !== localChromePath) {
|
||||
setLocalChromePath(browserSettings.chromeExecutablePath || "")
|
||||
}
|
||||
}, [localBrowserSettings.chromeExecutablePath])
|
||||
// Removed sync for local disableToolUse state
|
||||
}, [browserSettings.chromeExecutablePath, browserSettings.disableToolUse])
|
||||
|
||||
// Debounced connection check function
|
||||
const debouncedCheckConnection = useCallback(
|
||||
debounce(() => {
|
||||
if (localBrowserSettings.remoteBrowserEnabled) {
|
||||
if (browserSettings.remoteBrowserEnabled) {
|
||||
setIsCheckingConnection(true)
|
||||
setConnectionStatus(null)
|
||||
if (localBrowserSettings.remoteBrowserHost) {
|
||||
if (browserSettings.remoteBrowserHost) {
|
||||
// Use gRPC for testBrowserConnection
|
||||
BrowserServiceClient.testBrowserConnection(
|
||||
StringRequest.create({ value: localBrowserSettings.remoteBrowserHost }),
|
||||
)
|
||||
BrowserServiceClient.testBrowserConnection(StringRequest.create({ value: browserSettings.remoteBrowserHost }))
|
||||
.then((result) => {
|
||||
setConnectionStatus(result.success)
|
||||
setIsCheckingConnection(false)
|
||||
@@ -135,70 +126,153 @@ export const BrowserSettingsSection: React.FC<BrowserSettingsSectionProps> = ({
|
||||
}
|
||||
}
|
||||
}, 1000),
|
||||
[localBrowserSettings.remoteBrowserEnabled, localBrowserSettings.remoteBrowserHost],
|
||||
[browserSettings.remoteBrowserEnabled, browserSettings.remoteBrowserHost],
|
||||
)
|
||||
|
||||
// Check connection when component mounts or when remote settings change
|
||||
useEffect(() => {
|
||||
if (localBrowserSettings.remoteBrowserEnabled) {
|
||||
if (browserSettings.remoteBrowserEnabled) {
|
||||
debouncedCheckConnection()
|
||||
} else {
|
||||
setConnectionStatus(null)
|
||||
}
|
||||
}, [localBrowserSettings.remoteBrowserEnabled, localBrowserSettings.remoteBrowserHost, debouncedCheckConnection])
|
||||
}, [browserSettings.remoteBrowserEnabled, browserSettings.remoteBrowserHost, debouncedCheckConnection])
|
||||
|
||||
const handleViewportChange = (event: Event) => {
|
||||
const target = event.target as HTMLSelectElement
|
||||
const selectedSize = BROWSER_VIEWPORT_PRESETS[target.value as keyof typeof BROWSER_VIEWPORT_PRESETS]
|
||||
if (selectedSize) {
|
||||
onBrowserSettingsChange({
|
||||
...localBrowserSettings,
|
||||
viewport: {
|
||||
width: selectedSize.width,
|
||||
height: selectedSize.height,
|
||||
},
|
||||
})
|
||||
BrowserServiceClient.updateBrowserSettings(
|
||||
UpdateBrowserSettingsRequest.create({
|
||||
metadata: {},
|
||||
viewport: {
|
||||
width: selectedSize.width,
|
||||
height: selectedSize.height,
|
||||
},
|
||||
remoteBrowserEnabled: browserSettings.remoteBrowserEnabled,
|
||||
remoteBrowserHost: browserSettings.remoteBrowserHost,
|
||||
chromeExecutablePath: browserSettings.chromeExecutablePath,
|
||||
disableToolUse: browserSettings.disableToolUse,
|
||||
}),
|
||||
)
|
||||
.then((response) => {
|
||||
if (!response.value) {
|
||||
console.error("Failed to update browser settings")
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error updating browser settings:", error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const updateRemoteBrowserEnabled = (enabled: boolean) => {
|
||||
onBrowserSettingsChange({
|
||||
...localBrowserSettings,
|
||||
remoteBrowserEnabled: enabled,
|
||||
// If disabling, also clear the host
|
||||
remoteBrowserHost: enabled ? localBrowserSettings.remoteBrowserHost : undefined,
|
||||
})
|
||||
BrowserServiceClient.updateBrowserSettings(
|
||||
UpdateBrowserSettingsRequest.create({
|
||||
metadata: {},
|
||||
viewport: {
|
||||
width: browserSettings.viewport.width,
|
||||
height: browserSettings.viewport.height,
|
||||
},
|
||||
remoteBrowserEnabled: enabled,
|
||||
// If disabling, also clear the host
|
||||
remoteBrowserHost: enabled ? browserSettings.remoteBrowserHost : undefined,
|
||||
chromeExecutablePath: browserSettings.chromeExecutablePath,
|
||||
disableToolUse: browserSettings.disableToolUse,
|
||||
}),
|
||||
)
|
||||
.then((response) => {
|
||||
if (!response.value) {
|
||||
console.error("Failed to update browser settings")
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error updating browser settings:", error)
|
||||
})
|
||||
}
|
||||
|
||||
const updateRemoteBrowserHost = (host: string | undefined) => {
|
||||
onBrowserSettingsChange({
|
||||
...localBrowserSettings,
|
||||
remoteBrowserHost: host,
|
||||
})
|
||||
BrowserServiceClient.updateBrowserSettings(
|
||||
UpdateBrowserSettingsRequest.create({
|
||||
metadata: {},
|
||||
viewport: {
|
||||
width: browserSettings.viewport.width,
|
||||
height: browserSettings.viewport.height,
|
||||
},
|
||||
remoteBrowserEnabled: browserSettings.remoteBrowserEnabled,
|
||||
remoteBrowserHost: host,
|
||||
chromeExecutablePath: browserSettings.chromeExecutablePath,
|
||||
disableToolUse: browserSettings.disableToolUse,
|
||||
}),
|
||||
)
|
||||
.then((response) => {
|
||||
if (!response.value) {
|
||||
console.error("Failed to update browser settings")
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error updating browser settings:", error)
|
||||
})
|
||||
}
|
||||
|
||||
const debouncedUpdateChromePath = useCallback(
|
||||
debounce((newPath: string | undefined) => {
|
||||
onBrowserSettingsChange({
|
||||
...localBrowserSettings,
|
||||
chromeExecutablePath: newPath,
|
||||
})
|
||||
BrowserServiceClient.updateBrowserSettings(
|
||||
UpdateBrowserSettingsRequest.create({
|
||||
metadata: {},
|
||||
viewport: {
|
||||
width: browserSettings.viewport.width,
|
||||
height: browserSettings.viewport.height,
|
||||
},
|
||||
remoteBrowserEnabled: browserSettings.remoteBrowserEnabled,
|
||||
remoteBrowserHost: browserSettings.remoteBrowserHost,
|
||||
chromeExecutablePath: newPath,
|
||||
disableToolUse: browserSettings.disableToolUse,
|
||||
}),
|
||||
)
|
||||
.then((response) => {
|
||||
if (!response.value) {
|
||||
console.error("Failed to update browser settings for chromeExecutablePath")
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error updating browser settings for chromeExecutablePath:", error)
|
||||
})
|
||||
}, 500),
|
||||
[localBrowserSettings, onBrowserSettingsChange],
|
||||
[browserSettings],
|
||||
)
|
||||
|
||||
const updateChromeExecutablePath = (path: string | undefined) => {
|
||||
setLocalChromePath(path || "")
|
||||
debouncedUpdateChromePath(path)
|
||||
BrowserServiceClient.updateBrowserSettings(
|
||||
UpdateBrowserSettingsRequest.create({
|
||||
metadata: {},
|
||||
viewport: {
|
||||
width: browserSettings.viewport.width,
|
||||
height: browserSettings.viewport.height,
|
||||
},
|
||||
remoteBrowserEnabled: browserSettings.remoteBrowserEnabled,
|
||||
remoteBrowserHost: browserSettings.remoteBrowserHost,
|
||||
chromeExecutablePath: path,
|
||||
disableToolUse: browserSettings.disableToolUse,
|
||||
}),
|
||||
)
|
||||
.then((response) => {
|
||||
if (!response.value) {
|
||||
console.error("Failed to update browser settings")
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error updating browser settings:", error)
|
||||
})
|
||||
}
|
||||
|
||||
// Function to check connection once without changing UI state immediately
|
||||
const checkConnectionOnce = useCallback(() => {
|
||||
// Don't show the spinner for every check to avoid UI flicker
|
||||
// We'll rely on the response to update the connectionStatus
|
||||
if (localBrowserSettings.remoteBrowserHost) {
|
||||
if (browserSettings.remoteBrowserHost) {
|
||||
// Use gRPC for testBrowserConnection
|
||||
BrowserServiceClient.testBrowserConnection(StringRequest.create({ value: localBrowserSettings.remoteBrowserHost }))
|
||||
BrowserServiceClient.testBrowserConnection(StringRequest.create({ value: browserSettings.remoteBrowserHost }))
|
||||
.then((result) => {
|
||||
setConnectionStatus(result.success)
|
||||
})
|
||||
@@ -216,12 +290,12 @@ export const BrowserSettingsSection: React.FC<BrowserSettingsSectionProps> = ({
|
||||
setConnectionStatus(false)
|
||||
})
|
||||
}
|
||||
}, [localBrowserSettings.remoteBrowserHost])
|
||||
}, [browserSettings.remoteBrowserHost])
|
||||
|
||||
// Setup continuous polling for connection status when remote browser is enabled
|
||||
useEffect(() => {
|
||||
// Only poll if remote browser mode is enabled
|
||||
if (!localBrowserSettings.remoteBrowserEnabled) {
|
||||
if (!browserSettings.remoteBrowserEnabled) {
|
||||
// Make sure we're not showing checking state when disabled
|
||||
setIsCheckingConnection(false)
|
||||
return
|
||||
@@ -237,13 +311,30 @@ export const BrowserSettingsSection: React.FC<BrowserSettingsSectionProps> = ({
|
||||
|
||||
// Cleanup the interval if the component unmounts or remote browser is disabled
|
||||
return () => clearInterval(pollInterval)
|
||||
}, [localBrowserSettings.remoteBrowserEnabled, checkConnectionOnce])
|
||||
}, [browserSettings.remoteBrowserEnabled, checkConnectionOnce])
|
||||
|
||||
const updateDisableToolUse = (disabled: boolean) => {
|
||||
onBrowserSettingsChange({
|
||||
...localBrowserSettings,
|
||||
disableToolUse: disabled,
|
||||
})
|
||||
BrowserServiceClient.updateBrowserSettings(
|
||||
UpdateBrowserSettingsRequest.create({
|
||||
metadata: {},
|
||||
viewport: {
|
||||
width: browserSettings.viewport.width,
|
||||
height: browserSettings.viewport.height,
|
||||
},
|
||||
remoteBrowserEnabled: browserSettings.remoteBrowserEnabled,
|
||||
remoteBrowserHost: browserSettings.remoteBrowserHost,
|
||||
chromeExecutablePath: browserSettings.chromeExecutablePath,
|
||||
disableToolUse: disabled,
|
||||
}),
|
||||
)
|
||||
.then((response) => {
|
||||
if (!response.value) {
|
||||
console.error("Failed to update disableToolUse setting")
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error updating disableToolUse setting:", error)
|
||||
})
|
||||
}
|
||||
|
||||
const relaunchChromeDebugMode = () => {
|
||||
@@ -270,16 +361,16 @@ export const BrowserSettingsSection: React.FC<BrowserSettingsSectionProps> = ({
|
||||
}
|
||||
|
||||
// Determine if we should show the relaunch button
|
||||
const isRemoteEnabled = Boolean(localBrowserSettings.remoteBrowserEnabled)
|
||||
const isRemoteEnabled = Boolean(browserSettings.remoteBrowserEnabled)
|
||||
const shouldShowRelaunchButton = isRemoteEnabled && connectionStatus === false
|
||||
const isSubSettingsOpen = !(localBrowserSettings.disableToolUse || false)
|
||||
const isSubSettingsOpen = !(browserSettings.disableToolUse || false)
|
||||
|
||||
return (
|
||||
<div id="browser-settings-section" style={{ marginBottom: 20 }}>
|
||||
{/* Master Toggle */}
|
||||
<div style={{ marginBottom: isSubSettingsOpen ? 0 : 10 }}>
|
||||
<VSCodeCheckbox
|
||||
checked={localBrowserSettings.disableToolUse || false}
|
||||
checked={browserSettings.disableToolUse || false}
|
||||
onChange={(e) => updateDisableToolUse((e.target as HTMLInputElement).checked)}>
|
||||
Disable browser tool usage
|
||||
</VSCodeCheckbox>
|
||||
@@ -303,8 +394,8 @@ export const BrowserSettingsSection: React.FC<BrowserSettingsSectionProps> = ({
|
||||
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
|
||||
typedSize.width === browserSettings.viewport.width &&
|
||||
typedSize.height === browserSettings.viewport.height
|
||||
)
|
||||
})?.[0]
|
||||
}
|
||||
@@ -331,14 +422,14 @@ export const BrowserSettingsSection: React.FC<BrowserSettingsSectionProps> = ({
|
||||
{/* This div now contains Remote Connection & Chrome Path */}
|
||||
<div style={{ marginBottom: 4, display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
||||
<VSCodeCheckbox
|
||||
checked={localBrowserSettings.remoteBrowserEnabled}
|
||||
checked={browserSettings.remoteBrowserEnabled}
|
||||
onChange={(e) => updateRemoteBrowserEnabled((e.target as HTMLInputElement).checked)}>
|
||||
Use remote browser connection
|
||||
</VSCodeCheckbox>
|
||||
<ConnectionStatusIndicator
|
||||
isChecking={isCheckingConnection}
|
||||
isConnected={connectionStatus}
|
||||
remoteBrowserEnabled={localBrowserSettings.remoteBrowserEnabled}
|
||||
remoteBrowserEnabled={browserSettings.remoteBrowserEnabled}
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
@@ -350,7 +441,7 @@ export const BrowserSettingsSection: React.FC<BrowserSettingsSectionProps> = ({
|
||||
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 ? (
|
||||
{browserSettings.remoteBrowserEnabled ? (
|
||||
<>
|
||||
{" "}
|
||||
manually (<code>--remote-debugging-port=9222</code>) or using the button below. Enter the host
|
||||
@@ -361,10 +452,10 @@ export const BrowserSettingsSection: React.FC<BrowserSettingsSectionProps> = ({
|
||||
)}
|
||||
</p>
|
||||
{/* Moved remote-specific settings to appear directly after enabling remote connection */}
|
||||
{localBrowserSettings.remoteBrowserEnabled && (
|
||||
{browserSettings.remoteBrowserEnabled && (
|
||||
<div style={{ marginLeft: 0, marginTop: 8 }}>
|
||||
<VSCodeTextField
|
||||
value={localBrowserSettings.remoteBrowserHost || ""}
|
||||
value={browserSettings.remoteBrowserHost || ""}
|
||||
placeholder="http://localhost:9222"
|
||||
style={{ width: "100%", marginBottom: 8 }}
|
||||
onChange={(e: any) => updateRemoteBrowserHost(e.target.value || undefined)}
|
||||
@@ -416,7 +507,8 @@ export const BrowserSettingsSection: React.FC<BrowserSettingsSectionProps> = ({
|
||||
style={{ width: "100%" }}
|
||||
onChange={(e: any) => {
|
||||
const newValue = e.target.value || ""
|
||||
updateChromeExecutablePath(newValue)
|
||||
setLocalChromePath(newValue)
|
||||
debouncedUpdateChromePath(newValue) // Send "" if empty, not undefined
|
||||
}}
|
||||
/>
|
||||
<p
|
||||
|
||||
@@ -43,13 +43,13 @@ export interface OpenRouterModelPickerProps {
|
||||
// Featured models for Cline provider
|
||||
const featuredModels = [
|
||||
{
|
||||
id: "google/gemini-2.5-pro",
|
||||
description: "Large 1M context window, great value",
|
||||
id: "anthropic/claude-3.7-sonnet",
|
||||
description: "Recommended for agentic coding in Cline",
|
||||
label: "Best",
|
||||
},
|
||||
{
|
||||
id: "anthropic/claude-sonnet-4",
|
||||
description: "Recommended for agentic coding in Cline",
|
||||
id: "google/gemini-2.5-pro",
|
||||
description: "Large 1M context window, great value",
|
||||
label: "Trending",
|
||||
},
|
||||
{
|
||||
@@ -334,8 +334,8 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
|
||||
If you're unsure which model to choose, Cline works best with{" "}
|
||||
<VSCodeLink
|
||||
style={{ display: "inline", fontSize: "inherit" }}
|
||||
onClick={() => handleModelChange("google/gemini-2.5-pro")}>
|
||||
google/gemini-2.5-pro.
|
||||
onClick={() => handleModelChange("anthropic/claude-3.7-sonnet")}>
|
||||
anthropic/claude-3.7-sonnet.
|
||||
</VSCodeLink>
|
||||
You can also try searching "free" for no-cost options currently available.
|
||||
</>
|
||||
|
||||
@@ -15,7 +15,6 @@ 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 Section from "./Section"
|
||||
@@ -112,10 +111,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
const [isUnsavedChangesDialogOpen, setIsUnsavedChangesDialogOpen] = useState(false)
|
||||
// Store the action to perform after confirmation
|
||||
const pendingAction = useRef<() => void>()
|
||||
// Track if we're currently switching modes
|
||||
const [isSwitchingMode, setIsSwitchingMode] = useState(false)
|
||||
// Track pending mode switch when there are unsaved changes
|
||||
const [pendingModeSwitch, setPendingModeSwitch] = useState<"plan" | "act" | null>(null)
|
||||
const {
|
||||
apiConfiguration,
|
||||
version,
|
||||
@@ -143,12 +138,8 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
mcpResponsesCollapsed,
|
||||
setMcpResponsesCollapsed,
|
||||
setApiConfiguration,
|
||||
browserSettings,
|
||||
} = useExtensionState()
|
||||
|
||||
// Local state for browser settings
|
||||
const [localBrowserSettings, setLocalBrowserSettings] = useState<BrowserSettings>(browserSettings)
|
||||
|
||||
// Store the original state to detect changes
|
||||
const originalState = useRef({
|
||||
apiConfiguration,
|
||||
@@ -163,7 +154,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
terminalReuseEnabled,
|
||||
terminalOutputLineLimit,
|
||||
defaultTerminalProfile,
|
||||
browserSettings,
|
||||
})
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
|
||||
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
|
||||
@@ -217,23 +207,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
} as StringRequest)
|
||||
}
|
||||
|
||||
// Update browser settings if they have changed
|
||||
if (JSON.stringify(localBrowserSettings) !== JSON.stringify(originalState.current.browserSettings)) {
|
||||
const { BrowserServiceClient } = await import("@/services/grpc-client")
|
||||
const { UpdateBrowserSettingsRequest } = await import("@shared/proto/browser")
|
||||
|
||||
await BrowserServiceClient.updateBrowserSettings(
|
||||
UpdateBrowserSettingsRequest.create({
|
||||
metadata: {},
|
||||
viewport: localBrowserSettings.viewport,
|
||||
remoteBrowserEnabled: localBrowserSettings.remoteBrowserEnabled,
|
||||
remoteBrowserHost: localBrowserSettings.remoteBrowserHost,
|
||||
chromeExecutablePath: localBrowserSettings.chromeExecutablePath,
|
||||
disableToolUse: localBrowserSettings.disableToolUse,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// Update the original state to reflect the saved changes
|
||||
originalState.current = {
|
||||
apiConfiguration,
|
||||
@@ -248,7 +221,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
terminalReuseEnabled,
|
||||
terminalOutputLineLimit,
|
||||
defaultTerminalProfile,
|
||||
browserSettings: localBrowserSettings,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to update settings:", error)
|
||||
@@ -264,32 +236,8 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
setModelIdErrorMessage(undefined)
|
||||
}, [apiConfiguration])
|
||||
|
||||
// Track the previous mode to detect mode switches
|
||||
const previousMode = useRef(chatSettings.mode)
|
||||
|
||||
// Update original state when mode changes
|
||||
useEffect(() => {
|
||||
// Detect if the mode has changed
|
||||
if (previousMode.current !== chatSettings.mode) {
|
||||
// Mode has changed, update the original state immediately to reflect the new apiConfiguration and chatSettings
|
||||
originalState.current = {
|
||||
...originalState.current,
|
||||
apiConfiguration: apiConfiguration,
|
||||
chatSettings: chatSettings,
|
||||
}
|
||||
|
||||
// Update the previous mode reference
|
||||
previousMode.current = chatSettings.mode
|
||||
}
|
||||
}, [chatSettings.mode, apiConfiguration, chatSettings])
|
||||
|
||||
// Check for unsaved changes by comparing current state with original state
|
||||
useEffect(() => {
|
||||
// Don't check for changes while switching modes
|
||||
if (isSwitchingMode) {
|
||||
return
|
||||
}
|
||||
|
||||
const hasChanges =
|
||||
JSON.stringify(apiConfiguration) !== JSON.stringify(originalState.current.apiConfiguration) ||
|
||||
telemetrySetting !== originalState.current.telemetrySetting ||
|
||||
@@ -299,11 +247,11 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
mcpRichDisplayEnabled !== originalState.current.mcpRichDisplayEnabled ||
|
||||
JSON.stringify(chatSettings) !== JSON.stringify(originalState.current.chatSettings) ||
|
||||
mcpResponsesCollapsed !== originalState.current.mcpResponsesCollapsed ||
|
||||
JSON.stringify(chatSettings) !== JSON.stringify(originalState.current.chatSettings) ||
|
||||
shellIntegrationTimeout !== originalState.current.shellIntegrationTimeout ||
|
||||
terminalOutputLineLimit !== originalState.current.terminalOutputLineLimit ||
|
||||
terminalReuseEnabled !== originalState.current.terminalReuseEnabled ||
|
||||
defaultTerminalProfile !== originalState.current.defaultTerminalProfile ||
|
||||
JSON.stringify(localBrowserSettings) !== JSON.stringify(originalState.current.browserSettings)
|
||||
defaultTerminalProfile !== originalState.current.defaultTerminalProfile
|
||||
|
||||
setHasUnsavedChanges(hasChanges)
|
||||
}, [
|
||||
@@ -319,7 +267,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
terminalReuseEnabled,
|
||||
terminalOutputLineLimit,
|
||||
defaultTerminalProfile,
|
||||
isSwitchingMode,
|
||||
])
|
||||
|
||||
// Handle cancel button click
|
||||
@@ -372,8 +319,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
if (typeof setMcpResponsesCollapsed === "function") {
|
||||
setMcpResponsesCollapsed(originalState.current.mcpResponsesCollapsed ?? false)
|
||||
}
|
||||
// Reset browser settings
|
||||
setLocalBrowserSettings(originalState.current.browserSettings)
|
||||
// Close settings view
|
||||
onDone()
|
||||
}
|
||||
@@ -395,134 +340,17 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
])
|
||||
|
||||
// Handle confirmation dialog actions
|
||||
const handleConfirmDiscard = useCallback(async () => {
|
||||
const handleConfirmDiscard = useCallback(() => {
|
||||
setIsUnsavedChangesDialogOpen(false)
|
||||
|
||||
// Check if this is for a mode switch
|
||||
if (pendingModeSwitch) {
|
||||
// Reset all state to original values (discard changes)
|
||||
setTelemetrySetting(originalState.current.telemetrySetting)
|
||||
setPlanActSeparateModelsSetting(originalState.current.planActSeparateModelsSetting)
|
||||
setChatSettings(originalState.current.chatSettings)
|
||||
if (typeof setApiConfiguration === "function") {
|
||||
setApiConfiguration(originalState.current.apiConfiguration ?? {})
|
||||
}
|
||||
if (typeof setEnableCheckpointsSetting === "function") {
|
||||
setEnableCheckpointsSetting(
|
||||
typeof originalState.current.enableCheckpointsSetting === "boolean"
|
||||
? originalState.current.enableCheckpointsSetting
|
||||
: false,
|
||||
)
|
||||
}
|
||||
if (typeof setMcpMarketplaceEnabled === "function") {
|
||||
setMcpMarketplaceEnabled(
|
||||
typeof originalState.current.mcpMarketplaceEnabled === "boolean"
|
||||
? originalState.current.mcpMarketplaceEnabled
|
||||
: false,
|
||||
)
|
||||
}
|
||||
if (typeof setMcpRichDisplayEnabled === "function") {
|
||||
setMcpRichDisplayEnabled(
|
||||
typeof originalState.current.mcpRichDisplayEnabled === "boolean"
|
||||
? originalState.current.mcpRichDisplayEnabled
|
||||
: true,
|
||||
)
|
||||
}
|
||||
// Reset terminal settings
|
||||
if (typeof setShellIntegrationTimeout === "function") {
|
||||
setShellIntegrationTimeout(originalState.current.shellIntegrationTimeout)
|
||||
}
|
||||
if (typeof setTerminalOutputLineLimit === "function") {
|
||||
setTerminalOutputLineLimit(originalState.current.terminalOutputLineLimit)
|
||||
}
|
||||
if (typeof setTerminalReuseEnabled === "function") {
|
||||
setTerminalReuseEnabled(originalState.current.terminalReuseEnabled ?? true)
|
||||
}
|
||||
if (typeof setDefaultTerminalProfile === "function") {
|
||||
setDefaultTerminalProfile(originalState.current.defaultTerminalProfile ?? "default")
|
||||
}
|
||||
if (typeof setMcpResponsesCollapsed === "function") {
|
||||
setMcpResponsesCollapsed(originalState.current.mcpResponsesCollapsed ?? false)
|
||||
}
|
||||
|
||||
// Now perform the mode switch
|
||||
const targetMode = pendingModeSwitch
|
||||
setPendingModeSwitch(null)
|
||||
setIsSwitchingMode(true)
|
||||
|
||||
try {
|
||||
await StateServiceClient.togglePlanActMode(
|
||||
TogglePlanActModeRequest.create({
|
||||
chatSettings: {
|
||||
mode: targetMode === "plan" ? PlanActMode.PLAN : PlanActMode.ACT,
|
||||
preferredLanguage: chatSettings.preferredLanguage,
|
||||
openAiReasoningEffort: chatSettings.openAIReasoningEffort,
|
||||
},
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle Plan/Act mode:", error)
|
||||
} finally {
|
||||
setIsSwitchingMode(false)
|
||||
}
|
||||
} else if (pendingAction.current) {
|
||||
// Regular cancel button flow
|
||||
if (pendingAction.current) {
|
||||
pendingAction.current()
|
||||
pendingAction.current = undefined
|
||||
}
|
||||
}, [
|
||||
pendingModeSwitch,
|
||||
setTelemetrySetting,
|
||||
setPlanActSeparateModelsSetting,
|
||||
setChatSettings,
|
||||
setApiConfiguration,
|
||||
setEnableCheckpointsSetting,
|
||||
setMcpMarketplaceEnabled,
|
||||
setMcpRichDisplayEnabled,
|
||||
setShellIntegrationTimeout,
|
||||
setTerminalOutputLineLimit,
|
||||
setTerminalReuseEnabled,
|
||||
setDefaultTerminalProfile,
|
||||
setMcpResponsesCollapsed,
|
||||
chatSettings.preferredLanguage,
|
||||
chatSettings.openAIReasoningEffort,
|
||||
])
|
||||
|
||||
// Handle save and switch for mode changes
|
||||
const handleSaveAndSwitch = useCallback(async () => {
|
||||
setIsUnsavedChangesDialogOpen(false)
|
||||
|
||||
if (pendingModeSwitch) {
|
||||
// Save the current settings first
|
||||
await handleSubmit(true)
|
||||
|
||||
// Now perform the mode switch
|
||||
const targetMode = pendingModeSwitch
|
||||
setPendingModeSwitch(null)
|
||||
setIsSwitchingMode(true)
|
||||
|
||||
try {
|
||||
await StateServiceClient.togglePlanActMode(
|
||||
TogglePlanActModeRequest.create({
|
||||
chatSettings: {
|
||||
mode: targetMode === "plan" ? PlanActMode.PLAN : PlanActMode.ACT,
|
||||
preferredLanguage: chatSettings.preferredLanguage,
|
||||
openAiReasoningEffort: chatSettings.openAIReasoningEffort,
|
||||
},
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle Plan/Act mode:", error)
|
||||
} finally {
|
||||
setIsSwitchingMode(false)
|
||||
}
|
||||
}
|
||||
}, [pendingModeSwitch, handleSubmit, chatSettings.preferredLanguage, chatSettings.openAIReasoningEffort])
|
||||
}, [])
|
||||
|
||||
const handleCancelDiscard = useCallback(() => {
|
||||
setIsUnsavedChangesDialogOpen(false)
|
||||
pendingAction.current = undefined
|
||||
setPendingModeSwitch(null)
|
||||
}, [])
|
||||
|
||||
// validate as soon as the component is mounted
|
||||
@@ -590,25 +418,14 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
}
|
||||
|
||||
const handlePlanActModeChange = async (tab: "plan" | "act") => {
|
||||
// Prevent switching if already in that mode or if currently switching
|
||||
if (tab === chatSettings.mode || isSwitchingMode) {
|
||||
if (tab === chatSettings.mode) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if there are unsaved changes
|
||||
if (hasUnsavedChanges) {
|
||||
// Store the pending mode switch
|
||||
setPendingModeSwitch(tab)
|
||||
// Show the unsaved changes dialog
|
||||
setIsUnsavedChangesDialogOpen(true)
|
||||
return
|
||||
}
|
||||
|
||||
// No unsaved changes, proceed with the switch
|
||||
setIsSwitchingMode(true)
|
||||
// Update settings first to ensure any changes to the current tab are saved
|
||||
await handleSubmit(true)
|
||||
|
||||
try {
|
||||
// Perform the mode switch
|
||||
await StateServiceClient.togglePlanActMode(
|
||||
TogglePlanActModeRequest.create({
|
||||
chatSettings: {
|
||||
@@ -620,9 +437,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle Plan/Act mode:", error)
|
||||
} finally {
|
||||
// Always re-enable mode switching, even on error
|
||||
setIsSwitchingMode(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -770,27 +584,13 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
<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"}
|
||||
onClick={() => handlePlanActModeChange("plan")}>
|
||||
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"}
|
||||
onClick={() => handlePlanActModeChange("act")}>
|
||||
Act Mode
|
||||
</TabButton>
|
||||
</div>
|
||||
|
||||
@@ -889,10 +689,7 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
<div>
|
||||
{renderSectionHeader("browser")}
|
||||
<Section>
|
||||
<BrowserSettingsSection
|
||||
localBrowserSettings={localBrowserSettings}
|
||||
onBrowserSettingsChange={setLocalBrowserSettings}
|
||||
/>
|
||||
<BrowserSettingsSection />
|
||||
</Section>
|
||||
</div>
|
||||
)}
|
||||
@@ -959,16 +756,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
onOpenChange={setIsUnsavedChangesDialogOpen}
|
||||
onConfirm={handleConfirmDiscard}
|
||||
onCancel={handleCancelDiscard}
|
||||
onSave={pendingModeSwitch ? handleSaveAndSwitch : undefined}
|
||||
title={pendingModeSwitch ? "Save Changes?" : "Unsaved Changes"}
|
||||
description={
|
||||
pendingModeSwitch
|
||||
? `Do you want to save your changes to ${chatSettings.mode === "plan" ? "Plan" : "Act"} mode before switching to ${pendingModeSwitch === "plan" ? "Plan" : "Act"} mode?`
|
||||
: "You have unsaved changes. Are you sure you want to discard them?"
|
||||
}
|
||||
confirmText={pendingModeSwitch ? "Switch Without Saving" : "Discard Changes"}
|
||||
saveText="Save & Switch"
|
||||
showSaveOption={!!pendingModeSwitch}
|
||||
/>
|
||||
</Tab>
|
||||
)
|
||||
|
||||
@@ -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,8 +195,10 @@ export const ModelInfoView = ({ selectedModelId, modelInfo, isPopup }: ModelInfo
|
||||
</span>
|
||||
),
|
||||
outputPriceElement, // Add the generated output price block
|
||||
isGeminiProvider && (
|
||||
<span key="geminiPricing" style={{ fontStyle: "italic" }}>
|
||||
isGemini && (
|
||||
<span key="geminiInfo" style={{ fontStyle: "italic" }}>
|
||||
* Free up to {selectedModelId && selectedModelId.includes("flash") ? "15" : "2"} requests per minute. After that,
|
||||
billing depends on prompt size.{" "}
|
||||
<VSCodeLink href="https://ai.google.dev/pricing" style={{ display: "inline", fontSize: "inherit" }}>
|
||||
For more info, see pricing details.
|
||||
</VSCodeLink>
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import { ApiConfiguration, anthropicModels } from "@shared/api"
|
||||
import { ApiKeyField } from "../common/ApiKeyField"
|
||||
import { BaseUrlField } from "../common/BaseUrlField"
|
||||
import { ModelSelector } from "../common/ModelSelector"
|
||||
import { ModelInfoView } from "../common/ModelInfoView"
|
||||
import { normalizeApiConfiguration } from "../utils/providerUtils"
|
||||
import ThinkingBudgetSlider from "../ThinkingBudgetSlider"
|
||||
|
||||
// Anthropic models that support thinking/reasoning mode
|
||||
const SUPPORTED_THINKING_MODELS = ["claude-3-7-sonnet-20250219", "claude-sonnet-4-20250514", "claude-opus-4-20250514"]
|
||||
|
||||
/**
|
||||
* Props for the AnthropicProvider component
|
||||
*/
|
||||
interface AnthropicProviderProps {
|
||||
apiConfiguration: ApiConfiguration
|
||||
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
|
||||
showModelOptions: boolean
|
||||
isPopup?: boolean
|
||||
setApiConfiguration?: (config: ApiConfiguration) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* The Anthropic provider configuration component
|
||||
*/
|
||||
export const AnthropicProvider = ({
|
||||
apiConfiguration,
|
||||
handleInputChange,
|
||||
showModelOptions,
|
||||
isPopup,
|
||||
setApiConfiguration,
|
||||
}: AnthropicProviderProps) => {
|
||||
// Get the normalized configuration
|
||||
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
|
||||
|
||||
// Create a wrapper for handling field changes more directly
|
||||
const handleFieldChange = (field: keyof ApiConfiguration) => (value: string) => {
|
||||
handleInputChange(field)({ target: { value } })
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ApiKeyField
|
||||
value={apiConfiguration?.apiKey || ""}
|
||||
onChange={handleInputChange("apiKey")}
|
||||
providerName="Anthropic"
|
||||
signupUrl="https://console.anthropic.com/settings/keys"
|
||||
/>
|
||||
|
||||
<BaseUrlField
|
||||
value={apiConfiguration?.anthropicBaseUrl}
|
||||
onChange={handleFieldChange("anthropicBaseUrl")}
|
||||
placeholder="Default: https://api.anthropic.com"
|
||||
label="Use custom base URL"
|
||||
/>
|
||||
|
||||
{showModelOptions && (
|
||||
<>
|
||||
<ModelSelector
|
||||
models={anthropicModels}
|
||||
selectedModelId={selectedModelId}
|
||||
onChange={handleInputChange("apiModelId")}
|
||||
label="Model"
|
||||
/>
|
||||
|
||||
{SUPPORTED_THINKING_MODELS.includes(selectedModelId) && setApiConfiguration && (
|
||||
<ThinkingBudgetSlider
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfiguration={setApiConfiguration}
|
||||
maxBudget={selectedModelInfo.thinkingConfig?.maxBudget}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { ApiConfiguration, askSageModels, askSageDefaultURL } from "@shared/api"
|
||||
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import { ApiKeyField } from "../common/ApiKeyField"
|
||||
import { ModelSelector } from "../common/ModelSelector"
|
||||
import { ModelInfoView } from "../common/ModelInfoView"
|
||||
import { normalizeApiConfiguration } from "../utils/providerUtils"
|
||||
|
||||
/**
|
||||
* Props for the AskSageProvider component
|
||||
*/
|
||||
interface AskSageProviderProps {
|
||||
apiConfiguration: ApiConfiguration
|
||||
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
|
||||
showModelOptions: boolean
|
||||
isPopup?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The AskSage provider configuration component
|
||||
*/
|
||||
export const AskSageProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: AskSageProviderProps) => {
|
||||
// Get the normalized configuration
|
||||
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ApiKeyField
|
||||
value={apiConfiguration?.asksageApiKey || ""}
|
||||
onChange={handleInputChange("asksageApiKey")}
|
||||
providerName="AskSage"
|
||||
helpText="This key is stored locally and only used to make API requests from this extension."
|
||||
/>
|
||||
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.asksageApiUrl || askSageDefaultURL}
|
||||
style={{ width: "100%" }}
|
||||
type="url"
|
||||
onInput={handleInputChange("asksageApiUrl")}
|
||||
placeholder="Enter AskSage API URL...">
|
||||
<span style={{ fontWeight: 500 }}>AskSage API URL</span>
|
||||
</VSCodeTextField>
|
||||
|
||||
{showModelOptions && (
|
||||
<>
|
||||
<ModelSelector
|
||||
models={askSageModels}
|
||||
selectedModelId={selectedModelId}
|
||||
onChange={handleInputChange("apiModelId")}
|
||||
label="Model"
|
||||
/>
|
||||
|
||||
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,311 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { ApiConfiguration, cerebrasModels } 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 CerebrasProvider component
|
||||
*/
|
||||
interface CerebrasProviderProps {
|
||||
apiConfiguration: ApiConfiguration
|
||||
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
|
||||
showModelOptions: boolean
|
||||
isPopup?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The Cerebras provider configuration component
|
||||
*/
|
||||
export const CerebrasProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: CerebrasProviderProps) => {
|
||||
// Get the normalized configuration
|
||||
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ApiKeyField
|
||||
value={apiConfiguration?.cerebrasApiKey || ""}
|
||||
onChange={handleInputChange("cerebrasApiKey")}
|
||||
providerName="Cerebras"
|
||||
signupUrl="https://cloud.cerebras.ai/"
|
||||
/>
|
||||
|
||||
{showModelOptions && (
|
||||
<>
|
||||
<ModelSelector
|
||||
models={cerebrasModels}
|
||||
selectedModelId={selectedModelId}
|
||||
onChange={handleInputChange("apiModelId")}
|
||||
label="Model"
|
||||
/>
|
||||
|
||||
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { ApiConfiguration, claudeCodeModels } from "@shared/api"
|
||||
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import { ModelSelector } from "../common/ModelSelector"
|
||||
import { ModelInfoView } from "../common/ModelInfoView"
|
||||
import { normalizeApiConfiguration } from "../utils/providerUtils"
|
||||
|
||||
/**
|
||||
* Props for the ClaudeCodeProvider component
|
||||
*/
|
||||
interface ClaudeCodeProviderProps {
|
||||
apiConfiguration: ApiConfiguration
|
||||
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
|
||||
showModelOptions: boolean
|
||||
isPopup?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The Claude Code provider configuration component
|
||||
*/
|
||||
export const ClaudeCodeProvider = ({
|
||||
apiConfiguration,
|
||||
handleInputChange,
|
||||
showModelOptions,
|
||||
isPopup,
|
||||
}: ClaudeCodeProviderProps) => {
|
||||
// Get the normalized configuration
|
||||
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.claudeCodePath || ""}
|
||||
style={{ width: "100%", marginTop: 3 }}
|
||||
type="text"
|
||||
onInput={handleInputChange("claudeCodePath")}
|
||||
placeholder="Default: claude">
|
||||
<span style={{ fontWeight: 500 }}>Claude Code CLI Path</span>
|
||||
</VSCodeTextField>
|
||||
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: 3,
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
Path to the Claude Code CLI.
|
||||
</p>
|
||||
|
||||
{showModelOptions && (
|
||||
<>
|
||||
<ModelSelector
|
||||
models={claudeCodeModels}
|
||||
selectedModelId={selectedModelId}
|
||||
onChange={handleInputChange("apiModelId")}
|
||||
label="Model"
|
||||
/>
|
||||
|
||||
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
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,47 +0,0 @@
|
||||
import { ApiConfiguration, doubaoModels } 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 DoubaoProvider component
|
||||
*/
|
||||
interface DoubaoProviderProps {
|
||||
apiConfiguration: ApiConfiguration
|
||||
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
|
||||
showModelOptions: boolean
|
||||
isPopup?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The ByteDance Doubao provider configuration component
|
||||
*/
|
||||
export const DoubaoProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: DoubaoProviderProps) => {
|
||||
// Get the normalized configuration
|
||||
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ApiKeyField
|
||||
value={apiConfiguration?.doubaoApiKey || ""}
|
||||
onChange={handleInputChange("doubaoApiKey")}
|
||||
providerName="Doubao"
|
||||
signupUrl="https://console.volcengine.com/home"
|
||||
/>
|
||||
|
||||
{showModelOptions && (
|
||||
<>
|
||||
<ModelSelector
|
||||
models={doubaoModels}
|
||||
selectedModelId={selectedModelId}
|
||||
onChange={handleInputChange("apiModelId")}
|
||||
label="Model"
|
||||
/>
|
||||
|
||||
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import { ApiConfiguration } from "@shared/api"
|
||||
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import { ApiKeyField } from "../common/ApiKeyField"
|
||||
|
||||
/**
|
||||
* Props for the FireworksProvider component
|
||||
*/
|
||||
interface FireworksProviderProps {
|
||||
apiConfiguration: ApiConfiguration
|
||||
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
|
||||
showModelOptions: boolean
|
||||
isPopup?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The Fireworks provider configuration component
|
||||
*/
|
||||
export const FireworksProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: FireworksProviderProps) => {
|
||||
// Handler for number input fields with validation
|
||||
const handleNumberInputChange = (field: keyof ApiConfiguration) => (e: any) => {
|
||||
const value = (e.target as HTMLInputElement).value
|
||||
if (!value) {
|
||||
return
|
||||
}
|
||||
const num = parseInt(value, 10)
|
||||
if (isNaN(num)) {
|
||||
return
|
||||
}
|
||||
handleInputChange(field)({
|
||||
target: {
|
||||
value: num,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ApiKeyField
|
||||
value={apiConfiguration?.fireworksApiKey || ""}
|
||||
onChange={handleInputChange("fireworksApiKey")}
|
||||
providerName="Fireworks"
|
||||
signupUrl="https://fireworks.ai/settings/users/api-keys"
|
||||
/>
|
||||
|
||||
{showModelOptions && (
|
||||
<>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.fireworksModelId || ""}
|
||||
style={{ width: "100%" }}
|
||||
onInput={handleInputChange("fireworksModelId")}
|
||||
placeholder={"Enter Model ID..."}>
|
||||
<span style={{ fontWeight: 500 }}>Model ID</span>
|
||||
</VSCodeTextField>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: 3,
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
<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>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.fireworksModelMaxCompletionTokens?.toString() || ""}
|
||||
style={{ width: "100%", marginBottom: 8 }}
|
||||
onInput={handleNumberInputChange("fireworksModelMaxCompletionTokens")}
|
||||
placeholder={"2000"}>
|
||||
<span style={{ fontWeight: 500 }}>Max Completion Tokens</span>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.fireworksModelMaxTokens?.toString() || ""}
|
||||
style={{ width: "100%", marginBottom: 8 }}
|
||||
onInput={handleNumberInputChange("fireworksModelMaxTokens")}
|
||||
placeholder={"4000"}>
|
||||
<span style={{ fontWeight: 500 }}>Max Context Tokens</span>
|
||||
</VSCodeTextField>
|
||||
</>
|
||||
)}
|
||||
</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)
|
||||
@@ -1,79 +0,0 @@
|
||||
import { ApiConfiguration, geminiModels } from "@shared/api"
|
||||
import { ApiKeyField } from "../common/ApiKeyField"
|
||||
import { BaseUrlField } from "../common/BaseUrlField"
|
||||
import { ModelSelector } from "../common/ModelSelector"
|
||||
import { ModelInfoView } from "../common/ModelInfoView"
|
||||
import { normalizeApiConfiguration } from "../utils/providerUtils"
|
||||
import ThinkingBudgetSlider from "../ThinkingBudgetSlider"
|
||||
|
||||
// Gemini models that support thinking/reasoning mode
|
||||
const SUPPORTED_THINKING_MODELS = ["gemini-2.5-pro", "gemini-2.5-flash"]
|
||||
|
||||
/**
|
||||
* Props for the GeminiProvider component
|
||||
*/
|
||||
interface GeminiProviderProps {
|
||||
apiConfiguration: ApiConfiguration
|
||||
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
|
||||
showModelOptions: boolean
|
||||
isPopup?: boolean
|
||||
setApiConfiguration?: (config: ApiConfiguration) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* The Gemini provider configuration component
|
||||
*/
|
||||
export const GeminiProvider = ({
|
||||
apiConfiguration,
|
||||
handleInputChange,
|
||||
showModelOptions,
|
||||
isPopup,
|
||||
setApiConfiguration,
|
||||
}: GeminiProviderProps) => {
|
||||
// Get the normalized configuration
|
||||
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
|
||||
|
||||
// Create a wrapper for handling field changes more directly
|
||||
const handleFieldChange = (field: keyof ApiConfiguration) => (value: string) => {
|
||||
handleInputChange(field)({ target: { value } })
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ApiKeyField
|
||||
value={apiConfiguration?.geminiApiKey || ""}
|
||||
onChange={handleInputChange("geminiApiKey")}
|
||||
providerName="Gemini"
|
||||
signupUrl="https://aistudio.google.com/apikey"
|
||||
/>
|
||||
|
||||
<BaseUrlField
|
||||
value={apiConfiguration?.geminiBaseUrl}
|
||||
onChange={handleFieldChange("geminiBaseUrl")}
|
||||
placeholder="Default: https://generativelanguage.googleapis.com"
|
||||
label="Use custom base URL"
|
||||
/>
|
||||
|
||||
{showModelOptions && (
|
||||
<>
|
||||
<ModelSelector
|
||||
models={geminiModels}
|
||||
selectedModelId={selectedModelId}
|
||||
onChange={handleInputChange("apiModelId")}
|
||||
label="Model"
|
||||
/>
|
||||
|
||||
{SUPPORTED_THINKING_MODELS.includes(selectedModelId) && setApiConfiguration && (
|
||||
<ThinkingBudgetSlider
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfiguration={setApiConfiguration}
|
||||
maxBudget={selectedModelInfo.thinkingConfig?.maxBudget}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
import { ApiConfiguration } from "@shared/api"
|
||||
import { VSCodeTextField, 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 OllamaModelPicker from "../OllamaModelPicker"
|
||||
import { BaseUrlField } from "../common/BaseUrlField"
|
||||
|
||||
/**
|
||||
* Props for the OllamaProvider component
|
||||
*/
|
||||
interface OllamaProviderProps {
|
||||
apiConfiguration: ApiConfiguration
|
||||
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
|
||||
showModelOptions: boolean
|
||||
isPopup?: boolean
|
||||
setApiConfiguration: (config: ApiConfiguration) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* The Ollama provider configuration component
|
||||
*/
|
||||
export const OllamaProvider = ({
|
||||
apiConfiguration,
|
||||
handleInputChange,
|
||||
showModelOptions,
|
||||
isPopup,
|
||||
setApiConfiguration,
|
||||
}: OllamaProviderProps) => {
|
||||
const [ollamaModels, setOllamaModels] = useState<string[]>([])
|
||||
|
||||
// Poll ollama models
|
||||
const requestOllamaModels = useCallback(async () => {
|
||||
try {
|
||||
const response = await ModelsServiceClient.getOllamaModels(
|
||||
StringRequest.create({
|
||||
value: apiConfiguration?.ollamaBaseUrl || "",
|
||||
}),
|
||||
)
|
||||
if (response && response.values) {
|
||||
setOllamaModels(response.values)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch Ollama models:", error)
|
||||
setOllamaModels([])
|
||||
}
|
||||
}, [apiConfiguration?.ollamaBaseUrl])
|
||||
|
||||
useEffect(() => {
|
||||
requestOllamaModels()
|
||||
}, [requestOllamaModels])
|
||||
|
||||
useInterval(requestOllamaModels, 2000)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<BaseUrlField
|
||||
value={apiConfiguration?.ollamaBaseUrl}
|
||||
onChange={(value) => handleInputChange("ollamaBaseUrl")({ target: { value } })}
|
||||
placeholder="Default: http://localhost:11434"
|
||||
label="Use custom base URL"
|
||||
/>
|
||||
|
||||
{/* Model selection - use filterable picker */}
|
||||
<label htmlFor="ollama-model-selection">
|
||||
<span style={{ fontWeight: 500 }}>Model</span>
|
||||
</label>
|
||||
<OllamaModelPicker
|
||||
ollamaModels={ollamaModels}
|
||||
selectedModelId={apiConfiguration?.ollamaModelId || ""}
|
||||
onModelChange={(modelId) => {
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
ollamaModelId: modelId,
|
||||
})
|
||||
}}
|
||||
placeholder={ollamaModels.length > 0 ? "Search and select a model..." : "e.g. llama3.1"}
|
||||
/>
|
||||
|
||||
{/* Show status message based on model availability */}
|
||||
{ollamaModels.length === 0 && (
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: "3px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
fontStyle: "italic",
|
||||
}}>
|
||||
Unable to fetch models from Ollama server. Please ensure Ollama is running and accessible, or enter the model
|
||||
ID manually above.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.ollamaApiOptionsCtxNum || "32768"}
|
||||
style={{ width: "100%" }}
|
||||
onInput={handleInputChange("ollamaApiOptionsCtxNum")}
|
||||
placeholder={"e.g. 32768"}>
|
||||
<span style={{ fontWeight: 500 }}>Model Context Window</span>
|
||||
</VSCodeTextField>
|
||||
|
||||
{showModelOptions && (
|
||||
<>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.requestTimeoutMs ? apiConfiguration.requestTimeoutMs.toString() : "30000"}
|
||||
style={{ width: "100%" }}
|
||||
onInput={(e: any) => {
|
||||
const value = e.target.value
|
||||
// Convert to number, with validation
|
||||
const numValue = parseInt(value, 10)
|
||||
if (!isNaN(numValue) && numValue > 0) {
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
requestTimeoutMs: numValue,
|
||||
})
|
||||
}
|
||||
}}
|
||||
placeholder="Default: 30000 (30 seconds)">
|
||||
<span style={{ fontWeight: 500 }}>Request Timeout (ms)</span>
|
||||
</VSCodeTextField>
|
||||
<p style={{ fontSize: "12px", marginTop: 3, color: "var(--vscode-descriptionForeground)" }}>
|
||||
Maximum time in milliseconds to wait for API responses before timing out.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: "5px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
Ollama allows you to run models locally on your computer. For instructions on how to get started, see their{" "}
|
||||
<VSCodeLink
|
||||
href="https://github.com/ollama/ollama/blob/main/README.md"
|
||||
style={{ display: "inline", fontSize: "inherit" }}>
|
||||
quickstart guide.
|
||||
</VSCodeLink>{" "}
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { ApiConfiguration, openAiNativeModels } 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 OpenAINativeProvider component
|
||||
*/
|
||||
interface OpenAINativeProviderProps {
|
||||
apiConfiguration: ApiConfiguration
|
||||
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
|
||||
showModelOptions: boolean
|
||||
isPopup?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The OpenAI (native) provider configuration component
|
||||
*/
|
||||
export const OpenAINativeProvider = ({
|
||||
apiConfiguration,
|
||||
handleInputChange,
|
||||
showModelOptions,
|
||||
isPopup,
|
||||
}: OpenAINativeProviderProps) => {
|
||||
// Get the normalized configuration
|
||||
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ApiKeyField
|
||||
value={apiConfiguration?.openAiNativeApiKey || ""}
|
||||
onChange={handleInputChange("openAiNativeApiKey")}
|
||||
providerName="OpenAI"
|
||||
signupUrl="https://platform.openai.com/api-keys"
|
||||
/>
|
||||
|
||||
{showModelOptions && (
|
||||
<>
|
||||
<ModelSelector
|
||||
models={openAiNativeModels}
|
||||
selectedModelId={selectedModelId}
|
||||
onChange={handleInputChange("apiModelId")}
|
||||
label="Model"
|
||||
/>
|
||||
|
||||
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { ApiConfiguration } from "@shared/api"
|
||||
import { ApiKeyField } from "../common/ApiKeyField"
|
||||
import RequestyModelPicker from "../RequestyModelPicker"
|
||||
|
||||
/**
|
||||
* Props for the RequestyProvider component
|
||||
*/
|
||||
interface RequestyProviderProps {
|
||||
apiConfiguration: ApiConfiguration
|
||||
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
|
||||
showModelOptions: boolean
|
||||
isPopup?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The Requesty provider configuration component
|
||||
*/
|
||||
export const RequestyProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: RequestyProviderProps) => {
|
||||
return (
|
||||
<div>
|
||||
<ApiKeyField
|
||||
value={apiConfiguration?.requestyApiKey || ""}
|
||||
onChange={handleInputChange("requestyApiKey")}
|
||||
providerName="Requesty"
|
||||
signupUrl="https://app.requesty.ai/manage-api"
|
||||
/>
|
||||
|
||||
{showModelOptions && <RequestyModelPicker isPopup={isPopup} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
import { ApiConfiguration, sapAiCoreModels } from "@shared/api"
|
||||
import { VSCodeTextField, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
import { ModelSelector } from "../common/ModelSelector"
|
||||
import { ModelInfoView } from "../common/ModelInfoView"
|
||||
import { normalizeApiConfiguration } from "../utils/providerUtils"
|
||||
|
||||
/**
|
||||
* Props for the SapAiCoreProvider component
|
||||
*/
|
||||
interface SapAiCoreProviderProps {
|
||||
apiConfiguration: ApiConfiguration
|
||||
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
|
||||
showModelOptions: boolean
|
||||
isPopup?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The SAP AI Core provider configuration component
|
||||
*/
|
||||
export const SapAiCoreProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: SapAiCoreProviderProps) => {
|
||||
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.sapAiCoreClientId || ""}
|
||||
style={{ width: "100%" }}
|
||||
type="password"
|
||||
onInput={handleInputChange("sapAiCoreClientId")}
|
||||
placeholder="Enter AI Core Client Id...">
|
||||
<span style={{ fontWeight: 500 }}>AI Core Client Id</span>
|
||||
</VSCodeTextField>
|
||||
{apiConfiguration?.sapAiCoreClientId && (
|
||||
<p style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)" }}>
|
||||
Client Id is set. To change it, please re-enter the value.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.sapAiCoreClientSecret ? "********" : ""}
|
||||
style={{ width: "100%" }}
|
||||
type="password"
|
||||
onInput={handleInputChange("sapAiCoreClientSecret")}
|
||||
placeholder="Enter AI Core Client Secret...">
|
||||
<span style={{ fontWeight: 500 }}>AI Core Client Secret</span>
|
||||
</VSCodeTextField>
|
||||
{apiConfiguration?.sapAiCoreClientSecret && (
|
||||
<p style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)" }}>
|
||||
Client Secret is set. To change it, please re-enter the value.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.sapAiCoreBaseUrl || ""}
|
||||
style={{ width: "100%" }}
|
||||
onInput={handleInputChange("sapAiCoreBaseUrl")}
|
||||
placeholder="Enter AI Core Base URL...">
|
||||
<span style={{ fontWeight: 500 }}>AI Core Base URL</span>
|
||||
</VSCodeTextField>
|
||||
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.sapAiCoreTokenUrl || ""}
|
||||
style={{ width: "100%" }}
|
||||
onInput={handleInputChange("sapAiCoreTokenUrl")}
|
||||
placeholder="Enter AI Core Auth URL...">
|
||||
<span style={{ fontWeight: 500 }}>AI Core Auth URL</span>
|
||||
</VSCodeTextField>
|
||||
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.sapAiResourceGroup || ""}
|
||||
style={{ width: "100%" }}
|
||||
onInput={handleInputChange("sapAiResourceGroup")}
|
||||
placeholder="Enter AI Core Resource Group...">
|
||||
<span style={{ fontWeight: 500 }}>AI Core Resource Group</span>
|
||||
</VSCodeTextField>
|
||||
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: "5px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
These credentials are stored locally and only used to make API requests from this extension.
|
||||
<VSCodeLink
|
||||
href="https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/access-sap-ai-core-via-api"
|
||||
style={{ display: "inline" }}>
|
||||
You can find more information about SAP AI Core API access here.
|
||||
</VSCodeLink>
|
||||
</p>
|
||||
|
||||
{showModelOptions && (
|
||||
<>
|
||||
<ModelSelector
|
||||
models={sapAiCoreModels}
|
||||
selectedModelId={selectedModelId}
|
||||
onChange={handleInputChange("apiModelId")}
|
||||
label="Model"
|
||||
/>
|
||||
|
||||
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
import { ApiConfiguration, vertexGlobalModels, vertexModels } from "@shared/api"
|
||||
import { VSCodeTextField, VSCodeDropdown, VSCodeOption, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
import { ModelSelector } from "../common/ModelSelector"
|
||||
import { ModelInfoView } from "../common/ModelInfoView"
|
||||
import { normalizeApiConfiguration } from "../utils/providerUtils"
|
||||
import { DropdownContainer, DROPDOWN_Z_INDEX } from "../ApiOptions"
|
||||
import ThinkingBudgetSlider from "../ThinkingBudgetSlider"
|
||||
|
||||
/**
|
||||
* Props for the VertexProvider component
|
||||
*/
|
||||
interface VertexProviderProps {
|
||||
apiConfiguration: ApiConfiguration
|
||||
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
|
||||
showModelOptions: boolean
|
||||
isPopup?: boolean
|
||||
setApiConfiguration: (config: ApiConfiguration) => void
|
||||
}
|
||||
|
||||
// Vertex models that support thinking
|
||||
const SUPPORTED_THINKING_MODELS = [
|
||||
"claude-3-7-sonnet@20250219",
|
||||
"claude-sonnet-4@20250514",
|
||||
"claude-opus-4@20250514",
|
||||
"gemini-2.5-flash",
|
||||
"gemini-2.5-pro",
|
||||
]
|
||||
|
||||
/**
|
||||
* The GCP Vertex AI provider configuration component
|
||||
*/
|
||||
export const VertexProvider = ({
|
||||
apiConfiguration,
|
||||
handleInputChange,
|
||||
showModelOptions,
|
||||
isPopup,
|
||||
setApiConfiguration,
|
||||
}: VertexProviderProps) => {
|
||||
// Get the normalized configuration
|
||||
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
|
||||
|
||||
// Determine which models to use based on region
|
||||
const modelsToUse = apiConfiguration?.vertexRegion === "global" ? vertexGlobalModels : vertexModels
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 5,
|
||||
}}>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.vertexProjectId || ""}
|
||||
style={{ width: "100%" }}
|
||||
onInput={handleInputChange("vertexProjectId")}
|
||||
placeholder="Enter Project ID...">
|
||||
<span style={{ fontWeight: 500 }}>Google Cloud Project ID</span>
|
||||
</VSCodeTextField>
|
||||
|
||||
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 1} className="dropdown-container">
|
||||
<label htmlFor="vertex-region-dropdown">
|
||||
<span style={{ fontWeight: 500 }}>Google Cloud Region</span>
|
||||
</label>
|
||||
<VSCodeDropdown
|
||||
id="vertex-region-dropdown"
|
||||
value={apiConfiguration?.vertexRegion || ""}
|
||||
style={{ width: "100%" }}
|
||||
onChange={handleInputChange("vertexRegion")}>
|
||||
<VSCodeOption value="">Select a region...</VSCodeOption>
|
||||
<VSCodeOption value="us-east5">us-east5</VSCodeOption>
|
||||
<VSCodeOption value="us-central1">us-central1</VSCodeOption>
|
||||
<VSCodeOption value="europe-west1">europe-west1</VSCodeOption>
|
||||
<VSCodeOption value="europe-west4">europe-west4</VSCodeOption>
|
||||
<VSCodeOption value="asia-southeast1">asia-southeast1</VSCodeOption>
|
||||
<VSCodeOption value="global">global</VSCodeOption>
|
||||
</VSCodeDropdown>
|
||||
</DropdownContainer>
|
||||
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: "5px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
To use Google Cloud Vertex AI, you need to
|
||||
<VSCodeLink
|
||||
href="https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#before_you_begin"
|
||||
style={{ display: "inline", fontSize: "inherit" }}>
|
||||
{"1) create a Google Cloud account › enable the Vertex AI API › enable the desired Claude models,"}
|
||||
</VSCodeLink>{" "}
|
||||
<VSCodeLink
|
||||
href="https://cloud.google.com/docs/authentication/provide-credentials-adc#google-idp"
|
||||
style={{ display: "inline", fontSize: "inherit" }}>
|
||||
{"2) install the Google Cloud CLI › configure Application Default Credentials."}
|
||||
</VSCodeLink>
|
||||
</p>
|
||||
|
||||
{showModelOptions && (
|
||||
<>
|
||||
<ModelSelector
|
||||
models={modelsToUse}
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
import { ApiConfiguration, xaiModels } from "@shared/api"
|
||||
import { VSCodeCheckbox, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useState } from "react"
|
||||
import { ApiKeyField } from "../common/ApiKeyField"
|
||||
import { ModelSelector, DropdownContainer } from "../common/ModelSelector"
|
||||
import { ModelInfoView } from "../common/ModelInfoView"
|
||||
import { normalizeApiConfiguration } from "../utils/providerUtils"
|
||||
import { DROPDOWN_Z_INDEX } from "../ApiOptions"
|
||||
|
||||
/**
|
||||
* Props for the XaiProvider component
|
||||
*/
|
||||
interface XaiProviderProps {
|
||||
apiConfiguration: ApiConfiguration
|
||||
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
|
||||
showModelOptions: boolean
|
||||
isPopup?: boolean
|
||||
setApiConfiguration: (config: ApiConfiguration) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* The xAI provider configuration component
|
||||
*/
|
||||
export const XaiProvider = ({
|
||||
apiConfiguration,
|
||||
handleInputChange,
|
||||
showModelOptions,
|
||||
isPopup,
|
||||
setApiConfiguration,
|
||||
}: XaiProviderProps) => {
|
||||
// Get the normalized configuration
|
||||
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
|
||||
|
||||
// Local state for reasoning effort toggle
|
||||
const [reasoningEffortSelected, setReasoningEffortSelected] = useState(!!apiConfiguration?.reasoningEffort)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<ApiKeyField
|
||||
value={apiConfiguration?.xaiApiKey || ""}
|
||||
onChange={handleInputChange("xaiApiKey")}
|
||||
providerName="X AI"
|
||||
signupUrl="https://x.ai"
|
||||
/>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: -10,
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
<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>
|
||||
|
||||
{showModelOptions && (
|
||||
<>
|
||||
<ModelSelector
|
||||
models={xaiModels}
|
||||
selectedModelId={selectedModelId}
|
||||
onChange={handleInputChange("apiModelId")}
|
||||
label="Model"
|
||||
/>
|
||||
|
||||
{selectedModelId && selectedModelId.includes("3-mini") && (
|
||||
<>
|
||||
<VSCodeCheckbox
|
||||
style={{ marginTop: 0 }}
|
||||
checked={reasoningEffortSelected}
|
||||
onChange={(e: any) => {
|
||||
const isChecked = e.target.checked === true
|
||||
setReasoningEffortSelected(isChecked)
|
||||
if (!isChecked) {
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
reasoningEffort: "",
|
||||
})
|
||||
}
|
||||
}}>
|
||||
Modify reasoning effort
|
||||
</VSCodeCheckbox>
|
||||
|
||||
{reasoningEffortSelected && (
|
||||
<div>
|
||||
<label htmlFor="reasoning-effort-dropdown">
|
||||
<span style={{}}>Reasoning Effort</span>
|
||||
</label>
|
||||
<DropdownContainer className="dropdown-container" zIndex={DROPDOWN_Z_INDEX - 100}>
|
||||
<VSCodeDropdown
|
||||
id="reasoning-effort-dropdown"
|
||||
style={{ width: "100%", marginTop: 3 }}
|
||||
value={apiConfiguration?.reasoningEffort || "high"}
|
||||
onChange={(e: any) => {
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
reasoningEffort: e.target.value,
|
||||
})
|
||||
}}>
|
||||
<VSCodeOption value="low">low</VSCodeOption>
|
||||
<VSCodeOption value="high">high</VSCodeOption>
|
||||
</VSCodeDropdown>
|
||||
</DropdownContainer>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: 3,
|
||||
marginBottom: 0,
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
High effort may produce more thorough analysis but takes longer and uses more tokens.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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":
|
||||
|
||||
@@ -50,8 +50,8 @@ const WelcomeView = memo(() => {
|
||||
</div>
|
||||
<p>
|
||||
I can do all kinds of tasks thanks to breakthroughs in{" "}
|
||||
<VSCodeLink href="https://www.anthropic.com/claude/sonnet" className="inline">
|
||||
Claude 4 Sonnet's
|
||||
<VSCodeLink href="https://www.anthropic.com/news/claude-3-7-sonnet" className="inline">
|
||||
Claude 3.7 Sonnet's
|
||||
</VSCodeLink>
|
||||
agentic coding capabilities and access to tools that let me create & edit files, explore complex projects, use
|
||||
a browser, and execute terminal commands <i>(with your permission, of course)</i>. I can even use MCP to
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user