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
25 changed files with 262 additions and 424 deletions
-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
-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
-3
View File
@@ -246,7 +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);
}
+53 -11
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
@@ -210,10 +223,19 @@ export class Controller {
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) {
sendOpenRouterModelsEvent(OpenRouterCompatibleModelInfo.create({ models: 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.
@@ -223,7 +245,10 @@ export class Controller {
getGlobalState(this.context, "mcpMarketplaceCatalog").then((mcpMarketplaceCatalog) => {
if (mcpMarketplaceCatalog) {
sendMcpMarketplaceCatalogEvent(mcpMarketplaceCatalog as McpMarketplaceCatalog)
this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
mcpMarketplaceCatalog: mcpMarketplaceCatalog as McpMarketplaceCatalog,
})
}
})
this.silentlyRefreshMcpMarketplace()
@@ -734,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
@@ -779,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)
@@ -807,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)
}
}
@@ -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)
}
@@ -19,7 +19,7 @@ export async function refreshOpenRouterModels(
): 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":
@@ -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,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)
}
+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
}),
+4
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[]
-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 }
@@ -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)
@@ -18,11 +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 { convertOpenRouterCompatibleModelInfoToModelInfoRecord } from "../../../src/shared/proto-conversions/models/openrouter-models-conversion"
import { vscode } from "../utils/vscode"
import { OpenRouterCompatibleModelInfo } from "@shared/proto/models"
interface ExtensionStateContextType extends ExtensionState {
didHydrateState: boolean
@@ -199,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)
@@ -220,6 +232,12 @@ export const ExtensionStateContextProvider: React.FC<{
setMcpServers(message.mcpServers ?? [])
break
}
case "mcpMarketplaceCatalog": {
if (message.mcpMarketplaceCatalog) {
setMcpMarketplaceCatalog(message.mcpMarketplaceCatalog)
}
break
}
}
}, [])
@@ -233,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(() => {
@@ -421,58 +436,6 @@ 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) => {
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")
},
})
// Still send the webviewDidLaunch message for other initialization
vscode.postMessage({ type: "webviewDidLaunch" })
@@ -521,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))