Compare commits

..

2 Commits

Author SHA1 Message Date
Elephant Lumps 3254a5099f merge conflicts 2025-06-04 21:43:48 -07:00
Elephant Lumps 4fa16e9c7a migrate partialMessage 2025-06-04 21:40:27 -07:00
45 changed files with 379 additions and 708 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fixed issue where telemetry warning popup was created for every new Cline window
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Prioritize active files in file context menu
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Migrate settingsButtonClicked to protobus
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Context menu is default to File option on start up
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
the response of the mcps is displayed with a collapsible which allows to focus on the model responses.
+1 -2
View File
@@ -52,8 +52,7 @@
"env": {
"GRPC_TRACE": "all",
"GRPC_VERBOSITY": "DEBUG",
"NODE_PATH": "${workspaceFolder}/dist-standalone/node_modules",
"CLINE_DIR": "${userHome}/.cline-standalone"
"NODE_PATH": "${workspaceFolder}/dist-standalone/node_modules"
},
"program": "standalone.js"
}
-4
View File
@@ -1,9 +1,5 @@
# Changelog
## [3.17.11]
- Add support for Gemini 2.5 Pro Preview 06-05 model to Vertex AI and Google Gemini providers
## [3.17.10]
- Add support for Qwen 3 series models with thinking mode options (Thanks @Jonny-china!)
+33
View File
@@ -0,0 +1,33 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { StreamingResponseHandler } from "./host-grpc-handler"
import { handleUriServiceRequest, handleUriServiceStreamingRequest } from "./uri/index"
import { handleWatchServiceRequest, handleWatchServiceStreamingRequest } from "./watch/index"
/**
* Configuration for a host service handler
*/
export interface HostServiceHandlerConfig {
requestHandler: (method: string, message: any) => Promise<any>
streamingHandler: (
method: string,
message: any,
responseStream: StreamingResponseHandler,
requestId?: string,
) => Promise<void>
}
/**
* Map of host service names to their handler configurations
*/
export const hostServiceHandlers: Record<string, HostServiceHandlerConfig> = {
"host.UriService": {
requestHandler: handleUriServiceRequest,
streamingHandler: handleUriServiceStreamingRequest,
},
"host.WatchService": {
requestHandler: handleWatchServiceRequest,
streamingHandler: handleWatchServiceStreamingRequest,
},
}
+22
View File
@@ -0,0 +1,22 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../host-grpc-service"
import { StreamingResponseHandler } from "../host-grpc-handler"
import { registerAllMethods } from "./methods"
// Create uri service registry
const uriService = createServiceRegistry("uri")
// Export the method handler types and registration function
export type UriMethodHandler = ServiceMethodHandler
export type UriStreamingMethodHandler = StreamingMethodHandler
export const registerMethod = uriService.registerMethod
// Export the request handlers
export const handleUriServiceRequest = uriService.handleRequest
export const handleUriServiceStreamingRequest = uriService.handleStreamingRequest
export const isStreamingMethod = uriService.isStreamingMethod
// Register all uri methods
registerAllMethods()
+16
View File
@@ -0,0 +1,16 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Import all method implementations
import { registerMethod } from "./index"
import { file } from "./file"
import { joinPath } from "./joinPath"
import { parse } from "./parse"
// Register all uri service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("file", file)
registerMethod("joinPath", joinPath)
registerMethod("parse", parse)
}
+22
View File
@@ -0,0 +1,22 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../host-grpc-service"
import { StreamingResponseHandler } from "../host-grpc-handler"
import { registerAllMethods } from "./methods"
// Create watch service registry
const watchService = createServiceRegistry("watch")
// Export the method handler types and registration function
export type WatchMethodHandler = ServiceMethodHandler
export type WatchStreamingMethodHandler = StreamingMethodHandler
export const registerMethod = watchService.registerMethod
// Export the request handlers
export const handleWatchServiceRequest = watchService.handleRequest
export const handleWatchServiceStreamingRequest = watchService.handleStreamingRequest
export const isStreamingMethod = watchService.isStreamingMethod
// Register all watch methods
registerAllMethods()
+15
View File
@@ -0,0 +1,15 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Import all method implementations
import { registerMethod } from "./index"
import { subscribeToFile } from "./subscribeToFile"
// Streaming methods for this service
export const streamingMethods = ["subscribeToFile"]
// Register all watch service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("subscribeToFile", subscribeToFile, { isStreaming: true })
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.17.11",
"version": "3.17.10",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.17.11",
"version": "3.17.10",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.12.4",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.17.11",
"version": "3.17.10",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
-3
View File
@@ -16,9 +16,6 @@ service McpService {
rpc toggleToolAutoApprove(ToggleToolAutoApproveRequest) returns (McpServers);
rpc refreshMcpMarketplace(EmptyRequest) returns (McpMarketplaceCatalog);
rpc openMcpSettings(EmptyRequest) returns (Empty);
// Subscribe to MCP marketplace catalog updates
rpc subscribeToMcpMarketplaceCatalog(EmptyRequest) returns (stream McpMarketplaceCatalog);
}
message ToggleMcpServerRequest {
+8 -35
View File
@@ -20,8 +20,6 @@ service ModelsService {
rpc refreshOpenAiModels(OpenAiModelsRequest) returns (StringArray);
// Refreshes and returns Requesty models
rpc refreshRequestyModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Subscribe to OpenRouter models updates
rpc subscribeToOpenRouterModels(EmptyRequest) returns (stream OpenRouterCompatibleModelInfo);
}
// List of VS Code LM models
@@ -37,42 +35,17 @@ message VsCodeLmModel {
string id = 4;
}
// Price tier for tiered pricing models
message PriceTier {
int32 token_limit = 1; // Upper limit (inclusive) of input tokens for this price
double price = 2; // Price per million tokens for this tier
}
// Thinking configuration for models that support thinking/reasoning
message ThinkingConfig {
optional int32 max_budget = 1; // Max allowed thinking budget tokens
optional double output_price = 2; // Output price per million tokens when budget > 0
repeated PriceTier output_price_tiers = 3; // Optional: Tiered output price when budget > 0
}
// Model tier for tiered pricing structures
message ModelTier {
int32 context_window = 1;
optional double input_price = 2;
optional double output_price = 3;
optional double cache_writes_price = 4;
optional double cache_reads_price = 5;
}
// For OpenRouterCompatibleModelInfo structure in OpenRouterModels
message OpenRouterModelInfo {
optional int32 max_tokens = 1;
optional int32 context_window = 2;
optional bool supports_images = 3;
int32 max_tokens = 1;
int32 context_window = 2;
bool supports_images = 3;
bool supports_prompt_cache = 4;
optional double input_price = 5;
optional double output_price = 6;
optional double cache_writes_price = 7;
optional double cache_reads_price = 8;
optional string description = 9;
optional ThinkingConfig thinking_config = 10;
optional bool supports_global_endpoint = 11;
repeated ModelTier tiers = 12;
double input_price = 5;
double output_price = 6;
double cache_writes_price = 7;
double cache_reads_price = 8;
string description = 9;
}
// Shared response message for model information
-6
View File
@@ -246,10 +246,4 @@ service UiService {
// Subscribe to partial message updates (streaming Cline messages as they're built)
rpc subscribeToPartialMessage(EmptyRequest) returns (stream ClineMessage);
// Subscribe to theme change events
rpc subscribeToTheme(EmptyRequest) returns (stream String);
// Initialize webview when it launches
rpc initializeWebview(EmptyRequest) returns (Empty);
}
+104 -15
View File
@@ -1,6 +1,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import axios from "axios"
import { v4 as uuidv4 } from "uuid"
import fs from "fs/promises"
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import pWaitFor from "p-wait-for"
@@ -12,8 +13,12 @@ import { EmptyRequest } from "@shared/proto/common"
import { buildApiHandler } from "@api/index"
import { cleanupLegacyCheckpoints } from "@integrations/checkpoints/CheckpointMigration"
import { downloadTask } from "@integrations/misc/export-markdown"
import { fetchOpenGraphData } from "@integrations/misc/link-preview"
import { handleFileServiceRequest } from "./file"
import { getTheme } from "@integrations/theme/getTheme"
import WorkspaceTracker from "@integrations/workspace/WorkspaceTracker"
import { ClineAccountService } from "@services/account/ClineAccountService"
import { BrowserSession } from "@services/browser/BrowserSession"
import { McpHub } from "@services/mcp/McpHub"
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
import { ApiProvider, ModelInfo } from "@shared/api"
@@ -21,31 +26,39 @@ import { ChatContent } from "@shared/ChatContent"
import { ChatSettings } from "@shared/ChatSettings"
import { ExtensionMessage, ExtensionState, Platform } from "@shared/ExtensionMessage"
import { HistoryItem } from "@shared/HistoryItem"
import { McpMarketplaceCatalog } from "@shared/mcp"
import { McpDownloadResponse, McpMarketplaceCatalog, McpServer } from "@shared/mcp"
import { TelemetrySetting } from "@shared/TelemetrySetting"
import { WebviewMessage } from "@shared/WebviewMessage"
import { fileExistsAtPath } from "@utils/fs"
import { getWorkingState } from "@utils/git"
import { extractCommitMessage } from "@integrations/git/commit-message-generator"
import { ensureMcpServersDirectoryExists, ensureSettingsDirectoryExists, GlobalFileNames } from "../storage/disk"
import { getTotalTasksSize } from "@utils/storage"
import {
ensureMcpServersDirectoryExists,
ensureSettingsDirectoryExists,
GlobalFileNames,
ensureWorkflowsDirectoryExists,
} from "../storage/disk"
import {
getAllExtensionState,
getGlobalState,
getSecret,
getWorkspaceState,
resetExtensionState,
storeSecret,
updateApiConfiguration,
updateGlobalState,
updateWorkspaceState,
} from "../storage/state"
import { Task } from "../task"
import { Task, cwd } from "../task"
import { ClineRulesToggles } from "@shared/cline-rules"
import { sendStateUpdate } from "./state/subscribeToState"
import { sendAddToInputEvent } from "./ui/subscribeToAddToInput"
import { sendAuthCallbackEvent } from "./account/subscribeToAuthCallback"
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
import { sendOpenRouterModelsEvent } from "./models/subscribeToOpenRouterModels"
import { OpenRouterCompatibleModelInfo } from "@/shared/proto/models"
import { sendChatButtonClickedEvent } from "./ui/subscribeToChatButtonClicked"
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
import { refreshExternalRulesToggles } from "@core/context/instructions/user-instructions/external-rules"
import { refreshWorkflowToggles } from "@core/context/instructions/user-instructions/workflows"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -207,6 +220,71 @@ export class Controller {
await this.setUserInfo(message.user || undefined)
await this.postStateToWebview()
break
case "webviewDidLaunch":
this.postStateToWebview()
this.workspaceTracker?.populateFilePaths() // don't await
getTheme().then((theme) =>
this.postMessageToWebview({
type: "theme",
text: JSON.stringify(theme),
}),
)
// post last cached models in case the call to endpoint fails
this.readOpenRouterModels().then((openRouterModels) => {
if (openRouterModels) {
this.postMessageToWebview({
type: "openRouterModels",
openRouterModels,
})
}
})
// gui relies on model info to be up-to-date to provide the most accurate pricing, so we need to fetch the latest details on launch.
// we do this for all users since many users switch between api providers and if they were to switch back to openrouter it would be showing outdated model info if we hadn't retrieved the latest at this point
// (see normalizeApiConfiguration > openrouter)
// Prefetch marketplace and OpenRouter models
getGlobalState(this.context, "mcpMarketplaceCatalog").then((mcpMarketplaceCatalog) => {
if (mcpMarketplaceCatalog) {
this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
mcpMarketplaceCatalog: mcpMarketplaceCatalog as McpMarketplaceCatalog,
})
}
})
this.silentlyRefreshMcpMarketplace()
handleModelsServiceRequest(this, "refreshOpenRouterModels", EmptyRequest.create()).then(async (response) => {
if (response && response.models) {
// update model info in state (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there)
const { apiConfiguration } = await getAllExtensionState(this.context)
if (apiConfiguration.openRouterModelId && response.models[apiConfiguration.openRouterModelId]) {
await updateGlobalState(
this.context,
"openRouterModelInfo",
response.models[apiConfiguration.openRouterModelId],
)
await this.postStateToWebview()
}
}
})
// Initialize telemetry service with user's current setting
this.getStateToPostToWebview().then((state) => {
const { telemetrySetting } = state
const isOptedIn = telemetrySetting !== "disabled"
telemetryService.updateTelemetryState(isOptedIn)
})
break
case "newTask":
// Code that should run in response to the hello message command
//vscode.window.showInformationMessage(message.text!)
// Send a message to our webview.
// You can send any JSON serializable data.
// Could also do this in extension .ts
//this.postMessageToWebview({ type: "text", text: `Extension: ${Date.now()}` })
// initializing new instance of Cline will make sure that any agentically running promises in old instance don't affect our new task. this essentially creates a fresh slate for the new task
await this.initTask(message.text, message.images, message.files)
break
case "apiConfiguration":
if (message.apiConfiguration) {
await updateApiConfiguration(this.context, message.apiConfiguration)
@@ -317,10 +395,6 @@ export class Controller {
await updateGlobalState(this.context, "mcpMarketplaceEnabled", message.mcpMarketplaceEnabled)
}
if (typeof message.mcpResponsesCollapsed === "boolean") {
await updateGlobalState(this.context, "mcpResponsesCollapsed", message.mcpResponsesCollapsed)
}
// chat settings (including preferredLanguage and openAIReasoningEffort)
if (message.chatSettings) {
await updateGlobalState(this.context, "chatSettings", message.chatSettings)
@@ -685,6 +759,10 @@ export class Controller {
console.error("Failed to fetch MCP marketplace:", error)
if (!silent) {
const errorMessage = error instanceof Error ? error.message : "Failed to fetch MCP marketplace"
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
error: errorMessage,
})
vscode.window.showErrorMessage(errorMessage)
}
return undefined
@@ -730,7 +808,10 @@ export class Controller {
try {
const catalog = await this.fetchMcpMarketplaceFromApi(true)
if (catalog) {
await sendMcpMarketplaceCatalogEvent(catalog)
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
mcpMarketplaceCatalog: catalog,
})
}
} catch (error) {
console.error("Failed to silently refresh MCP marketplace:", error)
@@ -758,17 +839,27 @@ export class Controller {
| McpMarketplaceCatalog
| undefined
if (!forceRefresh && cachedCatalog?.items) {
await sendMcpMarketplaceCatalogEvent(cachedCatalog)
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
mcpMarketplaceCatalog: cachedCatalog,
})
return
}
const catalog = await this.fetchMcpMarketplaceFromApi(false)
if (catalog) {
await sendMcpMarketplaceCatalogEvent(catalog)
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
mcpMarketplaceCatalog: catalog,
})
}
} catch (error) {
console.error("Failed to handle cached MCP marketplace:", error)
const errorMessage = error instanceof Error ? error.message : "Failed to handle cached MCP marketplace"
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
error: errorMessage,
})
vscode.window.showErrorMessage(errorMessage)
}
}
@@ -1093,7 +1184,6 @@ export class Controller {
shellIntegrationTimeout,
terminalReuseEnabled,
isNewUser,
mcpResponsesCollapsed,
} = await getAllExtensionState(this.context)
const localClineRulesToggles =
@@ -1139,7 +1229,6 @@ export class Controller {
shellIntegrationTimeout,
terminalReuseEnabled,
isNewUser,
mcpResponsesCollapsed,
}
}
@@ -1,55 +0,0 @@
import { Controller } from "../index"
import { EmptyRequest } from "@shared/proto/common"
import { McpMarketplaceCatalog } from "@shared/proto/mcp"
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
// Keep track of active subscriptions
const activeMcpMarketplaceSubscriptions = new Set<StreamingResponseHandler>()
/**
* Subscribe to MCP marketplace catalog updates
* @param controller The controller instance
* @param request The empty request
* @param responseStream The streaming response handler
* @param requestId The ID of the request (passed by the gRPC handler)
*/
export async function subscribeToMcpMarketplaceCatalog(
controller: Controller,
request: EmptyRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
// Add this subscription to the active subscriptions
activeMcpMarketplaceSubscriptions.add(responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
activeMcpMarketplaceSubscriptions.delete(responseStream)
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "mcp_marketplace_subscription" }, responseStream)
}
}
/**
* Send an MCP marketplace catalog event to all active subscribers
*/
export async function sendMcpMarketplaceCatalogEvent(catalog: McpMarketplaceCatalog): Promise<void> {
// Send the event to all active subscribers
const promises = Array.from(activeMcpMarketplaceSubscriptions).map(async (responseStream) => {
try {
await responseStream(
catalog,
false, // Not the last message
)
} catch (error) {
console.error("Error sending MCP marketplace catalog event:", error)
// Remove the subscription if there was an error
activeMcpMarketplaceSubscriptions.delete(responseStream)
}
})
await Promise.all(promises)
}
@@ -15,11 +15,11 @@ import { GlobalFileNames } from "@core/storage/disk"
*/
export async function refreshOpenRouterModels(
controller: Controller,
_request: EmptyRequest,
request: EmptyRequest,
): Promise<OpenRouterCompatibleModelInfo> {
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.openRouterModels)
let models: Record<string, OpenRouterModelInfo> = {}
let models: Record<string, Partial<OpenRouterModelInfo>> = {}
try {
const response = await axios.get("https://openrouter.ai/api/v1/models")
@@ -32,20 +32,15 @@ export async function refreshOpenRouterModels(
return undefined
}
for (const rawModel of rawModels) {
const modelInfo = OpenRouterModelInfo.create({
maxTokens: rawModel.top_provider?.max_completion_tokens ?? 0,
contextWindow: rawModel.context_length ?? 0,
supportsImages: rawModel.architecture?.modality?.includes("image") ?? false,
const modelInfo: Partial<OpenRouterModelInfo> = {
maxTokens: rawModel.top_provider?.max_completion_tokens,
contextWindow: rawModel.context_length,
supportsImages: rawModel.architecture?.modality?.includes("image"),
supportsPromptCache: false,
inputPrice: parsePrice(rawModel.pricing?.prompt) ?? 0,
outputPrice: parsePrice(rawModel.pricing?.completion) ?? 0,
cacheWritesPrice: 0,
cacheReadsPrice: 0,
description: rawModel.description ?? "",
thinkingConfig: rawModel.thinking_config ?? undefined,
supportsGlobalEndpoint: rawModel.supports_global_endpoint ?? undefined,
tiers: rawModel.tiers ?? [],
})
inputPrice: parsePrice(rawModel.pricing?.prompt),
outputPrice: parsePrice(rawModel.pricing?.completion),
description: rawModel.description,
}
switch (rawModel.id) {
case "anthropic/claude-sonnet-4":
@@ -123,7 +118,7 @@ export async function refreshOpenRouterModels(
console.error("Invalid response from OpenRouter API")
}
await fs.writeFile(openRouterModelsFilePath, JSON.stringify(models))
console.log("OpenRouter models fetched and saved", JSON.stringify(models).slice(0, 300))
console.log("OpenRouter models fetched and saved", models)
} catch (error) {
console.error("Error fetching OpenRouter models:", error)
@@ -134,13 +129,30 @@ export async function refreshOpenRouterModels(
}
}
return OpenRouterCompatibleModelInfo.create({ models })
// Convert the Record<string, Partial<OpenRouterModelInfo>> to Record<string, OpenRouterModelInfo>
// by filling in any missing required fields with defaults
const typedModels: Record<string, OpenRouterModelInfo> = {}
for (const [key, model] of Object.entries(models)) {
typedModels[key] = {
maxTokens: model.maxTokens ?? 0,
contextWindow: model.contextWindow ?? 0,
supportsImages: model.supportsImages ?? false,
supportsPromptCache: model.supportsPromptCache ?? false,
inputPrice: model.inputPrice ?? 0,
outputPrice: model.outputPrice ?? 0,
cacheWritesPrice: model.cacheWritesPrice ?? 0,
cacheReadsPrice: model.cacheReadsPrice ?? 0,
description: model.description ?? "",
}
}
return OpenRouterCompatibleModelInfo.create({ models: typedModels })
}
/**
* Reads cached OpenRouter models from disk
*/
async function readOpenRouterModels(controller: Controller): Promise<Record<string, OpenRouterModelInfo> | undefined> {
async function readOpenRouterModels(controller: Controller): Promise<Record<string, Partial<OpenRouterModelInfo>> | undefined> {
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.openRouterModels)
const fileExists = await fileExistsAtPath(openRouterModelsFilePath)
if (fileExists) {
@@ -1,60 +0,0 @@
import { Controller } from "../index"
import { EmptyRequest } from "@shared/proto/common"
import { OpenRouterCompatibleModelInfo } from "@shared/proto/models"
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
// Keep track of active OpenRouter models subscriptions
const activeOpenRouterModelsSubscriptions = new Set<StreamingResponseHandler>()
/**
* Subscribe to OpenRouter models events
* @param controller The controller instance
* @param request The empty request
* @param responseStream The streaming response handler
* @param requestId The ID of the request (passed by the gRPC handler)
*/
export async function subscribeToOpenRouterModels(
controller: Controller,
request: EmptyRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
console.log("[DEBUG] set up OpenRouter models subscription")
// Add this subscription to the active subscriptions
activeOpenRouterModelsSubscriptions.add(responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
activeOpenRouterModelsSubscriptions.delete(responseStream)
console.log("[DEBUG] Cleaned up OpenRouter models subscription")
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "openRouterModels_subscription" }, responseStream)
}
}
/**
* Send an OpenRouter models event to all active subscribers
* @param models The OpenRouter models to send
*/
export async function sendOpenRouterModelsEvent(models: OpenRouterCompatibleModelInfo): Promise<void> {
// Send the event to all active subscribers
const promises = Array.from(activeOpenRouterModelsSubscriptions).map(async (responseStream) => {
try {
await responseStream(
models,
false, // Not the last message
)
console.log("[DEBUG] sending OpenRouter models event")
} catch (error) {
console.error("Error sending OpenRouter models event:", error)
// Remove the subscription if there was an error
activeOpenRouterModelsSubscriptions.delete(responseStream)
}
})
await Promise.all(promises)
}
@@ -1,73 +0,0 @@
import type { Controller } from "../index"
import { EmptyRequest, Empty } from "@shared/proto/common"
import { handleModelsServiceRequest } from "../models"
import { getAllExtensionState, getGlobalState, updateGlobalState } from "../../storage/state"
import { sendOpenRouterModelsEvent } from "../models/subscribeToOpenRouterModels"
import { sendMcpMarketplaceCatalogEvent } from "../mcp/subscribeToMcpMarketplaceCatalog"
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
import { OpenRouterCompatibleModelInfo } from "@/shared/proto/models"
import { McpMarketplaceCatalog } from "@shared/mcp"
/**
* Initialize webview when it launches
* @param controller The controller instance
* @param request The empty request
* @returns Empty response
*/
export async function initializeWebview(controller: Controller, request: EmptyRequest): Promise<Empty> {
try {
// Populate file paths for workspace tracker (don't await)
controller.workspaceTracker?.populateFilePaths()
// Post last cached models in case the call to endpoint fails
controller.readOpenRouterModels().then((openRouterModels) => {
if (openRouterModels) {
sendOpenRouterModelsEvent(OpenRouterCompatibleModelInfo.create({ models: openRouterModels }))
}
})
// Refresh OpenRouter models from API
handleModelsServiceRequest(controller, "refreshOpenRouterModels", EmptyRequest.create()).then(async (response) => {
if (response && response.models) {
// Update model info in state (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there)
const { apiConfiguration } = await getAllExtensionState(controller.context)
if (apiConfiguration.openRouterModelId && response.models[apiConfiguration.openRouterModelId]) {
await updateGlobalState(
controller.context,
"openRouterModelInfo",
response.models[apiConfiguration.openRouterModelId],
)
await controller.postStateToWebview()
}
}
})
// GUI relies on model info to be up-to-date to provide the most accurate pricing, so we need to fetch the latest details on launch.
// We do this for all users since many users switch between api providers and if they were to switch back to openrouter it would be showing outdated model info if we hadn't retrieved the latest at this point
// (see normalizeApiConfiguration > openrouter)
// Prefetch marketplace and OpenRouter models
// Send cached MCP marketplace catalog if available
getGlobalState(controller.context, "mcpMarketplaceCatalog").then((mcpMarketplaceCatalog) => {
if (mcpMarketplaceCatalog) {
sendMcpMarketplaceCatalogEvent(mcpMarketplaceCatalog as McpMarketplaceCatalog)
}
})
// Silently refresh MCP marketplace catalog
controller.silentlyRefreshMcpMarketplace()
// Initialize telemetry service with user's current setting
controller.getStateToPostToWebview().then((state) => {
const { telemetrySetting } = state
const isOptedIn = telemetrySetting !== "disabled"
telemetryService.updateTelemetryState(isOptedIn)
})
return Empty.create({})
} catch (error) {
console.error("Failed to initialize webview:", error)
// Return empty response even on error to not break the frontend
return Empty.create({})
}
}
@@ -1,76 +0,0 @@
import { Controller } from "../index"
import { EmptyRequest, String } from "@shared/proto/common"
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
import { getTheme } from "@integrations/theme/getTheme"
// Keep track of active theme subscriptions
const activeThemeSubscriptions = new Set<StreamingResponseHandler>()
/**
* Subscribe to theme change events
* @param controller The controller instance
* @param request The empty request
* @param responseStream The streaming response handler
* @param requestId The ID of the request (passed by the gRPC handler)
*/
export async function subscribeToTheme(
controller: Controller,
request: EmptyRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
// Add this subscription to the active subscriptions
activeThemeSubscriptions.add(responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
activeThemeSubscriptions.delete(responseStream)
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "theme_subscription" }, responseStream)
}
// Send the current theme immediately upon subscription
const theme = await getTheme()
if (theme) {
try {
const themeEvent = String.create({
value: JSON.stringify(theme),
})
await responseStream(
themeEvent,
false, // Not the last message
)
} catch (error) {
console.error("Error sending initial theme:", error)
activeThemeSubscriptions.delete(responseStream)
}
}
}
/**
* Send a theme event to all active subscribers
* @param themeJson The JSON-stringified theme data
*/
export async function sendThemeEvent(themeJson: string): Promise<void> {
// Send the event to all active subscribers
const promises = Array.from(activeThemeSubscriptions).map(async (responseStream) => {
try {
const event = String.create({
value: themeJson,
})
await responseStream(
event,
false, // Not the last message
)
} catch (error) {
console.error("Error sending theme event:", error)
// Remove the subscription if there was an error
activeThemeSubscriptions.delete(responseStream)
}
})
await Promise.all(promises)
}
-1
View File
@@ -91,7 +91,6 @@ export type GlobalStateKey =
| "favoritedModelIds"
| "requestTimeoutMs"
| "shellIntegrationTimeout"
| "mcpResponsesCollapsed"
| "terminalReuseEnabled"
| "isNewUser"
-4
View File
@@ -164,7 +164,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
shellIntegrationTimeout,
enableCheckpointsSettingRaw,
mcpMarketplaceEnabledRaw,
mcpResponsesCollapsedRaw,
globalWorkflowToggles,
terminalReuseEnabled,
] = await Promise.all([
@@ -256,7 +255,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "shellIntegrationTimeout") as Promise<number | undefined>,
getGlobalState(context, "enableCheckpointsSetting") as Promise<boolean | undefined>,
getGlobalState(context, "mcpMarketplaceEnabled") as Promise<boolean | undefined>,
getGlobalState(context, "mcpResponsesCollapsed") as Promise<boolean | undefined>,
getGlobalState(context, "globalWorkflowToggles") as Promise<ClineRulesToggles | undefined>,
getGlobalState(context, "terminalReuseEnabled") as Promise<boolean | undefined>,
])
@@ -279,7 +277,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
const mcpMarketplaceEnabled = await migrateMcpMarketplaceEnableSetting(mcpMarketplaceEnabledRaw)
const enableCheckpointsSetting = await migrateEnableCheckpointsSetting(enableCheckpointsSettingRaw)
const mcpResponsesCollapsed = mcpResponsesCollapsedRaw ?? false
// Plan/Act separate models setting is a boolean indicating whether the user wants to use different models for plan and act. Existing users expect this to be enabled, while we want new users to opt in to this being disabled by default.
// On win11 state sometimes initializes as empty string instead of undefined
@@ -390,7 +387,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
previousModeAwsBedrockCustomSelected,
previousModeAwsBedrockCustomModelBaseId,
mcpMarketplaceEnabled: mcpMarketplaceEnabled,
mcpResponsesCollapsed: mcpResponsesCollapsed,
telemetrySetting: telemetrySetting || "unset",
planActSeparateModelsSetting,
enableCheckpointsSetting: enableCheckpointsSetting,
+5 -6
View File
@@ -8,7 +8,6 @@ import { findLast } from "@shared/array"
import { readFile } from "fs/promises"
import path from "node:path"
import { WebviewProviderType } from "@/shared/webview/types"
import { sendThemeEvent } from "@core/controller/ui/subscribeToTheme"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -140,11 +139,11 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
vscode.workspace.onDidChangeConfiguration(
async (e) => {
if (e && e.affectsConfiguration("workbench.colorTheme")) {
// Send theme update via gRPC subscription
const theme = await getTheme()
if (theme) {
await sendThemeEvent(JSON.stringify(theme))
}
// Sends latest theme name to webview
await this.controller.postMessageToWebview({
type: "theme",
text: JSON.stringify(await getTheme()),
})
}
if (e && e.affectsConfiguration("cline.mcpMarketplace.enabled")) {
// Update state when marketplace tab setting changes
+1 -12
View File
@@ -10,14 +10,6 @@ class WorkspaceTracker {
private disposables: vscode.Disposable[] = []
private filePaths: Set<string> = new Set()
private get activeFiles() {
return new Set(
vscode.window.tabGroups.activeTabGroup.tabs
.filter((tab) => tab.input instanceof vscode.TabInputText)
.map((tab) => (tab.input as vscode.TabInputText).uri.fsPath),
)
}
constructor(private readonly postMessageToWebview: (message: ExtensionMessage) => Promise<void>) {
this.postMessageToWebview = postMessageToWebview
this.registerListeners()
@@ -44,9 +36,6 @@ class WorkspaceTracker {
// Listen for file renaming
this.disposables.push(vscode.workspace.onDidRenameFiles(this.onFilesRenamed.bind(this)))
// Listen for tab groups changes
this.disposables.push(vscode.window.tabGroups.onDidChangeTabs(this.workspaceDidUpdate.bind(this)))
/*
An event that is emitted when a workspace folder is added or removed.
**Note:** this event will not fire if the first workspace folder is added, removed or changed,
@@ -97,7 +86,7 @@ class WorkspaceTracker {
}
this.postMessageToWebview({
type: "workspaceUpdated",
filePaths: Array.from(new Set([...this.activeFiles, ...this.filePaths])).map((file) => {
filePaths: Array.from(this.filePaths).map((file) => {
const relativePath = path.relative(cwd, file).toPosix()
return file.endsWith("/") ? relativePath + "/" : relativePath
}),
@@ -138,20 +138,17 @@ class TelemetryService {
if (globalTelemetryEnabled) {
this.telemetryEnabled = didUserOptIn
} else {
// Only show warning if user has opted in to Cline telemetry but VS Code telemetry is disabled
if (didUserOptIn) {
void vscode.window
.showWarningMessage(
"Anonymous Cline error and usage reporting is enabled, but VSCode telemetry is disabled. To enable error and usage reporting for this extension, enable VSCode telemetry in settings.",
"Open Settings",
)
.then((selection) => {
if (selection === "Open Settings") {
void vscode.commands.executeCommand("workbench.action.openSettings", "telemetry.telemetryLevel")
}
})
}
this.telemetryEnabled = false
// Show warning to user that global telemetry is disabled
void vscode.window
.showWarningMessage(
"VSCode telemetry is disabled. To enable telemetry for this extension, first enable VSCode telemetry in settings.",
"Open Settings",
)
.then((selection) => {
if (selection === "Open Settings") {
void vscode.commands.executeCommand("workbench.action.openSettings", "telemetry.telemetryLevel")
}
})
}
// Update PostHog client state based on telemetry preference
+4 -1
View File
@@ -19,11 +19,14 @@ export interface ExtensionMessage {
| "selectedImages"
| "ollamaModels"
| "lmStudioModels"
| "theme"
| "workspaceUpdated"
| "openRouterModels"
| "openAiModels"
| "requestyModels"
| "mcpServers"
| "relinquishControl"
| "mcpMarketplaceCatalog"
| "mcpDownloadDetails"
| "commitSearchResults"
| "openGraphData"
@@ -42,6 +45,7 @@ export interface ExtensionMessage {
lmStudioModels?: string[]
vsCodeLmModels?: { vendor?: string; family?: string; version?: string; id?: string }[]
filePaths?: string[]
openRouterModels?: Record<string, ModelInfo>
openAiModels?: string[]
requestyModels?: Record<string, ModelInfo>
mcpServers?: McpServer[]
@@ -123,7 +127,6 @@ export interface ExtensionState {
globalWorkflowToggles: ClineRulesToggles
localCursorRulesToggles: ClineRulesToggles
localWindsurfRulesToggles: ClineRulesToggles
mcpResponsesCollapsed?: boolean
}
export interface ClineMessage {
+2 -1
View File
@@ -9,6 +9,8 @@ import { McpViewTab } from "./mcp"
export interface WebviewMessage {
type:
| "apiConfiguration"
| "webviewDidLaunch"
| "newTask"
| "condense"
| "reportBug"
| "requestVsCodeLmModels"
@@ -51,7 +53,6 @@ export interface WebviewMessage {
planActSeparateModelsSetting?: boolean
enableCheckpointsSetting?: boolean
mcpMarketplaceEnabled?: boolean
mcpResponsesCollapsed?: boolean
telemetrySetting?: TelemetrySetting
customInstructionsSetting?: string
mentionsRequestId?: string
-48
View File
@@ -575,30 +575,6 @@ export const vertexModels = {
},
],
},
"gemini-2.5-pro-preview-06-05": {
maxTokens: 65536,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: true,
supportsGlobalEndpoint: true,
inputPrice: 2.5,
outputPrice: 15,
cacheReadsPrice: 0.31,
tiers: [
{
contextWindow: 200000,
inputPrice: 1.25,
outputPrice: 10,
cacheReadsPrice: 0.31,
},
{
contextWindow: Infinity,
inputPrice: 2.5,
outputPrice: 15,
cacheReadsPrice: 0.625,
},
],
},
"gemini-2.5-flash-preview-04-17": {
maxTokens: 65536,
contextWindow: 1_048_576,
@@ -743,30 +719,6 @@ export const geminiModels = {
},
],
},
"gemini-2.5-pro-preview-06-05": {
maxTokens: 65536,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: true,
supportsGlobalEndpoint: true,
inputPrice: 2.5,
outputPrice: 15,
cacheReadsPrice: 0.31,
tiers: [
{
contextWindow: 200000,
inputPrice: 1.25,
outputPrice: 10,
cacheReadsPrice: 0.31,
},
{
contextWindow: Infinity,
inputPrice: 2.5,
outputPrice: 15,
cacheReadsPrice: 0.625,
},
],
},
"gemini-2.5-flash-preview-05-20": {
maxTokens: 65536,
contextWindow: 1_048_576,
@@ -0,0 +1,11 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createGrpcClient } from "./host-grpc-client-base"
import { UriServiceDefinition } from "@shared/proto/host/uri"
import { WatchServiceDefinition } from "@shared/proto/host/watch"
const UriServiceClient = createGrpcClient(UriServiceDefinition)
const WatchServiceClient = createGrpcClient(WatchServiceDefinition)
export { UriServiceClient, WatchServiceClient }
+4 -3
View File
@@ -32,11 +32,12 @@ function main() {
const host = "127.0.0.1:50051"
server.bindAsync(host, grpc.ServerCredentials.createInsecure(), (err) => {
if (err) {
log(`Error: Failed to bind to ${host}, port may be unavailable. ${err.message}`)
log(`Error: Failed to bind to ${host}, port may be unavailable ${err.message}`)
process.exit(1)
} else {
server.start()
log(`gRPC server listening on ${host}`)
}
server.start()
log(`gRPC server listening on ${host}`)
})
}
+1 -1
View File
@@ -15,7 +15,7 @@ const outputChannel: vscode.OutputChannel = {
}
function postMessage(message: ExtensionMessage): Promise<boolean> {
log("postMessage stub called:", JSON.stringify(message).slice(0, 200))
log("postMessage stub called:", message)
return Promise.resolve(true)
}
+2 -11
View File
@@ -1,22 +1,13 @@
import { URI } from "vscode-uri"
import path from "path"
import { mkdirSync } from "fs"
import type { Extension, ExtensionContext } from "vscode"
import { ExtensionKind, ExtensionMode } from "vscode"
import { outputChannel, postMessage } from "./vscode-context-stubs"
import { EnvironmentVariableCollection, MementoStore, readJson, SecretStore } from "./vscode-context-utils"
import { log } from "./utils"
if (!process.env.CLINE_DIR) {
console.warn("Environment variable CLINE_DIR was not set.")
process.exit(1)
}
const DATA_DIR = path.join(process.env.CLINE_DIR, "data")
mkdirSync(DATA_DIR, { recursive: true })
log("Using settings dir:", DATA_DIR)
const EXTENSION_DIR = path.join(process.env.CLINE_DIR, "core")
const DATA_DIR = process.env.DATA_DIR ?? "."
const EXTENSION_DIR = process.env.EXTENSION_DIR ?? "."
const EXTENSION_MODE = process.env.IS_DEV === "true" ? ExtensionMode.Development : ExtensionMode.Production
const extension: Extension<void> = {
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "cline-standalone",
"name": "Cline standalone",
"version": "1.0.0",
"main": "standalone.js",
"dependencies": {
@@ -53,7 +53,6 @@ vscode.window = {
tabGroups: {
all: [],
close: async () => {},
onDidChangeTabs: createStub("vscode.env.tabGroups.onDidChangeTabs"),
},
withProgress: async (_options, task) => {
console.log("Stubbed withProgress")
@@ -816,7 +816,7 @@ vscode.TextDocumentSaveReason = { Manual: 0, AfterDelay: 0, FocusOut: 0 }
vscode.workspace = {}
vscode.workspace.fs = createStub("vscode.workspace.fs")
vscode.workspace.rootPath = createStub("vscode.workspace.rootPath")
vscode.workspace.workspaceFolders = []
vscode.workspace.workspaceFolders = createStub("vscode.workspace.workspaceFolders")
vscode.workspace.name = createStub("vscode.workspace.name")
vscode.workspace.workspaceFile = createStub("vscode.workspace.workspaceFile")
vscode.workspace.onDidChangeWorkspaceFolders = createStub("vscode.workspace.onDidChangeWorkspaceFolders")
@@ -894,22 +894,10 @@ vscode.workspace.onWillDeleteFiles = createStub("vscode.workspace.onWillDeleteFi
vscode.workspace.onDidDeleteFiles = createStub("vscode.workspace.onDidDeleteFiles")
vscode.workspace.onWillRenameFiles = createStub("vscode.workspace.onWillRenameFiles")
vscode.workspace.onDidRenameFiles = createStub("vscode.workspace.onDidRenameFiles")
const workspaceConfigStore = {}
vscode.workspace.getConfiguration = function (section) {
return {
get: (key, defaultValue) => {
return workspaceConfigStore[`${section}.${key}`] ?? defaultValue
},
update: (key, value, global) => {
workspaceConfigStore[`${section}.${key}`] = value
},
has: (key) => {
return `${section}.${key}` in workspaceConfigStore
},
}
vscode.workspace.getConfiguration = function (section, scope) {
console.log("Called stubbed function: vscode.workspace.getConfiguration")
return createStub("unknown")
}
vscode.workspace.onDidChangeConfiguration = createStub("vscode.workspace.onDidChangeConfiguration")
vscode.workspace.registerTaskProvider = function (type, provider) {
console.log("Called stubbed function: vscode.workspace.registerTaskProvider")
@@ -10,7 +10,6 @@ import { FileServiceClient, StateServiceClient } from "@/services/grpc-client"
import {
ContextMenuOptionType,
getContextMenuOptions,
getContextMenuOptionIndex,
insertMention,
insertMentionDirectly,
removeMention,
@@ -61,9 +60,6 @@ const getImageDimensions = (dataUrl: string): Promise<{ width: number; height: n
})
}
// Set to "File" option by default
const DEFAULT_CONTEXT_MENU_OPTION = getContextMenuOptionIndex(ContextMenuOptionType.File)
interface ChatTextAreaProps {
inputValue: string
activeQuote: string | null
@@ -534,7 +530,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
if (event.key === "Escape") {
// event.preventDefault()
setSelectedType(null)
setSelectedMenuIndex(DEFAULT_CONTEXT_MENU_OPTION)
setSelectedMenuIndex(3) // File by default
return
}
@@ -774,7 +770,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
})
}, 200) // 200ms debounce
} else {
setSelectedMenuIndex(DEFAULT_CONTEXT_MENU_OPTION)
setSelectedMenuIndex(3) // Set to "File" option by default
}
} else {
setSearchQuery("")
@@ -1,6 +1,4 @@
import React, { useEffect, useState, useCallback } from "react"
import { VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react" // Import ProgressRing
import { useExtensionState } from "../../../context/ExtensionStateContext"
import LinkPreview from "./LinkPreview"
import ImagePreview from "./ImagePreview"
import styled from "styled-components"
@@ -30,10 +28,6 @@ const ResponseHeader = styled.div`
text-overflow: ellipsis;
margin-right: 8px;
}
.header-icon {
margin-right: 6px;
}
`
const ToggleSwitch = styled.div`
@@ -117,9 +111,7 @@ interface UrlMatch {
}
const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText }) => {
const { mcpResponsesCollapsed } = useExtensionState() // Get setting from context
const [isExpanded, setIsExpanded] = useState(!mcpResponsesCollapsed) // Initialize with context setting
const [isLoading, setIsLoading] = useState(false) // Initial loading state for rich content
const [isLoading, setIsLoading] = useState(true)
const [displayMode, setDisplayMode] = useState<"rich" | "plain">(() => {
// Get saved preference from localStorage, default to 'rich'
const savedMode = localStorage.getItem("mcpDisplayMode")
@@ -132,11 +124,14 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
const toggleDisplayMode = useCallback(() => {
const newMode = displayMode === "rich" ? "plain" : "rich"
// Force an immediate re-render
setForceUpdateCounter((prev) => prev + 1)
// Update display mode and save preference
setDisplayMode(newMode)
localStorage.setItem("mcpDisplayMode", newMode)
// If switching to plain mode, cancel any ongoing processing
if (newMode === "plain") {
console.log("Switching to plain mode - cancelling URL processing")
@@ -144,23 +139,13 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
} else {
// If switching to rich mode, the useEffect will re-run and fetch data
console.log("Switching to rich mode - will start URL processing")
setUrlMatches([])
}
}, [displayMode])
const toggleExpand = useCallback(() => {
setIsExpanded((prev) => !prev)
}, [])
// Effect to update isExpanded if mcpResponsesCollapsed changes from context
useEffect(() => {
setIsExpanded(!mcpResponsesCollapsed)
}, [])
// Find all URLs in the text and determine if they're images
useEffect(() => {
// Skip all processing if in plain mode
if (!isExpanded || displayMode === "plain") {
if (displayMode === "plain") {
setIsLoading(false)
setUrlMatches([]) // Clear any existing matches when in plain mode
return
@@ -168,10 +153,12 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
// Use a direct boolean for cancellation that's scoped to this effect run
let processingCanceled = false
const processResponse = async () => {
console.log("Processing MCP response for URL extraction")
setIsLoading(true)
setError(null)
try {
const text = responseText || ""
const matches: UrlMatch[] = []
@@ -280,24 +267,12 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
processingCanceled = true
console.log("Cleaning up URL processing")
}
}, [responseText, displayMode, forceUpdateCounter, isExpanded])
}, [responseText, displayMode, forceUpdateCounter])
// Function to render content based on display mode
const renderContent = () => {
if (!isExpanded) {
return null // Don't render content if not expanded
}
if (isLoading && displayMode === "rich") {
return (
<div style={{ display: "flex", justifyContent: "center", alignItems: "center", height: "50px" }}>
<VSCodeProgressRing />
</div>
)
}
// For plain text mode, just show the text
if (displayMode === "plain") {
if (displayMode === "plain" || isLoading) {
return <UrlText>{responseText}</UrlText>
}
@@ -312,7 +287,7 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
}
// For rich display mode, show the text with embedded content
if (displayMode === "rich") {
if (!isLoading) {
// We already know displayMode is "rich" if we get here
// Create an array of text segments and embedded content
const segments: JSX.Element[] = []
@@ -410,48 +385,30 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
try {
return (
<ResponseContainer>
<ResponseHeader
onClick={toggleExpand}
style={{
borderBottom: isExpanded ? "1px dashed var(--vscode-editorGroup-border)" : "none",
marginBottom: isExpanded ? "8px" : "0px",
}}>
<div className="header-title">
<span className={`codicon codicon-chevron-${isExpanded ? "down" : "right"} header-icon`}></span>
Response
</div>
<div style={{ minWidth: isExpanded ? "auto" : "0", visibility: isExpanded ? "visible" : "hidden" }}>
<ToggleSwitch onClick={(e) => e.stopPropagation()}>
<span className="toggle-label">{displayMode === "rich" ? "Rich Display" : "Plain Text"}</span>
<div
className={`toggle-container ${displayMode === "rich" ? "active" : ""}`}
onClick={toggleDisplayMode}>
<div className="toggle-handle"></div>
</div>
</ToggleSwitch>
</div>
<ResponseHeader>
<span className="header-title">Response</span>
<ToggleSwitch>
<span className="toggle-label">{displayMode === "rich" ? "Rich Display" : "Plain Text"}</span>
<div className={`toggle-container ${displayMode === "rich" ? "active" : ""}`} onClick={toggleDisplayMode}>
<div className="toggle-handle"></div>
</div>
</ToggleSwitch>
</ResponseHeader>
{isExpanded && <div className="response-content">{renderContent()}</div>}
<div className="response-content">{renderContent()}</div>
</ResponseContainer>
)
} catch (error) {
console.log("Error rendering MCP response - falling back to plain text") // Restored comment
// Fallback for critical rendering errors
console.log("Error rendering MCP response - falling back to plain text")
return (
<ResponseContainer>
<ResponseHeader onClick={toggleExpand}>
<div className="header-title">
<span className={`codicon codicon-chevron-${isExpanded ? "down" : "right"} header-icon`}></span>
Response (Error)
</div>
<ResponseHeader>
<span className="header-title">Response</span>
</ResponseHeader>
{isExpanded && (
<div className="response-content">
<div style={{ color: "var(--vscode-errorForeground)" }}>Error parsing response:</div>
<UrlText>{responseText}</UrlText>
</div>
)}
<div className="response-content">
<div>Error parsing response:</div>
<UrlText>{responseText}</UrlText>
</div>
</ResponseContainer>
)
}
@@ -14,7 +14,8 @@ import { vscode } from "@/utils/vscode"
import McpMarketplaceCard from "./McpMarketplaceCard"
import McpSubmitCard from "./McpSubmitCard"
const McpMarketplaceView = () => {
const { mcpServers, mcpMarketplaceCatalog } = useExtensionState()
const { mcpServers } = useExtensionState()
const [items, setItems] = useState<McpMarketplaceItem[]>([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [isRefreshing, setIsRefreshing] = useState(false)
@@ -22,8 +23,6 @@ const McpMarketplaceView = () => {
const [selectedCategory, setSelectedCategory] = useState<string | null>(null)
const [sortBy, setSortBy] = useState<"newest" | "stars" | "name" | "downloadCount">("downloadCount")
const items = mcpMarketplaceCatalog?.items || []
const categories = useMemo(() => {
const uniqueCategories = new Set(items.map((item) => item.category))
return Array.from(uniqueCategories).sort()
@@ -59,7 +58,16 @@ const McpMarketplaceView = () => {
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
const message = event.data
if (message.type === "mcpDownloadDetails") {
if (message.type === "mcpMarketplaceCatalog") {
if (message.error) {
setError(message.error)
} else {
setItems(message.mcpMarketplaceCatalog?.items || [])
setError(null)
}
setIsLoading(false)
setIsRefreshing(false)
} else if (message.type === "mcpDownloadDetails") {
if (message.error) {
setError(message.error)
}
@@ -68,7 +76,7 @@ const McpMarketplaceView = () => {
window.addEventListener("message", handleMessage)
// Fetch marketplace catalog on initial load
// Fetch marketplace catalog
fetchMarketplace()
return () => {
@@ -76,15 +84,6 @@ const McpMarketplaceView = () => {
}
}, [])
useEffect(() => {
// Update loading state when catalog arrives
if (mcpMarketplaceCatalog?.items) {
setIsLoading(false)
setIsRefreshing(false)
setError(null)
}
}, [mcpMarketplaceCatalog])
const fetchMarketplace = (forceRefresh: boolean = false) => {
if (forceRefresh) {
setIsRefreshing(true)
@@ -9,8 +9,6 @@ const FeatureSettingsSection = () => {
setEnableCheckpointsSetting,
mcpMarketplaceEnabled,
setMcpMarketplaceEnabled,
mcpResponsesCollapsed,
setMcpResponsesCollapsed,
chatSettings,
setChatSettings,
} = useExtensionState()
@@ -44,19 +42,6 @@ const FeatureSettingsSection = () => {
Enables the MCP Marketplace tab for discovering and installing MCP servers.
</p>
</div>
<div style={{ marginTop: 10 }}>
<VSCodeCheckbox
checked={mcpResponsesCollapsed}
onChange={(e: any) => {
const checked = e.target.checked === true
setMcpResponsesCollapsed(checked)
}}>
Collapse MCP Responses
</VSCodeCheckbox>
<p className="text-xs text-[var(--vscode-descriptionForeground)]">
Sets the default display mode for MCP response panels
</p>
</div>
<div style={{ marginTop: 10 }}>
<label
htmlFor="openai-reasoning-effort-dropdown"
@@ -130,8 +130,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
setShellIntegrationTimeout,
terminalReuseEnabled,
setTerminalReuseEnabled,
mcpResponsesCollapsed,
setMcpResponsesCollapsed,
setApiConfiguration,
} = useExtensionState()
@@ -143,7 +141,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
planActSeparateModelsSetting,
enableCheckpointsSetting,
mcpMarketplaceEnabled,
mcpResponsesCollapsed,
chatSettings,
shellIntegrationTimeout,
terminalReuseEnabled,
@@ -189,7 +186,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
mcpMarketplaceEnabled,
shellIntegrationTimeout,
terminalReuseEnabled,
mcpResponsesCollapsed,
apiConfiguration: apiConfigurationToSubmit,
})
@@ -212,7 +208,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
planActSeparateModelsSetting !== originalState.current.planActSeparateModelsSetting ||
enableCheckpointsSetting !== originalState.current.enableCheckpointsSetting ||
mcpMarketplaceEnabled !== originalState.current.mcpMarketplaceEnabled ||
mcpResponsesCollapsed !== originalState.current.mcpResponsesCollapsed ||
JSON.stringify(chatSettings) !== JSON.stringify(originalState.current.chatSettings) ||
shellIntegrationTimeout !== originalState.current.shellIntegrationTimeout ||
terminalReuseEnabled !== originalState.current.terminalReuseEnabled
@@ -225,7 +220,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
planActSeparateModelsSetting,
enableCheckpointsSetting,
mcpMarketplaceEnabled,
mcpResponsesCollapsed,
chatSettings,
shellIntegrationTimeout,
terminalReuseEnabled,
@@ -266,9 +260,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
if (typeof setTerminalReuseEnabled === "function") {
setTerminalReuseEnabled(originalState.current.terminalReuseEnabled ?? true)
}
if (typeof setMcpResponsesCollapsed === "function") {
setMcpResponsesCollapsed(originalState.current.mcpResponsesCollapsed ?? false)
}
// Close settings view
onDone()
}
@@ -286,7 +277,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
setApiConfiguration,
setEnableCheckpointsSetting,
setMcpMarketplaceEnabled,
setMcpResponsesCollapsed,
])
// Handle confirmation dialog actions
@@ -18,10 +18,9 @@ import {
requestyDefaultModelInfo,
} from "../../../src/shared/api"
import { McpMarketplaceCatalog, McpServer, McpViewTab } from "../../../src/shared/mcp"
import { ModelsServiceClient, StateServiceClient, UiServiceClient, McpServiceClient } from "../services/grpc-client"
import { ModelsServiceClient, StateServiceClient, UiServiceClient } from "../services/grpc-client"
import { convertTextMateToHljs } from "../utils/textMateToHljs"
import { vscode } from "../utils/vscode"
import { OpenRouterCompatibleModelInfo } from "@shared/proto/models"
interface ExtensionStateContextType extends ExtensionState {
didHydrateState: boolean
@@ -52,7 +51,6 @@ interface ExtensionStateContextType extends ExtensionState {
setPlanActSeparateModelsSetting: (value: boolean) => void
setEnableCheckpointsSetting: (value: boolean) => void
setMcpMarketplaceEnabled: (value: boolean) => void
setMcpResponsesCollapsed: (value: boolean) => void
setShellIntegrationTimeout: (value: number) => void
setTerminalReuseEnabled: (value: boolean) => void
setChatSettings: (value: ChatSettings) => void
@@ -180,7 +178,6 @@ export const ExtensionStateContextProvider: React.FC<{
shellIntegrationTimeout: 4000, // default timeout for shell integration
terminalReuseEnabled: true, // default to enabled for backward compatibility
isNewUser: false,
mcpResponsesCollapsed: false, // Default value (expanded), will be overwritten by extension state
})
const [didHydrateState, setDidHydrateState] = useState(false)
const [showWelcome, setShowWelcome] = useState(false)
@@ -200,10 +197,24 @@ export const ExtensionStateContextProvider: React.FC<{
const handleMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
switch (message.type) {
case "theme": {
if (message.text) {
setTheme(convertTextMateToHljs(JSON.parse(message.text)))
}
break
}
case "workspaceUpdated": {
setFilePaths(message.filePaths ?? [])
break
}
case "openRouterModels": {
const updatedModels = message.openRouterModels ?? {}
setOpenRouterModels({
[openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
...updatedModels,
})
break
}
case "openAiModels": {
const updatedModels = message.openAiModels ?? []
setOpenAiModels(updatedModels)
@@ -221,6 +232,12 @@ export const ExtensionStateContextProvider: React.FC<{
setMcpServers(message.mcpServers ?? [])
break
}
case "mcpMarketplaceCatalog": {
if (message.mcpMarketplaceCatalog) {
setMcpMarketplaceCatalog(message.mcpMarketplaceCatalog)
}
break
}
}
}, [])
@@ -234,9 +251,6 @@ export const ExtensionStateContextProvider: React.FC<{
const accountButtonClickedSubscriptionRef = useRef<(() => void) | null>(null)
const settingsButtonClickedSubscriptionRef = useRef<(() => void) | null>(null)
const partialMessageUnsubscribeRef = useRef<(() => void) | null>(null)
const mcpMarketplaceUnsubscribeRef = useRef<(() => void) | null>(null)
const themeSubscriptionRef = useRef<(() => void) | null>(null)
const openRouterModelsUnsubscribeRef = useRef<(() => void) | null>(null)
// Subscribe to state updates and UI events using the gRPC streaming API
useEffect(() => {
@@ -422,67 +436,8 @@ export const ExtensionStateContextProvider: React.FC<{
},
})
// Subscribe to MCP marketplace catalog updates
mcpMarketplaceUnsubscribeRef.current = McpServiceClient.subscribeToMcpMarketplaceCatalog(EmptyRequest.create({}), {
onResponse: (catalog) => {
console.log("[DEBUG] Received MCP marketplace catalog update from gRPC stream")
setMcpMarketplaceCatalog(catalog)
},
onError: (error) => {
console.error("Error in MCP marketplace catalog subscription:", error)
},
onComplete: () => {
console.log("MCP marketplace catalog subscription completed")
},
})
// Subscribe to theme changes
themeSubscriptionRef.current = UiServiceClient.subscribeToTheme(EmptyRequest.create({}), {
onResponse: (response) => {
if (response.value) {
try {
const themeData = JSON.parse(response.value)
setTheme(convertTextMateToHljs(themeData))
console.log("[DEBUG] Received theme update from gRPC stream")
} catch (error) {
console.error("Error parsing theme data:", error)
}
}
},
onError: (error) => {
console.error("Error in theme subscription:", error)
},
onComplete: () => {
console.log("Theme subscription completed")
},
})
// Subscribe to OpenRouter models updates
openRouterModelsUnsubscribeRef.current = ModelsServiceClient.subscribeToOpenRouterModels(EmptyRequest.create({}), {
onResponse: (response: OpenRouterCompatibleModelInfo) => {
console.log("[DEBUG] Received OpenRouter models update from gRPC stream")
const models = response.models
setOpenRouterModels({
[openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
...models,
})
},
onError: (error) => {
console.error("Error in OpenRouter models subscription:", error)
},
onComplete: () => {
console.log("OpenRouter models subscription completed")
},
})
// Initialize webview using gRPC
UiServiceClient.initializeWebview(EmptyRequest.create({}))
.then(() => {
console.log("[DEBUG] Webview initialization completed via gRPC")
})
.catch((error) => {
console.error("Failed to initialize webview via gRPC:", error)
})
// Still send the webviewDidLaunch message for other initialization
vscode.postMessage({ type: "webviewDidLaunch" })
// Set up account button clicked subscription
accountButtonClickedSubscriptionRef.current = UiServiceClient.subscribeToAccountButtonClicked(EmptyRequest.create(), {
@@ -529,28 +484,15 @@ export const ExtensionStateContextProvider: React.FC<{
partialMessageUnsubscribeRef.current()
partialMessageUnsubscribeRef.current = null
}
if (mcpMarketplaceUnsubscribeRef.current) {
mcpMarketplaceUnsubscribeRef.current()
mcpMarketplaceUnsubscribeRef.current = null
}
if (themeSubscriptionRef.current) {
themeSubscriptionRef.current()
themeSubscriptionRef.current = null
}
if (openRouterModelsUnsubscribeRef.current) {
openRouterModelsUnsubscribeRef.current()
openRouterModelsUnsubscribeRef.current = null
}
}
}, [])
const refreshOpenRouterModels = useCallback(() => {
ModelsServiceClient.refreshOpenRouterModels(EmptyRequest.create({}))
.then((response: OpenRouterCompatibleModelInfo) => {
const models = response.models
.then((res) => {
setOpenRouterModels({
[openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
...models,
...res.models,
})
})
.catch((error: Error) => console.error("Failed to refresh OpenRouter models:", error))
@@ -624,12 +566,6 @@ export const ExtensionStateContextProvider: React.FC<{
...prevState,
mcpMarketplaceEnabled: value,
})),
setMcpResponsesCollapsed: (value) => {
setState((prevState) => ({
...prevState,
mcpResponsesCollapsed: value,
}))
},
setShowAnnouncement,
setShouldShowAnnouncement: (value) =>
setState((prevState) => ({
@@ -664,7 +600,6 @@ export const ExtensionStateContextProvider: React.FC<{
planActSeparateModelsSetting: state.planActSeparateModelsSetting,
enableCheckpointsSetting: state.enableCheckpointsSetting,
mcpMarketplaceEnabled: state.mcpMarketplaceEnabled,
mcpResponsesCollapsed: state.mcpResponsesCollapsed,
})
},
setGlobalClineRulesToggles: (toggles) =>
+8 -14
View File
@@ -75,19 +75,6 @@ export interface ContextMenuQueryItem {
description?: string
}
const DEFAULT_CONTEXT_MENU_OPTIONS = [
ContextMenuOptionType.URL,
ContextMenuOptionType.Problems,
ContextMenuOptionType.Terminal,
ContextMenuOptionType.Git,
ContextMenuOptionType.Folder,
ContextMenuOptionType.File,
]
export function getContextMenuOptionIndex(option: ContextMenuOptionType) {
return DEFAULT_CONTEXT_MENU_OPTIONS.findIndex((item) => item === option)
}
export function getContextMenuOptions(
query: string,
selectedType: ContextMenuOptionType | null = null,
@@ -127,7 +114,14 @@ export function getContextMenuOptions(
return commits.length > 0 ? [workingChanges, ...commits] : [workingChanges]
}
return DEFAULT_CONTEXT_MENU_OPTIONS.map((type) => ({ type }))
return [
{ type: ContextMenuOptionType.URL },
{ type: ContextMenuOptionType.Problems },
{ type: ContextMenuOptionType.Terminal },
{ type: ContextMenuOptionType.Git },
{ type: ContextMenuOptionType.Folder },
{ type: ContextMenuOptionType.File },
]
}
const lowerQuery = query.toLowerCase()