mirror of
https://github.com/cline/cline.git
synced 2026-09-07 04:44:58 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e4f6783140 | |||
| 174c098cdf |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix issue with Cline accounts not showing user info in popout tabs
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Drag and drop of file/folders into cline chat
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
add coverage tests to github workflows
|
||||
+40
-286
@@ -11,12 +11,10 @@ graph TB
|
||||
subgraph VSCode Extension Host
|
||||
subgraph Core Extension
|
||||
ExtensionEntry[Extension Entry<br/>src/extension.ts]
|
||||
WebviewProvider[WebviewProvider<br/>src/core/webview/index.ts]
|
||||
Controller[Controller<br/>src/core/controller/index.ts]
|
||||
Task[Task<br/>src/core/task/index.ts]
|
||||
ClineProvider[ClineProvider<br/>src/core/webview/ClineProvider.ts]
|
||||
ClineClass[Cline Class<br/>src/core/Cline.ts]
|
||||
GlobalState[VSCode Global State]
|
||||
SecretsStorage[VSCode Secrets Storage]
|
||||
McpHub[McpHub<br/>src/services/mcp/McpHub.ts]
|
||||
end
|
||||
|
||||
subgraph Webview UI
|
||||
@@ -29,101 +27,45 @@ graph TB
|
||||
TaskStorage[Task Storage<br/>Per-Task Files & History]
|
||||
CheckpointSystem[Git-based Checkpoints]
|
||||
end
|
||||
|
||||
subgraph API Providers
|
||||
AnthropicAPI[Anthropic]
|
||||
OpenRouterAPI[OpenRouter]
|
||||
BedrockAPI[AWS Bedrock]
|
||||
OtherAPIs[Other Providers]
|
||||
end
|
||||
|
||||
subgraph MCP Servers
|
||||
ExternalMcpServers[External MCP Servers]
|
||||
end
|
||||
end
|
||||
|
||||
%% Core Extension Data Flow
|
||||
ExtensionEntry --> WebviewProvider
|
||||
WebviewProvider --> Controller
|
||||
Controller --> Task
|
||||
Controller --> McpHub
|
||||
Task --> GlobalState
|
||||
Task --> SecretsStorage
|
||||
Task --> TaskStorage
|
||||
Task --> CheckpointSystem
|
||||
Task --> |API Requests| API Providers
|
||||
McpHub --> |Connects to| ExternalMcpServers
|
||||
Task --> |Uses| McpHub
|
||||
ExtensionEntry --> ClineProvider
|
||||
ClineProvider --> ClineClass
|
||||
ClineClass --> GlobalState
|
||||
ClineClass --> SecretsStorage
|
||||
ClineClass --> TaskStorage
|
||||
ClineClass --> CheckpointSystem
|
||||
|
||||
%% Webview Data Flow
|
||||
WebviewApp --> ExtStateContext
|
||||
ExtStateContext --> ReactComponents
|
||||
|
||||
%% Bidirectional Communication
|
||||
WebviewProvider <-->|postMessage| ExtStateContext
|
||||
ClineProvider <-->|postMessage| ExtStateContext
|
||||
|
||||
style GlobalState fill:#f9f,stroke:#333,stroke-width:2px
|
||||
style SecretsStorage fill:#f9f,stroke:#333,stroke-width:2px
|
||||
style ExtStateContext fill:#bbf,stroke:#333,stroke-width:2px
|
||||
style WebviewProvider fill:#bfb,stroke:#333,stroke-width:2px
|
||||
style McpHub fill:#bfb,stroke:#333,stroke-width:2px
|
||||
style API Providers fill:#fdb,stroke:#333,stroke-width:2px
|
||||
style ClineProvider fill:#bfb,stroke:#333,stroke-width:2px
|
||||
```
|
||||
|
||||
## Definitions
|
||||
|
||||
- **Core Extension**: Anything inside the src folder, organized into modular components
|
||||
- **Core Extension State**: Managed by the Controller class in src/core/controller/index.ts, which serves as the single source of truth for the extension's state. It manages multiple types of persistent storage (global state, workspace state, and secrets), handles state distribution to both the core extension and webview components, and coordinates state across multiple extension instances. This includes managing API configurations, task history, settings, and MCP configurations.
|
||||
- **Webview**: Anything inside the webview-ui. All the react or view's seen by the user and user interaction components
|
||||
- **Webview State**: Managed by ExtensionStateContext in webview-ui/src/context/ExtensionStateContext.tsx, which provides React components with access to the extension's state through a context provider pattern. It maintains local state for UI components, handles real-time updates through message events, manages partial message updates, and provides methods for state modifications. The context includes extension version, messages, task history, theme, API configurations, MCP servers, marketplace catalog, and workspace file paths. It synchronizes with the core extension through VSCode's message passing system and provides type-safe access to state through a custom hook (useExtensionState).
|
||||
|
||||
### Core Extension Architecture
|
||||
|
||||
The core extension follows a clear hierarchical structure:
|
||||
|
||||
1. **WebviewProvider** (src/core/webview/index.ts): Manages the webview lifecycle and communication
|
||||
2. **Controller** (src/core/controller/index.ts): Handles webview messages and task management
|
||||
3. **Task** (src/core/task/index.ts): Executes API requests and tool operations
|
||||
|
||||
This architecture provides clear separation of concerns:
|
||||
- WebviewProvider focuses on VSCode webview integration
|
||||
- Controller manages state and coordinates tasks
|
||||
- Task handles the execution of AI requests and tool operations
|
||||
|
||||
### WebviewProvider Implementation
|
||||
|
||||
The WebviewProvider class in `src/core/webview/index.ts` is responsible for:
|
||||
|
||||
- Managing multiple active instances through a static set (`activeInstances`)
|
||||
- Handling webview lifecycle events (creation, visibility changes, disposal)
|
||||
- Implementing HTML content generation with proper CSP headers
|
||||
- Supporting Hot Module Replacement (HMR) for development
|
||||
- Setting up message listeners between the webview and extension
|
||||
|
||||
The WebviewProvider maintains a reference to the Controller and delegates message handling to it. It also handles the creation of both sidebar and tab panel webviews, allowing Cline to be used in different contexts within VSCode.
|
||||
- core extension: Anything inside the src folder starting with the Cline.ts file
|
||||
- core extension state: Managed by the ClineProvider class in src/core/webview/ClineProvider.ts, which serves as the single source of truth for the extension's state. It manages multiple types of persistent storage (global state, workspace state, and secrets), handles state distribution to both the core extension and webview components, and coordinates state across multiple extension instances. This includes managing API configurations, task history, settings, and MCP configurations.
|
||||
- webview: Anything inside the webview-ui. All the react or view's seen by the user and user interaction compone
|
||||
- webview state: Managed by ExtensionStateContext in webview-ui/src/context/ExtensionStateContext.tsx, which provides React components with access to the extension's state through a context provider pattern. It maintains local state for UI components, handles real-time updates through message events, manages partial message updates, and provides methods for state modifications. The context includes extension version, messages, task history, theme, API configurations, MCP servers, marketplace catalog, and workspace file paths. It synchronizes with the core extension through VSCode's message passing system and provides type-safe access to state through a custom hook (useExtensionState).
|
||||
|
||||
### Core Extension State
|
||||
|
||||
The `Controller` class manages multiple types of persistent storage:
|
||||
The `ClineProvider` class manages multiple types of persistent storage:
|
||||
|
||||
- **Global State:** Stored across all VSCode instances. Used for settings and data that should persist globally.
|
||||
- **Workspace State:** Specific to the current workspace. Used for task-specific data and settings.
|
||||
- **Secrets:** Secure storage for sensitive information like API keys.
|
||||
|
||||
The `Controller` handles the distribution of state to both the core extension and webview components. It also coordinates state across multiple extension instances, ensuring consistency.
|
||||
|
||||
State synchronization between instances is handled through:
|
||||
- File-based storage for task history and conversation data
|
||||
- VSCode's global state API for settings and configuration
|
||||
- Secrets storage for sensitive information
|
||||
- Event listeners for file changes and configuration updates
|
||||
|
||||
The Controller implements methods for:
|
||||
- Saving and loading task state
|
||||
- Managing API configurations
|
||||
- Handling user authentication
|
||||
- Coordinating MCP server connections
|
||||
- Managing task history and checkpoints
|
||||
The `ClineProvider` handles the distribution of state to both the core extension and webview components. It also coordinates state across multiple extension instances, ensuring consistency.
|
||||
|
||||
### Webview State
|
||||
|
||||
@@ -140,65 +82,16 @@ The `ExtensionStateContext` in `webview-ui/src/context/ExtensionStateContext.tsx
|
||||
|
||||
It synchronizes with the core extension through VSCode's message passing system and provides type-safe access to the state via a custom hook (`useExtensionState`).
|
||||
|
||||
The ExtensionStateContext handles:
|
||||
- Real-time updates through message events
|
||||
- Partial message updates for streaming content
|
||||
- State modifications through setter methods
|
||||
- Type-safe access to state through a custom hook
|
||||
## Core Extension (Cline.ts)
|
||||
|
||||
## API Provider System
|
||||
|
||||
Cline supports multiple AI providers through a modular API provider system. Each provider is implemented as a separate module in the `src/api/providers/` directory and follows a common interface.
|
||||
|
||||
### API Provider Architecture
|
||||
|
||||
The API system consists of:
|
||||
|
||||
1. **API Handlers**: Provider-specific implementations in `src/api/providers/`
|
||||
2. **API Transformers**: Stream transformation utilities in `src/api/transform/`
|
||||
3. **API Configuration**: User settings for API keys and endpoints
|
||||
4. **API Factory**: Builder function to create the appropriate handler
|
||||
|
||||
Key providers include:
|
||||
- **Anthropic**: Direct integration with Claude models
|
||||
- **OpenRouter**: Meta-provider supporting multiple model providers
|
||||
- **AWS Bedrock**: Integration with Amazon's AI services
|
||||
- **Gemini**: Google's AI models
|
||||
- **Ollama**: Local model hosting
|
||||
- **LM Studio**: Local model hosting
|
||||
- **VSCode LM**: VSCode's built-in language models
|
||||
|
||||
### API Configuration Management
|
||||
|
||||
API configurations are stored securely:
|
||||
- API keys are stored in VSCode's secrets storage
|
||||
- Model selections and non-sensitive settings are stored in global state
|
||||
- The Controller manages switching between providers and updating configurations
|
||||
|
||||
The system supports:
|
||||
- Secure storage of API keys
|
||||
- Model selection and configuration
|
||||
- Automatic retry and error handling
|
||||
- Token usage tracking and cost calculation
|
||||
- Context window management
|
||||
|
||||
### Plan/Act Mode API Configuration
|
||||
|
||||
Cline supports separate model configurations for Plan and Act modes:
|
||||
- Different models can be used for planning vs. execution
|
||||
- The system preserves model selections when switching modes
|
||||
- The Controller handles the transition between modes and updates the API configuration accordingly
|
||||
|
||||
## Task Execution System
|
||||
|
||||
The Task class is responsible for executing AI requests and tool operations. Each task runs in its own instance of the Task class, ensuring isolation and proper state management.
|
||||
The Cline class is the heart of the extension, managing task execution, state persistence, and tool coordination. Each task runs in its own instance of the Cline class, ensuring isolation and proper state management.
|
||||
|
||||
### Task Execution Loop
|
||||
|
||||
The core task execution loop follows this pattern:
|
||||
|
||||
```typescript
|
||||
class Task {
|
||||
class Cline {
|
||||
async initiateTaskLoop(userContent: UserContent, isNewTask: boolean) {
|
||||
while (!this.abort) {
|
||||
// 1. Make API request and stream response
|
||||
@@ -233,7 +126,7 @@ class Task {
|
||||
The streaming system handles real-time updates and partial content:
|
||||
|
||||
```typescript
|
||||
class Task {
|
||||
class Cline {
|
||||
async presentAssistantMessage() {
|
||||
// Handle streaming locks to prevent race conditions
|
||||
if (this.presentAssistantMessageLocked) {
|
||||
@@ -268,7 +161,7 @@ class Task {
|
||||
Tools follow a strict execution pattern:
|
||||
|
||||
```typescript
|
||||
class Task {
|
||||
class Cline {
|
||||
async executeToolWithApproval(block: ToolBlock) {
|
||||
// 1. Check auto-approval settings
|
||||
if (this.shouldAutoApproveTool(block.name)) {
|
||||
@@ -300,7 +193,7 @@ class Task {
|
||||
The system includes robust error handling:
|
||||
|
||||
```typescript
|
||||
class Task {
|
||||
class Cline {
|
||||
async handleError(action: string, error: Error) {
|
||||
// 1. Check if task was abandoned
|
||||
if (this.abandoned) return
|
||||
@@ -323,23 +216,23 @@ class Task {
|
||||
|
||||
### API Request & Token Management
|
||||
|
||||
The Task class handles API requests with built-in retry, streaming, and token management:
|
||||
The Cline class handles API requests with built-in retry, streaming, and token management:
|
||||
|
||||
```typescript
|
||||
class Task {
|
||||
class Cline {
|
||||
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
|
||||
// 1. Wait for MCP servers to connect
|
||||
await pWaitFor(() => this.controllerRef.deref()?.mcpHub?.isConnecting !== true)
|
||||
await pWaitFor(() => this.providerRef.deref()?.mcpHub?.isConnecting !== true)
|
||||
|
||||
// 2. Manage context window
|
||||
const previousRequest = this.clineMessages[previousApiReqIndex]
|
||||
if (previousRequest?.text) {
|
||||
const { tokensIn, tokensOut } = JSON.parse(previousRequest.text || "{}")
|
||||
const { tokensIn, tokensOut } = JSON.parse(previousRequest.text)
|
||||
const totalTokens = (tokensIn || 0) + (tokensOut || 0)
|
||||
|
||||
// Truncate conversation if approaching context limit
|
||||
if (totalTokens >= maxAllowedSize) {
|
||||
this.conversationHistoryDeletedRange = this.contextManager.getNextTruncationRange(
|
||||
this.conversationHistoryDeletedRange = getNextTruncationRange(
|
||||
this.apiConversationHistory,
|
||||
this.conversationHistoryDeletedRange,
|
||||
totalTokens / 2 > maxAllowedSize ? "quarter" : "half"
|
||||
@@ -406,32 +299,16 @@ Key features:
|
||||
- Cost calculation
|
||||
- Cache hit monitoring
|
||||
|
||||
### Context Management System
|
||||
|
||||
The Context Management System handles conversation history truncation to prevent context window overflow errors. Implemented in the `ContextManager` class, it ensures long-running conversations remain within model context limits while preserving critical context.
|
||||
|
||||
Key features:
|
||||
|
||||
1. **Model-Aware Sizing**: Dynamically adjusts based on different model context windows (64K for DeepSeek, 128K for most models, 200K for Claude).
|
||||
|
||||
2. **Proactive Truncation**: Monitors token usage and preemptively truncates conversations when approaching limits, maintaining buffers of 27K-40K tokens depending on the model.
|
||||
|
||||
3. **Intelligent Preservation**: Always preserves the original task message and maintains the user-assistant conversation structure when truncating.
|
||||
|
||||
4. **Adaptive Strategies**: Uses different truncation strategies based on context pressure - removing half of the conversation for moderate pressure or three-quarters for severe pressure.
|
||||
|
||||
5. **Error Recovery**: Includes specialized detection for context window errors from different providers with automatic retry and more aggressive truncation when needed.
|
||||
|
||||
### Task State & Resumption
|
||||
|
||||
The Task class provides robust task state management and resumption capabilities:
|
||||
The Cline class provides robust task state management and resumption capabilities:
|
||||
|
||||
```typescript
|
||||
class Task {
|
||||
class Cline {
|
||||
async resumeTaskFromHistory() {
|
||||
// 1. Load saved state
|
||||
this.clineMessages = await getSavedClineMessages(this.getContext(), this.taskId)
|
||||
this.apiConversationHistory = await getSavedApiConversationHistory(this.getContext(), this.taskId)
|
||||
this.clineMessages = await this.getSavedClineMessages()
|
||||
this.apiConversationHistory = await this.getSavedApiConversationHistory()
|
||||
|
||||
// 2. Handle interrupted tool executions
|
||||
const lastMessage = this.apiConversationHistory[this.apiConversationHistory.length - 1]
|
||||
@@ -461,14 +338,14 @@ class Task {
|
||||
|
||||
private async saveTaskState() {
|
||||
// Save conversation history
|
||||
await saveApiConversationHistory(this.getContext(), this.taskId, this.apiConversationHistory)
|
||||
await saveClineMessages(this.getContext(), this.taskId, this.clineMessages)
|
||||
await this.saveApiConversationHistory()
|
||||
await this.saveClineMessages()
|
||||
|
||||
// Create checkpoint
|
||||
const commitHash = await this.checkpointTracker?.commit()
|
||||
|
||||
// Update task history
|
||||
await this.controllerRef.deref()?.updateTaskHistory({
|
||||
await this.providerRef.deref()?.updateTaskHistory({
|
||||
id: this.taskId,
|
||||
ts: lastMessage.ts,
|
||||
task: taskMessage.text,
|
||||
@@ -504,54 +381,11 @@ Key aspects of task state management:
|
||||
- Resources are cleaned up properly
|
||||
- User is notified of state changes
|
||||
|
||||
## Plan/Act Mode System
|
||||
|
||||
Cline implements a dual-mode system that separates planning from execution:
|
||||
|
||||
### Mode Architecture
|
||||
|
||||
The Plan/Act mode system consists of:
|
||||
|
||||
1. **Mode State**: Stored in `chatSettings.mode` in the Controller's state
|
||||
2. **Mode Switching**: Handled by `togglePlanActModeWithChatSettings` in the Controller
|
||||
3. **Mode-specific Models**: Optional configuration to use different models for each mode
|
||||
4. **Mode-specific Prompting**: Different system prompts for planning vs. execution
|
||||
|
||||
### Mode Switching Process
|
||||
|
||||
When switching between modes:
|
||||
|
||||
1. The current model configuration is saved to mode-specific state
|
||||
2. The previous mode's model configuration is restored
|
||||
3. The Task instance is updated with the new mode
|
||||
4. The webview is notified of the mode change
|
||||
5. Telemetry events are captured for analytics
|
||||
|
||||
### Plan Mode
|
||||
|
||||
Plan mode is designed for:
|
||||
- Information gathering and context building
|
||||
- Asking clarifying questions
|
||||
- Creating detailed execution plans
|
||||
- Discussing approaches with the user
|
||||
|
||||
In Plan mode, the AI uses the `plan_mode_respond` tool to engage in conversational planning without executing actions.
|
||||
|
||||
### Act Mode
|
||||
|
||||
Act mode is designed for:
|
||||
- Executing the planned actions
|
||||
- Using tools to modify files, run commands, etc.
|
||||
- Implementing the solution
|
||||
- Providing results and completion feedback
|
||||
|
||||
In Act mode, the AI has access to all tools except `plan_mode_respond` and focuses on implementation rather than discussion.
|
||||
|
||||
## Data Flow & State Management
|
||||
|
||||
### Core Extension Role
|
||||
|
||||
The Controller acts as the single source of truth for all persistent state. It:
|
||||
The core extension (ClineProvider) acts as the single source of truth for all persistent state. It:
|
||||
- Manages VSCode global state and secrets storage
|
||||
- Coordinates state updates between components
|
||||
- Ensures state consistency across webview reloads
|
||||
@@ -560,10 +394,10 @@ The Controller acts as the single source of truth for all persistent state. It:
|
||||
|
||||
### Terminal Management
|
||||
|
||||
The Task class manages terminal instances and command execution:
|
||||
The Cline class manages terminal instances and command execution:
|
||||
|
||||
```typescript
|
||||
class Task {
|
||||
class Cline {
|
||||
async executeCommandTool(command: string): Promise<[boolean, ToolResponse]> {
|
||||
// 1. Get or create terminal
|
||||
const terminalInfo = await this.terminalManager.getOrCreateTerminal(cwd)
|
||||
@@ -619,10 +453,10 @@ Key features:
|
||||
|
||||
### Browser Session Management
|
||||
|
||||
The Task class handles browser automation through Puppeteer:
|
||||
The Cline class handles browser automation through Puppeteer:
|
||||
|
||||
```typescript
|
||||
class Task {
|
||||
class Cline {
|
||||
async executeBrowserAction(action: BrowserAction): Promise<BrowserActionResult> {
|
||||
switch (action) {
|
||||
case "launch":
|
||||
@@ -659,93 +493,13 @@ Key aspects:
|
||||
- Screenshot capture
|
||||
- Error recovery
|
||||
|
||||
## MCP (Model Context Protocol) Integration
|
||||
|
||||
### MCP Architecture
|
||||
|
||||
The MCP system consists of:
|
||||
|
||||
1. **McpHub Class**: Central manager in `src/services/mcp/McpHub.ts`
|
||||
2. **MCP Connections**: Manages connections to external MCP servers
|
||||
3. **MCP Settings**: Configuration stored in a JSON file
|
||||
4. **MCP Marketplace**: Online catalog of available MCP servers
|
||||
5. **MCP Tools & Resources**: Capabilities exposed by connected servers
|
||||
|
||||
The McpHub class:
|
||||
- Manages the lifecycle of MCP server connections
|
||||
- Handles server configuration through a settings file
|
||||
- Provides methods for calling tools and accessing resources
|
||||
- Implements auto-approval settings for MCP tools
|
||||
- Monitors server health and handles reconnection
|
||||
|
||||
### MCP Server Types
|
||||
|
||||
Cline supports two types of MCP server connections:
|
||||
- **Stdio**: Command-line based servers that communicate via standard I/O
|
||||
- **SSE**: HTTP-based servers that communicate via Server-Sent Events
|
||||
|
||||
### MCP Server Management
|
||||
|
||||
The McpHub class provides methods for:
|
||||
- Discovering and connecting to MCP servers
|
||||
- Monitoring server health and status
|
||||
- Restarting servers when needed
|
||||
- Managing server configurations
|
||||
- Setting timeouts and auto-approval rules
|
||||
|
||||
### MCP Tool Integration
|
||||
|
||||
MCP tools are integrated into the Task execution system:
|
||||
- Tools are discovered and registered at connection time
|
||||
- The Task class can call MCP tools through the McpHub
|
||||
- Tool results are streamed back to the AI
|
||||
- Auto-approval settings can be configured per tool
|
||||
|
||||
### MCP Marketplace
|
||||
|
||||
The MCP Marketplace provides:
|
||||
- A catalog of available MCP servers
|
||||
- One-click installation
|
||||
- README previews
|
||||
- Server status monitoring
|
||||
|
||||
The Controller class manages MCP servers through the McpHub service:
|
||||
|
||||
```typescript
|
||||
class Controller {
|
||||
mcpHub?: McpHub
|
||||
|
||||
constructor(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel, webviewProvider: WebviewProvider) {
|
||||
this.mcpHub = new McpHub(this)
|
||||
}
|
||||
|
||||
async downloadMcp(mcpId: string) {
|
||||
// Fetch server details from marketplace
|
||||
const response = await axios.post<McpDownloadResponse>(
|
||||
"https://api.cline.bot/v1/mcp/download",
|
||||
{ mcpId },
|
||||
{
|
||||
headers: { "Content-Type": "application/json" },
|
||||
timeout: 10000,
|
||||
}
|
||||
)
|
||||
|
||||
// Create task with context from README
|
||||
const task = `Set up the MCP server from ${mcpDetails.githubUrl}...`
|
||||
|
||||
// Initialize task and show chat view
|
||||
await this.initClineWithTask(task)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
This guide provides a comprehensive overview of the Cline extension architecture, with special focus on state management, data persistence, and code organization. Following these patterns ensures robust feature implementation with proper state handling across the extension's components.
|
||||
|
||||
Remember:
|
||||
- Always persist important state in the extension
|
||||
- The core extension follows a WebviewProvider -> Controller -> Task flow
|
||||
- The core extension exists in the src/ folder
|
||||
- Use proper typing for all state and messages
|
||||
- Handle errors and edge cases
|
||||
- Test state persistence across webview reloads
|
||||
|
||||
@@ -31,14 +31,6 @@ body:
|
||||
label: Relevant API REQUEST output
|
||||
description: Please copy and paste any relevant output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
- type: input
|
||||
id: provider-model
|
||||
attributes:
|
||||
label: Provider/Model
|
||||
description: What provider and model were you using when the issue occurred?
|
||||
placeholder: "e.g., cline:anthropic/claude-3.7-sonnet, gemini:gemini-2.5-pro-exp-03-25"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: operating-system
|
||||
attributes:
|
||||
|
||||
@@ -73,9 +73,6 @@ jobs:
|
||||
- name: Build Extension
|
||||
run: npm run compile
|
||||
|
||||
- name: Unit Tests
|
||||
run: npm run test:unit
|
||||
|
||||
# Run extension tests with coverage
|
||||
- name: Extension Tests with Coverage
|
||||
id: extension_coverage
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"extension": ["ts"],
|
||||
"spec": "src/**/__tests__/*.ts",
|
||||
"require": ["ts-node/register", "source-map-support/register"],
|
||||
"recursive": true
|
||||
}
|
||||
+6
-34
@@ -1,33 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [3.9.2]
|
||||
|
||||
- Add recommended models for Cline provider
|
||||
- Add ability to detect when user edits files manually so Cline knows to re-read, leading to reduced diff edit errors
|
||||
- Add improvements to file mention searching for faster searching
|
||||
- Add scoring logic to file mentions to sort and exlcude results based on relevance
|
||||
- Add Support for Bytedance Doubao (Thanks Tunixer!)
|
||||
- Fix to prevent duplicate BOM (Thanks bamps53!)
|
||||
|
||||
## [3.9.1]
|
||||
|
||||
- Add Gemini 2.5 Pro Preview 03-25 to Google Provider
|
||||
|
||||
## [3.9.0]
|
||||
|
||||
- Add Enable extended thinking for LiteLLM provider (Thanks @jorgegarciarey!)
|
||||
- Add a tab for configuring local MCP Servers
|
||||
- Fix issue with DeepSeek API provider token counting + context management
|
||||
- Fix issues with checkpoints hanging under certain conditions
|
||||
|
||||
## [3.8.6]
|
||||
|
||||
- Add UI for adding remote servers
|
||||
- Add Mentions Feature Guide and update related documentation
|
||||
- Fix bug where menu would open in sidebar and open tab
|
||||
- Fix issue with Cline accounts not showing user info in popout tabs
|
||||
- Fix bug where menu buttons wouldn't open view in sidebar
|
||||
|
||||
## [3.8.5]
|
||||
|
||||
- Add support for remote MCP Servers using SSE
|
||||
@@ -250,8 +222,8 @@
|
||||
## [3.1.0]
|
||||
|
||||
- Added checkpoints: Snapshots of workspace are automatically created whenever Cline uses a tool
|
||||
- Compare changes: Hover over any tool use to see a diff between the snapshot and current workspace state
|
||||
- Restore options: Choose to restore just the task state, just the workspace files, or both
|
||||
- Compare changes: Hover over any tool use to see a diff between the snapshot and current workspace state
|
||||
- Restore options: Choose to restore just the task state, just the workspace files, or both
|
||||
- New 'See new changes' button appears after task completion, providing an overview of all workspace changes
|
||||
- Task header now shows disk space usage with a delete button to help manage snapshot storage
|
||||
|
||||
@@ -433,10 +405,10 @@
|
||||
## [1.8.0]
|
||||
|
||||
- You can now use '@' in the textarea to add context!
|
||||
- @url: Paste in a URL for the extension to fetch and convert to markdown, useful when you want to give Claude the latest docs!
|
||||
- @problems: Add workspace errors and warnings for Claude to fix, no more back-and-forth about debugging
|
||||
- @file: Adds a file's contents so you don't have to waste API requests approving read file (+ type to search files)
|
||||
- @folder: Adds folder's files all at once to speed up your workflow even more
|
||||
- @url: Paste in a URL for the extension to fetch and convert to markdown, useful when you want to give Claude the latest docs!
|
||||
- @problems: Add workspace errors and warnings for Claude to fix, no more back-and-forth about debugging
|
||||
- @file: Adds a file's contents so you don't have to waste API requests approving read file (+ type to search files)
|
||||
- @folder: Adds folder's files all at once to speed up your workflow even more
|
||||
|
||||
## [1.7.0]
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ English | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md
|
||||
<a href="https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><strong>Feature Requests</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://docs.cline.bot/getting-started/for-new-coders" target="_blank"><strong>Getting Started</strong></a>
|
||||
<a href="https://docs.cline.bot/getting-started/getting-started-new-coders" target="_blank"><strong>Getting Started</strong></a>
|
||||
</td>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -18,7 +18,6 @@ Welcome to the Cline documentation - your comprehensive guide to using and exten
|
||||
- **Understand Cline's capabilities:**
|
||||
|
||||
- [Cline Tools Guide](tools/cline-tools-guide.md)
|
||||
- [Mentions Feature Guide](tools/mentions-guide.md)
|
||||
|
||||
- **Extend Cline with MCP Servers:**
|
||||
- [MCP Overview](mcp/README.md)
|
||||
|
||||
@@ -1,67 +1,41 @@
|
||||
flowchart TB
|
||||
subgraph "VSCode Extension Host"
|
||||
subgraph "Core Extension"
|
||||
ExtensionEntry["Extension Entry<br/>src/extension.ts"]
|
||||
WebviewProvider["WebviewProvider<br/>src/core/webview/index.ts"]
|
||||
Controller["Controller<br/>src/core/controller/index.ts"]
|
||||
Task["Task<br/>src/core/task/index.ts"]
|
||||
GlobalState["VSCode Global State"]
|
||||
SecretsStorage["VSCode Secrets Storage"]
|
||||
McpHub["McpHub<br/>src/services/mcp/McpHub.ts"]
|
||||
graph TB
|
||||
subgraph VSCode Extension Host
|
||||
subgraph Core Extension
|
||||
ExtensionEntry[Extension Entry<br/>src/extension.ts]
|
||||
ClineProvider[ClineProvider<br/>src/core/webview/ClineProvider.ts]
|
||||
ClineClass[Cline Class<br/>src/core/Cline.ts]
|
||||
GlobalState[VSCode Global State]
|
||||
SecretsStorage[VSCode Secrets Storage]
|
||||
end
|
||||
|
||||
subgraph "Webview UI"
|
||||
WebviewApp["React App<br/>webview-ui/src/App.tsx"]
|
||||
ExtStateContext["ExtensionStateContext<br/>webview-ui/src/context/ExtensionStateContext.tsx"]
|
||||
ReactComponents["React Components"]
|
||||
subgraph Webview UI
|
||||
WebviewApp[React App<br/>webview-ui/src/App.tsx]
|
||||
ExtStateContext[ExtensionStateContext<br/>webview-ui/src/context/ExtensionStateContext.tsx]
|
||||
ReactComponents[React Components]
|
||||
end
|
||||
|
||||
subgraph "Storage"
|
||||
TaskStorage["Task Storage<br/>Per-Task Files & History"]
|
||||
CheckpointSystem["Git-based Checkpoints"]
|
||||
end
|
||||
|
||||
subgraph "API Providers"
|
||||
AnthropicAPI["Anthropic"]
|
||||
OpenRouterAPI["OpenRouter"]
|
||||
BedrockAPI["AWS Bedrock"]
|
||||
OtherAPIs["Other Providers"]
|
||||
end
|
||||
|
||||
subgraph "MCP Servers"
|
||||
ExternalMcpServers["External MCP Servers"]
|
||||
subgraph Storage
|
||||
TaskStorage[Task Storage<br/>Per-Task Files & History]
|
||||
CheckpointSystem[Git-based Checkpoints]
|
||||
end
|
||||
end
|
||||
|
||||
%% Core Extension Data Flow
|
||||
ExtensionEntry --> WebviewProvider
|
||||
WebviewProvider --> Controller
|
||||
Controller --> Task
|
||||
Controller --> McpHub
|
||||
Task --> GlobalState
|
||||
Task --> SecretsStorage
|
||||
Task --> TaskStorage
|
||||
Task --> CheckpointSystem
|
||||
Task --> |"API Requests"| AnthropicAPI
|
||||
Task --> |"API Requests"| OpenRouterAPI
|
||||
Task --> |"API Requests"| BedrockAPI
|
||||
Task --> |"API Requests"| OtherAPIs
|
||||
McpHub --> |"Connects to"| ExternalMcpServers
|
||||
Task --> |"Uses"| McpHub
|
||||
ExtensionEntry --> ClineProvider
|
||||
ClineProvider --> ClineClass
|
||||
ClineClass --> GlobalState
|
||||
ClineClass --> SecretsStorage
|
||||
ClineClass --> TaskStorage
|
||||
ClineClass --> CheckpointSystem
|
||||
|
||||
%% Webview Data Flow
|
||||
WebviewApp --> ExtStateContext
|
||||
ExtStateContext --> ReactComponents
|
||||
|
||||
%% Bidirectional Communication
|
||||
WebviewProvider <--> |"postMessage"| ExtStateContext
|
||||
ClineProvider <-->|postMessage| ExtStateContext
|
||||
|
||||
classDef vscodeState fill:#f9f,stroke:#333,stroke-width:2px
|
||||
classDef contextClass fill:#bbf,stroke:#333,stroke-width:2px
|
||||
classDef providerClass fill:#bfb,stroke:#333,stroke-width:2px
|
||||
classDef apiClass fill:#fdb,stroke:#333,stroke-width:2px
|
||||
|
||||
class GlobalState,SecretsStorage vscodeState
|
||||
class ExtStateContext contextClass
|
||||
class WebviewProvider,McpHub providerClass
|
||||
class AnthropicAPI,OpenRouterAPI,BedrockAPI,OtherAPIs apiClass
|
||||
style GlobalState fill:#ff0066,stroke:#333,stroke-width:2px,color:#ffffff
|
||||
style SecretsStorage fill:#ff0066,stroke:#333,stroke-width:2px,color:#ffffff
|
||||
style ExtStateContext fill:#0066ff,stroke:#333,stroke-width:2px,color:#ffffff
|
||||
style ClineProvider fill:#00cc66,stroke:#333,stroke-width:2px,color:#ffffff
|
||||
|
||||
@@ -20,9 +20,8 @@ Cline is your AI assistant that can:
|
||||
|
||||
2. **Provide Context**
|
||||
|
||||
- Use @ mentions to add files, folders, URLs, diagnostics, terminal output, and more
|
||||
- Example: "@/src/components/App.tsx"
|
||||
- See the [Mentions Feature Guide](./mentions-guide.md) for details
|
||||
- Use @ mentions to add files, folders, or URLs
|
||||
- Example: "@file:src/components/App.tsx"
|
||||
|
||||
3. **Review Changes**
|
||||
- Cline will show diffs before making changes
|
||||
@@ -55,7 +54,7 @@ Cline is your AI assistant that can:
|
||||
|
||||
## Available Tools
|
||||
|
||||
For the most up-to-date implementation details, you can view the full source code in the [Cline repository](https://github.com/cline/cline/blob/main/src/core/task/index.ts).
|
||||
For the most up-to-date implementation details, you can view the full source code in the [Cline repository](https://github.com/cline/cline/blob/main/src/core/Cline.ts).
|
||||
|
||||
Cline has access to the following tools for various tasks:
|
||||
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
# Cline Mentions Feature Guide
|
||||
|
||||
## Overview
|
||||
|
||||
The mentions feature is a powerful capability that allows you to reference various resources in your conversations with Cline using the "@" symbol. This includes file contents, directory structures, webpage URLs, VSCode diagnostic information, terminal output, Git change status, and more - all easily incorporated into your conversations.
|
||||
|
||||
By using this feature, Cline can gain more accurate context and provide more relevant assistance for your tasks.
|
||||
|
||||
## Basic Syntax
|
||||
|
||||
Mentions always start with the "@" symbol, followed by the path or identifier of the resource you want to reference:
|
||||
|
||||
```
|
||||
@resource_identifier
|
||||
```
|
||||
|
||||
You can place mentions anywhere in your user messages, and Cline will automatically retrieve the referenced content.
|
||||
|
||||
## Supported Mention Types
|
||||
|
||||
### 1. File References
|
||||
|
||||
To reference file contents, use `@/` followed by the relative path within your project:
|
||||
|
||||
```
|
||||
@/path/to/file.js
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Please analyze the implementation in @/src/components/Button.tsx
|
||||
```
|
||||
|
||||
In this example, Cline automatically retrieves the contents of Button.tsx and uses it to perform the analysis.
|
||||
|
||||
### 2. Directory References
|
||||
|
||||
To reference directory contents, use `@/` followed by the relative path of the directory, ending with a trailing `/`:
|
||||
|
||||
```
|
||||
@/path/to/directory/
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
What components are available in the @/src/components/ directory?
|
||||
```
|
||||
|
||||
In this example, Cline retrieves a listing of the components directory and its contents.
|
||||
|
||||
### 3. URL References
|
||||
|
||||
To reference web page contents, use `@` followed by the URL:
|
||||
|
||||
```
|
||||
@https://example.com
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Please parse the JSON response from @https://api.github.com/users/octocat
|
||||
```
|
||||
|
||||
In this example, Cline fetches the response from the GitHub API and analyzes the JSON.
|
||||
|
||||
### 4. Diagnostic References
|
||||
|
||||
To reference VSCode diagnostic information (errors and warnings) in the current workspace, use `@problems`:
|
||||
|
||||
```
|
||||
@problems
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Check @problems and tell me which errors I should prioritize fixing
|
||||
```
|
||||
|
||||
In this example, Cline retrieves the current errors and warnings from your workspace and identifies high-priority issues.
|
||||
|
||||
### 5. Terminal Output References
|
||||
|
||||
To reference the latest terminal output, use `@terminal`:
|
||||
|
||||
```
|
||||
@terminal
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Please identify the cause of the error in the @terminal output
|
||||
```
|
||||
|
||||
In this example, Cline examines the latest terminal output and analyzes the error's cause.
|
||||
|
||||
### 6. Git Working Directory References
|
||||
|
||||
To reference the current Git working directory change status, use `@git-changes`:
|
||||
|
||||
```
|
||||
@git-changes
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Review the @git-changes and summarize the important changes that should be committed
|
||||
```
|
||||
|
||||
In this example, Cline retrieves the list of changed files in the current Git working directory and identifies candidates for commit.
|
||||
|
||||
### 7. Git Commit References
|
||||
|
||||
To reference information about a specific Git commit, use `@` followed by the commit hash:
|
||||
|
||||
```
|
||||
@commit_hash
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Analyze the commit @abcd123 and explain what changes were made
|
||||
```
|
||||
|
||||
In this example, Cline retrieves information about the specified commit hash and analyzes the changes made in that commit.
|
||||
|
||||
## Usage Scenarios
|
||||
|
||||
### Code Review
|
||||
|
||||
```
|
||||
Check @/src/components/Form.jsx and suggest improvements from a performance perspective. Also, if there are any @problems, please suggest how to fix them.
|
||||
```
|
||||
|
||||
### Debugging Assistance
|
||||
|
||||
```
|
||||
My npm install failed. Please examine the @terminal output and suggest a solution to the problem.
|
||||
```
|
||||
|
||||
### Project Analysis
|
||||
|
||||
```
|
||||
Analyze the code in the @/src/models/ directory and explain the relationships between the data models. Also, tell me how the utility functions in @/src/utils/ are used with these models.
|
||||
```
|
||||
|
||||
### Code Generation
|
||||
|
||||
```
|
||||
Create a new Input.tsx component using the same design language as @/src/components/Button.tsx
|
||||
```
|
||||
|
||||
### Version Control Integration
|
||||
|
||||
```
|
||||
Review the @git-changes and suggest a commit message for the feature I'm working on.
|
||||
```
|
||||
|
||||
## Combining Multiple Mentions
|
||||
|
||||
You can combine multiple mentions to provide more complex context:
|
||||
|
||||
```
|
||||
There seems to be a bug in @/src/api/users.js. Please check @problems and @terminal to identify and fix the issue.
|
||||
```
|
||||
|
||||
## Limitations and Considerations
|
||||
|
||||
1. **Large Files**: Referencing very large files may take time to process and could consume a significant amount of tokens.
|
||||
|
||||
2. **Binary Files**: Binary files (such as images) will not be properly processed and will show a "Binary file" message.
|
||||
|
||||
3. **Directory Structure**: Directory references will only show top-level files and directories, not recursively showing the contents of subdirectories.
|
||||
|
||||
4. **URL Limitations**: Some websites may block automated crawling, which could prevent accurate content retrieval.
|
||||
|
||||
5. **Path Syntax**: File paths or URLs with special characters (such as spaces) may not be recognized correctly.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Mentions Not Recognized
|
||||
|
||||
If your mentions aren't being recognized correctly, check that:
|
||||
|
||||
- There's no space after the `@` symbol
|
||||
- File paths are accurate (case-sensitive)
|
||||
- URLs include the full format (with `https://`)
|
||||
|
||||
### Content Not Retrieved
|
||||
|
||||
If the content of referenced resources can't be retrieved:
|
||||
|
||||
- Verify the file exists
|
||||
- Ensure you have access permissions for the file
|
||||
- Check that the file isn't too large or the URL too complex
|
||||
|
||||
### Performance Issues
|
||||
|
||||
If mention processing is slow:
|
||||
|
||||
- Reference smaller files or specific file sections
|
||||
- Reduce the number of mentions used at once
|
||||
|
||||
## Conclusion
|
||||
|
||||
Mastering the mentions feature makes your communication with Cline more efficient. By providing appropriate context, Cline can deliver more accurate assistance, significantly improving your development workflow.
|
||||
Generated
+8
-235
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.9.1",
|
||||
"version": "3.8.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.9.1",
|
||||
"version": "3.8.4",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/bedrock-sdk": "^0.12.4",
|
||||
@@ -33,7 +33,6 @@
|
||||
"execa": "^9.5.2",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"firebase": "^11.2.0",
|
||||
"fzf": "^0.5.2",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"globby": "^14.0.2",
|
||||
"ignore": "^7.0.3",
|
||||
@@ -67,7 +66,6 @@
|
||||
"@types/mocha": "^10.0.7",
|
||||
"@types/node": "20.x",
|
||||
"@types/pdf-parse": "^1.1.4",
|
||||
"@types/proxyquire": "^1.3.31",
|
||||
"@types/should": "^11.2.0",
|
||||
"@types/sinon": "^17.0.4",
|
||||
"@types/turndown": "^5.0.5",
|
||||
@@ -82,10 +80,8 @@
|
||||
"husky": "^9.1.7",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.3.3",
|
||||
"proxyquire": "^2.1.3",
|
||||
"should": "^13.2.3",
|
||||
"sinon": "^19.0.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4290,30 +4286,6 @@
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "0.3.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz",
|
||||
@@ -8597,34 +8569,6 @@
|
||||
"integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
|
||||
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node12": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node14": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node16": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/chai": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.0.1.tgz",
|
||||
@@ -8704,13 +8648,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/proxyquire": {
|
||||
"version": "1.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/proxyquire/-/proxyquire-1.3.31.tgz",
|
||||
"integrity": "sha512-uALowNG2TSM1HNPMMOR0AJwv4aPYPhqB0xlEhkeRTMuto5hjoSPZkvgu1nbPUkz3gEPAHv4sy4DmKsurZiEfRQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/should": {
|
||||
"version": "11.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/should/-/should-11.2.0.tgz",
|
||||
@@ -9175,19 +9112,6 @@
|
||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-walk": {
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
|
||||
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
|
||||
@@ -9291,13 +9215,6 @@
|
||||
"integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/arg": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
@@ -10256,13 +10173,6 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -10953,9 +10863,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
|
||||
"integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -11680,30 +11590,6 @@
|
||||
"node": "^10.12.0 || >=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-keys": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz",
|
||||
"integrity": "sha512-tcgI872xXjwFF4xgQmLxi76GnwJG3g/3isB1l4/G5Z4zrbddGpBjqZCO9oEAcB5wX0Hj/5iQB3toxfO7in1hHA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-object": "~1.0.1",
|
||||
"merge-descriptors": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-keys/node_modules/merge-descriptors": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
@@ -11997,12 +11883,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/fzf": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fzf/-/fzf-0.5.2.tgz",
|
||||
"integrity": "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/gauge": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/gauge/-/gauge-5.0.2.tgz",
|
||||
@@ -12978,16 +12858,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-object": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz",
|
||||
"integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-path-inside": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
|
||||
@@ -13576,13 +13446,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/mammoth": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.8.0.tgz",
|
||||
@@ -13988,13 +13851,6 @@
|
||||
"integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/module-not-found-error": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz",
|
||||
"integrity": "sha512-pEk4ECWQXV6z2zjhRZUongnLJNUeGQJ3w6OQ5ctGwD+i5o93qjRQUk2Rt6VdNeu3sEP0AB4LcfvdebpxBRVr4g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/monaco-vscode-textmate-theme-converter": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/monaco-vscode-textmate-theme-converter/-/monaco-vscode-textmate-theme-converter-0.1.7.tgz",
|
||||
@@ -15260,18 +15116,6 @@
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"node_modules/proxyquire": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.3.tgz",
|
||||
"integrity": "sha512-BQWfCqYM+QINd+yawJz23tbBM40VIGXOdDw3X344KcclI/gtBbdWF6SlQ4nK/bYhF9d27KYug9WzljHC6B9Ysg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-keys": "^1.0.2",
|
||||
"module-not-found-error": "^1.0.1",
|
||||
"resolve": "^1.11.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
|
||||
@@ -15764,9 +15608,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
||||
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
|
||||
"version": "7.6.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
|
||||
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -16764,60 +16608,6 @@
|
||||
"typescript": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
"@tsconfig/node12": "^1.0.7",
|
||||
"@tsconfig/node14": "^1.0.0",
|
||||
"@tsconfig/node16": "^1.0.2",
|
||||
"acorn": "^8.4.1",
|
||||
"acorn-walk": "^8.1.1",
|
||||
"arg": "^4.1.0",
|
||||
"create-require": "^1.1.0",
|
||||
"diff": "^4.0.1",
|
||||
"make-error": "^1.1.1",
|
||||
"v8-compile-cache-lib": "^3.0.1",
|
||||
"yn": "3.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"ts-node": "dist/bin.js",
|
||||
"ts-node-cwd": "dist/bin-cwd.js",
|
||||
"ts-node-esm": "dist/bin-esm.js",
|
||||
"ts-node-script": "dist/bin-script.js",
|
||||
"ts-node-transpile-only": "dist/bin-transpile.js",
|
||||
"ts-script": "dist/bin-script-deprecated.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/core": ">=1.2.50",
|
||||
"@swc/wasm": ">=1.2.50",
|
||||
"@types/node": "*",
|
||||
"typescript": ">=2.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/core": {
|
||||
"optional": true
|
||||
},
|
||||
"@swc/wasm": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ts-node/node_modules/diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
@@ -17142,13 +16932,6 @@
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/v8-to-istanbul": {
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
|
||||
@@ -17703,16 +17486,6 @@
|
||||
"fd-slicer": "~1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yn": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
||||
+1
-6
@@ -2,7 +2,7 @@
|
||||
"name": "claude-dev",
|
||||
"displayName": "Cline",
|
||||
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
|
||||
"version": "3.9.2",
|
||||
"version": "3.8.5",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -303,7 +303,6 @@
|
||||
"format:fix": "prettier . --write",
|
||||
"test": "vscode-test",
|
||||
"test:ci": "node scripts/test-ci.js",
|
||||
"test:unit": "TS_NODE_PROJECT='./tsconfig.unit-test.json' mocha",
|
||||
"test:coverage": "vscode-test --coverage",
|
||||
"install:all": "npm install && cd webview-ui && npm install",
|
||||
"dev:webview": "cd webview-ui && npm run dev",
|
||||
@@ -324,7 +323,6 @@
|
||||
"@types/mocha": "^10.0.7",
|
||||
"@types/node": "20.x",
|
||||
"@types/pdf-parse": "^1.1.4",
|
||||
"@types/proxyquire": "^1.3.31",
|
||||
"@types/should": "^11.2.0",
|
||||
"@types/sinon": "^17.0.4",
|
||||
"@types/turndown": "^5.0.5",
|
||||
@@ -339,10 +337,8 @@
|
||||
"husky": "^9.1.7",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.3.3",
|
||||
"proxyquire": "^2.1.3",
|
||||
"should": "^13.2.3",
|
||||
"sinon": "^19.0.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -370,7 +366,6 @@
|
||||
"execa": "^9.5.2",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"firebase": "^11.2.0",
|
||||
"fzf": "^0.5.2",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"globby": "^14.0.2",
|
||||
"ignore": "^7.0.3",
|
||||
|
||||
@@ -15,7 +15,6 @@ import { RequestyHandler } from "./providers/requesty"
|
||||
import { TogetherHandler } from "./providers/together"
|
||||
import { QwenHandler } from "./providers/qwen"
|
||||
import { MistralHandler } from "./providers/mistral"
|
||||
import { DoubaoHandler } from "./providers/doubao"
|
||||
import { VsCodeLmHandler } from "./providers/vscode-lm"
|
||||
import { ClineHandler } from "./providers/cline"
|
||||
import { LiteLlmHandler } from "./providers/litellm"
|
||||
@@ -62,8 +61,6 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
|
||||
return new TogetherHandler(options)
|
||||
case "qwen":
|
||||
return new QwenHandler(options)
|
||||
case "doubao":
|
||||
return new DoubaoHandler(options)
|
||||
case "mistral":
|
||||
return new MistralHandler(options)
|
||||
case "vscode-lm":
|
||||
|
||||
@@ -36,15 +36,14 @@ export class DeepSeekHandler implements ApiHandler {
|
||||
}
|
||||
const deepUsage = usage as DeepSeekUsage
|
||||
|
||||
const inputTokens = deepUsage?.prompt_tokens || 0 // sum of cache hits and misses
|
||||
const inputTokens = deepUsage?.prompt_tokens || 0
|
||||
const outputTokens = deepUsage?.completion_tokens || 0
|
||||
const cacheReadTokens = deepUsage?.prompt_cache_hit_tokens || 0
|
||||
const cacheWriteTokens = deepUsage?.prompt_cache_miss_tokens || 0
|
||||
const totalCost = calculateApiCostOpenAI(info, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
|
||||
const nonCachedInputTokens = Math.max(0, inputTokens - cacheReadTokens - cacheWriteTokens) // this will always be 0
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: nonCachedInputTokens,
|
||||
inputTokens: inputTokens,
|
||||
outputTokens: outputTokens,
|
||||
cacheWriteTokens: cacheWriteTokens,
|
||||
cacheReadTokens: cacheReadTokens,
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import { ApiHandler } from ".."
|
||||
import { ApiHandlerOptions, doubaoDefaultModelId, DoubaoModelId, doubaoModels, ModelInfo } from "../../shared/api"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
export class DoubaoHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: OpenAI
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://ark.cn-beijing.volces.com/api/v3/",
|
||||
apiKey: this.options.doubaoApiKey,
|
||||
})
|
||||
}
|
||||
|
||||
getModel(): { id: DoubaoModelId; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
if (modelId && modelId in doubaoModels) {
|
||||
const id = modelId as DoubaoModelId
|
||||
return { id, info: doubaoModels[id] }
|
||||
}
|
||||
return {
|
||||
id: doubaoDefaultModelId,
|
||||
info: doubaoModels[doubaoDefaultModelId],
|
||||
}
|
||||
}
|
||||
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const model = this.getModel()
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: model.id,
|
||||
max_completion_tokens: model.info.maxTokens,
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
temperature: 0,
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
cacheReadTokens: chunk.usage.prompt_cache_hit_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,16 +59,10 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
}
|
||||
const modelId = this.options.liteLlmModelId || liteLlmDefaultModelId
|
||||
const isOminiModel = modelId.includes("o1-mini") || modelId.includes("o3-mini")
|
||||
|
||||
// Configuration for extended thinking
|
||||
const budgetTokens = this.options.thinkingBudgetTokens || 0
|
||||
const reasoningOn = budgetTokens !== 0 ? true : false
|
||||
const thinkingConfig = reasoningOn ? { type: "enabled", budget_tokens: budgetTokens } : undefined
|
||||
|
||||
let temperature: number | undefined = 0
|
||||
|
||||
if (isOminiModel && reasoningOn) {
|
||||
temperature = undefined // Thinking mode doesn't support temperature
|
||||
if (isOminiModel) {
|
||||
temperature = undefined // does not support temperature
|
||||
}
|
||||
|
||||
const stream = await this.client.chat.completions.create({
|
||||
@@ -77,7 +71,6 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
temperature,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
...(thinkingConfig && { thinking: thinkingConfig }), // Add thinking configuration when applicable
|
||||
})
|
||||
|
||||
const inputCost = (await this.calculateCost(1e6, 0)) || 0
|
||||
@@ -85,8 +78,6 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
|
||||
// Handle normal text content
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
@@ -94,20 +85,6 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle reasoning events (thinking)
|
||||
// Thinking is not in the standard types but may be in the response
|
||||
interface ThinkingDelta {
|
||||
thinking?: string
|
||||
}
|
||||
|
||||
if ((delta as ThinkingDelta)?.thinking) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: (delta as ThinkingDelta).thinking || "",
|
||||
}
|
||||
}
|
||||
|
||||
// Handle token usage information
|
||||
if (chunk.usage) {
|
||||
const totalCost =
|
||||
(inputCost * chunk.usage.prompt_tokens) / 1e6 + (outputCost * chunk.usage.completion_tokens) / 1e6
|
||||
|
||||
@@ -26,15 +26,14 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {
|
||||
const inputTokens = usage?.prompt_tokens || 0 // sum of cache hits and misses
|
||||
const inputTokens = usage?.prompt_tokens || 0
|
||||
const outputTokens = usage?.completion_tokens || 0
|
||||
const cacheReadTokens = usage?.prompt_tokens_details?.cached_tokens || 0
|
||||
const cacheWriteTokens = 0
|
||||
const totalCost = calculateApiCostOpenAI(info, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
|
||||
const nonCachedInputTokens = Math.max(0, inputTokens - cacheReadTokens - cacheWriteTokens)
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: nonCachedInputTokens,
|
||||
inputTokens: inputTokens,
|
||||
outputTokens: outputTokens,
|
||||
cacheWriteTokens: cacheWriteTokens,
|
||||
cacheReadTokens: cacheReadTokens,
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
import { ContextManager } from "../ContextManager"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { expect } from "chai"
|
||||
|
||||
describe("ContextManager", () => {
|
||||
function createMessages(count: number): Anthropic.Messages.MessageParam[] {
|
||||
const messages: Anthropic.Messages.MessageParam[] = []
|
||||
|
||||
messages.push({
|
||||
role: "user",
|
||||
content: "Initial task message",
|
||||
})
|
||||
|
||||
let role: "user" | "assistant" = "assistant"
|
||||
for (let i = 1; i < count; i++) {
|
||||
messages.push({
|
||||
role,
|
||||
content: `Message ${i}`,
|
||||
})
|
||||
role = role === "user" ? "assistant" : "user"
|
||||
}
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
describe("getNextTruncationRange", () => {
|
||||
let contextManager: ContextManager
|
||||
|
||||
beforeEach(() => {
|
||||
contextManager = new ContextManager()
|
||||
})
|
||||
|
||||
it("first truncation with half keep", () => {
|
||||
const messages = createMessages(11)
|
||||
const result = contextManager.getNextTruncationRange(messages, undefined, "half")
|
||||
|
||||
expect(result).to.deep.equal([1, 4])
|
||||
})
|
||||
|
||||
it("first truncation with quarter keep", () => {
|
||||
const messages = createMessages(11)
|
||||
const result = contextManager.getNextTruncationRange(messages, undefined, "quarter")
|
||||
|
||||
expect(result).to.deep.equal([1, 6])
|
||||
})
|
||||
|
||||
it("sequential truncation with half keep", () => {
|
||||
const messages = createMessages(21)
|
||||
const firstRange = contextManager.getNextTruncationRange(messages, undefined, "half")
|
||||
expect(firstRange).to.deep.equal([1, 10])
|
||||
|
||||
// Pass the previous range for sequential truncation
|
||||
const secondRange = contextManager.getNextTruncationRange(messages, firstRange, "half")
|
||||
expect(secondRange).to.deep.equal([1, 14])
|
||||
})
|
||||
|
||||
it("sequential truncation with quarter keep", () => {
|
||||
const messages = createMessages(41)
|
||||
const firstRange = contextManager.getNextTruncationRange(messages, undefined, "quarter")
|
||||
|
||||
const secondRange = contextManager.getNextTruncationRange(messages, firstRange, "quarter")
|
||||
|
||||
expect(secondRange[0]).to.equal(1)
|
||||
expect(secondRange[1]).to.be.greaterThan(firstRange[1])
|
||||
})
|
||||
|
||||
it("ensures the last message in range is a user message", () => {
|
||||
const messages = createMessages(14)
|
||||
const result = contextManager.getNextTruncationRange(messages, undefined, "half")
|
||||
|
||||
// Check if the message at the end of range is a user message
|
||||
const lastRemovedMessage = messages[result[1]]
|
||||
expect(lastRemovedMessage.role).to.equal("user")
|
||||
|
||||
// Check if the next message after the range is an assistant message
|
||||
const nextMessage = messages[result[1] + 1]
|
||||
expect(nextMessage.role).to.equal("assistant")
|
||||
})
|
||||
|
||||
it("handles small message arrays", () => {
|
||||
const messages = createMessages(3)
|
||||
const result = contextManager.getNextTruncationRange(messages, undefined, "half")
|
||||
|
||||
expect(result).to.deep.equal([1, 0])
|
||||
})
|
||||
|
||||
it("preserves the message structure when truncating", () => {
|
||||
const messages = createMessages(20)
|
||||
const result = contextManager.getNextTruncationRange(messages, undefined, "half")
|
||||
|
||||
// Get messages after removing the range
|
||||
const effectiveMessages = [...messages.slice(0, result[0]), ...messages.slice(result[1] + 1)]
|
||||
|
||||
// Check first message and alternating pattern
|
||||
expect(effectiveMessages[0].role).to.equal("user")
|
||||
for (let i = 1; i < effectiveMessages.length; i++) {
|
||||
const expectedRole = i % 2 === 1 ? "assistant" : "user"
|
||||
expect(effectiveMessages[i].role).to.equal(expectedRole)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("getTruncatedMessages", () => {
|
||||
let contextManager: ContextManager
|
||||
|
||||
beforeEach(() => {
|
||||
contextManager = new ContextManager()
|
||||
})
|
||||
|
||||
it("returns original messages when no range is provided", () => {
|
||||
const messages = createMessages(3)
|
||||
|
||||
const result = contextManager.getTruncatedMessages(messages, undefined)
|
||||
expect(result).to.deep.equal(messages)
|
||||
})
|
||||
|
||||
it("correctly removes messages in the specified range", () => {
|
||||
const messages = createMessages(5)
|
||||
|
||||
const range: [number, number] = [1, 3]
|
||||
const result = contextManager.getTruncatedMessages(messages, range)
|
||||
|
||||
expect(result).to.have.lengthOf(2)
|
||||
expect(result[0]).to.deep.equal(messages[0])
|
||||
expect(result[1]).to.deep.equal(messages[4])
|
||||
})
|
||||
|
||||
it("works with a range that starts at the first message after task", () => {
|
||||
const messages = createMessages(4)
|
||||
|
||||
const range: [number, number] = [1, 2]
|
||||
const result = contextManager.getTruncatedMessages(messages, range)
|
||||
|
||||
expect(result).to.have.lengthOf(2)
|
||||
expect(result[0]).to.deep.equal(messages[0])
|
||||
expect(result[1]).to.deep.equal(messages[3])
|
||||
})
|
||||
|
||||
it("correctly handles removing a range while preserving alternation pattern", () => {
|
||||
const messages = createMessages(5)
|
||||
|
||||
const range: [number, number] = [1, 2]
|
||||
const result = contextManager.getTruncatedMessages(messages, range)
|
||||
|
||||
expect(result).to.have.lengthOf(3)
|
||||
expect(result[0]).to.deep.equal(messages[0])
|
||||
expect(result[1]).to.deep.equal(messages[3])
|
||||
expect(result[2]).to.deep.equal(messages[4])
|
||||
|
||||
expect(result[0].role).to.equal("user")
|
||||
expect(result[1].role).to.equal("assistant")
|
||||
expect(result[2].role).to.equal("user")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,271 +0,0 @@
|
||||
import { describe, it, beforeEach, afterEach } from "mocha"
|
||||
import { expect } from "chai"
|
||||
import * as sinon from "sinon"
|
||||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import { FileContextTracker } from "./FileContextTracker"
|
||||
import * as diskModule from "../storage/disk"
|
||||
import type { TaskMetadata, ControllerLike, FileMetadataEntry } from "./FileContextTrackerTypes"
|
||||
|
||||
describe("FileContextTracker", () => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
let mockController: ControllerLike
|
||||
let mockContext: vscode.ExtensionContext
|
||||
let mockWorkspace: sinon.SinonStub
|
||||
let mockFileSystemWatcher: any
|
||||
let tracker: FileContextTracker
|
||||
let taskId: string
|
||||
let mockTaskMetadata: TaskMetadata
|
||||
let getTaskMetadataStub: sinon.SinonStub
|
||||
let saveTaskMetadataStub: sinon.SinonStub
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Mock vscode workspace
|
||||
mockWorkspace = sandbox.stub(vscode.workspace, "workspaceFolders").value([
|
||||
{
|
||||
uri: {
|
||||
fsPath: "/mock/workspace",
|
||||
},
|
||||
} as vscode.WorkspaceFolder,
|
||||
])
|
||||
|
||||
// Mock file system watcher
|
||||
mockFileSystemWatcher = {
|
||||
dispose: sandbox.stub(),
|
||||
onDidChange: sandbox.stub().returns({ dispose: () => {} }),
|
||||
}
|
||||
|
||||
// Use a function replacement instead of a direct stub
|
||||
const originalCreateFileSystemWatcher = vscode.workspace.createFileSystemWatcher
|
||||
vscode.workspace.createFileSystemWatcher = function () {
|
||||
return mockFileSystemWatcher
|
||||
} as any
|
||||
|
||||
// Mock controller and context
|
||||
mockContext = {
|
||||
globalStorageUri: { fsPath: "/mock/storage" },
|
||||
} as unknown as vscode.ExtensionContext
|
||||
|
||||
mockController = {
|
||||
context: mockContext,
|
||||
}
|
||||
|
||||
// Mock disk module functions
|
||||
mockTaskMetadata = { files_in_context: [] }
|
||||
getTaskMetadataStub = sandbox.stub(diskModule, "getTaskMetadata").resolves(mockTaskMetadata)
|
||||
saveTaskMetadataStub = sandbox.stub(diskModule, "saveTaskMetadata").resolves()
|
||||
|
||||
// Create tracker instance
|
||||
taskId = "test-task-id"
|
||||
tracker = new FileContextTracker(mockController, taskId)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
it("should add a record when a file is read by a tool", async () => {
|
||||
const filePath = "src/test-file.ts"
|
||||
|
||||
await tracker.trackFileContext(filePath, "read_tool")
|
||||
|
||||
// Verify getTaskMetadata was called
|
||||
expect(getTaskMetadataStub.calledOnce).to.be.true
|
||||
expect(getTaskMetadataStub.firstCall.args[1]).to.equal(taskId)
|
||||
|
||||
// Verify saveTaskMetadata was called with the correct data
|
||||
expect(saveTaskMetadataStub.calledOnce).to.be.true
|
||||
|
||||
const savedMetadata = saveTaskMetadataStub.firstCall.args[2]
|
||||
expect(savedMetadata.files_in_context.length).to.equal(1)
|
||||
|
||||
const fileEntry = savedMetadata.files_in_context[0]
|
||||
expect(fileEntry.path).to.equal(filePath)
|
||||
expect(fileEntry.record_state).to.equal("active")
|
||||
expect(fileEntry.record_source).to.equal("read_tool")
|
||||
expect(fileEntry.cline_read_date).to.be.a("number")
|
||||
expect(fileEntry.cline_edit_date).to.be.null
|
||||
})
|
||||
|
||||
it("should add a record when a file is edited by Cline", async () => {
|
||||
const filePath = "src/test-file.ts"
|
||||
|
||||
await tracker.trackFileContext(filePath, "cline_edited")
|
||||
|
||||
// Verify saveTaskMetadata was called with the correct data
|
||||
expect(saveTaskMetadataStub.calledOnce).to.be.true
|
||||
const savedMetadata = saveTaskMetadataStub.firstCall.args[2]
|
||||
|
||||
// Check that we have at least one entry in files_in_context
|
||||
expect(savedMetadata.files_in_context).to.be.an("array").that.is.not.empty
|
||||
|
||||
// Find the active entry for this file
|
||||
const activeEntry = savedMetadata.files_in_context.find(
|
||||
(entry: FileMetadataEntry) => entry.path === filePath && entry.record_state === "active",
|
||||
)
|
||||
|
||||
// Assert that we found an active entry
|
||||
expect(activeEntry).to.exist
|
||||
|
||||
// Now check the properties of the active entry
|
||||
expect(activeEntry.path).to.equal(filePath)
|
||||
expect(activeEntry.record_state).to.equal("active")
|
||||
expect(activeEntry.record_source).to.equal("cline_edited")
|
||||
expect(activeEntry.cline_read_date).to.be.a("number")
|
||||
expect(activeEntry.cline_edit_date).to.be.a("number")
|
||||
})
|
||||
|
||||
it("should add a record when a file is mentioned", async () => {
|
||||
const filePath = "src/test-file.ts"
|
||||
|
||||
await tracker.trackFileContext(filePath, "file_mentioned")
|
||||
|
||||
// Verify saveTaskMetadata was called with the correct data
|
||||
const savedMetadata = saveTaskMetadataStub.firstCall.args[2]
|
||||
const fileEntry = savedMetadata.files_in_context[0]
|
||||
|
||||
expect(fileEntry.path).to.equal(filePath)
|
||||
expect(fileEntry.record_state).to.equal("active")
|
||||
expect(fileEntry.record_source).to.equal("file_mentioned")
|
||||
expect(fileEntry.cline_read_date).to.be.a("number")
|
||||
expect(fileEntry.cline_edit_date).to.be.null
|
||||
})
|
||||
|
||||
it("should add a record when a file is edited by the user", async () => {
|
||||
const filePath = "src/test-file.ts"
|
||||
|
||||
await tracker.trackFileContext(filePath, "user_edited")
|
||||
|
||||
// Verify saveTaskMetadata was called with the correct data
|
||||
const savedMetadata = saveTaskMetadataStub.firstCall.args[2]
|
||||
const fileEntry = savedMetadata.files_in_context[0]
|
||||
|
||||
expect(fileEntry.path).to.equal(filePath)
|
||||
expect(fileEntry.record_state).to.equal("active")
|
||||
expect(fileEntry.record_source).to.equal("user_edited")
|
||||
expect(fileEntry.user_edit_date).to.be.a("number")
|
||||
|
||||
// Verify the file was added to recentlyModifiedFiles
|
||||
const modifiedFiles = tracker.getAndClearRecentlyModifiedFiles()
|
||||
expect(modifiedFiles).to.include(filePath)
|
||||
})
|
||||
|
||||
it("should mark existing entries as stale when adding a new entry for the same file", async () => {
|
||||
const filePath = "src/test-file.ts"
|
||||
|
||||
// Add an initial entry
|
||||
mockTaskMetadata.files_in_context = [
|
||||
{
|
||||
path: filePath,
|
||||
record_state: "active",
|
||||
record_source: "read_tool",
|
||||
cline_read_date: Date.now() - 1000, // 1 second ago
|
||||
cline_edit_date: null,
|
||||
user_edit_date: null,
|
||||
},
|
||||
]
|
||||
|
||||
// Track a new operation on the same file
|
||||
await tracker.trackFileContext(filePath, "cline_edited")
|
||||
|
||||
// Verify the metadata now has two entries - one stale and one active
|
||||
const savedMetadata = saveTaskMetadataStub.firstCall.args[2]
|
||||
expect(savedMetadata.files_in_context.length).to.equal(2)
|
||||
|
||||
// First entry should be marked as stale
|
||||
expect(savedMetadata.files_in_context[0].record_state).to.equal("stale")
|
||||
|
||||
// New entry should be active
|
||||
const newEntry = savedMetadata.files_in_context[1]
|
||||
expect(newEntry.record_state).to.equal("active")
|
||||
expect(newEntry.record_source).to.equal("cline_edited")
|
||||
})
|
||||
|
||||
it("should setup a file watcher for tracked files", async () => {
|
||||
const filePath = "src/test-file.ts"
|
||||
|
||||
// Create a spy to track if createFileSystemWatcher was called
|
||||
const createWatcherSpy = sinon.spy(vscode.workspace, "createFileSystemWatcher")
|
||||
|
||||
await tracker.trackFileContext(filePath, "read_tool")
|
||||
|
||||
// Verify createFileSystemWatcher was called
|
||||
expect(createWatcherSpy.called).to.be.true
|
||||
createWatcherSpy.restore()
|
||||
|
||||
// Verify onDidChange was called to set up the change listener
|
||||
expect(mockFileSystemWatcher.onDidChange.called).to.be.true
|
||||
})
|
||||
|
||||
it("should track user edits when file watcher detects changes", async () => {
|
||||
const filePath = "src/test-file.ts"
|
||||
|
||||
// First track the file to set up the watcher
|
||||
await tracker.trackFileContext(filePath, "read_tool")
|
||||
|
||||
// Reset the stubs to check the next calls
|
||||
getTaskMetadataStub.resetHistory()
|
||||
saveTaskMetadataStub.resetHistory()
|
||||
|
||||
// Create a spy on trackFileContext to verify it's called with the right parameters
|
||||
const trackFileContextSpy = sandbox.spy(tracker, "trackFileContext")
|
||||
|
||||
// Get the callback that was registered with onDidChange
|
||||
const callback = mockFileSystemWatcher.onDidChange.firstCall.args[0]
|
||||
|
||||
// Directly call the callback to simulate a file change event
|
||||
callback(vscode.Uri.file(path.resolve("/mock/workspace", filePath)))
|
||||
|
||||
// Verify trackFileContext was called with the right parameters
|
||||
expect(trackFileContextSpy.calledWith(filePath, "user_edited")).to.be.true
|
||||
|
||||
// Verify the file was added to recentlyModifiedFiles
|
||||
const modifiedFiles = tracker.getAndClearRecentlyModifiedFiles()
|
||||
expect(modifiedFiles).to.include(filePath)
|
||||
})
|
||||
|
||||
it("should not track Cline edits as user edits", async () => {
|
||||
const filePath = "src/test-file.ts"
|
||||
|
||||
// First track the file to set up the watcher
|
||||
await tracker.trackFileContext(filePath, "read_tool")
|
||||
|
||||
// Mark the file as edited by Cline
|
||||
tracker.markFileAsEditedByCline(filePath)
|
||||
|
||||
// Reset the stubs to check the next calls
|
||||
getTaskMetadataStub.resetHistory()
|
||||
saveTaskMetadataStub.resetHistory()
|
||||
|
||||
// Create a spy on trackFileContext to verify it's not called
|
||||
const trackFileContextSpy = sandbox.spy(tracker, "trackFileContext")
|
||||
|
||||
// Get the callback that was registered with onDidChange
|
||||
const callback = mockFileSystemWatcher.onDidChange.firstCall.args[0]
|
||||
|
||||
// Directly call the callback to simulate a file change event
|
||||
callback(vscode.Uri.file(path.resolve("/mock/workspace", filePath)))
|
||||
|
||||
// Verify trackFileContext was not called with user_edited
|
||||
expect(trackFileContextSpy.calledWith(filePath, "user_edited")).to.be.false
|
||||
|
||||
// Verify the file was not added to recentlyModifiedFiles
|
||||
const modifiedFiles = tracker.getAndClearRecentlyModifiedFiles()
|
||||
expect(modifiedFiles).to.not.include(filePath)
|
||||
})
|
||||
|
||||
it("should dispose file watchers when dispose is called", async () => {
|
||||
const filePath = "src/test-file.ts"
|
||||
|
||||
// Track a file to set up the watcher
|
||||
await tracker.trackFileContext(filePath, "read_tool")
|
||||
|
||||
// Call dispose
|
||||
tracker.dispose()
|
||||
|
||||
// Verify the watcher was disposed
|
||||
expect(mockFileSystemWatcher.dispose.called).to.be.true
|
||||
})
|
||||
})
|
||||
@@ -1,187 +0,0 @@
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { getTaskMetadata, saveTaskMetadata } from "../storage/disk"
|
||||
import type { FileMetadataEntry, ControllerLike } from "./FileContextTrackerTypes"
|
||||
|
||||
// This class is responsible for tracking file operations that may result in stale context.
|
||||
// If a user modifies a file outside of Cline, the context may become stale and need to be updated.
|
||||
// We do not want Cline to reload the context every time a file is modified, so we use this class merely
|
||||
// to inform Cline that the change has occurred, and tell Cline to reload the file before making
|
||||
// any changes to it. This fixes an issue with diff editing, where Cline was unable to complete a diff edit.
|
||||
// a diff edit because the file was modified since Cline last read it.
|
||||
|
||||
// FileContextTracker
|
||||
//
|
||||
// This class is responsible for tracking file operations.
|
||||
// If the full contents of a file are pass to Cline via a tool, mention, or edit, the file is marked as active.
|
||||
// If a file is modified outside of Cline, we detect and track this change to prevent stale context.
|
||||
export class FileContextTracker {
|
||||
readonly taskId: string
|
||||
private controllerRef: WeakRef<ControllerLike>
|
||||
|
||||
// File tracking and watching
|
||||
private fileWatchers = new Map<string, vscode.FileSystemWatcher>()
|
||||
private recentlyModifiedFiles = new Set<string>()
|
||||
private recentlyEditedByCline = new Set<string>()
|
||||
|
||||
constructor(controller: ControllerLike, taskId: string) {
|
||||
this.controllerRef = new WeakRef(controller)
|
||||
this.taskId = taskId
|
||||
}
|
||||
|
||||
// While a task is ref'd by a controller, it will always have access to the extension context
|
||||
// This error is thrown if the controller derefs the task after e.g., aborting the task
|
||||
private context(): vscode.ExtensionContext {
|
||||
const context = this.controllerRef.deref()?.context
|
||||
if (!context) {
|
||||
throw new Error("Unable to access extension context")
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
// Gets the current working directory or returns undefined if it cannot be determined
|
||||
private getCwd(): string | undefined {
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
||||
if (!cwd) {
|
||||
console.info("No workspace folder available - cannot determine current working directory")
|
||||
}
|
||||
return cwd
|
||||
}
|
||||
|
||||
// File watchers are set up for each file that is tracked in the task metadata.
|
||||
async setupFileWatcher(filePath: string) {
|
||||
// Only setup watcher if it doesn't already exist for this file
|
||||
if (this.fileWatchers.has(filePath)) {
|
||||
return
|
||||
}
|
||||
|
||||
const cwd = this.getCwd()
|
||||
if (!cwd) {
|
||||
return
|
||||
}
|
||||
|
||||
// Create a file system watcher for this specific file
|
||||
const fileUri = vscode.Uri.file(path.resolve(cwd, filePath))
|
||||
const watcher = vscode.workspace.createFileSystemWatcher(
|
||||
new vscode.RelativePattern(path.dirname(fileUri.fsPath), path.basename(fileUri.fsPath)),
|
||||
)
|
||||
|
||||
// Track file changes
|
||||
watcher.onDidChange(() => {
|
||||
if (this.recentlyEditedByCline.has(filePath)) {
|
||||
this.recentlyEditedByCline.delete(filePath) // This was an edit by Cline, no need to inform Cline
|
||||
} else {
|
||||
this.recentlyModifiedFiles.add(filePath) // This was a user edit, we will inform Cline
|
||||
this.trackFileContext(filePath, "user_edited") // Update the task metadata with file tracking
|
||||
}
|
||||
})
|
||||
|
||||
// Store the watcher so we can dispose it later
|
||||
this.fileWatchers.set(filePath, watcher)
|
||||
}
|
||||
|
||||
// Tracks a file operation in metadata and sets up a watcher for the file
|
||||
// This is the main entry point for FileContextTracker and is called when a file is passed to Cline via a tool, mention, or edit.
|
||||
async trackFileContext(filePath: string, operation: "read_tool" | "user_edited" | "cline_edited" | "file_mentioned") {
|
||||
try {
|
||||
const cwd = this.getCwd()
|
||||
if (!cwd) {
|
||||
return
|
||||
}
|
||||
|
||||
const context = this.context()
|
||||
// Add file to metadata
|
||||
await this.addFileToFileContextTracker(context, this.taskId, filePath, operation)
|
||||
|
||||
// Set up file watcher for this file
|
||||
await this.setupFileWatcher(filePath)
|
||||
} catch (error) {
|
||||
console.error("Failed to track file operation:", error)
|
||||
}
|
||||
}
|
||||
|
||||
// Adds a file to the metadata tracker
|
||||
// This handles the business logic of determining if the file is new, stale, or active.
|
||||
// It also updates the metadata with the latest read/edit dates.
|
||||
async addFileToFileContextTracker(
|
||||
context: vscode.ExtensionContext,
|
||||
taskId: string,
|
||||
filePath: string,
|
||||
source: FileMetadataEntry["record_source"],
|
||||
) {
|
||||
try {
|
||||
const metadata = await getTaskMetadata(context, taskId)
|
||||
const now = Date.now()
|
||||
|
||||
// Mark existing entries for this file as stale
|
||||
metadata.files_in_context.forEach((entry) => {
|
||||
if (entry.path === filePath && entry.record_state === "active") {
|
||||
entry.record_state = "stale"
|
||||
}
|
||||
})
|
||||
|
||||
// Helper to get the latest date for a specific field and file
|
||||
const getLatestDateForField = (path: string, field: keyof FileMetadataEntry): number | null => {
|
||||
const relevantEntries = metadata.files_in_context
|
||||
.filter((entry) => entry.path === path && entry[field])
|
||||
.sort((a, b) => (b[field] as number) - (a[field] as number))
|
||||
|
||||
return relevantEntries.length > 0 ? (relevantEntries[0][field] as number) : null
|
||||
}
|
||||
|
||||
let newEntry: FileMetadataEntry = {
|
||||
path: filePath,
|
||||
record_state: "active",
|
||||
record_source: source,
|
||||
cline_read_date: getLatestDateForField(filePath, "cline_read_date"),
|
||||
cline_edit_date: getLatestDateForField(filePath, "cline_edit_date"),
|
||||
user_edit_date: getLatestDateForField(filePath, "user_edit_date"),
|
||||
}
|
||||
|
||||
switch (source) {
|
||||
// user_edited: The user has edited the file
|
||||
case "user_edited":
|
||||
newEntry.user_edit_date = now
|
||||
this.recentlyModifiedFiles.add(filePath)
|
||||
break
|
||||
|
||||
// cline_edited: Cline has edited the file
|
||||
case "cline_edited":
|
||||
newEntry.cline_read_date = now
|
||||
newEntry.cline_edit_date = now
|
||||
break
|
||||
|
||||
// read_tool/file_mentioned: Cline has read the file via a tool or file mention
|
||||
case "read_tool":
|
||||
case "file_mentioned":
|
||||
newEntry.cline_read_date = now
|
||||
break
|
||||
}
|
||||
|
||||
metadata.files_in_context.push(newEntry)
|
||||
await saveTaskMetadata(context, taskId, metadata)
|
||||
} catch (error) {
|
||||
console.error("Failed to add file to metadata:", error)
|
||||
}
|
||||
}
|
||||
|
||||
// Returns (and then clears) the set of recently modified files
|
||||
getAndClearRecentlyModifiedFiles(): string[] {
|
||||
const files = Array.from(this.recentlyModifiedFiles)
|
||||
this.recentlyModifiedFiles.clear()
|
||||
return files
|
||||
}
|
||||
|
||||
// Marks a file as edited by Cline to prevent false positives in file watchers
|
||||
markFileAsEditedByCline(filePath: string): void {
|
||||
this.recentlyEditedByCline.add(filePath)
|
||||
}
|
||||
|
||||
// Disposes all file watchers
|
||||
dispose(): void {
|
||||
for (const watcher of this.fileWatchers.values()) {
|
||||
watcher.dispose()
|
||||
}
|
||||
this.fileWatchers.clear()
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import * as vscode from "vscode"
|
||||
|
||||
// Type definitions for FileContextTracker
|
||||
export interface FileMetadataEntry {
|
||||
path: string
|
||||
record_state: "active" | "stale"
|
||||
record_source: "read_tool" | "user_edited" | "cline_edited" | "file_mentioned"
|
||||
cline_read_date: number | null
|
||||
cline_edit_date: number | null
|
||||
user_edit_date?: number | null
|
||||
}
|
||||
|
||||
export interface TaskMetadata {
|
||||
files_in_context: FileMetadataEntry[]
|
||||
}
|
||||
|
||||
// Interface for the controller to avoid direct dependency
|
||||
export interface ControllerLike {
|
||||
context: vscode.ExtensionContext
|
||||
}
|
||||
@@ -44,8 +44,6 @@ import {
|
||||
} from "../storage/state"
|
||||
import { WebviewProvider } from "../webview"
|
||||
import { GlobalFileNames } from "../storage/disk"
|
||||
import { searchWorkspaceFiles } from "../../services/search/file-search"
|
||||
import { getWorkspacePath } from "../../utils/path"
|
||||
|
||||
/*
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
@@ -171,22 +169,9 @@ export class Controller {
|
||||
case "addRemoteServer": {
|
||||
try {
|
||||
await this.mcpHub?.addRemoteServer(message.serverName!, message.serverUrl!)
|
||||
await this.postMessageToWebview({
|
||||
type: "addRemoteServerResult",
|
||||
addRemoteServerResult: {
|
||||
success: true,
|
||||
serverName: message.serverName!,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
await this.postMessageToWebview({
|
||||
type: "addRemoteServerResult",
|
||||
addRemoteServerResult: {
|
||||
success: false,
|
||||
serverName: message.serverName!,
|
||||
error: error.message,
|
||||
},
|
||||
})
|
||||
// We handle the errorin McpHub.ts where the function is defined
|
||||
console.error(`Failed to add remote server ${message.serverName}:`, error)
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -663,86 +648,6 @@ export class Controller {
|
||||
this.postMessageToWebview({ type: "relinquishControl" })
|
||||
break
|
||||
}
|
||||
case "getRelativePaths": {
|
||||
if (message.uris && message.uris.length > 0) {
|
||||
const resolvedPaths = await Promise.all(
|
||||
message.uris.map(async (uriString) => {
|
||||
try {
|
||||
const fileUri = vscode.Uri.parse(uriString, true)
|
||||
const relativePath = vscode.workspace.asRelativePath(fileUri, false)
|
||||
|
||||
if (path.isAbsolute(relativePath)) {
|
||||
console.warn(`Dropped file ${relativePath} is outside the workspace. Sending original path.`)
|
||||
return fileUri.fsPath.replace(/\\/g, "/")
|
||||
} else {
|
||||
let finalPath = "/" + relativePath.replace(/\\/g, "/")
|
||||
try {
|
||||
const stat = await vscode.workspace.fs.stat(fileUri)
|
||||
if (stat.type === vscode.FileType.Directory) {
|
||||
finalPath += "/"
|
||||
}
|
||||
} catch (statError) {
|
||||
console.error(`Error stating file ${fileUri.fsPath}:`, statError)
|
||||
}
|
||||
return finalPath
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error calculating relative path for ${uriString}:`, error)
|
||||
return null
|
||||
}
|
||||
}),
|
||||
)
|
||||
await this.postMessageToWebview({
|
||||
type: "relativePathsResponse",
|
||||
paths: resolvedPaths,
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "searchFiles": {
|
||||
const workspacePath = getWorkspacePath()
|
||||
|
||||
if (!workspacePath) {
|
||||
// Handle case where workspace path is not available
|
||||
await this.postMessageToWebview({
|
||||
type: "fileSearchResults",
|
||||
results: [],
|
||||
mentionsRequestId: message.mentionsRequestId,
|
||||
error: "No workspace path available",
|
||||
})
|
||||
break
|
||||
}
|
||||
try {
|
||||
// Call file search service with query from message
|
||||
const results = await searchWorkspaceFiles(
|
||||
message.query || "",
|
||||
workspacePath,
|
||||
20, // Use default limit, as filtering is now done in the backend
|
||||
)
|
||||
|
||||
// debug logging to be removed
|
||||
//console.log(`controller/index.ts: Search results: ${results.length}`)
|
||||
|
||||
// Send results back to webview
|
||||
await this.postMessageToWebview({
|
||||
type: "fileSearchResults",
|
||||
results,
|
||||
mentionsRequestId: message.mentionsRequestId,
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
|
||||
// Send error response to webview
|
||||
await this.postMessageToWebview({
|
||||
type: "fileSearchResults",
|
||||
results: [],
|
||||
error: errorMessage,
|
||||
mentionsRequestId: message.mentionsRequestId,
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
// Add more switch case statements here as more webview message commands
|
||||
// are created within the webview context (i.e. inside media/main.js)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import { diagnosticsToProblemsString } from "../../integrations/diagnostics"
|
||||
import { getLatestTerminalOutput } from "../../integrations/terminal/get-latest-output"
|
||||
import { getCommitInfo } from "../../utils/git"
|
||||
import { getWorkingState } from "../../utils/git"
|
||||
import { FileContextTracker } from "../context-tracking/FileContextTracker"
|
||||
|
||||
export function openMention(mention?: string): void {
|
||||
if (!mention) {
|
||||
@@ -39,12 +38,7 @@ export function openMention(mention?: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
export async function parseMentions(
|
||||
text: string,
|
||||
cwd: string,
|
||||
urlContentFetcher: UrlContentFetcher,
|
||||
fileContextTracker?: FileContextTracker,
|
||||
): Promise<string> {
|
||||
export async function parseMentions(text: string, cwd: string, urlContentFetcher: UrlContentFetcher): Promise<string> {
|
||||
const mentions: Set<string> = new Set()
|
||||
let parsedText = text.replace(mentionRegexGlobal, (match, mention) => {
|
||||
mentions.add(mention)
|
||||
@@ -104,10 +98,6 @@ export async function parseMentions(
|
||||
parsedText += `\n\n<folder_content path="${mentionPath}">\n${content}\n</folder_content>`
|
||||
} else {
|
||||
parsedText += `\n\n<file_content path="${mentionPath}">\n${content}\n</file_content>`
|
||||
// Track that this file was mentioned and its content was included
|
||||
if (fileContextTracker) {
|
||||
await fileContextTracker.trackFileContext(mentionPath, "file_mentioned")
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (mention.endsWith("/")) {
|
||||
|
||||
@@ -137,7 +137,7 @@ Otherwise, if you have not completed the task and do not need additional informa
|
||||
responseText
|
||||
? `\n\n${mode === "plan" ? "New message to respond to with plan_mode_respond tool (be sure to provide your response in the <response> parameter)" : "New instructions for task continuation"}:\n<user_message>\n${responseText}\n</user_message>`
|
||||
: mode === "plan"
|
||||
? "(The user did not provide a new message. Consider asking them how they'd like you to proceed, or suggest to them to switch to Act mode to continue with the task.)"
|
||||
? "(The user did not provide a new message. Consider asking them how they'd like you to proceed, or to switch to Act mode to continue with the task.)"
|
||||
: ""
|
||||
}`
|
||||
},
|
||||
|
||||
@@ -243,9 +243,13 @@ Your final result description here
|
||||
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.
|
||||
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.)
|
||||
- options: (optional) An array of 2-5 options for the user to choose from. Each option should be a string describing a possible choice or path forward in the planning process. This can help guide the discussion and make it easier for the user to provide input on key decisions. You may not always need to provide options, but it may be helpful in many cases where it can save the user from having to type out a response manually. Do NOT present an option to toggle to Act mode, as this will be something you need to direct the user to do manually themselves.
|
||||
Usage:
|
||||
<plan_mode_respond>
|
||||
<response>Your response here</response>
|
||||
<options>
|
||||
Array of options here (optional), e.g. ["Option 1", "Option 2", "Option 3"]
|
||||
</options>
|
||||
</plan_mode_respond>
|
||||
|
||||
# Tool Use Examples
|
||||
|
||||
@@ -5,26 +5,12 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { ClineMessage } from "../../shared/ExtensionMessage"
|
||||
|
||||
export interface FileMetadataEntry {
|
||||
path: string
|
||||
record_state: "active" | "stale"
|
||||
record_source: "read_tool" | "user_edited" | "cline_edited" | "file_mentioned"
|
||||
cline_read_date: number | null
|
||||
cline_edit_date: number | null
|
||||
user_edit_date?: number | null
|
||||
}
|
||||
|
||||
export interface TaskMetadata {
|
||||
files_in_context: FileMetadataEntry[]
|
||||
}
|
||||
|
||||
export const GlobalFileNames = {
|
||||
apiConversationHistory: "api_conversation_history.json",
|
||||
uiMessages: "ui_messages.json",
|
||||
openRouterModels: "openrouter_models.json",
|
||||
mcpSettings: "cline_mcp_settings.json",
|
||||
clineRules: ".clinerules",
|
||||
taskMetadata: "task_metadata.json",
|
||||
}
|
||||
|
||||
export async function ensureTaskDirectoryExists(context: vscode.ExtensionContext, taskId: string): Promise<string> {
|
||||
@@ -85,25 +71,3 @@ export async function saveClineMessages(context: vscode.ExtensionContext, taskId
|
||||
console.error("Failed to save ui messages:", error)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTaskMetadata(context: vscode.ExtensionContext, taskId: string): Promise<TaskMetadata> {
|
||||
const filePath = path.join(await ensureTaskDirectoryExists(context, taskId), GlobalFileNames.taskMetadata)
|
||||
try {
|
||||
if (await fileExistsAtPath(filePath)) {
|
||||
return JSON.parse(await fs.readFile(filePath, "utf8"))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to read task metadata:", error)
|
||||
}
|
||||
return { files_in_context: [] }
|
||||
}
|
||||
|
||||
export async function saveTaskMetadata(context: vscode.ExtensionContext, taskId: string, metadata: TaskMetadata) {
|
||||
try {
|
||||
const taskDir = await ensureTaskDirectoryExists(context, taskId)
|
||||
const filePath = path.join(taskDir, GlobalFileNames.taskMetadata)
|
||||
await fs.writeFile(filePath, JSON.stringify(metadata, null, 2))
|
||||
} catch (error) {
|
||||
console.error("Failed to save task metadata:", error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ export type SecretKey =
|
||||
| "requestyApiKey"
|
||||
| "togetherApiKey"
|
||||
| "qwenApiKey"
|
||||
| "doubaoApiKey"
|
||||
| "mistralApiKey"
|
||||
| "liteLlmApiKey"
|
||||
| "authNonce"
|
||||
|
||||
@@ -86,7 +86,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
togetherApiKey,
|
||||
togetherModelId,
|
||||
qwenApiKey,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
azureApiVersion,
|
||||
openRouterModelId,
|
||||
@@ -151,7 +150,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getSecret(context, "togetherApiKey") as Promise<string | undefined>,
|
||||
getGlobalState(context, "togetherModelId") as Promise<string | undefined>,
|
||||
getSecret(context, "qwenApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "doubaoApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "mistralApiKey") as Promise<string | undefined>,
|
||||
getGlobalState(context, "azureApiVersion") as Promise<string | undefined>,
|
||||
getGlobalState(context, "openRouterModelId") as Promise<string | undefined>,
|
||||
@@ -256,7 +254,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
togetherModelId,
|
||||
qwenApiKey,
|
||||
qwenApiLine,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
azureApiVersion,
|
||||
openRouterModelId,
|
||||
@@ -326,7 +323,6 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
togetherApiKey,
|
||||
togetherModelId,
|
||||
qwenApiKey,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
azureApiVersion,
|
||||
openRouterModelId,
|
||||
@@ -375,7 +371,6 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
await storeSecret(context, "requestyApiKey", requestyApiKey)
|
||||
await storeSecret(context, "togetherApiKey", togetherApiKey)
|
||||
await storeSecret(context, "qwenApiKey", qwenApiKey)
|
||||
await storeSecret(context, "doubaoApiKey", doubaoApiKey)
|
||||
await storeSecret(context, "mistralApiKey", mistralApiKey)
|
||||
await storeSecret(context, "liteLlmApiKey", liteLlmApiKey)
|
||||
await storeSecret(context, "xaiApiKey", xaiApiKey)
|
||||
@@ -413,7 +408,6 @@ export async function resetExtensionState(context: vscode.ExtensionContext) {
|
||||
"requestyApiKey",
|
||||
"togetherApiKey",
|
||||
"qwenApiKey",
|
||||
"doubaoApiKey",
|
||||
"mistralApiKey",
|
||||
"clineApiKey",
|
||||
"liteLlmApiKey",
|
||||
|
||||
+2
-51
@@ -14,6 +14,7 @@ import { AnthropicHandler } from "../../api/providers/anthropic"
|
||||
import { ClineHandler } from "../../api/providers/cline"
|
||||
import { OpenRouterHandler } from "../../api/providers/openrouter"
|
||||
import { ApiStream } from "../../api/transform/stream"
|
||||
import { GlobalFileNames } from "../storage/disk"
|
||||
import CheckpointTracker from "../../integrations/checkpoints/CheckpointTracker"
|
||||
import { DIFF_VIEW_URI_SCHEME, DiffViewProvider } from "../../integrations/editor/DiffViewProvider"
|
||||
import { formatContentBlockToMarkdown } from "../../integrations/misc/export-markdown"
|
||||
@@ -64,7 +65,6 @@ import { ClineIgnoreController } from ".././ignore/ClineIgnoreController"
|
||||
import { parseMentions } from ".././mentions"
|
||||
import { formatResponse } from ".././prompts/responses"
|
||||
import { addUserInstructions, SYSTEM_PROMPT } from ".././prompts/system"
|
||||
import { FileContextTracker } from "../context-tracking/FileContextTracker"
|
||||
import {
|
||||
checkIsAnthropicContextWindowError,
|
||||
checkIsOpenRouterContextWindowError,
|
||||
@@ -76,7 +76,6 @@ import {
|
||||
getSavedClineMessages,
|
||||
saveApiConversationHistory,
|
||||
saveClineMessages,
|
||||
GlobalFileNames,
|
||||
} from "../storage/disk"
|
||||
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
|
||||
@@ -118,9 +117,6 @@ export class Task {
|
||||
isAwaitingPlanResponse = false
|
||||
didRespondToPlanAskBySwitchingMode = false
|
||||
|
||||
// File tracking
|
||||
private fileContextTracker: FileContextTracker
|
||||
|
||||
// streaming
|
||||
isWaitingForFirstChunk = false
|
||||
isStreaming = false
|
||||
@@ -172,9 +168,6 @@ export class Task {
|
||||
throw new Error("Either historyItem or task/images must be provided")
|
||||
}
|
||||
|
||||
// Initialize file context tracker
|
||||
this.fileContextTracker = new FileContextTracker(controller, this.taskId)
|
||||
|
||||
// Now that taskId is initialized, we can build the API handler
|
||||
this.api = buildApiHandler({
|
||||
...apiConfiguration,
|
||||
@@ -986,7 +979,6 @@ export class Task {
|
||||
this.urlContentFetcher.closeBrowser()
|
||||
this.browserSession.closeBrowser()
|
||||
this.clineIgnoreController.dispose()
|
||||
this.fileContextTracker.dispose()
|
||||
await this.diffViewProvider.revertChanges() // need to await for when we want to make sure directories/files are reverted before re-starting the task from a checkpoint
|
||||
}
|
||||
|
||||
@@ -1812,20 +1804,10 @@ export class Task {
|
||||
}
|
||||
}
|
||||
|
||||
// Mark the file as edited by Cline to prevent false "recently modified" warnings
|
||||
this.fileContextTracker.markFileAsEditedByCline(relPath)
|
||||
|
||||
const { newProblemsMessage, userEdits, autoFormattingEdits, finalContent } =
|
||||
await this.diffViewProvider.saveChanges()
|
||||
this.didEditFile = true // used to determine if we should wait for busy terminal to update before sending api request
|
||||
|
||||
// Track file edit operation
|
||||
await this.fileContextTracker.trackFileContext(relPath, "cline_edited")
|
||||
|
||||
if (userEdits) {
|
||||
// Track file edit operation
|
||||
await this.fileContextTracker.trackFileContext(relPath, "user_edited")
|
||||
|
||||
await this.say(
|
||||
"user_feedback_diff",
|
||||
JSON.stringify({
|
||||
@@ -1933,10 +1915,6 @@ export class Task {
|
||||
}
|
||||
// now execute the tool like normal
|
||||
const content = await extractTextFromFile(absolutePath)
|
||||
|
||||
// Track file read operation
|
||||
await this.fileContextTracker.trackFileContext(relPath, "read_tool")
|
||||
|
||||
pushToolResult(content)
|
||||
|
||||
break
|
||||
@@ -2671,9 +2649,6 @@ export class Task {
|
||||
})
|
||||
}
|
||||
|
||||
// Store the number of options for telemetry
|
||||
const options = parsePartialArrayString(optionsRaw || "[]")
|
||||
|
||||
const { text, images } = await this.ask("followup", JSON.stringify(sharedMessage), false)
|
||||
|
||||
// Check if options contains the text response
|
||||
@@ -2687,11 +2662,9 @@ export class Task {
|
||||
selected: text,
|
||||
} satisfies ClineAskQuestion)
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
telemetryService.captureOptionSelected(this.taskId, options.length, "act")
|
||||
}
|
||||
} else {
|
||||
// Option not selected, send user feedback
|
||||
telemetryService.captureOptionsIgnored(this.taskId, options.length, "act")
|
||||
await this.say("user_feedback", text ?? "", images)
|
||||
}
|
||||
|
||||
@@ -2732,9 +2705,6 @@ export class Task {
|
||||
// })
|
||||
// }
|
||||
|
||||
// Store the number of options for telemetry
|
||||
const options = parsePartialArrayString(optionsRaw || "[]")
|
||||
|
||||
this.isAwaitingPlanResponse = true
|
||||
let { text, images } = await this.ask("plan_mode_respond", JSON.stringify(sharedMessage), false)
|
||||
this.isAwaitingPlanResponse = false
|
||||
@@ -2755,12 +2725,10 @@ export class Task {
|
||||
selected: text,
|
||||
} satisfies ClinePlanModeResponse)
|
||||
await this.saveClineMessagesAndUpdateHistory()
|
||||
telemetryService.captureOptionSelected(this.taskId, options.length, "plan")
|
||||
}
|
||||
} else {
|
||||
// Option not selected, send user feedback
|
||||
if (text || images?.length) {
|
||||
telemetryService.captureOptionsIgnored(this.taskId, options.length, "plan")
|
||||
await this.say("user_feedback", text ?? "", images)
|
||||
}
|
||||
}
|
||||
@@ -3383,16 +3351,9 @@ export class Task {
|
||||
block.text.includes("<task>") ||
|
||||
block.text.includes("<user_message>")
|
||||
) {
|
||||
const parsedText = await parseMentions(
|
||||
block.text,
|
||||
cwd,
|
||||
this.urlContentFetcher,
|
||||
this.fileContextTracker,
|
||||
)
|
||||
|
||||
return {
|
||||
...block,
|
||||
text: parsedText,
|
||||
text: await parseMentions(block.text, cwd, this.urlContentFetcher),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3529,16 +3490,6 @@ export class Task {
|
||||
details += terminalDetails
|
||||
}
|
||||
|
||||
// Add recently modified files section
|
||||
const recentlyModifiedFiles = this.fileContextTracker.getAndClearRecentlyModifiedFiles()
|
||||
if (recentlyModifiedFiles.length > 0) {
|
||||
details +=
|
||||
"\n\n# Recently Modified Files\nThese files have been modified since you last accessed them (file was just edited so you may need to re-read it before editing):"
|
||||
for (const filePath of recentlyModifiedFiles) {
|
||||
details += `\n${filePath}`
|
||||
}
|
||||
}
|
||||
|
||||
// Add current time information with timezone
|
||||
const now = new Date()
|
||||
const formatter = new Intl.DateTimeFormat(undefined, {
|
||||
|
||||
@@ -44,18 +44,6 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
|
||||
return findLast(Array.from(this.activeInstances), (instance) => instance.view?.visible === true)
|
||||
}
|
||||
|
||||
public static getAllInstances(): WebviewProvider[] {
|
||||
return Array.from(this.activeInstances)
|
||||
}
|
||||
|
||||
public static getSidebarInstance() {
|
||||
return Array.from(this.activeInstances).find((instance) => instance.view && "onDidChangeVisibility" in instance.view)
|
||||
}
|
||||
|
||||
public static getTabInstances(): WebviewProvider[] {
|
||||
return Array.from(this.activeInstances).filter((instance) => instance.view && "onDidChangeViewState" in instance.view)
|
||||
}
|
||||
|
||||
async resolveWebviewView(webviewView: vscode.WebviewView | vscode.WebviewPanel) {
|
||||
this.view = webviewView
|
||||
|
||||
|
||||
+54
-67
@@ -41,37 +41,35 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
)
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("cline.plusButtonClicked", async (webview: any) => {
|
||||
const openChat = async (instance?: WebviewProvider) => {
|
||||
await instance?.controller.clearTask()
|
||||
await instance?.controller.postStateToWebview()
|
||||
await instance?.controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
}
|
||||
const isSidebar = !webview
|
||||
if (isSidebar) {
|
||||
openChat(WebviewProvider.getSidebarInstance())
|
||||
} else {
|
||||
WebviewProvider.getTabInstances().forEach(openChat)
|
||||
vscode.commands.registerCommand("cline.plusButtonClicked", async () => {
|
||||
Logger.log("Plus button Clicked")
|
||||
const visibleWebview = WebviewProvider.getVisibleInstance()
|
||||
if (!visibleWebview) {
|
||||
Logger.log("Cannot find any visible Cline instances.")
|
||||
return
|
||||
}
|
||||
|
||||
await visibleWebview.controller.clearTask()
|
||||
await visibleWebview.controller.postStateToWebview()
|
||||
await visibleWebview.controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("cline.mcpButtonClicked", (webview: any) => {
|
||||
const openMcp = (instance?: WebviewProvider) =>
|
||||
instance?.controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "mcpButtonClicked",
|
||||
})
|
||||
const isSidebar = !webview
|
||||
if (isSidebar) {
|
||||
openMcp(WebviewProvider.getSidebarInstance())
|
||||
} else {
|
||||
WebviewProvider.getTabInstances().forEach(openMcp)
|
||||
vscode.commands.registerCommand("cline.mcpButtonClicked", () => {
|
||||
const visibleWebview = WebviewProvider.getVisibleInstance()
|
||||
if (!visibleWebview) {
|
||||
Logger.log("Cannot find any visible Cline instances.")
|
||||
return
|
||||
}
|
||||
|
||||
visibleWebview.controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "mcpButtonClicked",
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -112,58 +110,47 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
context.subscriptions.push(vscode.commands.registerCommand("cline.openInNewTab", openClineInNewTab))
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("cline.settingsButtonClicked", (webview: any) => {
|
||||
WebviewProvider.getAllInstances().forEach((instance) => {
|
||||
const openSettings = async (instance?: WebviewProvider) => {
|
||||
instance?.controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "settingsButtonClicked",
|
||||
})
|
||||
}
|
||||
const isSidebar = !webview
|
||||
if (isSidebar) {
|
||||
openSettings(WebviewProvider.getSidebarInstance())
|
||||
} else {
|
||||
WebviewProvider.getTabInstances().forEach(openSettings)
|
||||
}
|
||||
vscode.commands.registerCommand("cline.settingsButtonClicked", () => {
|
||||
//vscode.window.showInformationMessage(message)
|
||||
const visibleWebview = WebviewProvider.getVisibleInstance()
|
||||
if (!visibleWebview) {
|
||||
Logger.log("Cannot find any visible Cline instances.")
|
||||
return
|
||||
}
|
||||
|
||||
visibleWebview.controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "settingsButtonClicked",
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("cline.historyButtonClicked", (webview: any) => {
|
||||
WebviewProvider.getAllInstances().forEach((instance) => {
|
||||
const openHistory = async (instance?: WebviewProvider) => {
|
||||
instance?.controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "historyButtonClicked",
|
||||
})
|
||||
}
|
||||
const isSidebar = !webview
|
||||
if (isSidebar) {
|
||||
openHistory(WebviewProvider.getSidebarInstance())
|
||||
} else {
|
||||
WebviewProvider.getTabInstances().forEach(openHistory)
|
||||
}
|
||||
vscode.commands.registerCommand("cline.historyButtonClicked", () => {
|
||||
const visibleWebview = WebviewProvider.getVisibleInstance()
|
||||
if (!visibleWebview) {
|
||||
Logger.log("Cannot find any visible Cline instances.")
|
||||
return
|
||||
}
|
||||
|
||||
visibleWebview.controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "historyButtonClicked",
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("cline.accountButtonClicked", (webview: any) => {
|
||||
WebviewProvider.getAllInstances().forEach((instance) => {
|
||||
const openAccount = async (instance?: WebviewProvider) => {
|
||||
instance?.controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "accountButtonClicked",
|
||||
})
|
||||
}
|
||||
const isSidebar = !webview
|
||||
if (isSidebar) {
|
||||
openAccount(WebviewProvider.getSidebarInstance())
|
||||
} else {
|
||||
WebviewProvider.getTabInstances().forEach(openAccount)
|
||||
}
|
||||
vscode.commands.registerCommand("cline.accountButtonClicked", () => {
|
||||
const visibleWebview = WebviewProvider.getVisibleInstance()
|
||||
if (!visibleWebview) {
|
||||
Logger.log("Cannot find any visible Cline instances.")
|
||||
return
|
||||
}
|
||||
|
||||
visibleWebview.controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "accountButtonClicked",
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -90,11 +90,7 @@ export class GitOperations {
|
||||
const lfsPatterns = await getLfsPatterns(cwd)
|
||||
await writeExcludesFile(gitPath, lfsPatterns)
|
||||
|
||||
const addFilesResult = await this.addCheckpointFiles(git)
|
||||
if (!addFilesResult.success) {
|
||||
console.error("Failed to add at least one file(s) to checkpoints shadow git")
|
||||
throw new Error("Failed to add at least one file(s) to checkpoints shadow git")
|
||||
}
|
||||
await this.addCheckpointFiles(git)
|
||||
|
||||
// Initial commit only on first repo creation
|
||||
await git.commit("initial commit", { "--allow-empty": null })
|
||||
@@ -146,7 +142,6 @@ export class GitOperations {
|
||||
ignore: [".git"], // Ignore root level .git
|
||||
dot: true,
|
||||
markDirectories: false,
|
||||
suppressErrors: true,
|
||||
})
|
||||
|
||||
// For each nested .git directory, rename it based on operation
|
||||
@@ -195,18 +190,18 @@ export class GitOperations {
|
||||
await this.renameNestedGitRepos(true)
|
||||
console.info("Starting checkpoint add operation...")
|
||||
|
||||
// Attempt to add all files. Any files with permissions errors will not be added,
|
||||
// but the process will proceed and add the rest (--ignore-errors).
|
||||
try {
|
||||
await git.add([".", "--ignore-errors"])
|
||||
await git.add(".")
|
||||
const durationMs = Math.round(performance.now() - startTime)
|
||||
console.debug(`Checkpoint add operation completed in ${durationMs}ms`)
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
return { success: false }
|
||||
console.error("Checkpoint add operation failed:", error)
|
||||
throw error
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false }
|
||||
console.error("Failed to add files to checkpoint", error)
|
||||
throw error
|
||||
} finally {
|
||||
await this.renameNestedGitRepos(false)
|
||||
}
|
||||
|
||||
@@ -165,10 +165,7 @@ class CheckpointTracker {
|
||||
|
||||
console.info(`Using shadow git at: ${gitPath}`)
|
||||
|
||||
const addFilesResult = await this.gitOperations.addCheckpointFiles(git)
|
||||
if (!addFilesResult.success) {
|
||||
console.error("Failed to add at least one file(s) to checkpoints shadow git")
|
||||
}
|
||||
await this.gitOperations.addCheckpointFiles(git)
|
||||
|
||||
const commitMessage = "checkpoint-" + this.cwdHash + "-" + this.taskId
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdir, access, constants } from "fs/promises"
|
||||
import { mkdir } from "fs/promises"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import os from "os"
|
||||
@@ -31,9 +31,7 @@ export async function getShadowGitPath(globalStoragePath: string, taskId: string
|
||||
/**
|
||||
* Gets the current working directory from the VS Code workspace.
|
||||
* Validates that checkpoints are not being used in protected directories
|
||||
* like home, Desktop, Documents, or Downloads. Checks to confirm that the workspace
|
||||
* is accessible and that we will not encounter breaking permissions issues when
|
||||
* creating checkpoints.
|
||||
* like home, Desktop, Documents, or Downloads.
|
||||
*
|
||||
* Protected directories:
|
||||
* - User's home directory
|
||||
@@ -42,23 +40,13 @@ export async function getShadowGitPath(globalStoragePath: string, taskId: string
|
||||
* - Downloads
|
||||
*
|
||||
* @returns Promise<string> The absolute path to the current working directory
|
||||
* @throws Error if no workspace is detected, if in a protected directory, or if no read access
|
||||
* @throws Error if no workspace is detected or if in a protected directory
|
||||
*/
|
||||
export async function getWorkingDirectory(): Promise<string> {
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
||||
if (!cwd) {
|
||||
throw new Error("No workspace detected. Please open Cline in a workspace to use checkpoints.")
|
||||
}
|
||||
|
||||
// Check if directory exists and we have read permissions
|
||||
try {
|
||||
await access(cwd, constants.R_OK)
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Cannot access workspace directory. Please ensure VS Code has permission to access your workspace. Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
|
||||
const homedir = os.homedir()
|
||||
const desktopPath = path.join(homedir, "Desktop")
|
||||
const documentsPath = path.join(homedir, "Documents")
|
||||
|
||||
@@ -79,15 +79,6 @@ export class DiffViewProvider {
|
||||
if (!this.relPath || !this.activeLineController || !this.fadedOverlayController) {
|
||||
throw new Error("Required values not set")
|
||||
}
|
||||
|
||||
// --- Fix to prevent duplicate BOM ---
|
||||
// Strip potential BOM from incoming content. VS Code's `applyEdit` might implicitly handle the BOM
|
||||
// when replacing from the start (0,0), and we want to avoid duplication.
|
||||
// Final BOM is handled in `saveChanges`.
|
||||
if (accumulatedContent.startsWith("\ufeff")) {
|
||||
accumulatedContent = accumulatedContent.slice(1) // Remove the BOM character
|
||||
}
|
||||
|
||||
this.newContent = accumulatedContent
|
||||
const accumulatedLines = accumulatedContent.split("\n")
|
||||
if (!isFinal) {
|
||||
|
||||
+10
-13
@@ -659,27 +659,24 @@ export class McpHub {
|
||||
autoApprove: [],
|
||||
}
|
||||
|
||||
const parsedConfig = ServerConfigSchema.parse(serverConfig)
|
||||
// TS expects the server config to be a McpServerConfig, but we know it's valid
|
||||
// The issue is that the type is not having the transportType field added to it
|
||||
|
||||
settings.mcpServers[serverName] = parsedConfig
|
||||
// ToDo: Add input types reflecting the non-transformed version
|
||||
settings.mcpServers[serverName] = serverConfig as unknown as McpServerConfig
|
||||
const settingsPath = await this.getMcpSettingsFilePath()
|
||||
|
||||
// We don't write the zod-transformed version to the file.
|
||||
// The above parse() call adds the transportType field to the server config
|
||||
// It would be fine if this was written, but we don't want to clutter up the file with internal details
|
||||
|
||||
// ToDo: We could benefit from input / output types reflecting the non-transformed / transformed versions
|
||||
await fs.writeFile(
|
||||
settingsPath,
|
||||
JSON.stringify({ mcpServers: { ...settings.mcpServers, [serverName]: serverConfig } }, null, 2),
|
||||
)
|
||||
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2))
|
||||
|
||||
await this.updateServerConnections(settings.mcpServers)
|
||||
|
||||
vscode.window.showInformationMessage(`Added ${serverName} MCP server`)
|
||||
vscode.window.showInformationMessage(`Added and connected to ${serverName} MCP server`)
|
||||
} catch (error) {
|
||||
console.error("Failed to add remote MCP server:", error)
|
||||
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to add remote MCP server: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ interface SearchResult {
|
||||
|
||||
const MAX_RESULTS = 300
|
||||
|
||||
export async function getBinPath(vscodeAppRoot: string): Promise<string | undefined> {
|
||||
async function getBinPath(vscodeAppRoot: string): Promise<string | undefined> {
|
||||
const checkPath = async (pkgFolder: string) => {
|
||||
const fullPath = path.join(vscodeAppRoot, pkgFolder, binName)
|
||||
return (await fileExistsAtPath(fullPath)) ? fullPath : undefined
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
import * as childProcess from "child_process"
|
||||
import * as readline from "readline"
|
||||
import { getBinPath } from "../ripgrep"
|
||||
import type { Fzf, FzfResultItem } from "fzf"
|
||||
|
||||
// Wrapper function for childProcess.spawn
|
||||
export type SpawnFunction = typeof childProcess.spawn
|
||||
export const getSpawnFunction = (): SpawnFunction => childProcess.spawn
|
||||
|
||||
export async function executeRipgrepForFiles(
|
||||
rgPath: string,
|
||||
workspacePath: string,
|
||||
limit: number = 5000,
|
||||
): Promise<{ path: string; type: "file" | "folder"; label?: string }[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Arguments for ripgrep to list files, follow symlinks, include hidden, and exclude common directories
|
||||
const args = [
|
||||
"--files",
|
||||
"--follow",
|
||||
"--hidden",
|
||||
"-g",
|
||||
"!**/{node_modules,.git,.github,out,dist,__pycache__,.venv,.env,venv,env,.cache,tmp,temp}/**",
|
||||
workspacePath,
|
||||
]
|
||||
|
||||
// Spawn the ripgrep process with the specified arguments
|
||||
const rgProcess = getSpawnFunction()(rgPath, args)
|
||||
const rl = readline.createInterface({ input: rgProcess.stdout })
|
||||
|
||||
// Array to store file results and Set to track unique directories
|
||||
const fileResults: { path: string; type: "file" | "folder"; label?: string }[] = []
|
||||
const dirSet = new Set<string>()
|
||||
let count = 0
|
||||
|
||||
// Handle each line of output from ripgrep (each line is a file path)
|
||||
rl.on("line", (line) => {
|
||||
if (count >= limit) {
|
||||
rl.close()
|
||||
rgProcess.kill()
|
||||
return
|
||||
}
|
||||
|
||||
// Convert absolute path to a relative path from workspace root
|
||||
const relativePath = path.relative(workspacePath, line)
|
||||
|
||||
// Add file result to array
|
||||
fileResults.push({
|
||||
path: relativePath,
|
||||
type: "file",
|
||||
label: path.basename(relativePath),
|
||||
})
|
||||
|
||||
// Extract and add parent directories to the set
|
||||
let dirPath = path.dirname(relativePath)
|
||||
while (dirPath && dirPath !== "." && dirPath !== "/") {
|
||||
dirSet.add(dirPath)
|
||||
dirPath = path.dirname(dirPath)
|
||||
}
|
||||
|
||||
count++
|
||||
})
|
||||
|
||||
// Capture any error output from ripgrep
|
||||
let errorOutput = ""
|
||||
rgProcess.stderr.on("data", (data) => (errorOutput += data.toString()))
|
||||
|
||||
// When ripgrep finishes or is closed
|
||||
rl.on("close", () => {
|
||||
if (errorOutput && fileResults.length === 0) {
|
||||
reject(new Error(`ripgrep process error: ${errorOutput.trim()}`))
|
||||
return
|
||||
}
|
||||
|
||||
// Transform directory paths from Set into structured results
|
||||
const dirResults = Array.from(dirSet, (dirPath): { path: string; type: "folder"; label?: string } => ({
|
||||
path: dirPath,
|
||||
type: "folder",
|
||||
label: path.basename(dirPath),
|
||||
}))
|
||||
|
||||
// Resolve combined results of files and directories
|
||||
resolve([...fileResults, ...dirResults])
|
||||
})
|
||||
|
||||
// Handle process-level errors
|
||||
rgProcess.on("error", (error) => reject(new Error(`ripgrep process error: ${error.message}`)))
|
||||
})
|
||||
}
|
||||
|
||||
export async function searchWorkspaceFiles(
|
||||
query: string,
|
||||
workspacePath: string,
|
||||
limit: number = 20,
|
||||
): Promise<{ path: string; type: "file" | "folder"; label?: string }[]> {
|
||||
try {
|
||||
const rgPath = await getBinPath(vscode.env.appRoot)
|
||||
|
||||
if (!rgPath) {
|
||||
throw new Error("Could not find ripgrep binary")
|
||||
}
|
||||
|
||||
// Get all files and directories
|
||||
const allItems = await executeRipgrepForFiles(rgPath, workspacePath, 5000)
|
||||
|
||||
// If no query, just return the top items
|
||||
if (!query.trim()) {
|
||||
return allItems.slice(0, limit)
|
||||
}
|
||||
|
||||
// Match Scoring - Prioritize the label (filename) by including it twice in the search string
|
||||
// Use multiple tiebreakers in order of importance: Match score, then length of match (shorter=better)
|
||||
// Get more (2x) results than needed for filtering, we pick the top half after sorting
|
||||
const fzfModule = await import("fzf")
|
||||
const fzf = new fzfModule.Fzf(allItems, {
|
||||
selector: (item: { label?: string; path: string }) => `${item.label || ""} ${item.label || ""} ${item.path}`,
|
||||
tiebreakers: [OrderbyMatchScore, fzfModule.byLengthAsc],
|
||||
limit: limit * 2,
|
||||
})
|
||||
|
||||
// The min threshold value will require some testing and tuning as the scores are exponential, and exagerated
|
||||
const MIN_SCORE_THRESHOLD = 100
|
||||
|
||||
// Filter results by score and map to original items
|
||||
// Use exponential scaling for normalization
|
||||
// This gives a more dramatic difference between good and bad matches
|
||||
const filteredResults = fzf
|
||||
.find(query)
|
||||
.filter(({ score }: { score: number }) => Math.exp(score / 20) >= MIN_SCORE_THRESHOLD)
|
||||
.slice(0, limit)
|
||||
|
||||
// Verify if the path exists and is actually a directory
|
||||
const verifiedResultsPromises = filteredResults.map(
|
||||
async ({ item }: { item: { path: string; type: "file" | "folder"; label?: string } }) => {
|
||||
const fullPath = path.join(workspacePath, item.path)
|
||||
let type = item.type
|
||||
|
||||
try {
|
||||
const stats = await fs.promises.lstat(fullPath)
|
||||
type = stats.isDirectory() ? "folder" : "file"
|
||||
} catch {
|
||||
// Keep original type if path doesn't exist
|
||||
}
|
||||
|
||||
return { ...item, type }
|
||||
},
|
||||
)
|
||||
|
||||
return await Promise.all(verifiedResultsPromises)
|
||||
} catch (error) {
|
||||
console.error("Error in searchWorkspaceFiles:", error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Custom match scoring for results ordering
|
||||
// Candidate score tiebreaker - fewer gaps between matched characters scores higher
|
||||
export const OrderbyMatchScore = (a: FzfResultItem<any>, b: FzfResultItem<any>) => {
|
||||
const countGaps = (positions: Iterable<number>) => {
|
||||
let gaps = 0,
|
||||
prev = -Infinity
|
||||
for (const pos of positions) {
|
||||
if (prev !== -Infinity && pos - prev > 1) {
|
||||
gaps++
|
||||
}
|
||||
prev = pos
|
||||
}
|
||||
return gaps
|
||||
}
|
||||
|
||||
return countGaps(a.positions) - countGaps(b.positions)
|
||||
}
|
||||
@@ -28,10 +28,6 @@ class PostHogClient {
|
||||
TOKEN_USAGE: "task.tokens",
|
||||
// Tracks switches between plan and act modes
|
||||
MODE_SWITCH: "task.mode",
|
||||
// Tracks when users select an option from AI-generated followup questions
|
||||
OPTION_SELECTED: "task.option_selected",
|
||||
// Tracks when users type a custom response instead of selecting an option from AI-generated followup questions
|
||||
OPTIONS_IGNORED: "task.options_ignored",
|
||||
// Tracks usage of the git-based checkpoint system (shadow_git_initialized, commit_created, branch_created, branch_deleted_active, branch_deleted_inactive, restored)
|
||||
CHECKPOINT_USED: "task.checkpoint_used",
|
||||
// Tracks when tools (like file operations, commands) are used
|
||||
@@ -461,40 +457,6 @@ class PostHogClient {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when a user selects an option from AI-generated followup questions
|
||||
* @param taskId Unique identifier for the task
|
||||
* @param qty The quantity of options that were presented
|
||||
* @param mode The mode in which the option was selected ("plan" or "act")
|
||||
*/
|
||||
public captureOptionSelected(taskId: string, qty: number, mode: "plan" | "act") {
|
||||
this.capture({
|
||||
event: PostHogClient.EVENTS.TASK.OPTION_SELECTED,
|
||||
properties: {
|
||||
taskId,
|
||||
qty,
|
||||
mode,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when a user types a custom response instead of selecting one of the AI-generated followup questions
|
||||
* @param taskId Unique identifier for the task
|
||||
* @param qty The quantity of options that were presented
|
||||
* @param mode The mode in which the custom response was provided ("plan" or "act")
|
||||
*/
|
||||
public captureOptionsIgnored(taskId: string, qty: number, mode: "plan" | "act") {
|
||||
this.capture({
|
||||
event: PostHogClient.EVENTS.TASK.OPTIONS_IGNORED,
|
||||
properties: {
|
||||
taskId,
|
||||
qty,
|
||||
mode,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
public isTelemetryEnabled(): boolean {
|
||||
return this.telemetryEnabled
|
||||
}
|
||||
|
||||
@@ -35,16 +35,12 @@ export interface ExtensionMessage {
|
||||
| "openGraphData"
|
||||
| "isImageUrlResult"
|
||||
| "didUpdateSettings"
|
||||
| "addRemoteServerResult"
|
||||
| "userCreditsBalance"
|
||||
| "userCreditsUsage"
|
||||
| "userCreditsPayments"
|
||||
| "totalTasksSize"
|
||||
| "addToInput"
|
||||
| "relativePathsResponse" // Handles single and multiple path responses
|
||||
| "fileSearchResults"
|
||||
text?: string
|
||||
paths?: (string | null)[] // Used for relativePathsResponse
|
||||
action?:
|
||||
| "chatButtonClicked"
|
||||
| "mcpButtonClicked"
|
||||
@@ -84,17 +80,6 @@ export interface ExtensionMessage {
|
||||
userCreditsUsage?: UsageTransaction[]
|
||||
userCreditsPayments?: PaymentTransaction[]
|
||||
totalTasksSize?: number | null
|
||||
mentionsRequestId?: string
|
||||
results?: Array<{
|
||||
path: string
|
||||
type: "file" | "folder"
|
||||
label?: string
|
||||
}>
|
||||
addRemoteServerResult?: {
|
||||
success: boolean
|
||||
serverName: string
|
||||
error?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type Invoke = "sendMessage" | "primaryButtonClick" | "secondaryButtonClick"
|
||||
|
||||
@@ -67,11 +67,8 @@ export interface WebviewMessage {
|
||||
| "optionsResponse"
|
||||
| "requestTotalTasksSize"
|
||||
| "taskFeedback"
|
||||
| "getRelativePaths" // Handles single and multiple URI resolution
|
||||
| "searchFiles"
|
||||
// | "relaunchChromeDebugMode"
|
||||
text?: string
|
||||
uris?: string[] // Used for getRelativePaths
|
||||
disabled?: boolean
|
||||
askResponse?: ClineAskResponse
|
||||
apiConfiguration?: ApiConfiguration
|
||||
@@ -100,8 +97,6 @@ export interface WebviewMessage {
|
||||
customInstructionsSetting?: string
|
||||
// For task feedback
|
||||
feedbackType?: TaskFeedbackType
|
||||
mentionsRequestId?: string
|
||||
query?: string
|
||||
}
|
||||
|
||||
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
import { expect } from "chai"
|
||||
|
||||
import { mentionRegex, mentionRegexGlobal } from "../context-mentions"
|
||||
|
||||
interface TestResult {
|
||||
actual: string | null
|
||||
expected: string | null
|
||||
}
|
||||
|
||||
function testMention(input: string, expected: string | null): TestResult {
|
||||
const match = mentionRegex.exec(input)
|
||||
return {
|
||||
actual: match ? match[0] : null,
|
||||
expected,
|
||||
}
|
||||
}
|
||||
|
||||
function assertMatch(result: TestResult) {
|
||||
expect(result.actual).eq(result.expected)
|
||||
return true
|
||||
}
|
||||
|
||||
describe("Mention Regex", () => {
|
||||
describe("Windows Path Support", () => {
|
||||
it("matches simple Windows paths", () => {
|
||||
const cases: Array<[string, string]> = [
|
||||
["@/C:\\folder\\file.txt", "@/C:\\folder\\file.txt"],
|
||||
["@/C:\\file.txt", "@/C:\\file.txt"],
|
||||
]
|
||||
|
||||
cases.forEach(([input, expected]) => {
|
||||
const result = testMention(input, expected)
|
||||
assertMatch(result)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Edge Cases", () => {
|
||||
it("handles edge cases correctly", () => {
|
||||
const cases: Array<[string, string]> = [
|
||||
["@/C:\\Users\\name\\path\\to\\文件夹\\file.txt", "@/C:\\Users\\name\\path\\to\\文件夹\\file.txt"],
|
||||
["@/path123/file-name_2.0.txt", "@/path123/file-name_2.0.txt"],
|
||||
]
|
||||
|
||||
cases.forEach(([input, expected]) => {
|
||||
const result = testMention(input, expected)
|
||||
assertMatch(result)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Existing Functionality", () => {
|
||||
it("matches Unix paths", () => {
|
||||
const cases: Array<[string, string]> = [
|
||||
["@/usr/local/bin/file", "@/usr/local/bin/file"],
|
||||
["@/path/to/file.txt", "@/path/to/file.txt"],
|
||||
["@//etc/host", "@//etc/host"],
|
||||
]
|
||||
|
||||
cases.forEach(([input, expected]) => {
|
||||
const result = testMention(input, expected)
|
||||
assertMatch(result)
|
||||
})
|
||||
})
|
||||
|
||||
it("matches URLs", () => {
|
||||
const cases: Array<[string, string]> = [
|
||||
["@http://example.com", "@http://example.com"],
|
||||
["@https://example.com/path/to/file.html", "@https://example.com/path/to/file.html"],
|
||||
["@ftp://server.example.com/file.zip", "@ftp://server.example.com/file.zip"],
|
||||
]
|
||||
|
||||
cases.forEach(([input, expected]) => {
|
||||
const result = testMention(input, expected)
|
||||
assertMatch(result)
|
||||
})
|
||||
})
|
||||
|
||||
it("matches git hashes", () => {
|
||||
const cases: Array<[string, string]> = [
|
||||
["@abcdef1234567890abcdef1234567890abcdef12", "@abcdef1234567890abcdef1234567890abcdef12"],
|
||||
]
|
||||
|
||||
cases.forEach(([input, expected]) => {
|
||||
const result = testMention(input, expected)
|
||||
assertMatch(result)
|
||||
})
|
||||
})
|
||||
|
||||
it("matches special keywords", () => {
|
||||
const cases: Array<[string, string]> = [
|
||||
["@problems", "@problems"],
|
||||
["@git-changes", "@git-changes"],
|
||||
["@terminal", "@terminal"],
|
||||
]
|
||||
|
||||
cases.forEach(([input, expected]) => {
|
||||
const result = testMention(input, expected)
|
||||
assertMatch(result)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Invalid Patterns", () => {
|
||||
it("rejects invalid patterns", () => {
|
||||
const cases: Array<[string, null]> = [
|
||||
["C:\\folder\\file.txt", null],
|
||||
["@", null],
|
||||
["@ C:\\file.txt", null],
|
||||
]
|
||||
|
||||
cases.forEach(([input, expected]) => {
|
||||
const result = testMention(input, expected)
|
||||
assertMatch(result)
|
||||
})
|
||||
})
|
||||
|
||||
it("matches only until invalid characters", () => {
|
||||
const result = testMention("@/C:\\folder\\file.txt invalid suffix", "@/C:\\folder\\file.txt")
|
||||
assertMatch(result)
|
||||
})
|
||||
})
|
||||
|
||||
describe("In Context", () => {
|
||||
it("matches mentions within text", () => {
|
||||
const cases: Array<[string, string]> = [
|
||||
["Check the file at @/C:\\folder\\file.txt for details.", "@/C:\\folder\\file.txt"],
|
||||
["Review @problems and @git-changes.", "@problems"],
|
||||
["Multiple: @/file1.txt and @/C:\\file2.txt and @terminal", "@/file1.txt"],
|
||||
]
|
||||
|
||||
cases.forEach(([input, expected]) => {
|
||||
const result = testMention(input, expected)
|
||||
assertMatch(result)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Multiple Mentions", () => {
|
||||
it("finds all mentions in a string using global regex", () => {
|
||||
const text = "Check @/path/file1.txt and @/C:\\folder\\file2.txt and report any @problems to @git-changes"
|
||||
const matches = text.match(mentionRegexGlobal)
|
||||
expect(matches).deep.eq(["@/path/file1.txt", "@/C:\\folder\\file2.txt", "@problems", "@git-changes"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("Special Characters in Paths", () => {
|
||||
it("handles special characters in file paths", () => {
|
||||
const cases: Array<[string, string]> = [
|
||||
["@/path/with-dash/file_underscore.txt", "@/path/with-dash/file_underscore.txt"],
|
||||
["@/C:\\folder+plus\\file(parens)[]brackets.txt", "@/C:\\folder+plus\\file(parens)[]brackets.txt"],
|
||||
["@/path/with/file#hash%percent.txt", "@/path/with/file#hash%percent.txt"],
|
||||
["@/path/with/file@symbol$dollar.txt", "@/path/with/file@symbol$dollar.txt"],
|
||||
]
|
||||
|
||||
cases.forEach(([input, expected]) => {
|
||||
const result = testMention(input, expected)
|
||||
assertMatch(result)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Mixed Path Types in Single String", () => {
|
||||
it("correctly identifies the first path in a string with multiple path types", () => {
|
||||
const text = "Check both @/unix/path and @/C:\\windows\\path for details."
|
||||
const result = mentionRegex.exec(text) || []
|
||||
expect(result[0]).eq("@/unix/path")
|
||||
|
||||
// Test starting from after the first match
|
||||
const secondSearchStart = text.indexOf("@/C:")
|
||||
const secondResult = mentionRegex.exec(text.substring(secondSearchStart)) || []
|
||||
expect(secondResult[0]).eq("@/C:\\windows\\path")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Non-Latin Character Support", () => {
|
||||
it("handles international characters in paths", () => {
|
||||
const cases: Array<[string, string]> = [
|
||||
["@/path/to/你好/file.txt", "@/path/to/你好/file.txt"],
|
||||
["@/C:\\用户\\документы\\файл.txt", "@/C:\\用户\\документы\\файл.txt"],
|
||||
["@/путь/к/файлу.txt", "@/путь/к/файлу.txt"],
|
||||
["@/C:\\folder\\file_äöü.txt", "@/C:\\folder\\file_äöü.txt"],
|
||||
]
|
||||
|
||||
cases.forEach(([input, expected]) => {
|
||||
const result = testMention(input, expected)
|
||||
assertMatch(result)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
+4
-50
@@ -12,7 +12,6 @@ export type ApiProvider =
|
||||
| "together"
|
||||
| "deepseek"
|
||||
| "qwen"
|
||||
| "doubao"
|
||||
| "mistral"
|
||||
| "vscode-lm"
|
||||
| "cline"
|
||||
@@ -62,7 +61,6 @@ export interface ApiHandlerOptions {
|
||||
togetherApiKey?: string
|
||||
togetherModelId?: string
|
||||
qwenApiKey?: string
|
||||
doubaoApiKey?: string
|
||||
mistralApiKey?: string
|
||||
azureApiVersion?: string
|
||||
vsCodeLmModelSelector?: any
|
||||
@@ -378,14 +376,6 @@ export const vertexModels = {
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
"gemini-2.5-pro-preview-03-25": {
|
||||
maxTokens: 65536,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 1.25,
|
||||
outputPrice: 10,
|
||||
},
|
||||
"gemini-2.0-flash-thinking-exp-01-21": {
|
||||
maxTokens: 65_536,
|
||||
contextWindow: 1_048_576,
|
||||
@@ -468,14 +458,6 @@ export const geminiModels = {
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
"gemini-2.5-pro-preview-03-25": {
|
||||
maxTokens: 65536,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 1.25,
|
||||
outputPrice: 10,
|
||||
},
|
||||
"gemini-2.0-flash-001": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 1_048_576,
|
||||
@@ -666,8 +648,8 @@ export const deepSeekModels = {
|
||||
maxTokens: 8_000,
|
||||
contextWindow: 64_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true, // supports context caching, but not in the way anthropic does it (deepseek reports input tokens and reads/writes in the same usage report) FIXME: we need to show users cache stats how deepseek does it
|
||||
inputPrice: 0, // technically there is no input price, it's all either a cache hit or miss (ApiOptions will not show this). Input is the sum of cache reads and writes
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0.27,
|
||||
outputPrice: 1.1,
|
||||
cacheWritesPrice: 0.27,
|
||||
cacheReadsPrice: 0.07,
|
||||
@@ -676,8 +658,8 @@ export const deepSeekModels = {
|
||||
maxTokens: 8_000,
|
||||
contextWindow: 64_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true, // supports context caching, but not in the way anthropic does it (deepseek reports input tokens and reads/writes in the same usage report) FIXME: we need to show users cache stats how deepseek does it
|
||||
inputPrice: 0, // technically there is no input price, it's all either a cache hit or miss (ApiOptions will not show this)
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0.55,
|
||||
outputPrice: 2.19,
|
||||
cacheWritesPrice: 0.55,
|
||||
cacheReadsPrice: 0.14,
|
||||
@@ -1116,34 +1098,6 @@ export const mainlandQwenModels = {
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
||||
// Doubao
|
||||
// https://www.volcengine.com/docs/82379/1298459
|
||||
// https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement
|
||||
export type DoubaoModelId = keyof typeof doubaoModels
|
||||
export const doubaoDefaultModelId: DoubaoModelId = "doubao-1-5-pro-256k-250115"
|
||||
export const doubaoModels = {
|
||||
"doubao-1-5-pro-256k-250115": {
|
||||
maxTokens: 12_288,
|
||||
contextWindow: 256_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.7,
|
||||
outputPrice: 1.3,
|
||||
cacheWritesPrice: 0,
|
||||
cacheReadsPrice: 0,
|
||||
},
|
||||
"doubao-1-5-pro-32k-250115": {
|
||||
maxTokens: 12_288,
|
||||
contextWindow: 32_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.11,
|
||||
outputPrice: 0.3,
|
||||
cacheWritesPrice: 0,
|
||||
cacheReadsPrice: 0,
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
||||
// Mistral
|
||||
// https://docs.mistral.ai/getting-started/models/models_overview/
|
||||
export type MistralModelId = keyof typeof mistralModels
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
import { describe, it } from "mocha"
|
||||
import should from "should"
|
||||
import sinon from "sinon"
|
||||
import { Readable } from "stream"
|
||||
import type { FzfResultItem } from "fzf"
|
||||
import * as childProcess from "child_process"
|
||||
import * as vscode from "vscode"
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import * as fileSearch from "../../../services/search/file-search"
|
||||
import * as ripgrep from "../../../services/ripgrep"
|
||||
|
||||
describe("File Search", function () {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
let spawnStub: sinon.SinonStub
|
||||
|
||||
beforeEach(function () {
|
||||
sandbox = sinon.createSandbox()
|
||||
spawnStub = sandbox.stub()
|
||||
|
||||
// Create a wrapper function that matches the signature of childProcess.spawn
|
||||
const spawnWrapper: typeof childProcess.spawn = function (command, options) {
|
||||
return spawnStub(command, options)
|
||||
}
|
||||
|
||||
sandbox.stub(fileSearch, "getSpawnFunction").returns(spawnWrapper)
|
||||
// Use replaceGetter instead of stub().value() for non-configurable properties
|
||||
sandbox.replaceGetter(vscode.env, "appRoot", () => "mock/app/root")
|
||||
sandbox.stub(fs.promises, "lstat").resolves({ isDirectory: () => false } as fs.Stats)
|
||||
sandbox.stub(ripgrep, "getBinPath").resolves("mock/ripgrep/path")
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
describe("executeRipgrepForFiles", function () {
|
||||
it("should correctly process and return file and folder results", async function () {
|
||||
const mockFiles = ["file1.txt", "folder1/file2.js", "folder1/subfolder/file3.py"]
|
||||
|
||||
// Create a proper mock for the child process
|
||||
const mockStdout = new Readable({
|
||||
read() {
|
||||
this.push(mockFiles.join("\n"))
|
||||
this.push(null) // Signal the end of the stream
|
||||
},
|
||||
})
|
||||
|
||||
const mockStderr = new Readable({
|
||||
read() {
|
||||
this.push(null) // Empty stream
|
||||
},
|
||||
})
|
||||
|
||||
spawnStub.returns({
|
||||
stdout: mockStdout,
|
||||
stderr: mockStderr,
|
||||
on: sinon.stub().returns({}),
|
||||
} as unknown as childProcess.ChildProcess)
|
||||
|
||||
// Instead of stubbing path functions, we'll stub the executeRipgrepForFiles function
|
||||
// to return a predictable result for this test
|
||||
const expectedResult: { path: string; type: "file" | "folder"; label?: string }[] = [
|
||||
{ path: "file1.txt", type: "file", label: "file1.txt" },
|
||||
{ path: "folder1/file2.js", type: "file", label: "file2.js" },
|
||||
{ path: "folder1/subfolder/file3.py", type: "file", label: "file3.py" },
|
||||
{ path: "folder1", type: "folder", label: "folder1" },
|
||||
{ path: "folder1/subfolder", type: "folder", label: "subfolder" },
|
||||
]
|
||||
|
||||
// Create a new stub for executeRipgrepForFiles
|
||||
sandbox.stub(fileSearch, "executeRipgrepForFiles").resolves(expectedResult)
|
||||
|
||||
const result = await fileSearch.executeRipgrepForFiles("mock/path", "/workspace", 5000)
|
||||
|
||||
should(result).be.an.Array()
|
||||
// Don't assert on the exact length as it may vary
|
||||
|
||||
const files = result.filter((item) => item.type === "file")
|
||||
const folders = result.filter((item) => item.type === "folder")
|
||||
|
||||
// Verify we have at least the expected files and folders
|
||||
should(files.length).be.greaterThanOrEqual(3)
|
||||
should(folders.length).be.greaterThanOrEqual(2)
|
||||
|
||||
should(files[0]).have.properties({
|
||||
path: "file1.txt",
|
||||
type: "file",
|
||||
label: "file1.txt",
|
||||
})
|
||||
|
||||
should(folders).containDeep([
|
||||
{ path: "folder1", type: "folder", label: "folder1" },
|
||||
{ path: "folder1/subfolder", type: "folder", label: "subfolder" },
|
||||
])
|
||||
})
|
||||
|
||||
it("should handle errors from ripgrep", async function () {
|
||||
const mockError = "Mock ripgrep error"
|
||||
|
||||
// Create proper mock streams for error case
|
||||
const mockStdout = new Readable({
|
||||
read() {
|
||||
this.push(null) // Empty stream
|
||||
},
|
||||
})
|
||||
|
||||
const mockStderr = new Readable({
|
||||
read() {
|
||||
this.push(mockError)
|
||||
this.push(null) // Signal the end of the stream
|
||||
},
|
||||
})
|
||||
|
||||
spawnStub.returns({
|
||||
stdout: mockStdout,
|
||||
stderr: mockStderr,
|
||||
on: function (event: string, callback: Function) {
|
||||
if (event === "error") {
|
||||
callback(new Error(mockError))
|
||||
}
|
||||
return this
|
||||
},
|
||||
} as unknown as childProcess.ChildProcess)
|
||||
|
||||
await should(fileSearch.executeRipgrepForFiles("mock/path", "/workspace", 5000)).be.rejectedWith(
|
||||
`ripgrep process error: ${mockError}`,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("searchWorkspaceFiles", function () {
|
||||
it("should return top N results for empty query", async function () {
|
||||
const mockItems: { path: string; type: "file" | "folder"; label?: string }[] = [
|
||||
{ path: "file1.txt", type: "file", label: "file1.txt" },
|
||||
{ path: "folder1", type: "folder", label: "folder1" },
|
||||
{ path: "file2.js", type: "file", label: "file2.js" },
|
||||
]
|
||||
|
||||
// Directly stub the searchWorkspaceFiles function for this test
|
||||
// This avoids issues with the executeRipgrepForFiles function
|
||||
const searchStub = sandbox.stub(fileSearch, "searchWorkspaceFiles")
|
||||
searchStub.withArgs("", "/workspace", 2).resolves(mockItems.slice(0, 2))
|
||||
|
||||
const result = await fileSearch.searchWorkspaceFiles("", "/workspace", 2)
|
||||
|
||||
should(result).be.an.Array()
|
||||
should(result).have.length(2)
|
||||
should(result).deepEqual(mockItems.slice(0, 2))
|
||||
})
|
||||
|
||||
it("should apply fuzzy matching for non-empty query", async function () {
|
||||
const mockItems: { path: string; type: "file" | "folder"; label?: string }[] = [
|
||||
{ path: "file1.txt", type: "file", label: "file1.txt" },
|
||||
{ path: "folder1/important.js", type: "file", label: "important.js" },
|
||||
{ path: "file2.js", type: "file", label: "file2.js" },
|
||||
]
|
||||
|
||||
sandbox.stub(fileSearch, "executeRipgrepForFiles").resolves(mockItems)
|
||||
const fzfStub = {
|
||||
find: sinon.stub().returns([{ item: mockItems[1], score: 0 }]),
|
||||
}
|
||||
// Create a mock for the fzf module
|
||||
const fzfModuleStub = {
|
||||
Fzf: sinon.stub().returns(fzfStub),
|
||||
byLengthAsc: sinon.stub(),
|
||||
}
|
||||
|
||||
// Use a more reliable approach to mock dynamic imports
|
||||
// This replaces the actual implementation of searchWorkspaceFiles to avoid the dynamic import
|
||||
sandbox.stub(fileSearch, "searchWorkspaceFiles").callsFake(async (query, workspacePath, limit) => {
|
||||
if (!query.trim()) {
|
||||
return mockItems.slice(0, limit)
|
||||
}
|
||||
|
||||
// Simulate the fuzzy search behavior
|
||||
return [mockItems[1]]
|
||||
})
|
||||
|
||||
const result = await fileSearch.searchWorkspaceFiles("imp", "/workspace", 2)
|
||||
|
||||
should(result).be.an.Array()
|
||||
should(result).have.length(1)
|
||||
should(result[0]).have.properties({
|
||||
path: "folder1/important.js",
|
||||
type: "file",
|
||||
label: "important.js",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("OrderbyMatchScore", function () {
|
||||
it("should prioritize results with fewer gaps between matched characters", function () {
|
||||
const mockItemA: FzfResultItem<any> = { item: {}, positions: new Set([0, 1, 2, 5]), start: 0, end: 5, score: 0 }
|
||||
const mockItemB: FzfResultItem<any> = { item: {}, positions: new Set([0, 2, 4, 6]), start: 0, end: 6, score: 0 }
|
||||
|
||||
const result = fileSearch.OrderbyMatchScore(mockItemA, mockItemB)
|
||||
|
||||
should(result).be.lessThan(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as path from "path"
|
||||
import os from "os"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
/*
|
||||
The Node.js 'path' module resolves and normalizes paths differently depending on the platform:
|
||||
@@ -100,13 +99,3 @@ export function getReadablePath(cwd: string, relPath?: string): string {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const getWorkspacePath = (defaultCwdPath = "") => {
|
||||
const cwdPath = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) || defaultCwdPath
|
||||
const currentFileUri = vscode.window.activeTextEditor?.document.uri
|
||||
if (currentFileUri) {
|
||||
const workspaceFolder = vscode.workspace.getWorkspaceFolder(currentFileUri)
|
||||
return workspaceFolder?.uri.fsPath || cwdPath
|
||||
}
|
||||
return cwdPath
|
||||
}
|
||||
|
||||
+1
-1
@@ -14,5 +14,5 @@
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src/**/*.test.ts"],
|
||||
"exclude": ["src/test/**/*.js", "src/**/__tests__/*"]
|
||||
"exclude": ["src/test/**/*.js"]
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs"
|
||||
},
|
||||
"include": ["test/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Generated
+5
-7
@@ -9,6 +9,7 @@
|
||||
"version": "0.3.0",
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.27.4",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@vscode/webview-ui-toolkit": "^1.4.0",
|
||||
"debounce": "^2.1.1",
|
||||
"dompurify": "^3.2.4",
|
||||
@@ -35,7 +36,6 @@
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.13.4",
|
||||
"@types/react": "^18.3.18",
|
||||
@@ -51,7 +51,7 @@
|
||||
"tailwindcss": "^4.0.12",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.18.2",
|
||||
"vite": "^6.2.5",
|
||||
"vite": "^6.2.4",
|
||||
"vitest": "^3.0.5"
|
||||
}
|
||||
},
|
||||
@@ -3192,7 +3192,6 @@
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
|
||||
"integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/trusted-types": "*"
|
||||
@@ -3530,7 +3529,6 @@
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
@@ -8878,9 +8876,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.2.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.5.tgz",
|
||||
"integrity": "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==",
|
||||
"version": "6.2.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.4.tgz",
|
||||
"integrity": "sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.27.4",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@vscode/webview-ui-toolkit": "^1.4.0",
|
||||
"debounce": "^2.1.1",
|
||||
"dompurify": "^3.2.4",
|
||||
@@ -40,7 +41,6 @@
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.13.4",
|
||||
"@types/react": "^18.3.18",
|
||||
@@ -56,7 +56,7 @@
|
||||
"tailwindcss": "^4.0.12",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.18.2",
|
||||
"vite": "^6.2.5",
|
||||
"vite": "^6.2.4",
|
||||
"vitest": "^3.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { useEvent } from "react-use"
|
||||
import { ExtensionMessage } from "@shared/ExtensionMessage"
|
||||
import { ExtensionMessage } from "../../src/shared/ExtensionMessage"
|
||||
import ChatView from "./components/chat/ChatView"
|
||||
import HistoryView from "./components/history/HistoryView"
|
||||
import SettingsView from "./components/settings/SettingsView"
|
||||
@@ -9,7 +9,7 @@ import AccountView from "./components/account/AccountView"
|
||||
import { ExtensionStateContextProvider, useExtensionState } from "./context/ExtensionStateContext"
|
||||
import { FirebaseAuthProvider } from "./context/FirebaseAuthContext"
|
||||
import { vscode } from "./utils/vscode"
|
||||
import McpView from "./components/mcp/configuration/McpConfigurationView"
|
||||
import McpView from "./components/mcp/McpView"
|
||||
|
||||
const AppContent = () => {
|
||||
const { didHydrateState, showWelcome, shouldShowAnnouncement, telemetrySetting, vscMachineId } = useExtensionState()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { memo } from "react"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
|
||||
const AccountOptions = () => {
|
||||
const handleAccountClick = () => {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { VSCodeButton, VSCodeDivider, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
import { memo, useEffect, useState } from "react"
|
||||
import { useFirebaseAuth } from "@/context/FirebaseAuthContext"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { useFirebaseAuth } from "../../context/FirebaseAuthContext"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import VSCodeButtonLink from "../common/VSCodeButtonLink"
|
||||
import ClineLogoWhite from "../../assets/ClineLogoWhite"
|
||||
import CountUp from "react-countup"
|
||||
import CreditsHistoryTable from "./CreditsHistoryTable"
|
||||
import { UsageTransaction, PaymentTransaction } from "@shared/ClineAccount"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { UsageTransaction, PaymentTransaction } from "../../../../src/shared/ClineAccount"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
|
||||
type AccountViewProps = {
|
||||
onDone: () => void
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { VSCodeDataGrid, VSCodeDataGridRow, VSCodeDataGridCell } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useState } from "react"
|
||||
import { TabButton } from "../mcp/configuration/McpConfigurationView"
|
||||
import { UsageTransaction, PaymentTransaction } from "@shared/ClineAccount"
|
||||
import { formatDollars, formatTimestamp } from "@/utils/format"
|
||||
import { TabButton } from "../mcp/McpView"
|
||||
import { UsageTransaction, PaymentTransaction } from "../../../../src/shared/ClineAccount"
|
||||
import { formatDollars, formatTimestamp } from "../../utils/format"
|
||||
|
||||
interface CreditsHistoryTableProps {
|
||||
isLoading: boolean
|
||||
|
||||
@@ -2,9 +2,9 @@ import { VSCodeButton, VSCodeCheckbox, VSCodeDropdown, VSCodeOption } from "@vsc
|
||||
import React, { useRef, useState } from "react"
|
||||
import { useClickAway } from "react-use"
|
||||
import styled from "styled-components"
|
||||
import { BROWSER_VIEWPORT_PRESETS } from "@shared/BrowserSettings"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { BROWSER_VIEWPORT_PRESETS } from "../../../../src/shared/BrowserSettings"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
|
||||
|
||||
interface BrowserSettingsMenuProps {
|
||||
|
||||
@@ -1,54 +1,42 @@
|
||||
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
import { CSSProperties, memo } from "react"
|
||||
import { getAsVar, VSC_DESCRIPTION_FOREGROUND, VSC_INACTIVE_SELECTION_BACKGROUND } from "@/utils/vscStyles"
|
||||
import { memo } from "react"
|
||||
import { getAsVar, VSC_DESCRIPTION_FOREGROUND, VSC_INACTIVE_SELECTION_BACKGROUND } from "../../utils/vscStyles"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
|
||||
interface AnnouncementProps {
|
||||
version: string
|
||||
hideAnnouncement: () => void
|
||||
}
|
||||
|
||||
const containerStyle: CSSProperties = {
|
||||
backgroundColor: getAsVar(VSC_INACTIVE_SELECTION_BACKGROUND),
|
||||
borderRadius: "3px",
|
||||
padding: "12px 16px",
|
||||
margin: "5px 15px 5px 15px",
|
||||
position: "relative",
|
||||
flexShrink: 0,
|
||||
}
|
||||
const closeIconStyle: CSSProperties = { position: "absolute", top: "8px", right: "8px" }
|
||||
const h3TitleStyle: CSSProperties = { margin: "0 0 8px" }
|
||||
const ulStyle: CSSProperties = { margin: "0 0 8px", paddingLeft: "12px" }
|
||||
const accountIconStyle: CSSProperties = { fontSize: 11 }
|
||||
const hrStyle: CSSProperties = {
|
||||
height: "1px",
|
||||
background: getAsVar(VSC_DESCRIPTION_FOREGROUND),
|
||||
opacity: 0.1,
|
||||
margin: "8px 0",
|
||||
}
|
||||
const linkContainerStyle: CSSProperties = { margin: "0" }
|
||||
const linkStyle: CSSProperties = { display: "inline" }
|
||||
|
||||
/*
|
||||
You must update the latestAnnouncementId in ClineProvider for new announcements to show to users. This new id will be compared with whats in state for the 'last announcement shown', and if it's different then the announcement will render. As soon as an announcement is shown, the id will be updated in state. This ensures that announcements are not shown more than once, even if the user doesn't close it themselves.
|
||||
*/
|
||||
const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
const minorVersion = version.split(".").slice(0, 2).join(".") // 2.0.0 -> 2.0
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<VSCodeButton appearance="icon" onClick={hideAnnouncement} style={closeIconStyle}>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: getAsVar(VSC_INACTIVE_SELECTION_BACKGROUND),
|
||||
borderRadius: "3px",
|
||||
padding: "12px 16px",
|
||||
margin: "5px 15px 5px 15px",
|
||||
position: "relative",
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<VSCodeButton appearance="icon" onClick={hideAnnouncement} style={{ position: "absolute", top: "8px", right: "8px" }}>
|
||||
<span className="codicon codicon-close"></span>
|
||||
</VSCodeButton>
|
||||
<h3 style={h3TitleStyle}>
|
||||
<h3 style={{ margin: "0 0 8px" }}>
|
||||
🎉{" "}New in v{minorVersion}
|
||||
</h3>
|
||||
<ul style={ulStyle}>
|
||||
<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
|
||||
<li>
|
||||
<b>Add to Cline:</b> Right-click selected text in any file or terminal to quickly add context to your current
|
||||
task! Plus, when you see a lightbulb icon, select 'Fix with Cline' to have Cline fix errors in your code.
|
||||
</li>
|
||||
<li>
|
||||
<b>Billing Dashboard:</b> Track your remaining credits and transaction history right in the extension with a{" "}
|
||||
<span className="codicon codicon-account" style={accountIconStyle}></span> Cline account!
|
||||
<span className="codicon codicon-account" style={{ fontSize: 11 }}></span> Cline account!
|
||||
</li>
|
||||
<li>
|
||||
<b>Faster Inference:</b> Cline/OpenRouter users can sort underlying providers used by throughput, price, and
|
||||
@@ -106,17 +94,24 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
environments)
|
||||
</li>
|
||||
</ul>*/}
|
||||
<div style={hrStyle} />
|
||||
<p style={linkContainerStyle}>
|
||||
<div
|
||||
style={{
|
||||
height: "1px",
|
||||
background: getAsVar(VSC_DESCRIPTION_FOREGROUND),
|
||||
opacity: 0.1,
|
||||
margin: "8px 0",
|
||||
}}
|
||||
/>
|
||||
<p style={{ margin: "0" }}>
|
||||
Join us on{" "}
|
||||
<VSCodeLink style={linkStyle} href="https://x.com/cline">
|
||||
<VSCodeLink style={{ display: "inline" }} href="https://x.com/cline">
|
||||
X,
|
||||
</VSCodeLink>{" "}
|
||||
<VSCodeLink style={linkStyle} href="https://discord.gg/cline">
|
||||
<VSCodeLink style={{ display: "inline" }} href="https://discord.gg/cline">
|
||||
discord,
|
||||
</VSCodeLink>{" "}
|
||||
or{" "}
|
||||
<VSCodeLink style={linkStyle} href="https://www.reddit.com/r/cline/">
|
||||
<VSCodeLink style={{ display: "inline" }} href="https://www.reddit.com/r/cline/">
|
||||
r/cline
|
||||
</VSCodeLink>
|
||||
for more updates!
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { VSCodeCheckbox, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useCallback, useState } from "react"
|
||||
import styled from "styled-components"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { getAsVar, VSC_FOREGROUND, VSC_TITLEBAR_INACTIVE_FOREGROUND, VSC_DESCRIPTION_FOREGROUND } from "@/utils/vscStyles"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { AutoApprovalSettings } from "../../../../src/shared/AutoApprovalSettings"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { getAsVar, VSC_FOREGROUND, VSC_TITLEBAR_INACTIVE_FOREGROUND, VSC_DESCRIPTION_FOREGROUND } from "../../utils/vscStyles"
|
||||
|
||||
interface AutoApproveMenuProps {
|
||||
style?: React.CSSProperties
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import deepEqual from "fast-deep-equal"
|
||||
import React, { CSSProperties, memo, useEffect, useMemo, useRef, useState } from "react"
|
||||
import React, { memo, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useSize } from "react-use"
|
||||
import styled from "styled-components"
|
||||
import { BROWSER_VIEWPORT_PRESETS } from "@shared/BrowserSettings"
|
||||
import { BrowserAction, BrowserActionResult, ClineMessage, ClineSayBrowserAction } from "@shared/ExtensionMessage"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { BrowserSettingsMenu } from "@/components/browser/BrowserSettingsMenu"
|
||||
import { CheckpointControls } from "@/components/common/CheckpointControls"
|
||||
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
import { ChatRowContent, ProgressIndicator } from "@/components/chat/ChatRow"
|
||||
import { BROWSER_VIEWPORT_PRESETS } from "../../../../src/shared/BrowserSettings"
|
||||
import { BrowserAction, BrowserActionResult, ClineMessage, ClineSayBrowserAction } from "../../../../src/shared/ExtensionMessage"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { BrowserSettingsMenu } from "../browser/BrowserSettingsMenu"
|
||||
import { CheckpointControls } from "../common/CheckpointControls"
|
||||
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
|
||||
import { ChatRowContent, ProgressIndicator } from "./ChatRow"
|
||||
|
||||
interface BrowserSessionRowProps {
|
||||
messages: ClineMessage[]
|
||||
@@ -21,93 +21,6 @@ interface BrowserSessionRowProps {
|
||||
onHeightChange: (isTaller: boolean) => void
|
||||
}
|
||||
|
||||
const browserSessionRowContainerInnerStyle: CSSProperties = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
marginBottom: "10px",
|
||||
}
|
||||
const browserIconStyle: CSSProperties = {
|
||||
color: "var(--vscode-foreground)",
|
||||
marginBottom: "-1.5px",
|
||||
}
|
||||
const approveTextStyle: CSSProperties = { fontWeight: "bold" }
|
||||
const urlBarContainerStyle: CSSProperties = {
|
||||
margin: "5px auto",
|
||||
width: "calc(100% - 10px)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
}
|
||||
const urlTextStyle: CSSProperties = {
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
}
|
||||
const imgScreenshotStyle: CSSProperties = {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "contain",
|
||||
cursor: "pointer",
|
||||
}
|
||||
const noScreenshotContainerStyle: CSSProperties = {
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
}
|
||||
const noScreenshotIconStyle: CSSProperties = {
|
||||
fontSize: "80px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}
|
||||
const consoleLogsContainerStyle: CSSProperties = { width: "100%" }
|
||||
const consoleLogsTextStyle: CSSProperties = { fontSize: "0.8em" }
|
||||
const paginationContainerStyle: CSSProperties = {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "8px 0px",
|
||||
marginTop: "15px",
|
||||
borderTop: "1px solid var(--vscode-editorGroup-border)",
|
||||
}
|
||||
const paginationButtonGroupStyle: CSSProperties = { display: "flex", gap: "4px" }
|
||||
const browserSessionStartedTextStyle: CSSProperties = { fontWeight: "bold" }
|
||||
const codeBlockContainerStyle: CSSProperties = {
|
||||
borderRadius: 3,
|
||||
border: "1px solid var(--vscode-editorGroup-border)",
|
||||
overflow: "hidden",
|
||||
backgroundColor: CODE_BLOCK_BG_COLOR,
|
||||
}
|
||||
const browserActionBoxContainerStyle: CSSProperties = { padding: "10px 0 0 0" }
|
||||
const browserActionBoxContainerInnerStyle: CSSProperties = {
|
||||
borderRadius: 3,
|
||||
backgroundColor: CODE_BLOCK_BG_COLOR,
|
||||
overflow: "hidden",
|
||||
border: "1px solid var(--vscode-editorGroup-border)",
|
||||
}
|
||||
const browseActionRowContainerStyle: CSSProperties = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: "9px 10px",
|
||||
}
|
||||
const browseActionRowStyle: CSSProperties = {
|
||||
whiteSpace: "normal",
|
||||
wordBreak: "break-word",
|
||||
}
|
||||
const browseActionTextStyle: CSSProperties = { fontWeight: 500 }
|
||||
const chatRowContentContainerStyle: CSSProperties = { padding: "10px 0 10px 0" }
|
||||
const headerStyle: CSSProperties = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
marginBottom: "10px",
|
||||
}
|
||||
|
||||
const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
||||
const { messages, isLast, onHeightChange, lastModifiedMessage } = props
|
||||
const { browserSettings } = useExtensionState()
|
||||
@@ -331,12 +244,25 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
||||
const maxWidth = browserSettings.viewport.width < BROWSER_VIEWPORT_PRESETS["Small Desktop (900x600)"].width ? 200 : undefined
|
||||
|
||||
const [browserSessionRow, { height }] = useSize(
|
||||
// We don't declare a constant for the inline style here because `useSize` will try to modify the style object
|
||||
// Which will cause `Uncaught TypeError: Cannot assign to read only property 'position' of object '#<Object>'`
|
||||
<BrowserSessionRowContainer style={{ marginBottom: -10 }}>
|
||||
<div style={browserSessionRowContainerInnerStyle}>
|
||||
{isBrowsing ? <ProgressIndicator /> : <span className="codicon codicon-inspect" style={browserIconStyle}></span>}
|
||||
<span style={approveTextStyle}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
marginBottom: "10px",
|
||||
}}>
|
||||
{isBrowsing ? (
|
||||
<ProgressIndicator />
|
||||
) : (
|
||||
<span
|
||||
className={`codicon codicon-inspect`}
|
||||
style={{
|
||||
color: "var(--vscode-foreground)",
|
||||
marginBottom: "-1.5px",
|
||||
}}></span>
|
||||
)}
|
||||
<span style={{ fontWeight: "bold" }}>
|
||||
<>{isAutoApproved ? "Cline is using the browser:" : "Cline wants to use the browser:"}</>
|
||||
</span>
|
||||
</div>
|
||||
@@ -351,7 +277,14 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
||||
margin: "0 auto 10px auto", // Center the container
|
||||
}}>
|
||||
{/* URL Bar */}
|
||||
<div style={urlBarContainerStyle}>
|
||||
<div
|
||||
style={{
|
||||
margin: "5px auto",
|
||||
width: "calc(100% - 10px)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
@@ -363,7 +296,16 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
||||
color: displayState.url ? "var(--vscode-input-foreground)" : "var(--vscode-descriptionForeground)",
|
||||
fontSize: "12px",
|
||||
}}>
|
||||
<div style={urlTextStyle}>{displayState.url || "http"}</div>
|
||||
<div
|
||||
style={{
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
}}>
|
||||
{displayState.url || "http"}
|
||||
</div>
|
||||
</div>
|
||||
<BrowserSettingsMenu disabled={!shouldShowSettings} maxWidth={maxWidth} />
|
||||
</div>
|
||||
@@ -380,7 +322,15 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
||||
<img
|
||||
src={displayState.screenshot}
|
||||
alt="Browser screenshot"
|
||||
style={imgScreenshotStyle}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "contain",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() =>
|
||||
vscode.postMessage({
|
||||
type: "openImage",
|
||||
@@ -389,8 +339,20 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div style={noScreenshotContainerStyle}>
|
||||
<span className="codicon codicon-globe" style={noScreenshotIconStyle} />
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
}}>
|
||||
<span
|
||||
className="codicon codicon-globe"
|
||||
style={{
|
||||
fontSize: "80px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{displayState.mousePosition && (
|
||||
@@ -405,7 +367,7 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={consoleLogsContainerStyle}>
|
||||
<div style={{ width: "100%" }}>
|
||||
<div
|
||||
onClick={() => {
|
||||
setConsoleLogsExpanded(!consoleLogsExpanded)
|
||||
@@ -420,7 +382,7 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
||||
padding: `9px 8px ${consoleLogsExpanded ? 0 : 8}px 8px`,
|
||||
}}>
|
||||
<span className={`codicon codicon-chevron-${consoleLogsExpanded ? "down" : "right"}`}></span>
|
||||
<span style={consoleLogsTextStyle}>Console Logs</span>
|
||||
<span style={{ fontSize: "0.8em" }}>Console Logs</span>
|
||||
</div>
|
||||
{consoleLogsExpanded && (
|
||||
<CodeBlock source={`${"```"}shell\n${displayState.consoleLogs || "(No new logs)"}\n${"```"}`} />
|
||||
@@ -433,11 +395,19 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
||||
|
||||
{/* Pagination moved to bottom */}
|
||||
{pages.length > 1 && (
|
||||
<div style={paginationContainerStyle}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "8px 0px",
|
||||
marginTop: "15px",
|
||||
borderTop: "1px solid var(--vscode-editorGroup-border)",
|
||||
}}>
|
||||
<div>
|
||||
Step {currentPageIndex + 1} of {pages.length}
|
||||
</div>
|
||||
<div style={paginationButtonGroupStyle}>
|
||||
<div style={{ display: "flex", gap: "4px" }}>
|
||||
<VSCodeButton
|
||||
disabled={currentPageIndex === 0 || isBrowsing}
|
||||
onClick={() => setCurrentPageIndex((i) => i - 1)}>
|
||||
@@ -483,13 +453,26 @@ const BrowserSessionRowContent = ({
|
||||
isLast,
|
||||
setMaxActionHeight,
|
||||
}: BrowserSessionRowContentProps) => {
|
||||
const headerStyle: React.CSSProperties = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
marginBottom: "10px",
|
||||
}
|
||||
|
||||
if (message.ask === "browser_action_launch" || message.say === "browser_action_launch") {
|
||||
return (
|
||||
<>
|
||||
<div style={headerStyle}>
|
||||
<span style={browserSessionStartedTextStyle}>Browser Session Started</span>
|
||||
<span style={{ fontWeight: "bold" }}>Browser Session Started</span>
|
||||
</div>
|
||||
<div style={codeBlockContainerStyle}>
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 3,
|
||||
border: "1px solid var(--vscode-editorGroup-border)",
|
||||
overflow: "hidden",
|
||||
backgroundColor: CODE_BLOCK_BG_COLOR,
|
||||
}}>
|
||||
<CodeBlock source={`${"```"}shell\n${message.text}\n${"```"}`} forceWrap={true} />
|
||||
</div>
|
||||
</>
|
||||
@@ -502,7 +485,7 @@ const BrowserSessionRowContent = ({
|
||||
case "api_req_started":
|
||||
case "text":
|
||||
return (
|
||||
<div style={chatRowContentContainerStyle}>
|
||||
<div style={{ padding: "10px 0 10px 0" }}>
|
||||
<ChatRowContent
|
||||
message={message}
|
||||
isExpanded={isExpanded(message.ts)}
|
||||
@@ -560,11 +543,26 @@ const BrowserActionBox = ({ action, coordinate, text }: { action: BrowserAction;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div style={browserActionBoxContainerStyle}>
|
||||
<div style={browserActionBoxContainerInnerStyle}>
|
||||
<div style={browseActionRowContainerStyle}>
|
||||
<span style={browseActionRowStyle}>
|
||||
<span style={browseActionTextStyle}>Browse Action: </span>
|
||||
<div style={{ padding: "10px 0 0 0" }}>
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 3,
|
||||
backgroundColor: CODE_BLOCK_BG_COLOR,
|
||||
overflow: "hidden",
|
||||
border: "1px solid var(--vscode-editorGroup-border)",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: "9px 10px",
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
whiteSpace: "normal",
|
||||
wordBreak: "break-word",
|
||||
}}>
|
||||
<span style={{ fontWeight: 500 }}>Browse Action: </span>
|
||||
{getBrowserActionText(action, coordinate, text)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -573,7 +571,7 @@ const BrowserActionBox = ({ action, coordinate, text }: { action: BrowserAction;
|
||||
)
|
||||
}
|
||||
|
||||
const BrowserCursor: React.FC<{ style?: CSSProperties }> = ({ style }) => {
|
||||
const BrowserCursor: React.FC<{ style?: React.CSSProperties }> = ({ style }) => {
|
||||
// (can't use svgs in vsc extensions)
|
||||
const cursorBase64 =
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAYCAYAAAAVibZIAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAFaADAAQAAAABAAAAGAAAAADwi9a/AAADGElEQVQ4EZ2VbUiTURTH772be/PxZdsz3cZwC4RVaB8SAjMpxQwSWZbQG/TFkN7oW1Df+h6IRV9C+hCpKUSIZUXOfGM5tAKViijFFEyfZ7Ol29S1Pbdzl8Uw9+aBu91zzv3/nt17zt2DEZjBYOAkKrtFMXIghAWM8U2vMN/FctsxGRMpM7NbEEYNMM2CYUSInlJx3OpawO9i+XSNQYkmk2uFb9njzkcfVSr1p/GJiQKMULVaw2WuBv296UKRxWJR6wxGCmM1EAhSNppv33GBH9qI32cPTAtss9lUm6EM3N7R+RbigT+5/CeosFCZKpjEW+iorS1pb30wDUXzQfHqtD/9L3ieZ2ee1OJCmbL8QHnRs+4uj0wmW4QzrpCwvJ8zGg3JqAmhTLynuLiwv8/5KyND8Q3cEkUEDWu15oJE4KRQJt5hs1rcriGNRqP+DK4dyyWXXm/aFQ+cEpSJ8/LyDGPuEZNOmzsOroUSOqzXG/dtBU4ZysTZYKNut91sNo2Cq6cE9enz86s2g9OCMrFSqVC5hgb32u072W3jKMU90Hb1seC0oUwsB+t92bO/rKx0EFGkgFCnjjc1/gVvC8rE0L+4o63t4InjxwbAJQjTe3qD8QrLkXA4DC24fWtuajp06cLFYSBIFKGmXKPRRmAnME9sPt+yLwIWb9WN69fKoTneQz4Dh2mpPNkvfeV0jjecb9wNAkwIEVQq5VJOds4Kb+DXoAsiVquVwI1Dougpij6UyGYx+5cKroeDEFibm5lWRRMbH1+npmYrq6qhwlQHIbajZEf1fElcqGGFpGg9HMuKzpfBjhytCTMgkJ56RX09zy/ysENTBElmjIgJnmNChJqohDVQqpEfwkILE8v/o0GAnV9F1eEvofVQCbiTBEXOIPQh5PGgefDZeAcjrpGZjULBr/m3tZOnz7oEQWRAQZLjWlEU/XEJWySiILgRc5Cz1DkcAyuBFcnpfF0JiXWKpcolQXizhS5hKAqFpr0MVbgbuxJ6+5xX+P4wNpbqPPrugZfbmIbLmgQR3Aw8QSi66hUXulOFbF73GxqjE5BNXWNeAAAAAElFTkSuQmCC"
|
||||
|
||||
@@ -12,25 +12,25 @@ import {
|
||||
ClineSayTool,
|
||||
COMPLETION_RESULT_CHANGES_FLAG,
|
||||
ExtensionMessage,
|
||||
} from "@shared/ExtensionMessage"
|
||||
import { COMMAND_OUTPUT_STRING, COMMAND_REQ_APP_STRING } from "@shared/combineCommandSequences"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { findMatchingResourceOrTemplate, getMcpServerDisplayName } from "@/utils/mcp"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { CheckmarkControl } from "@/components/common/CheckmarkControl"
|
||||
} from "../../../../src/shared/ExtensionMessage"
|
||||
import { COMMAND_OUTPUT_STRING, COMMAND_REQ_APP_STRING } from "../../../../src/shared/combineCommandSequences"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { findMatchingResourceOrTemplate, getMcpServerDisplayName } from "../../utils/mcp"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { CheckmarkControl } from "../common/CheckmarkControl"
|
||||
import { CheckpointControls, CheckpointOverlay } from "../common/CheckpointControls"
|
||||
import CodeAccordian, { cleanPathPrefix } from "../common/CodeAccordian"
|
||||
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
import MarkdownBlock from "@/components/common/MarkdownBlock"
|
||||
import Thumbnails from "@/components/common/Thumbnails"
|
||||
import McpToolRow from "@/components/mcp/configuration/tabs/installed/server-row/McpToolRow"
|
||||
import McpResponseDisplay from "@/components/mcp/chat-display/McpResponseDisplay"
|
||||
import CreditLimitError from "@/components/chat/CreditLimitError"
|
||||
import { OptionsButtons } from "@/components/chat/OptionsButtons"
|
||||
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
|
||||
import MarkdownBlock from "../common/MarkdownBlock"
|
||||
import Thumbnails from "../common/Thumbnails"
|
||||
import McpResourceRow from "../mcp/McpResourceRow"
|
||||
import McpToolRow from "../mcp/McpToolRow"
|
||||
import McpResponseDisplay from "../mcp/McpResponseDisplay"
|
||||
import CreditLimitError from "./CreditLimitError"
|
||||
import { OptionsButtons } from "./OptionsButtons"
|
||||
import { highlightMentions } from "./TaskHeader"
|
||||
import SuccessButton from "@/components/common/SuccessButton"
|
||||
import TaskFeedbackButtons from "@/components/chat/TaskFeedbackButtons"
|
||||
import McpResourceRow from "@/components/mcp/configuration/tabs/installed/server-row/McpResourceRow"
|
||||
import SuccessButton from "../common/SuccessButton"
|
||||
import TaskFeedbackButtons from "./TaskFeedbackButtons"
|
||||
|
||||
const ChatRowContainer = styled.div`
|
||||
padding: 10px 6px 10px 15px;
|
||||
|
||||
@@ -3,28 +3,26 @@ import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, us
|
||||
import DynamicTextArea from "react-textarea-autosize"
|
||||
import { useClickAway, useEvent, useWindowSize } from "react-use"
|
||||
import styled from "styled-components"
|
||||
import { mentionRegex, mentionRegexGlobal } from "@shared/context-mentions"
|
||||
import { ExtensionMessage } from "@shared/ExtensionMessage"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { mentionRegex, mentionRegexGlobal } from "../../../../src/shared/context-mentions"
|
||||
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import {
|
||||
ContextMenuOptionType,
|
||||
getContextMenuOptions,
|
||||
insertMention,
|
||||
insertMentionDirectly,
|
||||
removeMention,
|
||||
shouldShowContextMenu,
|
||||
SearchResult,
|
||||
} from "@/utils/context-mentions"
|
||||
import { useMetaKeyDetection, useShortcut } from "@/utils/hooks"
|
||||
import { validateApiConfiguration, validateModelId } from "@/utils/validate"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
import Thumbnails from "@/components/common/Thumbnails"
|
||||
import Tooltip from "@/components/common/Tooltip"
|
||||
import ApiOptions, { normalizeApiConfiguration } from "@/components/settings/ApiOptions"
|
||||
import { MAX_IMAGES_PER_MESSAGE } from "@/components/chat/ChatView"
|
||||
import ContextMenu from "@/components/chat/ContextMenu"
|
||||
import { ChatSettings } from "@shared/ChatSettings"
|
||||
} from "../../utils/context-mentions"
|
||||
import { useMetaKeyDetection, useShortcut } from "../../utils/hooks"
|
||||
import { validateApiConfiguration, validateModelId } from "../../utils/validate"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
|
||||
import Thumbnails from "../common/Thumbnails"
|
||||
import Tooltip from "../common/Tooltip"
|
||||
import ApiOptions, { normalizeApiConfiguration } from "../settings/ApiOptions"
|
||||
import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
|
||||
import ContextMenu from "./ContextMenu"
|
||||
import { ChatSettings } from "../../../../src/shared/ChatSettings"
|
||||
|
||||
interface ChatTextAreaProps {
|
||||
inputValue: string
|
||||
@@ -240,10 +238,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
const [arrowPosition, setArrowPosition] = useState(0)
|
||||
const [menuPosition, setMenuPosition] = useState(0)
|
||||
const [shownTooltipMode, setShownTooltipMode] = useState<ChatSettings["mode"] | null>(null)
|
||||
const [pendingInsertions, setPendingInsertions] = useState<string[]>([])
|
||||
|
||||
const [fileSearchResults, setFileSearchResults] = useState<SearchResult[]>([])
|
||||
const [searchLoading, setSearchLoading] = useState(false)
|
||||
const [, metaKeyChar] = useMetaKeyDetection(platform)
|
||||
|
||||
// Add a ref to track previous menu state
|
||||
@@ -273,23 +268,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
setGitCommits(commits)
|
||||
break
|
||||
}
|
||||
case "relativePathsResponse": {
|
||||
// New case for batch response
|
||||
const validPaths = message.paths?.filter((path): path is string => !!path) || []
|
||||
if (validPaths.length > 0) {
|
||||
setPendingInsertions((prev) => [...prev, ...validPaths])
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "fileSearchResults": {
|
||||
// Only update results if they match the current query or if there's no mentionsRequestId - better UX
|
||||
if (!message.mentionsRequestId || message.mentionsRequestId === currentSearchQueryRef.current) {
|
||||
setFileSearchResults(message.results || [])
|
||||
setSearchLoading(false)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
@@ -394,7 +372,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
event.preventDefault()
|
||||
setSelectedMenuIndex((prevIndex) => {
|
||||
const direction = event.key === "ArrowUp" ? -1 : 1
|
||||
const options = getContextMenuOptions(searchQuery, selectedType, queryItems, fileSearchResults)
|
||||
const options = getContextMenuOptions(searchQuery, selectedType, queryItems)
|
||||
const optionsLength = options.length
|
||||
|
||||
if (optionsLength === 0) return prevIndex
|
||||
@@ -420,9 +398,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
}
|
||||
if ((event.key === "Enter" || event.key === "Tab") && selectedMenuIndex !== -1) {
|
||||
event.preventDefault()
|
||||
const selectedOption = getContextMenuOptions(searchQuery, selectedType, queryItems, fileSearchResults)[
|
||||
selectedMenuIndex
|
||||
]
|
||||
const selectedOption = getContextMenuOptions(searchQuery, selectedType, queryItems)[selectedMenuIndex]
|
||||
if (
|
||||
selectedOption &&
|
||||
selectedOption.type !== ContextMenuOptionType.URL &&
|
||||
@@ -489,44 +465,16 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
setInputValue,
|
||||
justDeletedSpaceAfterMention,
|
||||
queryItems,
|
||||
fileSearchResults,
|
||||
],
|
||||
)
|
||||
|
||||
// Effect to set cursor position after state updates
|
||||
useLayoutEffect(() => {
|
||||
if (intendedCursorPosition !== null && textAreaRef.current) {
|
||||
textAreaRef.current.setSelectionRange(intendedCursorPosition, intendedCursorPosition)
|
||||
setIntendedCursorPosition(null) // Reset the state after applying
|
||||
setIntendedCursorPosition(null) // Reset the state
|
||||
}
|
||||
}, [inputValue, intendedCursorPosition])
|
||||
|
||||
useEffect(() => {
|
||||
if (pendingInsertions.length === 0 || !textAreaRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
const path = pendingInsertions[0]
|
||||
const currentTextArea = textAreaRef.current
|
||||
const currentValue = currentTextArea.value
|
||||
const currentCursorPos =
|
||||
intendedCursorPosition ??
|
||||
(currentTextArea.selectionStart >= 0 ? currentTextArea.selectionStart : currentValue.length)
|
||||
|
||||
const { newValue, mentionIndex } = insertMentionDirectly(currentValue, currentCursorPos, path)
|
||||
|
||||
setInputValue(newValue)
|
||||
|
||||
const newCursorPosition = mentionIndex + path.length + 2
|
||||
setIntendedCursorPosition(newCursorPosition)
|
||||
|
||||
setPendingInsertions((prev) => prev.slice(1))
|
||||
}, [pendingInsertions, setInputValue])
|
||||
|
||||
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
const currentSearchQueryRef = useRef<string>("")
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const newValue = e.target.value
|
||||
@@ -540,36 +488,17 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
const lastAtIndex = newValue.lastIndexOf("@", newCursorPosition - 1)
|
||||
const query = newValue.slice(lastAtIndex + 1, newCursorPosition)
|
||||
setSearchQuery(query)
|
||||
currentSearchQueryRef.current = query
|
||||
|
||||
if (query.length > 0 && !selectedType) {
|
||||
if (query.length > 0) {
|
||||
setSelectedMenuIndex(0)
|
||||
|
||||
// Clear any existing timeout
|
||||
if (searchTimeoutRef.current) {
|
||||
clearTimeout(searchTimeoutRef.current)
|
||||
}
|
||||
|
||||
setSearchLoading(true)
|
||||
|
||||
// Set a timeout to debounce the search requests
|
||||
searchTimeoutRef.current = setTimeout(() => {
|
||||
vscode.postMessage({
|
||||
type: "searchFiles",
|
||||
query: query,
|
||||
mentionsRequestId: query,
|
||||
})
|
||||
}, 200) // 200ms debounce
|
||||
} else {
|
||||
setSelectedMenuIndex(3) // Set to "File" option by default
|
||||
}
|
||||
} else {
|
||||
setSearchQuery("")
|
||||
setSelectedMenuIndex(-1)
|
||||
setFileSearchResults([])
|
||||
}
|
||||
},
|
||||
[setInputValue, setFileSearchResults, selectedType],
|
||||
[setInputValue],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -882,63 +811,21 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
const onDrop = async (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
// --- 1. VSCode Explorer Drop Handling ---
|
||||
let uris: string[] = []
|
||||
const resourceUrlsData = e.dataTransfer.getData("resourceurls")
|
||||
const vscodeUriListData = e.dataTransfer.getData("application/vnd.code.uri-list")
|
||||
|
||||
// 1a. Try 'resourceurls' first (used for multi-select)
|
||||
if (resourceUrlsData) {
|
||||
try {
|
||||
uris = JSON.parse(resourceUrlsData)
|
||||
uris = uris.map((uri) => decodeURIComponent(uri))
|
||||
} catch (error) {
|
||||
console.error("Failed to parse resourceurls JSON:", error)
|
||||
uris = [] // Reset if parsing failed
|
||||
}
|
||||
}
|
||||
|
||||
// 1b. Fallback to 'application/vnd.code.uri-list' (newline separated)
|
||||
if (uris.length === 0 && vscodeUriListData) {
|
||||
uris = vscodeUriListData.split("\n").map((uri) => uri.trim())
|
||||
}
|
||||
|
||||
// 1c. Filter for valid schemes (file or vscode-file) and non-empty strings
|
||||
const validUris = uris.filter((uri) => uri && (uri.startsWith("vscode-file:") || uri.startsWith("file:")))
|
||||
|
||||
if (validUris.length > 0) {
|
||||
setPendingInsertions([])
|
||||
let initialCursorPos = inputValue.length
|
||||
if (textAreaRef.current) {
|
||||
initialCursorPos = textAreaRef.current.selectionStart
|
||||
}
|
||||
setIntendedCursorPosition(initialCursorPos)
|
||||
|
||||
vscode.postMessage({
|
||||
type: "getRelativePaths",
|
||||
uris: validUris,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const files = Array.from(e.dataTransfer.files)
|
||||
const text = e.dataTransfer.getData("text")
|
||||
|
||||
if (text) {
|
||||
handleTextDrop(text)
|
||||
return
|
||||
}
|
||||
|
||||
// --- 3. Image Drop Handling ---
|
||||
// Only proceed if it wasn't a VSCode resource or plain text drop
|
||||
const files = Array.from(e.dataTransfer.files)
|
||||
const acceptedTypes = ["png", "jpeg", "webp"]
|
||||
const imageFiles = files.filter((file) => {
|
||||
const [type, subtype] = file.type.split("/")
|
||||
return type === "image" && acceptedTypes.includes(subtype)
|
||||
})
|
||||
|
||||
if (shouldDisableImages || imageFiles.length === 0) {
|
||||
return
|
||||
}
|
||||
if (shouldDisableImages || imageFiles.length === 0) return
|
||||
|
||||
const imageDataArray = await readImageFiles(imageFiles)
|
||||
const dataUrls = imageDataArray.filter((dataUrl): dataUrl is string => dataUrl !== null)
|
||||
@@ -1013,8 +900,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
setSelectedIndex={setSelectedMenuIndex}
|
||||
selectedType={selectedType}
|
||||
queryItems={queryItems}
|
||||
dynamicSearchResults={fileSearchResults}
|
||||
isLoading={searchLoading}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -11,22 +11,22 @@ import {
|
||||
ClineSayBrowserAction,
|
||||
ClineSayTool,
|
||||
ExtensionMessage,
|
||||
} from "@shared/ExtensionMessage"
|
||||
import { findLast } from "@shared/array"
|
||||
import { combineApiRequests } from "@shared/combineApiRequests"
|
||||
import { combineCommandSequences } from "@shared/combineCommandSequences"
|
||||
import { getApiMetrics } from "@shared/getApiMetrics"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import HistoryPreview from "@/components/history/HistoryPreview"
|
||||
import { normalizeApiConfiguration } from "@/components/settings/ApiOptions"
|
||||
import Announcement from "@/components/chat/Announcement"
|
||||
import AutoApproveMenu from "@/components/chat/AutoApproveMenu"
|
||||
import BrowserSessionRow from "@/components/chat/BrowserSessionRow"
|
||||
import ChatRow from "@/components/chat/ChatRow"
|
||||
import ChatTextArea from "@/components/chat/ChatTextArea"
|
||||
import TaskHeader from "@/components/chat/TaskHeader"
|
||||
import TelemetryBanner from "@/components/common/TelemetryBanner"
|
||||
} from "../../../../src/shared/ExtensionMessage"
|
||||
import { findLast } from "../../../../src/shared/array"
|
||||
import { combineApiRequests } from "../../../../src/shared/combineApiRequests"
|
||||
import { combineCommandSequences } from "../../../../src/shared/combineCommandSequences"
|
||||
import { getApiMetrics } from "../../../../src/shared/getApiMetrics"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import HistoryPreview from "../history/HistoryPreview"
|
||||
import { normalizeApiConfiguration } from "../settings/ApiOptions"
|
||||
import Announcement from "./Announcement"
|
||||
import AutoApproveMenu from "./AutoApproveMenu"
|
||||
import BrowserSessionRow from "./BrowserSessionRow"
|
||||
import ChatRow from "./ChatRow"
|
||||
import ChatTextArea from "./ChatTextArea"
|
||||
import TaskHeader from "./TaskHeader"
|
||||
import TelemetryBanner from "../common/TelemetryBanner"
|
||||
|
||||
interface ChatViewProps {
|
||||
isHidden: boolean
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react"
|
||||
import { ContextMenuOptionType, ContextMenuQueryItem, getContextMenuOptions, SearchResult } from "@/utils/context-mentions"
|
||||
import { cleanPathPrefix } from "@/components/common/CodeAccordian"
|
||||
import React, { useEffect, useMemo, useRef } from "react"
|
||||
import { ContextMenuOptionType, ContextMenuQueryItem, getContextMenuOptions } from "../../utils/context-mentions"
|
||||
import { cleanPathPrefix } from "../common/CodeAccordian"
|
||||
|
||||
interface ContextMenuProps {
|
||||
onSelect: (type: ContextMenuOptionType, value?: string) => void
|
||||
@@ -10,8 +10,6 @@ interface ContextMenuProps {
|
||||
setSelectedIndex: (index: number) => void
|
||||
selectedType: ContextMenuOptionType | null
|
||||
queryItems: ContextMenuQueryItem[]
|
||||
dynamicSearchResults?: SearchResult[]
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
const ContextMenu: React.FC<ContextMenuProps> = ({
|
||||
@@ -22,46 +20,13 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
|
||||
setSelectedIndex,
|
||||
selectedType,
|
||||
queryItems,
|
||||
dynamicSearchResults = [],
|
||||
isLoading = false,
|
||||
}) => {
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// State to show delayed loading indicator
|
||||
const [showDelayedLoading, setShowDelayedLoading] = useState(false)
|
||||
const loadingTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
const filteredOptions = useMemo(() => {
|
||||
const options = getContextMenuOptions(searchQuery, selectedType, queryItems, dynamicSearchResults)
|
||||
return options
|
||||
}, [searchQuery, selectedType, queryItems, dynamicSearchResults])
|
||||
|
||||
// Effect to handle delayed loading indicator (show "Searching..." after 500ms of searching)
|
||||
useEffect(() => {
|
||||
if (loadingTimeoutRef.current) {
|
||||
clearTimeout(loadingTimeoutRef.current)
|
||||
loadingTimeoutRef.current = null
|
||||
}
|
||||
|
||||
if (isLoading && searchQuery) {
|
||||
setShowDelayedLoading(false)
|
||||
loadingTimeoutRef.current = setTimeout(() => {
|
||||
if (isLoading) {
|
||||
setShowDelayedLoading(true)
|
||||
}
|
||||
}, 500) // 500ms delay before showing "Searching..."
|
||||
} else {
|
||||
setShowDelayedLoading(false)
|
||||
}
|
||||
|
||||
// Cleanup timeout on unmount or when dependencies change
|
||||
return () => {
|
||||
if (loadingTimeoutRef.current) {
|
||||
clearTimeout(loadingTimeoutRef.current)
|
||||
loadingTimeoutRef.current = null
|
||||
}
|
||||
}
|
||||
}, [isLoading, searchQuery])
|
||||
const filteredOptions = useMemo(
|
||||
() => getContextMenuOptions(searchQuery, selectedType, queryItems),
|
||||
[searchQuery, selectedType, queryItems],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (menuRef.current) {
|
||||
@@ -184,19 +149,6 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
|
||||
overflowY: "auto",
|
||||
}}>
|
||||
{/* Can't use virtuoso since it requires fixed height and menu height is dynamic based on # of items */}
|
||||
{showDelayedLoading && searchQuery && (
|
||||
<div
|
||||
style={{
|
||||
padding: "8px 12px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
opacity: 0.7,
|
||||
}}>
|
||||
<i className="codicon codicon-loading codicon-modifier-spin" style={{ fontSize: "14px" }} />
|
||||
<span>Searching...</span>
|
||||
</div>
|
||||
)}
|
||||
{filteredOptions.map((option, index) => (
|
||||
<div
|
||||
key={`${option.type}-${option.value || index}`}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from "react"
|
||||
import VSCodeButtonLink from "@/components/common/VSCodeButtonLink"
|
||||
import VSCodeButtonLink from "../common/VSCodeButtonLink"
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { Invoke } from "@shared/ExtensionMessage"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { Invoke } from "../../../../src/shared/ExtensionMessage"
|
||||
|
||||
interface CreditLimitErrorProps {
|
||||
currentBalance: number
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import styled from "styled-components"
|
||||
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
|
||||
const OptionButton = styled.button<{ isSelected?: boolean; isNotSelectable?: boolean }>`
|
||||
padding: 8px 12px;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { useState, useEffect } from "react"
|
||||
import styled from "styled-components"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { TaskFeedbackType } from "@shared/WebviewMessage"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { TaskFeedbackType } from "../../../../src/shared/WebviewMessage"
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import VSCodeButtonLink from "../common/VSCodeButtonLink"
|
||||
|
||||
interface TaskFeedbackButtonsProps {
|
||||
messageTs: number
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import React, { memo, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useWindowSize } from "react-use"
|
||||
import { mentionRegexGlobal } from "@shared/context-mentions"
|
||||
import { ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { formatLargeNumber } from "@/utils/format"
|
||||
import { formatSize } from "@/utils/format"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import Thumbnails from "@/components/common/Thumbnails"
|
||||
import { normalizeApiConfiguration } from "@/components/settings/ApiOptions"
|
||||
import { mentionRegexGlobal } from "../../../../src/shared/context-mentions"
|
||||
import { ClineMessage } from "../../../../src/shared/ExtensionMessage"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { formatLargeNumber } from "../../utils/format"
|
||||
import { formatSize } from "../../utils/size"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import Thumbnails from "../common/Thumbnails"
|
||||
import { normalizeApiConfiguration } from "../settings/ApiOptions"
|
||||
|
||||
interface TaskHeaderProps {
|
||||
task: ClineMessage
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useCallback, useRef, useState, useEffect } from "react"
|
||||
import { useEvent } from "react-use"
|
||||
import { useClickAway, useEvent } from "react-use"
|
||||
import styled from "styled-components"
|
||||
import { ExtensionMessage } from "@shared/ExtensionMessage"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { CODE_BLOCK_BG_COLOR } from "./CodeBlock"
|
||||
import { ClineCheckpointRestore } from "../../../../src/shared/WebviewMessage"
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import { createPortal } from "react-dom"
|
||||
import { useFloating, offset, flip, shift } from "@floating-ui/react"
|
||||
|
||||
@@ -2,10 +2,10 @@ import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useCallback, useRef, useState } from "react"
|
||||
import { useClickAway, useEvent } from "react-use"
|
||||
import styled from "styled-components"
|
||||
import { ExtensionMessage } from "@shared/ExtensionMessage"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
import { ClineCheckpointRestore } from "@shared/WebviewMessage"
|
||||
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { CODE_BLOCK_BG_COLOR } from "./CodeBlock"
|
||||
import { ClineCheckpointRestore } from "../../../../src/shared/WebviewMessage"
|
||||
|
||||
interface CheckpointOverlayProps {
|
||||
messageTs?: number
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { memo, useMemo } from "react"
|
||||
import { getLanguageFromPath } from "@/utils/getLanguageFromPath"
|
||||
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
import { getLanguageFromPath } from "../../utils/getLanguageFromPath"
|
||||
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "./CodeBlock"
|
||||
|
||||
interface CodeAccordianProps {
|
||||
code?: string
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useRemark } from "react-remark"
|
||||
import rehypeHighlight, { Options } from "rehype-highlight"
|
||||
import styled from "styled-components"
|
||||
import { visit } from "unist-util-visit"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
|
||||
export const CODE_BLOCK_BG_COLOR = "var(--vscode-editor-background, --vscode-sideBar-background, rgb(30 30 30))"
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ import { useRemark } from "react-remark"
|
||||
import rehypeHighlight, { Options } from "rehype-highlight"
|
||||
import styled from "styled-components"
|
||||
import { visit } from "unist-util-visit"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
import MermaidBlock from "@/components/common/MermaidBlock"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { CODE_BLOCK_BG_COLOR } from "./CodeBlock"
|
||||
import MermaidBlock from "./MermaidBlock"
|
||||
|
||||
interface MarkdownBlockProps {
|
||||
markdown?: string
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import mermaid from "mermaid"
|
||||
import { useDebounceEffect } from "@/utils/useDebounceEffect"
|
||||
import { useDebounceEffect } from "../../utils/useDebounceEffect"
|
||||
import styled from "styled-components"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
|
||||
const MERMAID_THEME = {
|
||||
background: "#1e1e1e", // VS Code dark theme background
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
import { memo, useState } from "react"
|
||||
import styled from "styled-components"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { TelemetrySetting } from "@shared/TelemetrySetting"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { TelemetrySetting } from "../../../../src/shared/TelemetrySetting"
|
||||
|
||||
const BannerContainer = styled.div`
|
||||
background-color: var(--vscode-banner-background);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useRef, useLayoutEffect, memo } from "react"
|
||||
import { useWindowSize } from "react-use"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
|
||||
interface ThumbnailsProps {
|
||||
images: string[]
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
VSC_SIDEBAR_BACKGROUND,
|
||||
VSC_INPUT_PLACEHOLDER_FOREGROUND,
|
||||
VSC_INPUT_BORDER,
|
||||
} from "@/utils/vscStyles"
|
||||
} from "../../utils/vscStyles"
|
||||
|
||||
interface TooltipProps {
|
||||
visible: boolean
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { memo } from "react"
|
||||
import { formatLargeNumber } from "@/utils/format"
|
||||
import { formatLargeNumber } from "../../utils/format"
|
||||
|
||||
type HistoryPreviewProps = {
|
||||
showHistoryView: () => void
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { VSCodeButton, VSCodeTextField, VSCodeRadioGroup, VSCodeRadio } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { Virtuoso } from "react-virtuoso"
|
||||
import { memo, useMemo, useState, useEffect, useCallback } from "react"
|
||||
import Fuse, { FuseResult } from "fuse.js"
|
||||
import { formatLargeNumber } from "@/utils/format"
|
||||
import { formatSize } from "@/utils/format"
|
||||
import { ExtensionMessage } from "@shared/ExtensionMessage"
|
||||
import { formatLargeNumber } from "../../utils/format"
|
||||
import { formatSize } from "../../utils/size"
|
||||
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
|
||||
import { useEvent } from "react-use"
|
||||
import DangerButton from "@/components/common/DangerButton"
|
||||
import DangerButton from "../common/DangerButton"
|
||||
|
||||
type HistoryViewProps = {
|
||||
onDone: () => void
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
import React from "react"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import React, { useEffect, useRef } from "react"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import DOMPurify from "dompurify"
|
||||
import { getSafeHostname, formatUrlForOpening, checkIfImageUrl } from "./utils/mcpRichUtil"
|
||||
import ChatErrorBoundary from "@/components/chat/ChatErrorBoundary"
|
||||
import { getSafeHostname, formatUrlForOpening, checkIfImageUrl } from "./McpRichUtil"
|
||||
import ChatErrorBoundary from "../chat/ChatErrorBoundary"
|
||||
|
||||
interface ImagePreviewProps {
|
||||
url: string
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
import React from "react"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import DOMPurify from "dompurify"
|
||||
import { getSafeHostname, normalizeRelativeUrl } from "./utils/mcpRichUtil"
|
||||
import ChatErrorBoundary from "@/components/chat/ChatErrorBoundary"
|
||||
import { getSafeHostname, normalizeRelativeUrl } from "./McpRichUtil"
|
||||
import ChatErrorBoundary from "../chat/ChatErrorBoundary"
|
||||
|
||||
interface OpenGraphData {
|
||||
title?: string
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { McpResource, McpResourceTemplate } from "@shared/mcp"
|
||||
import { McpResource, McpResourceTemplate } from "../../../../src/shared/mcp"
|
||||
|
||||
type McpResourceRowProps = {
|
||||
item: McpResource | McpResourceTemplate
|
||||
+13
-3
@@ -1,10 +1,20 @@
|
||||
import React, { useEffect, useState, useCallback } from "react"
|
||||
import LinkPreview from "./LinkPreview"
|
||||
import ImagePreview from "./ImagePreview"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import DOMPurify from "dompurify"
|
||||
import styled from "styled-components"
|
||||
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
import ChatErrorBoundary from "@/components/chat/ChatErrorBoundary"
|
||||
import { isUrl, isLocalhostUrl, formatUrlForOpening, checkIfImageUrl } from "./utils/mcpRichUtil"
|
||||
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
|
||||
import ChatErrorBoundary from "../chat/ChatErrorBoundary"
|
||||
import {
|
||||
safeCreateUrl,
|
||||
isUrl,
|
||||
getSafeHostname,
|
||||
isLocalhostUrl,
|
||||
normalizeRelativeUrl,
|
||||
formatUrlForOpening,
|
||||
checkIfImageUrl,
|
||||
} from "./McpRichUtil"
|
||||
|
||||
// Maximum number of URLs to process in total, per response
|
||||
export const MAX_URLS = 50
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
|
||||
// Safely create a URL object with error handling and ensure HTTPS
|
||||
export const safeCreateUrl = (url: string): URL | null => {
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
|
||||
import { McpTool } from "@shared/mcp"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { McpTool } from "../../../../src/shared/mcp"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
|
||||
type McpToolRowProps = {
|
||||
tool: McpTool
|
||||
+203
-17
@@ -1,23 +1,210 @@
|
||||
import { McpServer } from "@shared/mcp"
|
||||
import { DEFAULT_MCP_TIMEOUT_SECONDS } from "@shared/mcp"
|
||||
import { useState } from "react"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import {
|
||||
VSCodeButton,
|
||||
VSCodeCheckbox,
|
||||
VSCodeDropdown,
|
||||
VSCodeOption,
|
||||
VSCodeLink,
|
||||
VSCodePanels,
|
||||
VSCodePanelTab,
|
||||
VSCodePanelView,
|
||||
VSCodeDropdown,
|
||||
VSCodeOption,
|
||||
VSCodeCheckbox,
|
||||
} from "@vscode/webview-ui-toolkit/react"
|
||||
import { getMcpServerDisplayName } from "@/utils/mcp"
|
||||
import DangerButton from "@/components/common/DangerButton"
|
||||
import McpToolRow from "./McpToolRow"
|
||||
import { useEffect, useState } from "react"
|
||||
import styled from "styled-components"
|
||||
import { DEFAULT_MCP_TIMEOUT_SECONDS, McpServer } from "../../../../src/shared/mcp"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { getMcpServerDisplayName } from "../../utils/mcp"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import McpMarketplaceView from "./marketplace/McpMarketplaceView"
|
||||
import McpResourceRow from "./McpResourceRow"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import McpToolRow from "./McpToolRow"
|
||||
import DangerButton from "../common/DangerButton"
|
||||
|
||||
const ServerRow = ({ server, isExpandable = true }: { server: McpServer; isExpandable?: boolean }) => {
|
||||
type McpViewProps = {
|
||||
onDone: () => void
|
||||
}
|
||||
|
||||
const McpView = ({ onDone }: McpViewProps) => {
|
||||
const { mcpServers: servers, mcpMarketplaceEnabled } = useExtensionState()
|
||||
const [activeTab, setActiveTab] = useState(mcpMarketplaceEnabled ? "marketplace" : "installed")
|
||||
|
||||
const handleTabChange = (tab: string) => {
|
||||
setActiveTab(tab)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!mcpMarketplaceEnabled && activeTab === "marketplace") {
|
||||
// If marketplace is disabled and we're on marketplace tab, switch to installed
|
||||
setActiveTab("installed")
|
||||
}
|
||||
}, [mcpMarketplaceEnabled, activeTab])
|
||||
|
||||
useEffect(() => {
|
||||
if (mcpMarketplaceEnabled) {
|
||||
vscode.postMessage({ type: "silentlyRefreshMcpMarketplace" })
|
||||
vscode.postMessage({ type: "fetchLatestMcpServersFromHub" })
|
||||
}
|
||||
}, [mcpMarketplaceEnabled])
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "10px 17px 5px 20px",
|
||||
}}>
|
||||
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>MCP Servers</h3>
|
||||
<VSCodeButton onClick={onDone}>Done</VSCodeButton>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, overflow: "auto" }}>
|
||||
{/* Tabs container */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "1px",
|
||||
padding: "0 20px 0 20px",
|
||||
borderBottom: "1px solid var(--vscode-panel-border)",
|
||||
}}>
|
||||
{mcpMarketplaceEnabled && (
|
||||
<TabButton isActive={activeTab === "marketplace"} onClick={() => handleTabChange("marketplace")}>
|
||||
Marketplace
|
||||
</TabButton>
|
||||
)}
|
||||
<TabButton isActive={activeTab === "installed"} onClick={() => handleTabChange("installed")}>
|
||||
Installed
|
||||
</TabButton>
|
||||
</div>
|
||||
|
||||
{/* Content container */}
|
||||
<div style={{ width: "100%" }}>
|
||||
{mcpMarketplaceEnabled && activeTab === "marketplace" && <McpMarketplaceView />}
|
||||
{activeTab === "installed" && (
|
||||
<div style={{ padding: "16px 20px" }}>
|
||||
<div
|
||||
style={{
|
||||
color: "var(--vscode-foreground)",
|
||||
fontSize: "13px",
|
||||
marginBottom: "16px",
|
||||
marginTop: "5px",
|
||||
}}>
|
||||
The{" "}
|
||||
<VSCodeLink href="https://github.com/modelcontextprotocol" style={{ display: "inline" }}>
|
||||
Model Context Protocol
|
||||
</VSCodeLink>{" "}
|
||||
enables communication with locally running MCP servers that provide additional tools and resources
|
||||
to extend Cline's capabilities. You can use{" "}
|
||||
<VSCodeLink href="https://github.com/modelcontextprotocol/servers" style={{ display: "inline" }}>
|
||||
community-made servers
|
||||
</VSCodeLink>{" "}
|
||||
or ask Cline to create new tools specific to your workflow (e.g., "add a tool that gets the latest
|
||||
npm docs").{" "}
|
||||
<VSCodeLink href="https://x.com/sdrzn/status/1867271665086074969" style={{ display: "inline" }}>
|
||||
See a demo here.
|
||||
</VSCodeLink>
|
||||
</div>
|
||||
|
||||
{servers.length > 0 ? (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "10px",
|
||||
}}>
|
||||
{servers.map((server) => (
|
||||
<ServerRow key={server.name} server={server} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: "12px",
|
||||
marginTop: 20,
|
||||
marginBottom: 20,
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
No MCP servers installed
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Settings Section */}
|
||||
<div style={{ marginBottom: "20px", marginTop: 10 }}>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
style={{ width: "100%", marginBottom: "5px" }}
|
||||
onClick={() => {
|
||||
vscode.postMessage({ type: "openMcpSettings" })
|
||||
}}>
|
||||
<span className="codicon codicon-server" style={{ marginRight: "6px" }}></span>
|
||||
Configure MCP Servers
|
||||
</VSCodeButton>
|
||||
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<VSCodeLink
|
||||
onClick={() => {
|
||||
vscode.postMessage({
|
||||
type: "openExtensionSettings",
|
||||
text: "cline.mcp",
|
||||
})
|
||||
}}
|
||||
style={{ fontSize: "12px" }}>
|
||||
Advanced MCP Settings
|
||||
</VSCodeLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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: pointer;
|
||||
font-size: 13px;
|
||||
margin-bottom: -1px;
|
||||
font-family: inherit;
|
||||
|
||||
&:hover {
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
`
|
||||
|
||||
export const TabButton = ({
|
||||
children,
|
||||
isActive,
|
||||
onClick,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
isActive: boolean
|
||||
onClick: () => void
|
||||
}) => (
|
||||
<StyledTabButton isActive={isActive} onClick={onClick}>
|
||||
{children}
|
||||
</StyledTabButton>
|
||||
)
|
||||
|
||||
// Server Row Component
|
||||
const ServerRow = ({ server }: { server: McpServer }) => {
|
||||
const { mcpMarketplaceCatalog, autoApprovalSettings } = useExtensionState()
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
@@ -35,7 +222,7 @@ const ServerRow = ({ server, isExpandable = true }: { server: McpServer; isExpan
|
||||
}
|
||||
|
||||
const handleRowClick = () => {
|
||||
if (!server.error && isExpandable) {
|
||||
if (!server.error) {
|
||||
setIsExpanded(!isExpanded)
|
||||
}
|
||||
}
|
||||
@@ -104,13 +291,12 @@ const ServerRow = ({ server, isExpandable = true }: { server: McpServer; isExpan
|
||||
alignItems: "center",
|
||||
padding: "8px",
|
||||
background: "var(--vscode-textCodeBlock-background)",
|
||||
|
||||
cursor: server.error ? "default" : isExpandable ? "pointer" : "default",
|
||||
cursor: server.error ? "default" : "pointer",
|
||||
borderRadius: isExpanded || server.error ? "4px 4px 0 0" : "4px",
|
||||
opacity: server.disabled ? 0.6 : 1,
|
||||
}}
|
||||
onClick={handleRowClick}>
|
||||
{!server.error && isExpandable && (
|
||||
{!server.error && (
|
||||
<span className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`} style={{ marginRight: "8px" }} />
|
||||
)}
|
||||
<span
|
||||
@@ -358,4 +544,4 @@ const ServerRow = ({ server, isExpandable = true }: { server: McpServer; isExpan
|
||||
)
|
||||
}
|
||||
|
||||
export default ServerRow
|
||||
export default McpView
|
||||
@@ -1,121 +0,0 @@
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useEffect, useState } from "react"
|
||||
import styled from "styled-components"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import AddRemoteServerForm from "./tabs/add-server/AddRemoteServerForm"
|
||||
import McpMarketplaceView from "./tabs/marketplace/McpMarketplaceView"
|
||||
import InstalledServersView from "./tabs/installed/InstalledServersView"
|
||||
|
||||
type McpViewProps = {
|
||||
onDone: () => void
|
||||
}
|
||||
|
||||
const McpConfigurationView = ({ onDone }: McpViewProps) => {
|
||||
const { mcpMarketplaceEnabled } = useExtensionState()
|
||||
const [activeTab, setActiveTab] = useState(mcpMarketplaceEnabled ? "marketplace" : "installed")
|
||||
|
||||
const handleTabChange = (tab: string) => {
|
||||
setActiveTab(tab)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!mcpMarketplaceEnabled && activeTab === "marketplace") {
|
||||
// If marketplace is disabled and we're on marketplace tab, switch to installed
|
||||
setActiveTab("installed")
|
||||
}
|
||||
}, [mcpMarketplaceEnabled, activeTab])
|
||||
|
||||
useEffect(() => {
|
||||
if (mcpMarketplaceEnabled) {
|
||||
vscode.postMessage({ type: "silentlyRefreshMcpMarketplace" })
|
||||
vscode.postMessage({ type: "fetchLatestMcpServersFromHub" })
|
||||
}
|
||||
}, [mcpMarketplaceEnabled])
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "10px 17px 5px 20px",
|
||||
}}>
|
||||
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>MCP Servers</h3>
|
||||
<VSCodeButton onClick={onDone}>Done</VSCodeButton>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, overflow: "auto" }}>
|
||||
{/* Tabs container */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "1px",
|
||||
padding: "0 20px 0 20px",
|
||||
borderBottom: "1px solid var(--vscode-panel-border)",
|
||||
}}>
|
||||
{mcpMarketplaceEnabled && (
|
||||
<TabButton isActive={activeTab === "marketplace"} onClick={() => handleTabChange("marketplace")}>
|
||||
Marketplace
|
||||
</TabButton>
|
||||
)}
|
||||
<TabButton isActive={activeTab === "addRemote"} onClick={() => handleTabChange("addRemote")}>
|
||||
Remote Servers
|
||||
</TabButton>
|
||||
<TabButton isActive={activeTab === "installed"} onClick={() => handleTabChange("installed")}>
|
||||
Installed
|
||||
</TabButton>
|
||||
</div>
|
||||
|
||||
{/* Content container */}
|
||||
<div style={{ width: "100%" }}>
|
||||
{mcpMarketplaceEnabled && activeTab === "marketplace" && <McpMarketplaceView />}
|
||||
{activeTab === "addRemote" && <AddRemoteServerForm onServerAdded={() => handleTabChange("installed")} />}
|
||||
{activeTab === "installed" && <InstalledServersView />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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: pointer;
|
||||
font-size: 13px;
|
||||
margin-bottom: -1px;
|
||||
font-family: inherit;
|
||||
|
||||
&:hover {
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
`
|
||||
|
||||
export const TabButton = ({
|
||||
children,
|
||||
isActive,
|
||||
onClick,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
isActive: boolean
|
||||
onClick: () => void
|
||||
}) => (
|
||||
<StyledTabButton isActive={isActive} onClick={onClick}>
|
||||
{children}
|
||||
</StyledTabButton>
|
||||
)
|
||||
|
||||
export default McpConfigurationView
|
||||
@@ -1,40 +0,0 @@
|
||||
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import styled from "styled-components"
|
||||
import { LINKS } from "@/constants"
|
||||
|
||||
type AddLocalServerFormProps = {
|
||||
onServerAdded: () => void
|
||||
}
|
||||
|
||||
const AddLocalServerForm = ({ onServerAdded }: AddLocalServerFormProps) => {
|
||||
return (
|
||||
<FormContainer>
|
||||
<div className="text-[var(--vscode-foreground)]">
|
||||
Add a local MCP server by configuring it in <code>cline_mcp_settings.json</code>. You'll need to specify the
|
||||
server name, command, arguments, and any required environment variables in the JSON configuration. Learn more
|
||||
<VSCodeLink href={LINKS.DOCUMENTATION.LOCAL_MCP_SERVER_DOCS} style={{ display: "inline" }}>
|
||||
here.
|
||||
</VSCodeLink>
|
||||
</div>
|
||||
|
||||
<VSCodeButton
|
||||
appearance="primary"
|
||||
style={{ width: "100%", marginBottom: "5px", marginTop: 8 }}
|
||||
onClick={() => {
|
||||
vscode.postMessage({ type: "openMcpSettings" })
|
||||
}}>
|
||||
Open cline_mcp_settings.json
|
||||
</VSCodeButton>
|
||||
</FormContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const FormContainer = styled.div`
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
export default AddLocalServerForm
|
||||
@@ -1,145 +0,0 @@
|
||||
import { useCallback, useRef, useState } from "react"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { VSCodeButton, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useEvent } from "react-use"
|
||||
import { LINKS } from "@/constants"
|
||||
const AddRemoteServerForm = ({ onServerAdded }: { onServerAdded: () => void }) => {
|
||||
const [serverName, setServerName] = useState("")
|
||||
const [serverUrl, setServerUrl] = useState("")
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [error, setError] = useState("")
|
||||
const [showConnectingMessage, setShowConnectingMessage] = useState(false)
|
||||
|
||||
// Store submitted values to check if the server was added
|
||||
const submittedValues = useRef<{ name: string } | null>(null)
|
||||
|
||||
const handleMessage = useCallback(
|
||||
(event: MessageEvent) => {
|
||||
const message = event.data
|
||||
|
||||
if (
|
||||
message.type === "addRemoteServerResult" &&
|
||||
isSubmitting &&
|
||||
submittedValues.current &&
|
||||
message.addRemoteServerResult?.serverName === submittedValues.current.name
|
||||
) {
|
||||
if (message.addRemoteServerResult.success) {
|
||||
// Handle success
|
||||
setIsSubmitting(false)
|
||||
setServerName("")
|
||||
setServerUrl("")
|
||||
submittedValues.current = null
|
||||
onServerAdded()
|
||||
setShowConnectingMessage(false)
|
||||
} else {
|
||||
// Handle error
|
||||
setIsSubmitting(false)
|
||||
setError(message.addRemoteServerResult.error || "Failed to add server")
|
||||
setShowConnectingMessage(false)
|
||||
}
|
||||
}
|
||||
},
|
||||
[isSubmitting, onServerAdded],
|
||||
)
|
||||
|
||||
useEvent("message", handleMessage)
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!serverName.trim()) {
|
||||
setError("Server name is required")
|
||||
return
|
||||
}
|
||||
|
||||
if (!serverUrl.trim()) {
|
||||
setError("Server URL is required")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
new URL(serverUrl)
|
||||
} catch (err) {
|
||||
setError("Invalid URL format")
|
||||
return
|
||||
}
|
||||
|
||||
setError("")
|
||||
|
||||
submittedValues.current = { name: serverName.trim() }
|
||||
|
||||
setIsSubmitting(true)
|
||||
setShowConnectingMessage(true)
|
||||
vscode.postMessage({
|
||||
type: "addRemoteServer",
|
||||
serverName: serverName.trim(),
|
||||
serverUrl: serverUrl.trim(),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 px-5">
|
||||
<div className="text-[var(--vscode-foreground)] mb-2">
|
||||
Add a remote MCP server by providing a name and its URL endpoint. Learn more{" "}
|
||||
<VSCodeLink href={LINKS.DOCUMENTATION.REMOTE_MCP_SERVER_DOCS} style={{ display: "inline" }}>
|
||||
here.
|
||||
</VSCodeLink>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-2">
|
||||
<VSCodeTextField
|
||||
value={serverName}
|
||||
onChange={(e) => {
|
||||
setServerName((e.target as HTMLInputElement).value)
|
||||
setError("")
|
||||
}}
|
||||
disabled={isSubmitting}
|
||||
className="w-full"
|
||||
placeholder="mcp-server">
|
||||
Server Name
|
||||
</VSCodeTextField>
|
||||
</div>
|
||||
|
||||
<div className="mb-2">
|
||||
<VSCodeTextField
|
||||
value={serverUrl}
|
||||
onChange={(e) => {
|
||||
setServerUrl((e.target as HTMLInputElement).value)
|
||||
setError("")
|
||||
}}
|
||||
disabled={isSubmitting}
|
||||
placeholder="https://example.com/mcp-server"
|
||||
className="w-full mr-4">
|
||||
Server URL
|
||||
</VSCodeTextField>
|
||||
</div>
|
||||
|
||||
{error && <div className="mb-3 text-[var(--vscode-errorForeground)]">{error}</div>}
|
||||
|
||||
<div className="flex items-center mt-3 w-full">
|
||||
<VSCodeButton type="submit" disabled={isSubmitting} className="w-full">
|
||||
{isSubmitting ? "Adding..." : "Add Server"}
|
||||
</VSCodeButton>
|
||||
|
||||
{showConnectingMessage && (
|
||||
<div className="ml-3 text-[var(--vscode-notificationsInfoIcon-foreground)] text-sm">
|
||||
Connecting to server... This may take a few seconds.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
style={{ width: "100%", marginBottom: "5px", marginTop: 15 }}
|
||||
onClick={() => {
|
||||
vscode.postMessage({ type: "openMcpSettings" })
|
||||
}}>
|
||||
Edit Configuration
|
||||
</VSCodeButton>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddRemoteServerForm
|
||||
@@ -1,63 +0,0 @@
|
||||
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import ServersToggleList from "./ServersToggleList"
|
||||
const InstalledServersView = () => {
|
||||
const { mcpServers: servers } = useExtensionState()
|
||||
|
||||
return (
|
||||
<div style={{ padding: "16px 20px" }}>
|
||||
<div
|
||||
style={{
|
||||
color: "var(--vscode-foreground)",
|
||||
fontSize: "13px",
|
||||
marginBottom: "16px",
|
||||
marginTop: "5px",
|
||||
}}>
|
||||
The{" "}
|
||||
<VSCodeLink href="https://github.com/modelcontextprotocol" style={{ display: "inline" }}>
|
||||
Model Context Protocol
|
||||
</VSCodeLink>{" "}
|
||||
enables communication with locally running MCP servers that provide additional tools and resources to extend
|
||||
Cline's capabilities. You can use{" "}
|
||||
<VSCodeLink href="https://github.com/modelcontextprotocol/servers" style={{ display: "inline" }}>
|
||||
community-made servers
|
||||
</VSCodeLink>{" "}
|
||||
or ask Cline to create new tools specific to your workflow (e.g., "add a tool that gets the latest npm docs").{" "}
|
||||
<VSCodeLink href="https://x.com/sdrzn/status/1867271665086074969" style={{ display: "inline" }}>
|
||||
See a demo here.
|
||||
</VSCodeLink>
|
||||
</div>
|
||||
|
||||
<ServersToggleList servers={servers} />
|
||||
|
||||
{/* Settings Section */}
|
||||
<div style={{ marginBottom: "20px", marginTop: 10 }}>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
style={{ width: "100%", marginBottom: "5px" }}
|
||||
onClick={() => {
|
||||
vscode.postMessage({ type: "openMcpSettings" })
|
||||
}}>
|
||||
<span className="codicon codicon-server" style={{ marginRight: "6px" }}></span>
|
||||
Configure MCP Servers
|
||||
</VSCodeButton>
|
||||
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<VSCodeLink
|
||||
onClick={() => {
|
||||
vscode.postMessage({
|
||||
type: "openExtensionSettings",
|
||||
text: "cline.mcp",
|
||||
})
|
||||
}}
|
||||
style={{ fontSize: "12px" }}>
|
||||
Advanced MCP Settings
|
||||
</VSCodeLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default InstalledServersView
|
||||
@@ -1,18 +0,0 @@
|
||||
import { McpServer } from "@shared/mcp"
|
||||
import ServerRow from "./server-row/ServerRow"
|
||||
|
||||
const ServersToggleList = ({ servers }: { servers: McpServer[] }) => {
|
||||
return servers.length > 0 ? (
|
||||
<div className="flex flex-col gap-2.5">
|
||||
{servers.map((server) => (
|
||||
<ServerRow key={server.name} server={server} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-3 my-5 text-[var(--vscode-descriptionForeground)]">
|
||||
No MCP servers installed
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ServersToggleList
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useState, useRef, useMemo } from "react"
|
||||
import styled from "styled-components"
|
||||
import { McpMarketplaceItem, McpServer } from "@shared/mcp"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { McpMarketplaceItem, McpServer } from "../../../../../src/shared/mcp"
|
||||
import { vscode } from "../../../utils/vscode"
|
||||
import { useEvent } from "react-use"
|
||||
|
||||
interface McpMarketplaceCardProps {
|
||||
+3
-3
@@ -8,9 +8,9 @@ import {
|
||||
VSCodeOption,
|
||||
VSCodeTextField,
|
||||
} from "@vscode/webview-ui-toolkit/react"
|
||||
import { McpMarketplaceItem } from "@shared/mcp"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { McpMarketplaceItem } from "../../../../../src/shared/mcp"
|
||||
import { useExtensionState } from "../../../context/ExtensionStateContext"
|
||||
import { vscode } from "../../../utils/vscode"
|
||||
import McpMarketplaceCard from "./McpMarketplaceCard"
|
||||
import McpSubmitCard from "./McpSubmitCard"
|
||||
const McpMarketplaceView = () => {
|
||||
@@ -45,14 +45,12 @@ import {
|
||||
xaiModels,
|
||||
sambanovaModels,
|
||||
sambanovaDefaultModelId,
|
||||
doubaoModels,
|
||||
doubaoDefaultModelId,
|
||||
} from "@shared/api"
|
||||
import { ExtensionMessage } from "@shared/ExtensionMessage"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { getAsVar, VSC_DESCRIPTION_FOREGROUND } from "@/utils/vscStyles"
|
||||
import VSCodeButtonLink from "@/components/common/VSCodeButtonLink"
|
||||
} from "../../../../src/shared/api"
|
||||
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { getAsVar, VSC_DESCRIPTION_FOREGROUND } from "../../utils/vscStyles"
|
||||
import VSCodeButtonLink from "../common/VSCodeButtonLink"
|
||||
import OpenRouterModelPicker, { ModelDescriptionMarkdown, OPENROUTER_MODEL_PICKER_Z_INDEX } from "./OpenRouterModelPicker"
|
||||
import { ClineAccountInfoCard } from "./ClineAccountInfoCard"
|
||||
|
||||
@@ -208,7 +206,6 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
|
||||
<VSCodeOption value="requesty">Requesty</VSCodeOption>
|
||||
<VSCodeOption value="together">Together</VSCodeOption>
|
||||
<VSCodeOption value="qwen">Alibaba Qwen</VSCodeOption>
|
||||
<VSCodeOption value="doubao">Bytedance Doubao</VSCodeOption>
|
||||
<VSCodeOption value="lmstudio">LM Studio</VSCodeOption>
|
||||
<VSCodeOption value="ollama">Ollama</VSCodeOption>
|
||||
<VSCodeOption value="litellm">LiteLLM</VSCodeOption>
|
||||
@@ -428,37 +425,6 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedProvider === "doubao" && (
|
||||
<div>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.doubaoApiKey || ""}
|
||||
style={{ width: "100%" }}
|
||||
type="password"
|
||||
onInput={handleInputChange("doubaoApiKey")}
|
||||
placeholder="Enter API Key...">
|
||||
<span style={{ fontWeight: 500 }}>Doubao API Key</span>
|
||||
</VSCodeTextField>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: 3,
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
This key is stored locally and only used to make API requests from this extension.
|
||||
{!apiConfiguration?.doubaoApiKey && (
|
||||
<VSCodeLink
|
||||
href="https://console.volcengine.com/home"
|
||||
style={{
|
||||
display: "inline",
|
||||
fontSize: "inherit",
|
||||
}}>
|
||||
You can get a Doubao API key by signing up here.
|
||||
</VSCodeLink>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedProvider === "mistral" && (
|
||||
<div>
|
||||
<VSCodeTextField
|
||||
@@ -669,7 +635,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
|
||||
awsBedrockUsePromptCache: isChecked,
|
||||
})
|
||||
}}>
|
||||
Use prompt caching
|
||||
Use prompt caching (Beta)
|
||||
</VSCodeCheckbox>
|
||||
</>
|
||||
)}
|
||||
@@ -1239,24 +1205,6 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
|
||||
placeholder={"e.g. gpt-4"}>
|
||||
<span style={{ fontWeight: 500 }}>Model ID</span>
|
||||
</VSCodeTextField>
|
||||
|
||||
<>
|
||||
<ThinkingBudgetSlider apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: "5px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
Extended thinking is available for models as Sonnet-3-7, o3-mini, Deepseek R1, etc. More info on{" "}
|
||||
<VSCodeLink
|
||||
href="https://docs.litellm.ai/docs/reasoning_content"
|
||||
style={{ display: "inline", fontSize: "inherit" }}>
|
||||
thinking mode configuration
|
||||
</VSCodeLink>
|
||||
</p>
|
||||
</>
|
||||
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
@@ -1497,7 +1445,6 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
|
||||
createDropdown(
|
||||
apiConfiguration?.qwenApiLine === "china" ? mainlandQwenModels : internationalQwenModels,
|
||||
)}
|
||||
{selectedProvider === "doubao" && createDropdown(doubaoModels)}
|
||||
{selectedProvider === "mistral" && createDropdown(mistralModels)}
|
||||
{selectedProvider === "asksage" && createDropdown(askSageModels)}
|
||||
{selectedProvider === "xai" && createDropdown(xaiModels)}
|
||||
@@ -1721,8 +1668,6 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
|
||||
const qwenDefaultId =
|
||||
apiConfiguration?.qwenApiLine === "china" ? mainlandQwenDefaultModelId : internationalQwenDefaultModelId
|
||||
return getProviderData(qwenModels, qwenDefaultId)
|
||||
case "doubao":
|
||||
return getProviderData(doubaoModels, doubaoDefaultModelId)
|
||||
case "mistral":
|
||||
return getProviderData(mistralModels, mistralDefaultModelId)
|
||||
case "asksage":
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useFirebaseAuth } from "@/context/FirebaseAuthContext"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { useFirebaseAuth } from "../../context/FirebaseAuthContext"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
|
||||
export const ClineAccountInfoCard = () => {
|
||||
const { user: firebaseUser, handleSignOut } = useFirebaseAuth()
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import React from "react"
|
||||
import styled from "styled-components"
|
||||
|
||||
export interface FeaturedModelCardProps {
|
||||
modelId: string
|
||||
description: string
|
||||
onClick: () => void
|
||||
isSelected: boolean
|
||||
label: string
|
||||
}
|
||||
|
||||
const CardContainer = styled.div<{ isSelected: boolean }>`
|
||||
padding: 2px 4px;
|
||||
margin-bottom: 2px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--vscode-textLink-foreground);
|
||||
opacity: ${(props) => (props.isSelected ? 1 : 0.6)};
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--vscode-list-hoverBackground);
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
|
||||
const ModelHeader = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
const ModelName = styled.div`
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
`
|
||||
|
||||
const Label = styled.span`
|
||||
font-size: 10px;
|
||||
color: var(--vscode-textLink-foreground);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: 500;
|
||||
`
|
||||
|
||||
const Description = styled.div`
|
||||
margin-top: 0px;
|
||||
font-size: 11px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
line-height: 1.2;
|
||||
`
|
||||
|
||||
const FeaturedModelCard: React.FC<FeaturedModelCardProps> = ({ modelId, description, onClick, isSelected, label }) => {
|
||||
return (
|
||||
<CardContainer isSelected={isSelected} onClick={onClick}>
|
||||
<ModelHeader>
|
||||
<ModelName>{modelId}</ModelName>
|
||||
<Label>{label}</Label>
|
||||
</ModelHeader>
|
||||
<Description>{description}</Description>
|
||||
</CardContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default FeaturedModelCard
|
||||
@@ -2,7 +2,7 @@ import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
import { memo, useEffect, useRef, useState } from "react"
|
||||
import { useRemark } from "react-remark"
|
||||
import styled from "styled-components"
|
||||
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
|
||||
|
||||
const StyledMarkdown = styled.div`
|
||||
font-family:
|
||||
|
||||
@@ -3,8 +3,8 @@ import Fuse from "fuse.js"
|
||||
import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useRemark } from "react-remark"
|
||||
import styled from "styled-components"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { highlight } from "../history/HistoryView"
|
||||
|
||||
const OpenAiModelPicker: React.FC = () => {
|
||||
|
||||
@@ -1,41 +1,21 @@
|
||||
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import { VSCodeCheckbox, VSCodeDropdown, VSCodeLink, VSCodeOption, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import Fuse from "fuse.js"
|
||||
import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useRemark } from "react-remark"
|
||||
import { useMount } from "react-use"
|
||||
import styled from "styled-components"
|
||||
import { openRouterDefaultModelId } from "@shared/api"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { openRouterDefaultModelId } from "../../../../src/shared/api"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { highlight } from "../history/HistoryView"
|
||||
import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions"
|
||||
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
import { DropdownContainer, ModelInfoView, normalizeApiConfiguration } from "./ApiOptions"
|
||||
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
|
||||
import ThinkingBudgetSlider from "./ThinkingBudgetSlider"
|
||||
import FeaturedModelCard from "./FeaturedModelCard"
|
||||
|
||||
export interface OpenRouterModelPickerProps {
|
||||
isPopup?: boolean
|
||||
}
|
||||
|
||||
// Featured models for Cline provider
|
||||
const featuredModels = [
|
||||
{
|
||||
id: "anthropic/claude-3.7-sonnet",
|
||||
description: "Leading model for agentic coding",
|
||||
label: "Best",
|
||||
},
|
||||
{
|
||||
id: "google/gemini-2.5-pro-preview-03-25",
|
||||
description: "Large 1M context window, great value",
|
||||
label: "Trending",
|
||||
},
|
||||
{
|
||||
id: "meta-llama/llama-4-maverick",
|
||||
description: "Efficient performance at lower cost",
|
||||
label: "New",
|
||||
},
|
||||
]
|
||||
|
||||
const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }) => {
|
||||
const { apiConfiguration, setApiConfiguration, openRouterModels } = useExtensionState()
|
||||
const [searchTerm, setSearchTerm] = useState(apiConfiguration?.openRouterModelId || openRouterDefaultModelId)
|
||||
@@ -167,8 +147,7 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
|
||||
const showBudgetSlider = useMemo(() => {
|
||||
return (
|
||||
selectedModelId?.toLowerCase().includes("claude-3-7-sonnet") ||
|
||||
selectedModelId?.toLowerCase().includes("claude-3.7-sonnet") ||
|
||||
selectedModelId?.toLowerCase().includes("claude-3.7-sonnet:thinking")
|
||||
selectedModelId?.toLowerCase().includes("claude-3.7-sonnet")
|
||||
)
|
||||
}, [selectedModelId])
|
||||
|
||||
@@ -186,25 +165,6 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
|
||||
<label htmlFor="model-search">
|
||||
<span style={{ fontWeight: 500 }}>Model</span>
|
||||
</label>
|
||||
|
||||
{apiConfiguration?.apiProvider === "cline" && (
|
||||
<div style={{ marginBottom: "6px", marginTop: 4 }}>
|
||||
{featuredModels.map((model) => (
|
||||
<FeaturedModelCard
|
||||
key={model.id}
|
||||
modelId={model.id}
|
||||
description={model.description}
|
||||
label={model.label}
|
||||
isSelected={selectedModelId === model.id}
|
||||
onClick={() => {
|
||||
handleModelChange(model.id)
|
||||
setIsDropdownVisible(false)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DropdownWrapper ref={dropdownRef}>
|
||||
<VSCodeTextField
|
||||
id="model-search"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user