mirror of
https://github.com/cline/cline.git
synced 2026-09-17 17:45:33 +08:00
Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9960a3c57c | ||
|
|
88947592f0 | ||
|
|
ef4d11df19 | ||
|
|
23dec509bc | ||
|
|
07ab6b19b8 | ||
|
|
0ddef94d1f | ||
|
|
b9ae83b1cd | ||
|
|
e4eaf34827 | ||
|
|
6d3ed43c74 | ||
|
|
cbb67b48f2 | ||
|
|
a5f6a97be8 | ||
|
|
f309b062e7 | ||
|
|
768df130ab | ||
|
|
9980cb0938 | ||
|
|
aca4f842fa | ||
|
|
3fc91e2afe | ||
|
|
5f4700ce95 | ||
|
|
dbaf5e3ee3 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Do not ignore `pkg` folder
|
||||
@@ -96,9 +96,8 @@ jobs:
|
||||
- name: Build Tests and Extension
|
||||
run: npm run pretest
|
||||
|
||||
# Unit Tests disabled due to module system conflicts between backend and webview-ui
|
||||
# - name: Unit Tests
|
||||
# run: npm run test:unit
|
||||
- name: Unit Tests
|
||||
run: npm run test:unit
|
||||
|
||||
# Run extension tests with coverage
|
||||
- name: Extension Tests with Coverage
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# Changelog
|
||||
|
||||
## [3.20.7]
|
||||
|
||||
- Fix circular dependency that affect the github workflow Tests / test (pull_request)
|
||||
|
||||
## [3.20.6]
|
||||
|
||||
- Fix login check on extension restart
|
||||
|
||||
## [3.20.5]
|
||||
|
||||
- Fix authentication persistence issues that could cause users to be logged out unexpectedly
|
||||
|
||||
## [3.20.4]
|
||||
|
||||
- Add new Cerebras models
|
||||
- Update rate limits for existing Cerebras models
|
||||
- Fix for delete task dialog
|
||||
|
||||
## [3.20.3]
|
||||
|
||||
- Add Huawei Cloud MaaS Provider (Thanks @ddling!)
|
||||
|
||||
@@ -85,7 +85,7 @@ directApiRuleTester.run("no-direct-vscode-api", noDirectVscodeApiRule, {
|
||||
filename: "workspace.ts",
|
||||
errors: [
|
||||
{
|
||||
messageId: "useHostBridge",
|
||||
messageId: "useHostBridgeWorkspace",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -194,10 +194,6 @@ module.exports = createRule({
|
||||
if (filename.includes("/standalone/runtime-files/")) {
|
||||
return true
|
||||
}
|
||||
// Skip unit tests
|
||||
if (filename.endsWith(".test.ts")) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.20.3",
|
||||
"version": "3.20.5",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.20.3",
|
||||
"version": "3.20.5",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
|
||||
+3
-2
@@ -2,7 +2,7 @@
|
||||
"name": "claude-dev",
|
||||
"displayName": "Cline",
|
||||
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
|
||||
"version": "3.20.3",
|
||||
"version": "3.20.7",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -119,7 +119,8 @@
|
||||
{
|
||||
"type": "webview",
|
||||
"id": "claude-dev.SidebarProvider",
|
||||
"name": ""
|
||||
"name": "",
|
||||
"icon": "assets/icons/icon.svg"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Regular → Executable
+1
@@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
const { execSync } = require("child_process")
|
||||
const esbuild = require("esbuild")
|
||||
|
||||
|
||||
Regular → Executable
@@ -612,101 +612,102 @@ describe("AwsBedrockHandler", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("getModelId", () => {
|
||||
it("should return raw model ID for custom models", async () => {
|
||||
const customOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
actModeAwsBedrockCustomSelected: true,
|
||||
actModeApiModelId:
|
||||
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
}
|
||||
const customHandler = new AwsBedrockHandler(customOptions)
|
||||
// TODO: Re-enable or remove these tests.
|
||||
// describe("getModelId", () => {
|
||||
// it("should return raw model ID for custom models", async () => {
|
||||
// const customOptions: ApiHandlerOptions = {
|
||||
// ...mockOptions,
|
||||
// actModeAwsBedrockCustomSelected: true,
|
||||
// actModeApiModelId:
|
||||
// "arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
// }
|
||||
// const customHandler = new AwsBedrockHandler(customOptions)
|
||||
|
||||
const modelId = await customHandler.getModelId()
|
||||
modelId.should.equal(
|
||||
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
)
|
||||
})
|
||||
// const modelId = await customHandler.getModelId()
|
||||
// modelId.should.equal(
|
||||
// "arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
// )
|
||||
// })
|
||||
|
||||
it("should not encode custom model IDs with slashes", async () => {
|
||||
const customOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
actModeAwsBedrockCustomSelected: true,
|
||||
actModeApiModelId: "my-namespace/my-custom-model",
|
||||
}
|
||||
const customHandler = new AwsBedrockHandler(customOptions)
|
||||
// it("should not encode custom model IDs with slashes", async () => {
|
||||
// const customOptions: ApiHandlerOptions = {
|
||||
// ...mockOptions,
|
||||
// actModeAwsBedrockCustomSelected: true,
|
||||
// actModeApiModelId: "my-namespace/my-custom-model",
|
||||
// }
|
||||
// const customHandler = new AwsBedrockHandler(customOptions)
|
||||
|
||||
const modelId = await customHandler.getModelId()
|
||||
modelId.should.equal("my-namespace/my-custom-model")
|
||||
modelId.should.not.match(/%2F/)
|
||||
})
|
||||
// const modelId = await customHandler.getModelId()
|
||||
// modelId.should.equal("my-namespace/my-custom-model")
|
||||
// modelId.should.not.match(/%2F/)
|
||||
// })
|
||||
|
||||
it("should apply cross-region prefix for non-custom models when enabled", async () => {
|
||||
const crossRegionOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
awsUseCrossRegionInference: true,
|
||||
awsRegion: "us-west-2",
|
||||
}
|
||||
const crossRegionHandler = new AwsBedrockHandler(crossRegionOptions)
|
||||
// it("should apply cross-region prefix for non-custom models when enabled", async () => {
|
||||
// const crossRegionOptions: ApiHandlerOptions = {
|
||||
// ...mockOptions,
|
||||
// awsUseCrossRegionInference: true,
|
||||
// awsRegion: "us-west-2",
|
||||
// }
|
||||
// const crossRegionHandler = new AwsBedrockHandler(crossRegionOptions)
|
||||
|
||||
const modelId = await crossRegionHandler.getModelId()
|
||||
modelId.should.equal("us.anthropic.claude-3-7-sonnet-20250219-v1:0")
|
||||
})
|
||||
// const modelId = await crossRegionHandler.getModelId()
|
||||
// modelId.should.equal("us.anthropic.claude-3-7-sonnet-20250219-v1:0")
|
||||
// })
|
||||
|
||||
it("should apply EU cross-region prefix", async () => {
|
||||
const euOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
awsUseCrossRegionInference: true,
|
||||
awsRegion: "eu-central-1",
|
||||
}
|
||||
const euHandler = new AwsBedrockHandler(euOptions)
|
||||
// it("should apply EU cross-region prefix", async () => {
|
||||
// const euOptions: ApiHandlerOptions = {
|
||||
// ...mockOptions,
|
||||
// awsUseCrossRegionInference: true,
|
||||
// awsRegion: "eu-central-1",
|
||||
// }
|
||||
// const euHandler = new AwsBedrockHandler(euOptions)
|
||||
|
||||
const modelId = await euHandler.getModelId()
|
||||
modelId.should.equal("eu.anthropic.claude-3-7-sonnet-20250219-v1:0")
|
||||
})
|
||||
// const modelId = await euHandler.getModelId()
|
||||
// modelId.should.equal("eu.anthropic.claude-3-7-sonnet-20250219-v1:0")
|
||||
// })
|
||||
|
||||
it("should apply APAC cross-region prefix", async () => {
|
||||
const apacOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
awsUseCrossRegionInference: true,
|
||||
awsRegion: "ap-northeast-1",
|
||||
}
|
||||
const apacHandler = new AwsBedrockHandler(apacOptions)
|
||||
// it("should apply APAC cross-region prefix", async () => {
|
||||
// const apacOptions: ApiHandlerOptions = {
|
||||
// ...mockOptions,
|
||||
// awsUseCrossRegionInference: true,
|
||||
// awsRegion: "ap-northeast-1",
|
||||
// }
|
||||
// const apacHandler = new AwsBedrockHandler(apacOptions)
|
||||
|
||||
const modelId = await apacHandler.getModelId()
|
||||
modelId.should.equal("apac.anthropic.claude-3-7-sonnet-20250219-v1:0")
|
||||
})
|
||||
// const modelId = await apacHandler.getModelId()
|
||||
// modelId.should.equal("apac.anthropic.claude-3-7-sonnet-20250219-v1:0")
|
||||
// })
|
||||
|
||||
it("should not apply cross-region prefix for custom models even when enabled", async () => {
|
||||
const customCrossRegionOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
actModeAwsBedrockCustomSelected: true,
|
||||
actModeApiModelId: "arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model",
|
||||
awsUseCrossRegionInference: true,
|
||||
}
|
||||
const customCrossRegionHandler = new AwsBedrockHandler(customCrossRegionOptions)
|
||||
// it("should not apply cross-region prefix for custom models even when enabled", async () => {
|
||||
// const customCrossRegionOptions: ApiHandlerOptions = {
|
||||
// ...mockOptions,
|
||||
// actModeAwsBedrockCustomSelected: true,
|
||||
// actModeApiModelId: "arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model",
|
||||
// awsUseCrossRegionInference: true,
|
||||
// }
|
||||
// const customCrossRegionHandler = new AwsBedrockHandler(customCrossRegionOptions)
|
||||
|
||||
const modelId = await customCrossRegionHandler.getModelId()
|
||||
modelId.should.equal("arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model")
|
||||
})
|
||||
// const modelId = await customCrossRegionHandler.getModelId()
|
||||
// modelId.should.equal("arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model")
|
||||
// })
|
||||
|
||||
it("should handle UltraThink model ARN correctly", async () => {
|
||||
const ultraThinkOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
actModeAwsBedrockCustomSelected: true,
|
||||
actModeApiModelId:
|
||||
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
actModeAwsBedrockCustomModelBaseId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||
}
|
||||
const ultraThinkHandler = new AwsBedrockHandler(ultraThinkOptions)
|
||||
// it("should handle UltraThink model ARN correctly", async () => {
|
||||
// const ultraThinkOptions: ApiHandlerOptions = {
|
||||
// ...mockOptions,
|
||||
// actModeAwsBedrockCustomSelected: true,
|
||||
// actModeApiModelId:
|
||||
// "arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
// actModeAwsBedrockCustomModelBaseId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||
// }
|
||||
// const ultraThinkHandler = new AwsBedrockHandler(ultraThinkOptions)
|
||||
|
||||
const modelId = await ultraThinkHandler.getModelId()
|
||||
// Should return the raw ARN without any encoding
|
||||
modelId.should.equal(
|
||||
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
)
|
||||
modelId.should.not.match(/%2F/)
|
||||
modelId.should.not.match(/%3A/)
|
||||
})
|
||||
})
|
||||
// const modelId = await ultraThinkHandler.getModelId()
|
||||
// // Should return the raw ARN without any encoding
|
||||
// modelId.should.equal(
|
||||
// "arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
// )
|
||||
// modelId.should.not.match(/%2F/)
|
||||
// modelId.should.not.match(/%3A/)
|
||||
// })
|
||||
// })
|
||||
})
|
||||
|
||||
@@ -102,6 +102,7 @@ export class CerebrasHandler implements ApiHandler {
|
||||
messages: cerebrasMessages,
|
||||
temperature: 0,
|
||||
stream: true,
|
||||
max_tokens: this.getModel().info.maxTokens,
|
||||
})
|
||||
|
||||
// Handle streaming response
|
||||
@@ -175,9 +176,15 @@ export class CerebrasHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
if (modelId && modelId in cerebrasModels) {
|
||||
const id = modelId as CerebrasModelId
|
||||
const originalModelId = this.options.apiModelId
|
||||
let apiModelId = originalModelId
|
||||
if (originalModelId === "qwen-3-coder-480b-free") {
|
||||
apiModelId = "qwen-3-coder-480b"
|
||||
return { id: apiModelId, info: cerebrasModels[originalModelId as CerebrasModelId] }
|
||||
}
|
||||
|
||||
if (originalModelId && originalModelId in cerebrasModels) {
|
||||
const id = originalModelId as CerebrasModelId
|
||||
return { id, info: cerebrasModels[id] }
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// Mock for @google/genai module to avoid ESM compatibility issues in tests
|
||||
|
||||
export class GoogleGenAI {
|
||||
constructor(options: any) {
|
||||
// Mock constructor
|
||||
}
|
||||
|
||||
models = {
|
||||
generateContentStream: async (params: any) => {
|
||||
// Mock implementation that returns an async iterator
|
||||
return {
|
||||
async *[Symbol.asyncIterator]() {
|
||||
yield {
|
||||
text: "Mock response",
|
||||
candidates: [],
|
||||
usageMetadata: {
|
||||
promptTokenCount: 100,
|
||||
candidatesTokenCount: 50,
|
||||
thoughtsTokenCount: 0,
|
||||
cachedContentTokenCount: 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
countTokens: async (params: any) => {
|
||||
// Mock token counting
|
||||
return {
|
||||
totalTokens: 100,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Export mock types
|
||||
export interface GenerateContentConfig {
|
||||
httpOptions?: any
|
||||
systemInstruction?: string
|
||||
temperature?: number
|
||||
thinkingConfig?: any
|
||||
}
|
||||
|
||||
export interface GenerateContentResponseUsageMetadata {
|
||||
promptTokenCount?: number
|
||||
candidatesTokenCount?: number
|
||||
thoughtsTokenCount?: number
|
||||
cachedContentTokenCount?: number
|
||||
}
|
||||
|
||||
export interface Part {
|
||||
thought?: boolean
|
||||
text?: string
|
||||
}
|
||||
@@ -30,12 +30,13 @@ import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { ensureMcpServersDirectoryExists, ensureSettingsDirectoryExists, GlobalFileNames } from "../storage/disk"
|
||||
import { getAllExtensionState, getGlobalState, getWorkspaceState, storeSecret, updateGlobalState } from "../storage/state"
|
||||
import { CacheService, PersistenceErrorEvent } from "../storage/CacheService"
|
||||
import { Task } from "../task"
|
||||
import { handleGrpcRequest, handleGrpcRequestCancel } from "./grpc-handler"
|
||||
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
|
||||
import { sendStateUpdate } from "./state/subscribeToState"
|
||||
import { sendAddToInputEvent } from "./ui/subscribeToAddToInput"
|
||||
import { getLatestAnnouncementId } from "@/extension"
|
||||
import { getLatestAnnouncementId } from "@/utils/announcements"
|
||||
|
||||
/*
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
@@ -53,7 +54,7 @@ export class Controller {
|
||||
workspaceTracker: WorkspaceTracker
|
||||
mcpHub: McpHub
|
||||
accountService: ClineAccountService
|
||||
authService: AuthService
|
||||
readonly cacheService: CacheService
|
||||
|
||||
constructor(
|
||||
readonly context: vscode.ExtensionContext,
|
||||
@@ -64,6 +65,38 @@ export class Controller {
|
||||
|
||||
HostProvider.get().logToChannel("ClineProvider instantiated")
|
||||
this.postMessage = postMessage
|
||||
this.accountService = ClineAccountService.getInstance()
|
||||
this.cacheService = new CacheService(context)
|
||||
const authService = AuthService.getInstance(this)
|
||||
|
||||
// Initialize cache service asynchronously - critical for extension functionality
|
||||
this.cacheService
|
||||
.initialize()
|
||||
.then(() => {
|
||||
authService.restoreRefreshTokenAndRetrieveAuthInfo()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("CRITICAL: Failed to initialize CacheService - extension may not function properly:", error)
|
||||
})
|
||||
|
||||
// Set up persistence error recovery
|
||||
this.cacheService.onPersistenceError = async ({ error }: PersistenceErrorEvent) => {
|
||||
console.error("Cache persistence failed, recovering:", error)
|
||||
try {
|
||||
await this.cacheService.reInitialize()
|
||||
await this.postStateToWebview()
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.WARNING,
|
||||
message: "Saving settings to storage failed.",
|
||||
})
|
||||
} catch (recoveryError) {
|
||||
console.error("Cache recovery failed:", recoveryError)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to save settings. Please restart the extension.",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.workspaceTracker = new WorkspaceTracker()
|
||||
this.mcpHub = new McpHub(
|
||||
@@ -72,9 +105,6 @@ export class Controller {
|
||||
(msg) => this.postMessageToWebview(msg),
|
||||
this.context.extension?.packageJSON?.version ?? "1.0.0",
|
||||
)
|
||||
this.accountService = ClineAccountService.getInstance()
|
||||
this.authService = AuthService.getInstance(context)
|
||||
this.authService.restoreRefreshTokenAndRetrieveAuthInfo()
|
||||
|
||||
// Clean up legacy checkpoints
|
||||
cleanupLegacyCheckpoints(this.context.globalStorageUri.fsPath).catch((error) => {
|
||||
@@ -109,12 +139,18 @@ export class Controller {
|
||||
async handleSignOut() {
|
||||
try {
|
||||
// TODO: update to clineAccountId and then move clineApiKey to a clear function.
|
||||
await storeSecret(this.context, "clineAccountId", undefined)
|
||||
this.cacheService.setSecret("clineAccountId", undefined)
|
||||
await updateGlobalState(this.context, "userInfo", undefined)
|
||||
await Promise.all([
|
||||
updateGlobalState(this.context, "planModeApiProvider", "openrouter"),
|
||||
updateGlobalState(this.context, "actModeApiProvider", "openrouter"),
|
||||
])
|
||||
|
||||
// Update API providers through cache service
|
||||
const apiConfiguration = this.cacheService.getApiConfiguration()
|
||||
const updatedConfig = {
|
||||
...apiConfiguration,
|
||||
planModeApiProvider: "openrouter" as ApiProvider,
|
||||
actModeApiProvider: "openrouter" as ApiProvider,
|
||||
}
|
||||
this.cacheService.setApiConfiguration(updatedConfig)
|
||||
|
||||
await this.postStateToWebview()
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
@@ -134,8 +170,11 @@ export class Controller {
|
||||
|
||||
async initTask(task?: string, images?: string[], files?: string[], historyItem?: HistoryItem) {
|
||||
await this.clearTask() // ensures that an existing task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
|
||||
|
||||
// Get API configuration from cache for immediate access
|
||||
const apiConfiguration = this.cacheService.getApiConfiguration()
|
||||
|
||||
const {
|
||||
apiConfiguration,
|
||||
autoApprovalSettings,
|
||||
browserSettings,
|
||||
preferredLanguage,
|
||||
@@ -185,6 +224,7 @@ export class Controller {
|
||||
defaultTerminalProfile ?? "default",
|
||||
enableCheckpointsSetting ?? true,
|
||||
await getCwd(getDesktopDir()),
|
||||
this.cacheService,
|
||||
task,
|
||||
images,
|
||||
files,
|
||||
@@ -252,7 +292,7 @@ export class Controller {
|
||||
|
||||
// Update API handler with new mode (buildApiHandler now selects provider based on mode)
|
||||
if (this.task) {
|
||||
const { apiConfiguration } = await getAllExtensionState(this.context)
|
||||
const apiConfiguration = this.cacheService.getApiConfiguration()
|
||||
this.task.api = buildApiHandler({ ...apiConfiguration, taskId: this.task.taskId }, modeToSwitchTo)
|
||||
}
|
||||
|
||||
@@ -311,7 +351,7 @@ export class Controller {
|
||||
|
||||
async handleAuthCallback(customToken: string, provider: string | null = null) {
|
||||
try {
|
||||
await this.authService.handleAuthCallback(customToken, provider ? provider : "google")
|
||||
await AuthService.getInstance(this).handleAuthCallback(customToken, provider ? provider : "google")
|
||||
|
||||
const clineProvider: ApiProvider = "cline"
|
||||
|
||||
@@ -319,27 +359,26 @@ export class Controller {
|
||||
const { planActSeparateModelsSetting } = await getAllExtensionState(this.context)
|
||||
const currentMode = await this.getCurrentMode()
|
||||
|
||||
// Get current API configuration from cache
|
||||
const currentApiConfiguration = this.cacheService.getApiConfiguration()
|
||||
|
||||
let updatedConfig = { ...currentApiConfiguration }
|
||||
|
||||
if (planActSeparateModelsSetting) {
|
||||
// Only update the current mode's provider
|
||||
if (currentMode === "plan") {
|
||||
await updateGlobalState(this.context, "planModeApiProvider", clineProvider)
|
||||
updatedConfig.planModeApiProvider = clineProvider
|
||||
} else {
|
||||
await updateGlobalState(this.context, "actModeApiProvider", clineProvider)
|
||||
updatedConfig.actModeApiProvider = clineProvider
|
||||
}
|
||||
} else {
|
||||
// Update both modes to keep them in sync
|
||||
await Promise.all([
|
||||
updateGlobalState(this.context, "planModeApiProvider", clineProvider),
|
||||
updateGlobalState(this.context, "actModeApiProvider", clineProvider),
|
||||
])
|
||||
updatedConfig.planModeApiProvider = clineProvider
|
||||
updatedConfig.actModeApiProvider = clineProvider
|
||||
}
|
||||
|
||||
// Get the updated API configuration (now includes the updated providers)
|
||||
const { apiConfiguration } = await getAllExtensionState(this.context)
|
||||
const updatedConfig = {
|
||||
...apiConfiguration,
|
||||
apiProvider: clineProvider,
|
||||
}
|
||||
// Update the API configuration through cache service
|
||||
this.cacheService.setApiConfiguration(updatedConfig)
|
||||
|
||||
// Mark welcome view as completed since user has successfully logged in
|
||||
await updateGlobalState(this.context, "welcomeViewCompleted", true)
|
||||
@@ -501,21 +540,20 @@ export class Controller {
|
||||
|
||||
const openrouter: ApiProvider = "openrouter"
|
||||
const currentMode = await this.getCurrentMode()
|
||||
await Promise.all([
|
||||
updateGlobalState(this.context, "planModeApiProvider", openrouter),
|
||||
updateGlobalState(this.context, "actModeApiProvider", openrouter),
|
||||
])
|
||||
await storeSecret(this.context, "openRouterApiKey", apiKey)
|
||||
|
||||
// Update API configuration through cache service
|
||||
const currentApiConfiguration = this.cacheService.getApiConfiguration()
|
||||
const updatedConfig = {
|
||||
...currentApiConfiguration,
|
||||
planModeApiProvider: openrouter,
|
||||
actModeApiProvider: openrouter,
|
||||
openRouterApiKey: apiKey,
|
||||
}
|
||||
this.cacheService.setApiConfiguration(updatedConfig)
|
||||
|
||||
await this.postStateToWebview()
|
||||
if (this.task) {
|
||||
// Get the updated API configuration (now includes the updated providers)
|
||||
const { apiConfiguration } = await getAllExtensionState(this.context)
|
||||
const updatedConfig = {
|
||||
...apiConfiguration,
|
||||
openRouterApiKey: apiKey,
|
||||
taskId: this.task.taskId,
|
||||
}
|
||||
this.task.api = buildApiHandler(updatedConfig, currentMode)
|
||||
this.task.api = buildApiHandler({ ...updatedConfig, taskId: this.task.taskId }, currentMode)
|
||||
}
|
||||
// await this.postMessageToWebview({ type: "action", action: "settingsButtonClicked" }) // bad ux if user is on welcome
|
||||
}
|
||||
@@ -689,8 +727,10 @@ export class Controller {
|
||||
}
|
||||
|
||||
async getStateToPostToWebview(): Promise<ExtensionState> {
|
||||
// Get API configuration from cache for immediate access
|
||||
const apiConfiguration = this.cacheService.getApiConfiguration()
|
||||
|
||||
const {
|
||||
apiConfiguration,
|
||||
lastShownAnnouncementId,
|
||||
taskHistory,
|
||||
autoApprovalSettings,
|
||||
@@ -831,18 +871,4 @@ export class Controller {
|
||||
await updateGlobalState(this.context, "taskHistory", history)
|
||||
return history
|
||||
}
|
||||
|
||||
// private async clearState() {
|
||||
// this.context.workspaceState.keys().forEach((key) => {
|
||||
// this.context.workspaceState.update(key, undefined)
|
||||
// })
|
||||
// this.context.globalState.keys().forEach((key) => {
|
||||
// this.context.globalState.update(key, undefined)
|
||||
// })
|
||||
// this.context.secrets.delete("apiKey")
|
||||
// }
|
||||
|
||||
// secrets
|
||||
|
||||
// dev
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Controller } from "../index"
|
||||
import { Empty } from "@shared/proto/cline/common"
|
||||
import { UpdateApiConfigurationRequest } from "@shared/proto/cline/models"
|
||||
import { updateApiConfiguration } from "../../storage/state"
|
||||
import { buildApiHandler } from "@api/index"
|
||||
import { convertProtoToApiConfiguration } from "@shared/proto-conversions/models/api-configuration-conversion"
|
||||
|
||||
@@ -25,7 +24,7 @@ export async function updateApiConfigurationProto(
|
||||
const appApiConfiguration = convertProtoToApiConfiguration(request.apiConfiguration)
|
||||
|
||||
// Update the API configuration in storage
|
||||
await updateApiConfiguration(controller.context, appApiConfiguration)
|
||||
controller.cacheService.setApiConfiguration(appApiConfiguration)
|
||||
|
||||
// Update the task's API handler if there's an active task
|
||||
if (controller.task) {
|
||||
|
||||
@@ -19,13 +19,13 @@ export async function resetState(controller: Controller, request: ResetStateRequ
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Resetting global state...",
|
||||
})
|
||||
await resetGlobalState(controller.context)
|
||||
await resetGlobalState(controller)
|
||||
} else {
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Resetting workspace state...",
|
||||
})
|
||||
await resetWorkspaceState(controller.context)
|
||||
await resetWorkspaceState(controller)
|
||||
}
|
||||
|
||||
if (controller.task) {
|
||||
|
||||
@@ -16,11 +16,7 @@ export async function toggleFavoriteModel(controller: Controller, request: Strin
|
||||
}
|
||||
|
||||
const modelId = request.value
|
||||
const { apiConfiguration } = await controller.getStateToPostToWebview()
|
||||
|
||||
if (!apiConfiguration) {
|
||||
throw new Error("API configuration not found")
|
||||
}
|
||||
const apiConfiguration = controller.cacheService.getApiConfiguration()
|
||||
|
||||
const favoritedModelIds = apiConfiguration.favoritedModelIds || []
|
||||
|
||||
@@ -29,7 +25,12 @@ export async function toggleFavoriteModel(controller: Controller, request: Strin
|
||||
? favoritedModelIds.filter((id) => id !== modelId)
|
||||
: [...favoritedModelIds, modelId]
|
||||
|
||||
await updateGlobalState(controller.context, "favoritedModelIds", updatedFavorites)
|
||||
// Update the complete API configuration through cache service
|
||||
const updatedApiConfiguration = {
|
||||
...apiConfiguration,
|
||||
favoritedModelIds: updatedFavorites,
|
||||
}
|
||||
controller.cacheService.setApiConfiguration(updatedApiConfiguration)
|
||||
|
||||
// Capture telemetry for model favorite toggle
|
||||
const isFavorited = !favoritedModelIds.includes(modelId)
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Controller } from ".."
|
||||
import { Empty } from "@shared/proto/cline/common"
|
||||
import { PlanActMode, UpdateSettingsRequest } from "@shared/proto/cline/state"
|
||||
import { updateApiConfiguration } from "../../storage/state"
|
||||
import { buildApiHandler } from "../../../api"
|
||||
import { convertProtoApiConfigurationToApiConfiguration } from "@shared/proto-conversions/state/settings-conversion"
|
||||
import { TelemetrySetting } from "@shared/TelemetrySetting"
|
||||
import { OpenaiReasoningEffort } from "@shared/storage/types"
|
||||
import { convertProtoApiConfigurationToApiConfiguration } from "../../../shared/proto-conversions/state/settings-conversion"
|
||||
import { TelemetrySetting } from "@/shared/TelemetrySetting"
|
||||
import { OpenaiReasoningEffort } from "@/shared/storage/types"
|
||||
|
||||
/**
|
||||
* Updates multiple extension settings in a single request
|
||||
@@ -18,7 +17,7 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
// Update API configuration
|
||||
if (request.apiConfiguration) {
|
||||
const apiConfiguration = convertProtoApiConfigurationToApiConfiguration(request.apiConfiguration)
|
||||
await updateApiConfiguration(controller.context, apiConfiguration)
|
||||
controller.cacheService.setApiConfiguration(apiConfiguration)
|
||||
|
||||
if (controller.task) {
|
||||
const currentMode = await controller.getCurrentMode()
|
||||
|
||||
@@ -30,7 +30,7 @@ export async function deleteTasksWithIds(controller: Controller, request: String
|
||||
options: { modal: true, items: ["Delete"] },
|
||||
})
|
||||
|
||||
if (userChoice === undefined) {
|
||||
if (userChoice.selectedOption !== "Delete") {
|
||||
return Empty.create()
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,8 @@ export async function initializeWebview(controller: Controller, request: EmptyRe
|
||||
refreshOpenRouterModels(controller, 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, planActSeparateModelsSetting } = await getAllExtensionState(controller.context)
|
||||
const apiConfiguration = controller.cacheService.getApiConfiguration()
|
||||
const { planActSeparateModelsSetting } = await getAllExtensionState(controller.context)
|
||||
const currentMode = await controller.getCurrentMode()
|
||||
|
||||
if (planActSeparateModelsSetting) {
|
||||
@@ -42,26 +43,32 @@ export async function initializeWebview(controller: Controller, request: EmptyRe
|
||||
const modelId = apiConfiguration[modelIdField]
|
||||
|
||||
if (modelId && response.models[modelId]) {
|
||||
await updateGlobalState(controller.context, modelInfoField, response.models[modelId])
|
||||
const updatedConfig = {
|
||||
...apiConfiguration,
|
||||
[modelInfoField]: response.models[modelId],
|
||||
}
|
||||
controller.cacheService.setApiConfiguration(updatedConfig)
|
||||
await controller.postStateToWebview()
|
||||
}
|
||||
} else {
|
||||
// Shared models: update both plan and act modes
|
||||
const planModelId = apiConfiguration.planModeOpenRouterModelId
|
||||
const actModelId = apiConfiguration.actModeOpenRouterModelId
|
||||
let updatedConfig = { ...apiConfiguration }
|
||||
|
||||
// Update plan mode model info if we have a model ID
|
||||
if (planModelId && response.models[planModelId]) {
|
||||
await updateGlobalState(controller.context, "planModeOpenRouterModelInfo", response.models[planModelId])
|
||||
updatedConfig.planModeOpenRouterModelInfo = response.models[planModelId]
|
||||
}
|
||||
|
||||
// Update act mode model info if we have a model ID
|
||||
if (actModelId && response.models[actModelId]) {
|
||||
await updateGlobalState(controller.context, "actModeOpenRouterModelInfo", response.models[actModelId])
|
||||
updatedConfig.actModeOpenRouterModelInfo = response.models[actModelId]
|
||||
}
|
||||
|
||||
// Post state update if we updated any model info
|
||||
if ((planModelId && response.models[planModelId]) || (actModelId && response.models[actModelId])) {
|
||||
controller.cacheService.setApiConfiguration(updatedConfig)
|
||||
await controller.postStateToWebview()
|
||||
}
|
||||
}
|
||||
@@ -71,7 +78,8 @@ export async function initializeWebview(controller: Controller, request: EmptyRe
|
||||
refreshGroqModels(controller, EmptyRequest.create()).then(async (response) => {
|
||||
if (response && response.models) {
|
||||
// Update model info in state for Groq (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, planActSeparateModelsSetting } = await getAllExtensionState(controller.context)
|
||||
const apiConfiguration = controller.cacheService.getApiConfiguration()
|
||||
const { planActSeparateModelsSetting } = await getAllExtensionState(controller.context)
|
||||
const currentMode = await controller.getCurrentMode()
|
||||
|
||||
if (planActSeparateModelsSetting) {
|
||||
@@ -81,26 +89,32 @@ export async function initializeWebview(controller: Controller, request: EmptyRe
|
||||
const modelId = apiConfiguration[modelIdField]
|
||||
|
||||
if (modelId && response.models[modelId]) {
|
||||
await updateGlobalState(controller.context, modelInfoField, response.models[modelId])
|
||||
const updatedConfig = {
|
||||
...apiConfiguration,
|
||||
[modelInfoField]: response.models[modelId],
|
||||
}
|
||||
controller.cacheService.setApiConfiguration(updatedConfig)
|
||||
await controller.postStateToWebview()
|
||||
}
|
||||
} else {
|
||||
// Shared models: update both plan and act modes
|
||||
const planModelId = apiConfiguration.planModeGroqModelId
|
||||
const actModelId = apiConfiguration.actModeGroqModelId
|
||||
let updatedConfig = { ...apiConfiguration }
|
||||
|
||||
// Update plan mode model info if we have a model ID
|
||||
if (planModelId && response.models[planModelId]) {
|
||||
await updateGlobalState(controller.context, "planModeGroqModelInfo", response.models[planModelId])
|
||||
updatedConfig.planModeGroqModelInfo = response.models[planModelId]
|
||||
}
|
||||
|
||||
// Update act mode model info if we have a model ID
|
||||
if (actModelId && response.models[actModelId]) {
|
||||
await updateGlobalState(controller.context, "actModeGroqModelInfo", response.models[actModelId])
|
||||
updatedConfig.actModeGroqModelInfo = response.models[actModelId]
|
||||
}
|
||||
|
||||
// Post state update if we updated any model info
|
||||
if ((planModelId && response.models[planModelId]) || (actModelId && response.models[actModelId])) {
|
||||
controller.cacheService.setApiConfiguration(updatedConfig)
|
||||
await controller.postStateToWebview()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { Boolean } from "@shared/proto/cline/common"
|
||||
import type { Controller } from "../index"
|
||||
import { updateGlobalState } from "../../storage/state"
|
||||
import { getLatestAnnouncementId } from "@/extension"
|
||||
import { getLatestAnnouncementId } from "@/utils/announcements"
|
||||
|
||||
/**
|
||||
* Marks the current announcement as shown
|
||||
|
||||
@@ -5,7 +5,7 @@ import { McpHub } from "@services/mcp/McpHub"
|
||||
import { BrowserSettings } from "@shared/BrowserSettings"
|
||||
import { SYSTEM_PROMPT_CLAUDE4_EXPERIMENTAL } from "@core/prompts/model_prompts/claude4-experimental"
|
||||
import { SYSTEM_PROMPT_CLAUDE4 } from "@core/prompts/model_prompts/claude4"
|
||||
import { USE_EXPERIMENTAL_CLAUDE4_FEATURES } from "@core/task/index";
|
||||
import { USE_EXPERIMENTAL_CLAUDE4_FEATURES } from "@core/task/index"
|
||||
|
||||
export const SYSTEM_PROMPT = async (
|
||||
cwd: string,
|
||||
@@ -14,14 +14,13 @@ export const SYSTEM_PROMPT = async (
|
||||
browserSettings: BrowserSettings,
|
||||
isNextGenModel: boolean = false,
|
||||
) => {
|
||||
|
||||
if (isNextGenModel && USE_EXPERIMENTAL_CLAUDE4_FEATURES) {
|
||||
return SYSTEM_PROMPT_CLAUDE4_EXPERIMENTAL(cwd, supportsBrowserUse, mcpHub, browserSettings)
|
||||
}
|
||||
|
||||
if (isNextGenModel) {
|
||||
return SYSTEM_PROMPT_CLAUDE4(cwd, supportsBrowserUse, mcpHub, browserSettings)
|
||||
}
|
||||
if (isNextGenModel) {
|
||||
return SYSTEM_PROMPT_CLAUDE4(cwd, supportsBrowserUse, mcpHub, browserSettings)
|
||||
}
|
||||
|
||||
return `You are Cline, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.
|
||||
|
||||
@@ -650,7 +649,6 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
|
||||
5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.`
|
||||
}
|
||||
|
||||
|
||||
export function addUserInstructions(
|
||||
globalClineRulesFileInstructions?: string,
|
||||
localClineRulesFileInstructions?: string,
|
||||
|
||||
@@ -0,0 +1,935 @@
|
||||
import { ApiConfiguration } from "@shared/api"
|
||||
import { updateGlobalState, updateWorkspaceState, getAllExtensionState, storeSecret } from "./state"
|
||||
import { SecretKey, GlobalStateKey, LocalStateKey } from "./state-keys"
|
||||
import { CACHE_SERVICE_NOT_INITIALIZED } from "./error-messages"
|
||||
import type { ExtensionContext } from "vscode"
|
||||
|
||||
/**
|
||||
* Interface for persistence error event data
|
||||
*/
|
||||
export interface PersistenceErrorEvent {
|
||||
error: Error
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory cache service for fast state access
|
||||
* Provides immediate reads/writes with async disk persistence
|
||||
*/
|
||||
export class CacheService {
|
||||
private globalStateCache: Map<GlobalStateKey, any> = new Map()
|
||||
private secretsCache: Map<SecretKey, string | undefined> = new Map()
|
||||
private workspaceStateCache: Map<LocalStateKey, any> = new Map()
|
||||
private context: ExtensionContext
|
||||
private isInitialized = false
|
||||
|
||||
// Debounced persistence state
|
||||
private pendingGlobalState = new Set<GlobalStateKey>()
|
||||
private pendingSecrets = new Set<SecretKey>()
|
||||
private pendingWorkspaceState = new Set<LocalStateKey>()
|
||||
private persistenceTimeout: NodeJS.Timeout | null = null
|
||||
private readonly PERSISTENCE_DELAY_MS = 500
|
||||
|
||||
// Callback for persistence errors
|
||||
onPersistenceError?: (event: PersistenceErrorEvent) => void
|
||||
|
||||
constructor(context: ExtensionContext) {
|
||||
this.context = context
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the cache by loading data from disk
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
try {
|
||||
// Load API configuration and populate cache with component keys
|
||||
const { apiConfiguration } = await getAllExtensionState(this.context)
|
||||
if (apiConfiguration) {
|
||||
// Populate the caches with the API configuration component keys
|
||||
// Use populate method to avoid triggering persistence during initialization
|
||||
this.populateApiConfigurationCache(apiConfiguration)
|
||||
}
|
||||
|
||||
this.isInitialized = true
|
||||
console.log("CacheService initialized successfully")
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize CacheService:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set method for global state keys - updates cache immediately and schedules debounced persistence
|
||||
*/
|
||||
setGlobalState<T>(key: GlobalStateKey, value: T): void {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
|
||||
// Update cache immediately for instant access
|
||||
this.globalStateCache.set(key, value)
|
||||
|
||||
// Add to pending persistence set and schedule debounced write
|
||||
this.pendingGlobalState.add(key)
|
||||
this.scheduleDebouncedPersistence()
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch set method for global state keys - updates cache immediately and schedules debounced persistence
|
||||
*/
|
||||
setGlobalStateBatch(updates: Partial<Record<GlobalStateKey, any>>): void {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
|
||||
// Update cache immediately for all keys
|
||||
Object.entries(updates).forEach(([key, value]) => {
|
||||
this.globalStateCache.set(key as GlobalStateKey, value)
|
||||
this.pendingGlobalState.add(key as GlobalStateKey)
|
||||
})
|
||||
|
||||
// Schedule debounced persistence
|
||||
this.scheduleDebouncedPersistence()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set method for secret keys - updates cache immediately and schedules debounced persistence
|
||||
*/
|
||||
setSecret(key: SecretKey, value: string | undefined): void {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
|
||||
// Update cache immediately for instant access
|
||||
this.secretsCache.set(key, value)
|
||||
|
||||
// Add to pending persistence set and schedule debounced write
|
||||
this.pendingSecrets.add(key)
|
||||
this.scheduleDebouncedPersistence()
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch set method for secret keys - updates cache immediately and schedules debounced persistence
|
||||
*/
|
||||
setSecretsBatch(updates: Partial<Record<SecretKey, string | undefined>>): void {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
|
||||
// Update cache immediately for all keys
|
||||
Object.entries(updates).forEach(([key, value]) => {
|
||||
this.secretsCache.set(key as SecretKey, value)
|
||||
this.pendingSecrets.add(key as SecretKey)
|
||||
})
|
||||
|
||||
// Schedule debounced persistence
|
||||
this.scheduleDebouncedPersistence()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set method for workspace state keys - updates cache immediately and schedules debounced persistence
|
||||
*/
|
||||
setWorkspaceState<T>(key: LocalStateKey, value: T): void {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
|
||||
// Update cache immediately for instant access
|
||||
this.workspaceStateCache.set(key, value)
|
||||
|
||||
// Add to pending persistence set and schedule debounced write
|
||||
this.pendingWorkspaceState.add(key)
|
||||
this.scheduleDebouncedPersistence()
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch set method for workspace state keys - updates cache immediately and schedules debounced persistence
|
||||
*/
|
||||
setWorkspaceStateBatch(updates: Partial<Record<LocalStateKey, any>>): void {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
|
||||
// Update cache immediately for all keys
|
||||
Object.entries(updates).forEach(([key, value]) => {
|
||||
this.workspaceStateCache.set(key as LocalStateKey, value)
|
||||
this.pendingWorkspaceState.add(key as LocalStateKey)
|
||||
})
|
||||
|
||||
// Schedule debounced persistence
|
||||
this.scheduleDebouncedPersistence()
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for getting API configuration
|
||||
* Ensures cache is initialized if not already done
|
||||
*/
|
||||
getApiConfiguration(): ApiConfiguration {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
|
||||
// Construct API configuration from cached component keys
|
||||
return this.constructApiConfigurationFromCache()
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for setting API configuration
|
||||
*/
|
||||
setApiConfiguration(apiConfiguration: ApiConfiguration): void {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
|
||||
const {
|
||||
apiKey,
|
||||
openRouterApiKey,
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
awsRegion,
|
||||
awsUseCrossRegionInference,
|
||||
awsBedrockUsePromptCache,
|
||||
awsBedrockEndpoint,
|
||||
awsBedrockApiKey,
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
openAiApiKey,
|
||||
openAiHeaders,
|
||||
ollamaBaseUrl,
|
||||
ollamaApiOptionsCtxNum,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
geminiApiKey,
|
||||
geminiBaseUrl,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
togetherApiKey,
|
||||
qwenApiKey,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
azureApiVersion,
|
||||
openRouterProviderSorting,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmApiKey,
|
||||
liteLlmUsePromptCache,
|
||||
qwenApiLine,
|
||||
moonshotApiLine,
|
||||
asksageApiKey,
|
||||
asksageApiUrl,
|
||||
xaiApiKey,
|
||||
clineAccountId,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
favoritedModelIds,
|
||||
fireworksApiKey,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
claudeCodePath,
|
||||
huggingFaceApiKey,
|
||||
requestTimeoutMs,
|
||||
// Plan mode configurations
|
||||
planModeApiProvider,
|
||||
planModeApiModelId,
|
||||
planModeThinkingBudgetTokens,
|
||||
planModeReasoningEffort,
|
||||
planModeVsCodeLmModelSelector,
|
||||
planModeAwsBedrockCustomSelected,
|
||||
planModeAwsBedrockCustomModelBaseId,
|
||||
planModeOpenRouterModelId,
|
||||
planModeOpenRouterModelInfo,
|
||||
planModeOpenAiModelId,
|
||||
planModeOpenAiModelInfo,
|
||||
planModeOllamaModelId,
|
||||
planModeLmStudioModelId,
|
||||
planModeLiteLlmModelId,
|
||||
planModeLiteLlmModelInfo,
|
||||
planModeRequestyModelId,
|
||||
planModeRequestyModelInfo,
|
||||
planModeTogetherModelId,
|
||||
planModeFireworksModelId,
|
||||
planModeSapAiCoreModelId,
|
||||
planModeGroqModelId,
|
||||
planModeGroqModelInfo,
|
||||
planModeHuggingFaceModelId,
|
||||
planModeHuggingFaceModelInfo,
|
||||
// Act mode configurations
|
||||
actModeApiProvider,
|
||||
actModeApiModelId,
|
||||
actModeThinkingBudgetTokens,
|
||||
actModeReasoningEffort,
|
||||
actModeVsCodeLmModelSelector,
|
||||
actModeAwsBedrockCustomSelected,
|
||||
actModeAwsBedrockCustomModelBaseId,
|
||||
actModeOpenRouterModelId,
|
||||
actModeOpenRouterModelInfo,
|
||||
actModeOpenAiModelId,
|
||||
actModeOpenAiModelInfo,
|
||||
actModeOllamaModelId,
|
||||
actModeLmStudioModelId,
|
||||
actModeLiteLlmModelId,
|
||||
actModeLiteLlmModelInfo,
|
||||
actModeRequestyModelId,
|
||||
actModeRequestyModelInfo,
|
||||
actModeTogetherModelId,
|
||||
actModeFireworksModelId,
|
||||
actModeSapAiCoreModelId,
|
||||
actModeGroqModelId,
|
||||
actModeGroqModelInfo,
|
||||
actModeHuggingFaceModelId,
|
||||
actModeHuggingFaceModelInfo,
|
||||
} = apiConfiguration
|
||||
|
||||
// Batch update global state keys
|
||||
this.setGlobalStateBatch({
|
||||
// Plan mode configuration updates
|
||||
planModeApiProvider,
|
||||
planModeApiModelId,
|
||||
planModeThinkingBudgetTokens,
|
||||
planModeReasoningEffort,
|
||||
planModeVsCodeLmModelSelector,
|
||||
planModeAwsBedrockCustomSelected,
|
||||
planModeAwsBedrockCustomModelBaseId,
|
||||
planModeOpenRouterModelId,
|
||||
planModeOpenRouterModelInfo,
|
||||
planModeOpenAiModelId,
|
||||
planModeOpenAiModelInfo,
|
||||
planModeOllamaModelId,
|
||||
planModeLmStudioModelId,
|
||||
planModeLiteLlmModelId,
|
||||
planModeLiteLlmModelInfo,
|
||||
planModeRequestyModelId,
|
||||
planModeRequestyModelInfo,
|
||||
planModeTogetherModelId,
|
||||
planModeFireworksModelId,
|
||||
planModeSapAiCoreModelId,
|
||||
planModeGroqModelId,
|
||||
planModeGroqModelInfo,
|
||||
planModeHuggingFaceModelId,
|
||||
planModeHuggingFaceModelInfo,
|
||||
|
||||
// Act mode configuration updates
|
||||
actModeApiProvider,
|
||||
actModeApiModelId,
|
||||
actModeThinkingBudgetTokens,
|
||||
actModeReasoningEffort,
|
||||
actModeVsCodeLmModelSelector,
|
||||
actModeAwsBedrockCustomSelected,
|
||||
actModeAwsBedrockCustomModelBaseId,
|
||||
actModeOpenRouterModelId,
|
||||
actModeOpenRouterModelInfo,
|
||||
actModeOpenAiModelId,
|
||||
actModeOpenAiModelInfo,
|
||||
actModeOllamaModelId,
|
||||
actModeLmStudioModelId,
|
||||
actModeLiteLlmModelId,
|
||||
actModeLiteLlmModelInfo,
|
||||
actModeRequestyModelId,
|
||||
actModeRequestyModelInfo,
|
||||
actModeTogetherModelId,
|
||||
actModeFireworksModelId,
|
||||
actModeSapAiCoreModelId,
|
||||
actModeGroqModelId,
|
||||
actModeGroqModelInfo,
|
||||
actModeHuggingFaceModelId,
|
||||
actModeHuggingFaceModelInfo,
|
||||
|
||||
// Global state updates
|
||||
awsRegion,
|
||||
awsUseCrossRegionInference,
|
||||
awsBedrockUsePromptCache,
|
||||
awsBedrockEndpoint,
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
openAiHeaders,
|
||||
ollamaBaseUrl,
|
||||
ollamaApiOptionsCtxNum,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
geminiBaseUrl,
|
||||
azureApiVersion,
|
||||
openRouterProviderSorting,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmUsePromptCache,
|
||||
qwenApiLine,
|
||||
moonshotApiLine,
|
||||
asksageApiUrl,
|
||||
favoritedModelIds,
|
||||
requestTimeoutMs,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
claudeCodePath,
|
||||
})
|
||||
|
||||
// Batch update secrets
|
||||
this.setSecretsBatch({
|
||||
apiKey,
|
||||
openRouterApiKey,
|
||||
clineAccountId,
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
awsBedrockApiKey,
|
||||
openAiApiKey,
|
||||
geminiApiKey,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
togetherApiKey,
|
||||
qwenApiKey,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
liteLlmApiKey,
|
||||
fireworksApiKey,
|
||||
asksageApiKey,
|
||||
xaiApiKey,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
huggingFaceApiKey,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get method for global state keys - reads from in-memory cache
|
||||
*/
|
||||
getGlobalStateKey<T>(key: GlobalStateKey): T | undefined {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
return this.globalStateCache.get(key) as T | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Get method for secret keys - reads from in-memory cache
|
||||
*/
|
||||
getSecretKey(key: SecretKey): string | undefined {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
return this.secretsCache.get(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get method for workspace state keys - reads from in-memory cache
|
||||
*/
|
||||
getWorkspaceStateKey<T>(key: LocalStateKey): T | undefined {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
return this.workspaceStateCache.get(key) as T | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Reinitialize the cache service by clearing all state and reloading from disk
|
||||
* Used for error recovery when write operations fail
|
||||
*/
|
||||
async reInitialize(): Promise<void> {
|
||||
// Clear all cached data and pending state
|
||||
this.dispose()
|
||||
|
||||
// Reinitialize from disk
|
||||
await this.initialize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of the cache service
|
||||
*/
|
||||
private dispose(): void {
|
||||
if (this.persistenceTimeout) {
|
||||
clearTimeout(this.persistenceTimeout)
|
||||
this.persistenceTimeout = null
|
||||
}
|
||||
|
||||
this.pendingGlobalState.clear()
|
||||
this.pendingSecrets.clear()
|
||||
this.pendingWorkspaceState.clear()
|
||||
|
||||
this.globalStateCache.clear()
|
||||
this.secretsCache.clear()
|
||||
this.workspaceStateCache.clear()
|
||||
|
||||
this.isInitialized = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule debounced persistence - simple timeout-based persistence
|
||||
*/
|
||||
private scheduleDebouncedPersistence(): void {
|
||||
// Clear existing timeout if one is pending
|
||||
if (this.persistenceTimeout) {
|
||||
clearTimeout(this.persistenceTimeout)
|
||||
}
|
||||
|
||||
// Schedule a new timeout to persist pending changes
|
||||
this.persistenceTimeout = setTimeout(async () => {
|
||||
try {
|
||||
await Promise.all([
|
||||
this.persistGlobalStateBatch(this.pendingGlobalState),
|
||||
this.persistSecretsBatch(this.pendingSecrets),
|
||||
this.persistWorkspaceStateBatch(this.pendingWorkspaceState),
|
||||
])
|
||||
|
||||
// Clear pending sets on successful persistence
|
||||
this.pendingGlobalState.clear()
|
||||
this.pendingSecrets.clear()
|
||||
this.pendingWorkspaceState.clear()
|
||||
this.persistenceTimeout = null
|
||||
} catch (error) {
|
||||
console.error("Failed to persist pending changes:", error)
|
||||
this.persistenceTimeout = null
|
||||
|
||||
// Call persistence error callback for error recovery
|
||||
this.onPersistenceError?.({ error: error as Error })
|
||||
}
|
||||
}, this.PERSISTENCE_DELAY_MS)
|
||||
}
|
||||
|
||||
/**
|
||||
* Private method to batch persist global state keys with Promise.all
|
||||
*/
|
||||
private async persistGlobalStateBatch(keys: Set<GlobalStateKey>): Promise<void> {
|
||||
try {
|
||||
await Promise.all(
|
||||
Array.from(keys).map((key) => {
|
||||
const value = this.globalStateCache.get(key)
|
||||
return this.context.globalState.update(key, value)
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Failed to persist global state batch:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Private method to batch persist secrets with Promise.all
|
||||
*/
|
||||
private async persistSecretsBatch(keys: Set<SecretKey>): Promise<void> {
|
||||
try {
|
||||
await Promise.all(
|
||||
Array.from(keys).map((key) => {
|
||||
const value = this.secretsCache.get(key)
|
||||
if (value) {
|
||||
return this.context.secrets.store(key, value)
|
||||
} else {
|
||||
return this.context.secrets.delete(key)
|
||||
}
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Failed to persist secrets batch:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Private method to batch persist workspace state keys with Promise.all
|
||||
*/
|
||||
private async persistWorkspaceStateBatch(keys: Set<LocalStateKey>): Promise<void> {
|
||||
try {
|
||||
await Promise.all(
|
||||
Array.from(keys).map((key) => {
|
||||
const value = this.workspaceStateCache.get(key)
|
||||
return this.context.workspaceState.update(key, value)
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Failed to persist workspace state batch:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Private method to populate API configuration cache without triggering persistence
|
||||
* Used during initialization
|
||||
*/
|
||||
private populateApiConfigurationCache(apiConfiguration: ApiConfiguration): void {
|
||||
const {
|
||||
apiKey,
|
||||
openRouterApiKey,
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
awsRegion,
|
||||
awsUseCrossRegionInference,
|
||||
awsBedrockUsePromptCache,
|
||||
awsBedrockEndpoint,
|
||||
awsBedrockApiKey,
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
openAiApiKey,
|
||||
openAiHeaders,
|
||||
ollamaBaseUrl,
|
||||
ollamaApiOptionsCtxNum,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
geminiApiKey,
|
||||
geminiBaseUrl,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
togetherApiKey,
|
||||
qwenApiKey,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
azureApiVersion,
|
||||
openRouterProviderSorting,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmApiKey,
|
||||
liteLlmUsePromptCache,
|
||||
qwenApiLine,
|
||||
moonshotApiLine,
|
||||
asksageApiKey,
|
||||
asksageApiUrl,
|
||||
xaiApiKey,
|
||||
clineAccountId,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
favoritedModelIds,
|
||||
fireworksApiKey,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
claudeCodePath,
|
||||
huggingFaceApiKey,
|
||||
requestTimeoutMs,
|
||||
// Plan mode configurations
|
||||
planModeApiProvider,
|
||||
planModeApiModelId,
|
||||
planModeThinkingBudgetTokens,
|
||||
planModeReasoningEffort,
|
||||
planModeVsCodeLmModelSelector,
|
||||
planModeAwsBedrockCustomSelected,
|
||||
planModeAwsBedrockCustomModelBaseId,
|
||||
planModeOpenRouterModelId,
|
||||
planModeOpenRouterModelInfo,
|
||||
planModeOpenAiModelId,
|
||||
planModeOpenAiModelInfo,
|
||||
planModeOllamaModelId,
|
||||
planModeLmStudioModelId,
|
||||
planModeLiteLlmModelId,
|
||||
planModeLiteLlmModelInfo,
|
||||
planModeRequestyModelId,
|
||||
planModeRequestyModelInfo,
|
||||
planModeTogetherModelId,
|
||||
planModeFireworksModelId,
|
||||
planModeSapAiCoreModelId,
|
||||
planModeGroqModelId,
|
||||
planModeGroqModelInfo,
|
||||
planModeHuggingFaceModelId,
|
||||
planModeHuggingFaceModelInfo,
|
||||
// Act mode configurations
|
||||
actModeApiProvider,
|
||||
actModeApiModelId,
|
||||
actModeThinkingBudgetTokens,
|
||||
actModeReasoningEffort,
|
||||
actModeVsCodeLmModelSelector,
|
||||
actModeAwsBedrockCustomSelected,
|
||||
actModeAwsBedrockCustomModelBaseId,
|
||||
actModeOpenRouterModelId,
|
||||
actModeOpenRouterModelInfo,
|
||||
actModeOpenAiModelId,
|
||||
actModeOpenAiModelInfo,
|
||||
actModeOllamaModelId,
|
||||
actModeLmStudioModelId,
|
||||
actModeLiteLlmModelId,
|
||||
actModeLiteLlmModelInfo,
|
||||
actModeRequestyModelId,
|
||||
actModeRequestyModelInfo,
|
||||
actModeTogetherModelId,
|
||||
actModeFireworksModelId,
|
||||
actModeSapAiCoreModelId,
|
||||
actModeGroqModelId,
|
||||
actModeGroqModelInfo,
|
||||
actModeHuggingFaceModelId,
|
||||
actModeHuggingFaceModelInfo,
|
||||
} = apiConfiguration
|
||||
|
||||
// Directly populate global state cache without triggering persistence
|
||||
const globalStateUpdates = {
|
||||
// Plan mode configuration updates
|
||||
planModeApiProvider,
|
||||
planModeApiModelId,
|
||||
planModeThinkingBudgetTokens,
|
||||
planModeReasoningEffort,
|
||||
planModeVsCodeLmModelSelector,
|
||||
planModeAwsBedrockCustomSelected,
|
||||
planModeAwsBedrockCustomModelBaseId,
|
||||
planModeOpenRouterModelId,
|
||||
planModeOpenRouterModelInfo,
|
||||
planModeOpenAiModelId,
|
||||
planModeOpenAiModelInfo,
|
||||
planModeOllamaModelId,
|
||||
planModeLmStudioModelId,
|
||||
planModeLiteLlmModelId,
|
||||
planModeLiteLlmModelInfo,
|
||||
planModeRequestyModelId,
|
||||
planModeRequestyModelInfo,
|
||||
planModeTogetherModelId,
|
||||
planModeFireworksModelId,
|
||||
planModeSapAiCoreModelId,
|
||||
planModeGroqModelId,
|
||||
planModeGroqModelInfo,
|
||||
planModeHuggingFaceModelId,
|
||||
planModeHuggingFaceModelInfo,
|
||||
|
||||
// Act mode configuration updates
|
||||
actModeApiProvider,
|
||||
actModeApiModelId,
|
||||
actModeThinkingBudgetTokens,
|
||||
actModeReasoningEffort,
|
||||
actModeVsCodeLmModelSelector,
|
||||
actModeAwsBedrockCustomSelected,
|
||||
actModeAwsBedrockCustomModelBaseId,
|
||||
actModeOpenRouterModelId,
|
||||
actModeOpenRouterModelInfo,
|
||||
actModeOpenAiModelId,
|
||||
actModeOpenAiModelInfo,
|
||||
actModeOllamaModelId,
|
||||
actModeLmStudioModelId,
|
||||
actModeLiteLlmModelId,
|
||||
actModeLiteLlmModelInfo,
|
||||
actModeRequestyModelId,
|
||||
actModeRequestyModelInfo,
|
||||
actModeTogetherModelId,
|
||||
actModeFireworksModelId,
|
||||
actModeSapAiCoreModelId,
|
||||
actModeGroqModelId,
|
||||
actModeGroqModelInfo,
|
||||
actModeHuggingFaceModelId,
|
||||
actModeHuggingFaceModelInfo,
|
||||
|
||||
// Global state updates
|
||||
awsRegion,
|
||||
awsUseCrossRegionInference,
|
||||
awsBedrockUsePromptCache,
|
||||
awsBedrockEndpoint,
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
openAiHeaders,
|
||||
ollamaBaseUrl,
|
||||
ollamaApiOptionsCtxNum,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
geminiBaseUrl,
|
||||
azureApiVersion,
|
||||
openRouterProviderSorting,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmUsePromptCache,
|
||||
qwenApiLine,
|
||||
moonshotApiLine,
|
||||
asksageApiUrl,
|
||||
favoritedModelIds,
|
||||
requestTimeoutMs,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
claudeCodePath,
|
||||
}
|
||||
|
||||
// Populate global state cache directly
|
||||
Object.entries(globalStateUpdates).forEach(([key, value]) => {
|
||||
this.globalStateCache.set(key as GlobalStateKey, value)
|
||||
})
|
||||
|
||||
// Directly populate secrets cache without triggering persistence
|
||||
const secretsUpdates = {
|
||||
apiKey,
|
||||
openRouterApiKey,
|
||||
clineAccountId,
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
awsBedrockApiKey,
|
||||
openAiApiKey,
|
||||
geminiApiKey,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
togetherApiKey,
|
||||
qwenApiKey,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
liteLlmApiKey,
|
||||
fireworksApiKey,
|
||||
asksageApiKey,
|
||||
xaiApiKey,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
huggingFaceApiKey,
|
||||
}
|
||||
|
||||
// Populate secrets cache directly
|
||||
Object.entries(secretsUpdates).forEach(([key, value]) => {
|
||||
this.secretsCache.set(key as SecretKey, value)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct API configuration from cached component keys
|
||||
*/
|
||||
private constructApiConfigurationFromCache(): ApiConfiguration {
|
||||
return {
|
||||
// Secrets
|
||||
apiKey: this.secretsCache.get("apiKey"),
|
||||
openRouterApiKey: this.secretsCache.get("openRouterApiKey"),
|
||||
clineAccountId: this.secretsCache.get("clineAccountId"),
|
||||
awsAccessKey: this.secretsCache.get("awsAccessKey"),
|
||||
awsSecretKey: this.secretsCache.get("awsSecretKey"),
|
||||
awsSessionToken: this.secretsCache.get("awsSessionToken"),
|
||||
awsBedrockApiKey: this.secretsCache.get("awsBedrockApiKey"),
|
||||
openAiApiKey: this.secretsCache.get("openAiApiKey"),
|
||||
geminiApiKey: this.secretsCache.get("geminiApiKey"),
|
||||
openAiNativeApiKey: this.secretsCache.get("openAiNativeApiKey"),
|
||||
deepSeekApiKey: this.secretsCache.get("deepSeekApiKey"),
|
||||
requestyApiKey: this.secretsCache.get("requestyApiKey"),
|
||||
togetherApiKey: this.secretsCache.get("togetherApiKey"),
|
||||
qwenApiKey: this.secretsCache.get("qwenApiKey"),
|
||||
doubaoApiKey: this.secretsCache.get("doubaoApiKey"),
|
||||
mistralApiKey: this.secretsCache.get("mistralApiKey"),
|
||||
liteLlmApiKey: this.secretsCache.get("liteLlmApiKey"),
|
||||
fireworksApiKey: this.secretsCache.get("fireworksApiKey"),
|
||||
asksageApiKey: this.secretsCache.get("asksageApiKey"),
|
||||
xaiApiKey: this.secretsCache.get("xaiApiKey"),
|
||||
sambanovaApiKey: this.secretsCache.get("sambanovaApiKey"),
|
||||
cerebrasApiKey: this.secretsCache.get("cerebrasApiKey"),
|
||||
groqApiKey: this.secretsCache.get("groqApiKey"),
|
||||
moonshotApiKey: this.secretsCache.get("moonshotApiKey"),
|
||||
nebiusApiKey: this.secretsCache.get("nebiusApiKey"),
|
||||
sapAiCoreClientId: this.secretsCache.get("sapAiCoreClientId"),
|
||||
sapAiCoreClientSecret: this.secretsCache.get("sapAiCoreClientSecret"),
|
||||
huggingFaceApiKey: this.secretsCache.get("huggingFaceApiKey"),
|
||||
|
||||
// Global state
|
||||
awsRegion: this.globalStateCache.get("awsRegion"),
|
||||
awsUseCrossRegionInference: this.globalStateCache.get("awsUseCrossRegionInference"),
|
||||
awsBedrockUsePromptCache: this.globalStateCache.get("awsBedrockUsePromptCache"),
|
||||
awsBedrockEndpoint: this.globalStateCache.get("awsBedrockEndpoint"),
|
||||
awsProfile: this.globalStateCache.get("awsProfile"),
|
||||
awsUseProfile: this.globalStateCache.get("awsUseProfile"),
|
||||
awsAuthentication: this.globalStateCache.get("awsAuthentication"),
|
||||
vertexProjectId: this.globalStateCache.get("vertexProjectId"),
|
||||
vertexRegion: this.globalStateCache.get("vertexRegion"),
|
||||
openAiBaseUrl: this.globalStateCache.get("openAiBaseUrl"),
|
||||
openAiHeaders: this.globalStateCache.get("openAiHeaders") || {},
|
||||
ollamaBaseUrl: this.globalStateCache.get("ollamaBaseUrl"),
|
||||
ollamaApiOptionsCtxNum: this.globalStateCache.get("ollamaApiOptionsCtxNum"),
|
||||
lmStudioBaseUrl: this.globalStateCache.get("lmStudioBaseUrl"),
|
||||
anthropicBaseUrl: this.globalStateCache.get("anthropicBaseUrl"),
|
||||
geminiBaseUrl: this.globalStateCache.get("geminiBaseUrl"),
|
||||
azureApiVersion: this.globalStateCache.get("azureApiVersion"),
|
||||
openRouterProviderSorting: this.globalStateCache.get("openRouterProviderSorting"),
|
||||
liteLlmBaseUrl: this.globalStateCache.get("liteLlmBaseUrl"),
|
||||
liteLlmUsePromptCache: this.globalStateCache.get("liteLlmUsePromptCache"),
|
||||
qwenApiLine: this.globalStateCache.get("qwenApiLine"),
|
||||
moonshotApiLine: this.globalStateCache.get("moonshotApiLine"),
|
||||
asksageApiUrl: this.globalStateCache.get("asksageApiUrl"),
|
||||
favoritedModelIds: this.globalStateCache.get("favoritedModelIds"),
|
||||
requestTimeoutMs: this.globalStateCache.get("requestTimeoutMs"),
|
||||
fireworksModelMaxCompletionTokens: this.globalStateCache.get("fireworksModelMaxCompletionTokens"),
|
||||
fireworksModelMaxTokens: this.globalStateCache.get("fireworksModelMaxTokens"),
|
||||
sapAiCoreBaseUrl: this.globalStateCache.get("sapAiCoreBaseUrl"),
|
||||
sapAiCoreTokenUrl: this.globalStateCache.get("sapAiCoreTokenUrl"),
|
||||
sapAiResourceGroup: this.globalStateCache.get("sapAiResourceGroup"),
|
||||
claudeCodePath: this.globalStateCache.get("claudeCodePath"),
|
||||
|
||||
// Plan mode configurations
|
||||
planModeApiProvider: this.globalStateCache.get("planModeApiProvider"),
|
||||
planModeApiModelId: this.globalStateCache.get("planModeApiModelId"),
|
||||
planModeThinkingBudgetTokens: this.globalStateCache.get("planModeThinkingBudgetTokens"),
|
||||
planModeReasoningEffort: this.globalStateCache.get("planModeReasoningEffort"),
|
||||
planModeVsCodeLmModelSelector: this.globalStateCache.get("planModeVsCodeLmModelSelector"),
|
||||
planModeAwsBedrockCustomSelected: this.globalStateCache.get("planModeAwsBedrockCustomSelected"),
|
||||
planModeAwsBedrockCustomModelBaseId: this.globalStateCache.get("planModeAwsBedrockCustomModelBaseId"),
|
||||
planModeOpenRouterModelId: this.globalStateCache.get("planModeOpenRouterModelId"),
|
||||
planModeOpenRouterModelInfo: this.globalStateCache.get("planModeOpenRouterModelInfo"),
|
||||
planModeOpenAiModelId: this.globalStateCache.get("planModeOpenAiModelId"),
|
||||
planModeOpenAiModelInfo: this.globalStateCache.get("planModeOpenAiModelInfo"),
|
||||
planModeOllamaModelId: this.globalStateCache.get("planModeOllamaModelId"),
|
||||
planModeLmStudioModelId: this.globalStateCache.get("planModeLmStudioModelId"),
|
||||
planModeLiteLlmModelId: this.globalStateCache.get("planModeLiteLlmModelId"),
|
||||
planModeLiteLlmModelInfo: this.globalStateCache.get("planModeLiteLlmModelInfo"),
|
||||
planModeRequestyModelId: this.globalStateCache.get("planModeRequestyModelId"),
|
||||
planModeRequestyModelInfo: this.globalStateCache.get("planModeRequestyModelInfo"),
|
||||
planModeTogetherModelId: this.globalStateCache.get("planModeTogetherModelId"),
|
||||
planModeFireworksModelId: this.globalStateCache.get("planModeFireworksModelId"),
|
||||
planModeSapAiCoreModelId: this.globalStateCache.get("planModeSapAiCoreModelId"),
|
||||
planModeGroqModelId: this.globalStateCache.get("planModeGroqModelId"),
|
||||
planModeGroqModelInfo: this.globalStateCache.get("planModeGroqModelInfo"),
|
||||
planModeHuggingFaceModelId: this.globalStateCache.get("planModeHuggingFaceModelId"),
|
||||
planModeHuggingFaceModelInfo: this.globalStateCache.get("planModeHuggingFaceModelInfo"),
|
||||
|
||||
// Act mode configurations
|
||||
actModeApiProvider: this.globalStateCache.get("actModeApiProvider"),
|
||||
actModeApiModelId: this.globalStateCache.get("actModeApiModelId"),
|
||||
actModeThinkingBudgetTokens: this.globalStateCache.get("actModeThinkingBudgetTokens"),
|
||||
actModeReasoningEffort: this.globalStateCache.get("actModeReasoningEffort"),
|
||||
actModeVsCodeLmModelSelector: this.globalStateCache.get("actModeVsCodeLmModelSelector"),
|
||||
actModeAwsBedrockCustomSelected: this.globalStateCache.get("actModeAwsBedrockCustomSelected"),
|
||||
actModeAwsBedrockCustomModelBaseId: this.globalStateCache.get("actModeAwsBedrockCustomModelBaseId"),
|
||||
actModeOpenRouterModelId: this.globalStateCache.get("actModeOpenRouterModelId"),
|
||||
actModeOpenRouterModelInfo: this.globalStateCache.get("actModeOpenRouterModelInfo"),
|
||||
actModeOpenAiModelId: this.globalStateCache.get("actModeOpenAiModelId"),
|
||||
actModeOpenAiModelInfo: this.globalStateCache.get("actModeOpenAiModelInfo"),
|
||||
actModeOllamaModelId: this.globalStateCache.get("actModeOllamaModelId"),
|
||||
actModeLmStudioModelId: this.globalStateCache.get("actModeLmStudioModelId"),
|
||||
actModeLiteLlmModelId: this.globalStateCache.get("actModeLiteLlmModelId"),
|
||||
actModeLiteLlmModelInfo: this.globalStateCache.get("actModeLiteLlmModelInfo"),
|
||||
actModeRequestyModelId: this.globalStateCache.get("actModeRequestyModelId"),
|
||||
actModeRequestyModelInfo: this.globalStateCache.get("actModeRequestyModelInfo"),
|
||||
actModeTogetherModelId: this.globalStateCache.get("actModeTogetherModelId"),
|
||||
actModeFireworksModelId: this.globalStateCache.get("actModeFireworksModelId"),
|
||||
actModeSapAiCoreModelId: this.globalStateCache.get("actModeSapAiCoreModelId"),
|
||||
actModeGroqModelId: this.globalStateCache.get("actModeGroqModelId"),
|
||||
actModeGroqModelInfo: this.globalStateCache.get("actModeGroqModelInfo"),
|
||||
actModeHuggingFaceModelId: this.globalStateCache.get("actModeHuggingFaceModelId"),
|
||||
actModeHuggingFaceModelInfo: this.globalStateCache.get("actModeHuggingFaceModelInfo"),
|
||||
} as ApiConfiguration
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const CACHE_SERVICE_NOT_INITIALIZED = "CacheService must be initialized before attempting to access state."
|
||||
+11
-256
@@ -12,6 +12,7 @@ import { UserInfo } from "@shared/UserInfo"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import { DEFAULT_MCP_DISPLAY_MODE, McpDisplayMode } from "@shared/McpDisplayMode"
|
||||
import { migrateEnableCheckpointsSetting, migrateMcpMarketplaceEnableSetting } from "./state-migrations"
|
||||
import { Controller } from "../controller"
|
||||
/*
|
||||
Storage
|
||||
https://dev.to/kompotkot/how-to-use-secretstorage-in-your-vscode-extensions-2hco
|
||||
@@ -578,263 +579,18 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateApiConfiguration(context: vscode.ExtensionContext, apiConfiguration: ApiConfiguration) {
|
||||
const {
|
||||
apiKey,
|
||||
openRouterApiKey,
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
awsRegion,
|
||||
awsUseCrossRegionInference,
|
||||
awsBedrockUsePromptCache,
|
||||
awsBedrockEndpoint,
|
||||
awsBedrockApiKey,
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
openAiApiKey,
|
||||
openAiHeaders,
|
||||
ollamaBaseUrl,
|
||||
ollamaApiOptionsCtxNum,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
geminiApiKey,
|
||||
geminiBaseUrl,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
togetherApiKey,
|
||||
qwenApiKey,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
azureApiVersion,
|
||||
openRouterProviderSorting,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmApiKey,
|
||||
liteLlmUsePromptCache,
|
||||
qwenApiLine,
|
||||
moonshotApiLine,
|
||||
asksageApiKey,
|
||||
asksageApiUrl,
|
||||
xaiApiKey,
|
||||
clineAccountId,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
favoritedModelIds,
|
||||
fireworksApiKey,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
claudeCodePath,
|
||||
huggingFaceApiKey,
|
||||
huaweiCloudMaasApiKey,
|
||||
// Plan mode configurations
|
||||
planModeApiProvider,
|
||||
planModeApiModelId,
|
||||
planModeThinkingBudgetTokens,
|
||||
planModeReasoningEffort,
|
||||
planModeVsCodeLmModelSelector,
|
||||
planModeAwsBedrockCustomSelected,
|
||||
planModeAwsBedrockCustomModelBaseId,
|
||||
planModeOpenRouterModelId,
|
||||
planModeOpenRouterModelInfo,
|
||||
planModeOpenAiModelId,
|
||||
planModeOpenAiModelInfo,
|
||||
planModeOllamaModelId,
|
||||
planModeLmStudioModelId,
|
||||
planModeLiteLlmModelId,
|
||||
planModeLiteLlmModelInfo,
|
||||
planModeRequestyModelId,
|
||||
planModeRequestyModelInfo,
|
||||
planModeTogetherModelId,
|
||||
planModeFireworksModelId,
|
||||
planModeSapAiCoreModelId,
|
||||
planModeGroqModelId,
|
||||
planModeGroqModelInfo,
|
||||
planModeHuggingFaceModelId,
|
||||
planModeHuggingFaceModelInfo,
|
||||
planModeHuaweiCloudMaasModelId,
|
||||
planModeHuaweiCloudMaasModelInfo,
|
||||
// Act mode configurations
|
||||
actModeApiProvider,
|
||||
actModeApiModelId,
|
||||
actModeThinkingBudgetTokens,
|
||||
actModeReasoningEffort,
|
||||
actModeVsCodeLmModelSelector,
|
||||
actModeAwsBedrockCustomSelected,
|
||||
actModeAwsBedrockCustomModelBaseId,
|
||||
actModeOpenRouterModelId,
|
||||
actModeOpenRouterModelInfo,
|
||||
actModeOpenAiModelId,
|
||||
actModeOpenAiModelInfo,
|
||||
actModeOllamaModelId,
|
||||
actModeLmStudioModelId,
|
||||
actModeLiteLlmModelId,
|
||||
actModeLiteLlmModelInfo,
|
||||
actModeRequestyModelId,
|
||||
actModeRequestyModelInfo,
|
||||
actModeTogetherModelId,
|
||||
actModeFireworksModelId,
|
||||
actModeSapAiCoreModelId,
|
||||
actModeGroqModelId,
|
||||
actModeGroqModelInfo,
|
||||
actModeHuggingFaceModelId,
|
||||
actModeHuggingFaceModelInfo,
|
||||
actModeHuaweiCloudMaasModelId,
|
||||
actModeHuaweiCloudMaasModelInfo,
|
||||
} = apiConfiguration
|
||||
export async function resetWorkspaceState(controller: Controller) {
|
||||
const context = controller.context
|
||||
await Promise.all(context.workspaceState.keys().map((key) => controller.context.workspaceState.update(key, undefined)))
|
||||
|
||||
// OPTIMIZED: Batch all global state updates into 2 operations instead of 47
|
||||
const batchedGlobalUpdates = {
|
||||
// Plan mode configuration updates
|
||||
planModeApiProvider,
|
||||
planModeApiModelId,
|
||||
planModeThinkingBudgetTokens,
|
||||
planModeReasoningEffort,
|
||||
planModeVsCodeLmModelSelector,
|
||||
planModeAwsBedrockCustomSelected,
|
||||
planModeAwsBedrockCustomModelBaseId,
|
||||
planModeOpenRouterModelId,
|
||||
planModeOpenRouterModelInfo,
|
||||
planModeOpenAiModelId,
|
||||
planModeOpenAiModelInfo,
|
||||
planModeOllamaModelId,
|
||||
planModeLmStudioModelId,
|
||||
planModeLiteLlmModelId,
|
||||
planModeLiteLlmModelInfo,
|
||||
planModeRequestyModelId,
|
||||
planModeRequestyModelInfo,
|
||||
planModeTogetherModelId,
|
||||
planModeFireworksModelId,
|
||||
planModeSapAiCoreModelId,
|
||||
planModeGroqModelId,
|
||||
planModeGroqModelInfo,
|
||||
planModeHuggingFaceModelId,
|
||||
planModeHuggingFaceModelInfo,
|
||||
planModeHuaweiCloudMaasModelId,
|
||||
planModeHuaweiCloudMaasModelInfo,
|
||||
|
||||
// Act mode configuration updates
|
||||
actModeApiProvider,
|
||||
actModeApiModelId,
|
||||
actModeThinkingBudgetTokens,
|
||||
actModeReasoningEffort,
|
||||
actModeVsCodeLmModelSelector,
|
||||
actModeAwsBedrockCustomSelected,
|
||||
actModeAwsBedrockCustomModelBaseId,
|
||||
actModeOpenRouterModelId,
|
||||
actModeOpenRouterModelInfo,
|
||||
actModeOpenAiModelId,
|
||||
actModeOpenAiModelInfo,
|
||||
actModeOllamaModelId,
|
||||
actModeLmStudioModelId,
|
||||
actModeLiteLlmModelId,
|
||||
actModeLiteLlmModelInfo,
|
||||
actModeRequestyModelId,
|
||||
actModeRequestyModelInfo,
|
||||
actModeTogetherModelId,
|
||||
actModeFireworksModelId,
|
||||
actModeSapAiCoreModelId,
|
||||
actModeGroqModelId,
|
||||
actModeGroqModelInfo,
|
||||
actModeHuggingFaceModelId,
|
||||
actModeHuggingFaceModelInfo,
|
||||
actModeHuaweiCloudMaasModelId,
|
||||
actModeHuaweiCloudMaasModelInfo,
|
||||
|
||||
// Global state updates (27 keys)
|
||||
awsRegion,
|
||||
awsUseCrossRegionInference,
|
||||
awsBedrockUsePromptCache,
|
||||
awsBedrockEndpoint,
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
openAiHeaders: openAiHeaders || {},
|
||||
ollamaBaseUrl,
|
||||
ollamaApiOptionsCtxNum,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
geminiBaseUrl,
|
||||
azureApiVersion,
|
||||
openRouterProviderSorting,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmUsePromptCache,
|
||||
qwenApiLine,
|
||||
moonshotApiLine,
|
||||
asksageApiUrl,
|
||||
favoritedModelIds,
|
||||
requestTimeoutMs: apiConfiguration.requestTimeoutMs,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
claudeCodePath,
|
||||
}
|
||||
|
||||
// OPTIMIZED: Batch all secret updates into 1 operation instead of 23
|
||||
const batchedSecretUpdates = {
|
||||
apiKey,
|
||||
openRouterApiKey,
|
||||
clineAccountId,
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
awsBedrockApiKey,
|
||||
openAiApiKey,
|
||||
geminiApiKey,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
togetherApiKey,
|
||||
qwenApiKey,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
liteLlmApiKey,
|
||||
fireworksApiKey,
|
||||
asksageApiKey,
|
||||
xaiApiKey,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
huggingFaceApiKey,
|
||||
huaweiCloudMaasApiKey,
|
||||
}
|
||||
|
||||
// Execute batched operations in parallel for maximum performance
|
||||
await Promise.all([updateGlobalStateBatch(context, batchedGlobalUpdates), updateSecretsBatch(context, batchedSecretUpdates)])
|
||||
await controller.cacheService.reInitialize()
|
||||
}
|
||||
|
||||
export async function resetWorkspaceState(context: vscode.ExtensionContext) {
|
||||
for (const key of context.workspaceState.keys()) {
|
||||
await context.workspaceState.update(key, undefined)
|
||||
}
|
||||
}
|
||||
|
||||
export async function resetGlobalState(context: vscode.ExtensionContext) {
|
||||
export async function resetGlobalState(controller: Controller) {
|
||||
// TODO: Reset all workspace states?
|
||||
for (const key of context.globalState.keys()) {
|
||||
await context.globalState.update(key, undefined)
|
||||
}
|
||||
const context = controller.context
|
||||
|
||||
await Promise.all(context.globalState.keys().map((key) => context.globalState.update(key, undefined)))
|
||||
const secretKeys: SecretKey[] = [
|
||||
"apiKey",
|
||||
"openRouterApiKey",
|
||||
@@ -864,7 +620,6 @@ export async function resetGlobalState(context: vscode.ExtensionContext) {
|
||||
"huggingFaceApiKey",
|
||||
"huaweiCloudMaasApiKey",
|
||||
]
|
||||
for (const key of secretKeys) {
|
||||
await storeSecret(context, key, undefined)
|
||||
}
|
||||
await Promise.all(secretKeys.map((key) => storeSecret(context, key, undefined)))
|
||||
await controller.cacheService.reInitialize()
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ import { ClineAskResponse } from "@shared/WebviewMessage"
|
||||
import { extractFileContent, FileContentResult } from "@integrations/misc/extract-file-content"
|
||||
import { COMMAND_REQ_APP_STRING } from "@shared/combineCommandSequences"
|
||||
import { fileExistsAtPath } from "@utils/fs"
|
||||
import { isClaude4ModelFamily, isGemini2dot5ModelFamily } from "@utils/model-utils"
|
||||
import { isClaude4ModelFamily, isGemini2dot5ModelFamily, isGrok4ModelFamily, modelDoesntSupportWebp } from "@utils/model-utils"
|
||||
import { fixModelHtmlEscaping, removeInvalidChars } from "@utils/string"
|
||||
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
|
||||
import os from "os"
|
||||
@@ -50,7 +50,7 @@ import { ContextManager } from "../context/context-management/ContextManager"
|
||||
import { loadMcpDocumentation } from "../prompts/loadMcpDocumentation"
|
||||
import { formatResponse } from "../prompts/responses"
|
||||
import { ensureTaskDirectoryExists } from "../storage/disk"
|
||||
import { getGlobalState, getWorkspaceState } from "../storage/state"
|
||||
import { CacheService } from "../storage/CacheService"
|
||||
import { TaskState } from "./TaskState"
|
||||
import { MessageStateHandler } from "./message-state"
|
||||
import { AutoApprove } from "./tools/autoApprove"
|
||||
@@ -86,6 +86,7 @@ export class ToolExecutor {
|
||||
private clineIgnoreController: ClineIgnoreController,
|
||||
private workspaceTracker: WorkspaceTracker,
|
||||
private contextManager: ContextManager,
|
||||
private cacheService: CacheService,
|
||||
|
||||
// Configuration & Settings
|
||||
private autoApprovalSettings: AutoApprovalSettings,
|
||||
@@ -124,7 +125,8 @@ export class ToolExecutor {
|
||||
}
|
||||
|
||||
private pushToolResult = (content: ToolResponse, block: ToolUse) => {
|
||||
const isNextGenModel = isClaude4ModelFamily(this.api) || isGemini2dot5ModelFamily(this.api)
|
||||
const isNextGenModel =
|
||||
isClaude4ModelFamily(this.api) || isGemini2dot5ModelFamily(this.api) || isGrok4ModelFamily(this.api)
|
||||
|
||||
if (typeof content === "string") {
|
||||
const resultText = content || "(tool did not return anything)"
|
||||
@@ -489,7 +491,8 @@ export class ToolExecutor {
|
||||
|
||||
const currentFullJson = block.params.diff
|
||||
// Check if we should use streaming (e.g., for specific models)
|
||||
const isNextGenModel = isClaude4ModelFamily(this.api) || isGemini2dot5ModelFamily(this.api)
|
||||
const isNextGenModel =
|
||||
isClaude4ModelFamily(this.api) || isGemini2dot5ModelFamily(this.api) || isGrok4ModelFamily(this.api)
|
||||
// Going through claude family of models
|
||||
if (isNextGenModel && USE_EXPERIMENTAL_CLAUDE4_FEATURES && currentFullJson) {
|
||||
const streamingResult = await this.handleStreamingJsonReplacement(block, relPath, currentFullJson)
|
||||
@@ -1182,7 +1185,9 @@ export class ToolExecutor {
|
||||
// Re-make browserSession to make sure latest settings apply
|
||||
if (this.context) {
|
||||
await this.browserSession.dispose()
|
||||
this.browserSession = new BrowserSession(this.context, this.browserSettings)
|
||||
|
||||
let useWebp = this.api ? !modelDoesntSupportWebp(this.api) : true
|
||||
this.browserSession = new BrowserSession(this.context, this.browserSettings, useWebp)
|
||||
} else {
|
||||
console.warn("no controller context available for browserSession")
|
||||
}
|
||||
@@ -1927,10 +1932,8 @@ export class ToolExecutor {
|
||||
vscode.extensions.getExtension("saoudrizwan.claude-dev")?.packageJSON.version || "Unknown"
|
||||
const systemInfo = `VSCode: ${vscode.version}, Node.js: ${process.version}, Architecture: ${os.arch()}`
|
||||
const currentMode = this.mode
|
||||
const apiProvider =
|
||||
currentMode === "plan"
|
||||
? await getGlobalState(this.context, "planModeApiProvider")
|
||||
: await getGlobalState(this.context, "actModeApiProvider")
|
||||
const apiConfig = this.cacheService.getApiConfiguration()
|
||||
const apiProvider = currentMode === "plan" ? apiConfig.planModeApiProvider : apiConfig.actModeApiProvider
|
||||
const providerAndModel = `${apiProvider} / ${this.api.getModel().id}`
|
||||
|
||||
// Ask user for confirmation
|
||||
|
||||
@@ -85,6 +85,7 @@ import { MessageStateHandler } from "./message-state"
|
||||
import { TaskState } from "./TaskState"
|
||||
import { ToolExecutor } from "./ToolExecutor"
|
||||
import { updateApiReqMsg } from "./utils"
|
||||
import { CacheService } from "../storage/CacheService"
|
||||
import { Mode, OpenaiReasoningEffort } from "@shared/storage/types"
|
||||
import { ShowMessageType } from "@/shared/proto/index.host"
|
||||
|
||||
@@ -130,6 +131,9 @@ export class Task {
|
||||
private reinitExistingTaskFromId: (taskId: string) => Promise<void>
|
||||
private cancelTask: () => Promise<void>
|
||||
|
||||
// Cache service
|
||||
private cacheService: CacheService
|
||||
|
||||
// User chat state
|
||||
autoApprovalSettings: AutoApprovalSettings
|
||||
browserSettings: BrowserSettings
|
||||
@@ -159,6 +163,7 @@ export class Task {
|
||||
defaultTerminalProfile: string,
|
||||
enableCheckpointsSetting: boolean,
|
||||
cwd: string,
|
||||
cacheService: CacheService,
|
||||
task?: string,
|
||||
images?: string[],
|
||||
files?: string[],
|
||||
@@ -202,6 +207,7 @@ export class Task {
|
||||
this.mode = mode
|
||||
this.enableCheckpoints = enableCheckpointsSetting
|
||||
this.cwd = cwd
|
||||
this.cacheService = cacheService
|
||||
|
||||
// Set up MCP notification callback for real-time notifications
|
||||
this.mcpHub.setNotificationCallback(async (serverName: string, level: string, message: string) => {
|
||||
@@ -318,6 +324,7 @@ export class Task {
|
||||
this.clineIgnoreController,
|
||||
this.workspaceTracker,
|
||||
this.contextManager,
|
||||
this.cacheService,
|
||||
this.autoApprovalSettings,
|
||||
this.browserSettings,
|
||||
cwd,
|
||||
@@ -1661,10 +1668,8 @@ export class Task {
|
||||
|
||||
private async getCurrentProviderInfo(): Promise<{ modelId: string; providerId: string }> {
|
||||
const modelId = this.api.getModel()?.id
|
||||
const providerId =
|
||||
this.mode === "plan"
|
||||
? ((await getGlobalState(this.getContext(), "planModeApiProvider")) as string)
|
||||
: ((await getGlobalState(this.getContext(), "actModeApiProvider")) as string)
|
||||
const apiConfig = this.cacheService.getApiConfiguration()
|
||||
const providerId = (this.mode === "plan" ? apiConfig.planModeApiProvider : apiConfig.actModeApiProvider) as string
|
||||
return { modelId, providerId }
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getNonce } from "./getNonce"
|
||||
|
||||
import { WebviewProviderType } from "@/shared/webview/types"
|
||||
import { Controller } from "@core/controller/index"
|
||||
import { CacheService } from "@core/storage/CacheService"
|
||||
import { findLast } from "@shared/array"
|
||||
import { readFile } from "fs/promises"
|
||||
import path from "node:path"
|
||||
@@ -30,6 +31,8 @@ export abstract class WebviewProvider {
|
||||
WebviewProvider.activeInstances.add(this)
|
||||
this.clientId = uuidv4()
|
||||
WebviewProvider.clientIdMap.set(this, this.clientId)
|
||||
|
||||
// Create controller with cache service
|
||||
this.controller = new Controller(context, (message) => this.postMessageToWebview(message), this.clientId)
|
||||
}
|
||||
|
||||
|
||||
+10
-43
@@ -38,6 +38,8 @@ import { VscodeWebviewProvider } from "./hosts/vscode/VscodeWebviewProvider"
|
||||
import { GitCommitGenerator } from "./integrations/git/commit-message-generator"
|
||||
import { AuthService } from "./services/auth/AuthService"
|
||||
import { ShowMessageType } from "./shared/proto/host/window"
|
||||
import { SharedUriHandler } from "./services/uri/SharedUriHandler"
|
||||
import { getLatestAnnouncementId } from "./utils/announcements"
|
||||
/*
|
||||
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
|
||||
|
||||
@@ -91,7 +93,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
// Use the same condition as announcements: focus when there's a new announcement to show
|
||||
const lastShownAnnouncementId = context.globalState.get<string>("lastShownAnnouncementId")
|
||||
const latestAnnouncementId = context.extension?.packageJSON?.version?.split(".").slice(0, 2).join(".") ?? ""
|
||||
const latestAnnouncementId = getLatestAnnouncementId(context)
|
||||
|
||||
if (lastShownAnnouncementId !== latestAnnouncementId) {
|
||||
// Focus Cline when there's a new announcement to show (major/minor updates or fresh installs)
|
||||
@@ -265,44 +267,10 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
})()
|
||||
context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(DIFF_VIEW_URI_SCHEME, diffContentProvider))
|
||||
|
||||
// URI Handler
|
||||
const handleUri = async (uri: vscode.Uri) => {
|
||||
console.log("URI Handler called with:", {
|
||||
path: uri.path,
|
||||
query: uri.query,
|
||||
scheme: uri.scheme,
|
||||
})
|
||||
|
||||
const path = uri.path
|
||||
const query = new URLSearchParams(uri.query.replace(/\+/g, "%2B"))
|
||||
const visibleWebview = WebviewProvider.getVisibleInstance()
|
||||
if (!visibleWebview) {
|
||||
return
|
||||
}
|
||||
switch (path) {
|
||||
case "/openrouter": {
|
||||
const code = query.get("code")
|
||||
if (code) {
|
||||
await visibleWebview?.controller.handleOpenRouterCallback(code)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "/auth": {
|
||||
console.log("Auth callback received:", uri.toString())
|
||||
|
||||
const token = query.get("idToken")
|
||||
const provider = query.get("provider")
|
||||
|
||||
console.log("Auth callback received:", { provider })
|
||||
|
||||
if (token) {
|
||||
await visibleWebview?.controller.handleAuthCallback(token, provider)
|
||||
// await authService.handleAuthCallback(token)
|
||||
}
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
const success = await SharedUriHandler.handleUri(uri)
|
||||
if (!success) {
|
||||
console.warn("Extension URI handler: Failed to process URI:", uri.toString())
|
||||
}
|
||||
}
|
||||
context.subscriptions.push(vscode.window.registerUriHandler({ handleUri }))
|
||||
@@ -664,7 +632,10 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
if (event.key === "clineAccountId") {
|
||||
// Check if the secret was removed (logout) or added/updated (login)
|
||||
const secretValue = await context.secrets.get("clineAccountId")
|
||||
const authService = AuthService.getInstance(context)
|
||||
const activeWebviewProvider = WebviewProvider.getVisibleInstance()
|
||||
const controller = activeWebviewProvider?.controller
|
||||
|
||||
const authService = AuthService.getInstance(controller)
|
||||
if (secretValue) {
|
||||
// Secret was added or updated - restore auth info (login from another window)
|
||||
authService?.restoreRefreshTokenAndRetrieveAuthInfo()
|
||||
@@ -679,10 +650,6 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
return createClineAPI(sidebarWebview.controller)
|
||||
}
|
||||
|
||||
export function getLatestAnnouncementId(context: vscode.ExtensionContext) {
|
||||
return context.extension?.packageJSON?.version?.split(".").slice(0, 2).join(".") ?? ""
|
||||
}
|
||||
|
||||
function maybeSetupHostProviders(context: ExtensionContext) {
|
||||
if (!HostProvider.isInitialized()) {
|
||||
console.log("Setting up vscode host providers...")
|
||||
|
||||
Vendored
+276
@@ -0,0 +1,276 @@
|
||||
import type { IncomingMessage, Server, ServerResponse } from "node:http"
|
||||
import http from "node:http"
|
||||
import type { AddressInfo } from "node:net"
|
||||
import { clineEnvConfig } from "@/config"
|
||||
import { openExternal } from "@/utils/env"
|
||||
import { SharedUriHandler } from "@/services/uri/SharedUriHandler"
|
||||
|
||||
const SERVER_TIMEOUT = 10 * 60 * 1000 // 10 minutes
|
||||
|
||||
/**
|
||||
* Handles OAuth authentication flow by creating a local server to receive tokens.
|
||||
*/
|
||||
export class AuthHandler {
|
||||
private static instance: AuthHandler | null = null
|
||||
|
||||
private port = 0
|
||||
private server: Server | null = null
|
||||
private serverCreationPromise: Promise<void> | null = null
|
||||
private timeoutId: NodeJS.Timeout | null = null
|
||||
private enabled: boolean = false
|
||||
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* Gets the singleton instance of AuthHandler
|
||||
* @returns The singleton AuthHandler instance
|
||||
*/
|
||||
public static getInstance(): AuthHandler {
|
||||
if (!AuthHandler.instance) {
|
||||
AuthHandler.instance = new AuthHandler()
|
||||
}
|
||||
return AuthHandler.instance
|
||||
}
|
||||
|
||||
public setEnabled(enabled: boolean): void {
|
||||
this.enabled = enabled
|
||||
}
|
||||
|
||||
public async getCallbackUri(): Promise<string | undefined> {
|
||||
try {
|
||||
if (!this.enabled) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (!this.server) {
|
||||
// If server creation is already in progress, wait for it
|
||||
if (this.serverCreationPromise) {
|
||||
await this.serverCreationPromise
|
||||
} else {
|
||||
// Start server creation and track the promise
|
||||
this.serverCreationPromise = this.createServer()
|
||||
await this.serverCreationPromise
|
||||
}
|
||||
} else {
|
||||
this.updateTimeout()
|
||||
}
|
||||
|
||||
return `http://127.0.0.1:${this.port}`
|
||||
} catch (error) {
|
||||
console.error("AuthHandler.getCallbackUri error:", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
private async createServer(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const server = http.createServer(this.handleRequest.bind(this))
|
||||
|
||||
// Use callback to ensure server is ready before getting address
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const address = server.address()
|
||||
if (!address) {
|
||||
console.error("AuthHandler: Failed to get server address")
|
||||
this.server = null
|
||||
this.port = 0
|
||||
this.serverCreationPromise = null
|
||||
reject(new Error("Failed to get server address"))
|
||||
return
|
||||
}
|
||||
|
||||
// Get the assigned port and set up the server
|
||||
this.port = (address as AddressInfo).port
|
||||
this.server = server
|
||||
console.log("AuthHandler: Server started on port", this.port)
|
||||
this.updateTimeout()
|
||||
this.serverCreationPromise = null
|
||||
resolve()
|
||||
})
|
||||
|
||||
server.on("error", (error) => {
|
||||
console.error("AuthHandler: Server error", error)
|
||||
this.server = null
|
||||
this.port = 0
|
||||
this.serverCreationPromise = null
|
||||
reject(error)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("AuthHandler: Failed to create server", error)
|
||||
this.server = null
|
||||
this.port = 0
|
||||
this.serverCreationPromise = null
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private updateTimeout(): void {
|
||||
if (this.timeoutId) {
|
||||
clearTimeout(this.timeoutId)
|
||||
}
|
||||
|
||||
this.timeoutId = setTimeout(() => this.stop(), SERVER_TIMEOUT)
|
||||
}
|
||||
|
||||
private async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
||||
console.log("AuthHandler: Received request", req.url)
|
||||
|
||||
if (!req.url) {
|
||||
this.sendResponse(res, 404, "text/plain", "Not found")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Convert HTTP URL to vscode.Uri and use shared handler directly
|
||||
const fullUrl = `http://127.0.0.1:${this.port}${req.url}`
|
||||
const uri = SharedUriHandler.convertHttpUrlToUri(fullUrl)
|
||||
|
||||
// Use SharedUriHandler directly - it handles all validation and processing
|
||||
const success = await SharedUriHandler.handleUri(uri)
|
||||
|
||||
if (success) {
|
||||
this.sendResponse(res, 200, "text/html", TOKEN_REQUEST_VIEW)
|
||||
} else {
|
||||
this.sendResponse(res, 400, "text/plain", "Bad request")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("AuthHandler: Error processing request", error)
|
||||
this.sendResponse(res, 400, "text/plain", "Bad request")
|
||||
} finally {
|
||||
// Stop the server after handling any request (success or failure)
|
||||
this.stop()
|
||||
}
|
||||
}
|
||||
|
||||
private sendResponse(res: ServerResponse, status: number, type: string, content: string): void {
|
||||
res.writeHead(status, { "Content-Type": type })
|
||||
res.end(content)
|
||||
}
|
||||
|
||||
private async openBrowser(callbackUrl: URL): Promise<void> {
|
||||
await openExternal(callbackUrl.toString())
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
if (this.timeoutId) {
|
||||
clearTimeout(this.timeoutId)
|
||||
this.timeoutId = null
|
||||
}
|
||||
|
||||
if (this.server) {
|
||||
this.server.close()
|
||||
this.server = null
|
||||
}
|
||||
|
||||
this.serverCreationPromise = null
|
||||
this.port = 0
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.stop()
|
||||
}
|
||||
}
|
||||
|
||||
const TOKEN_REQUEST_VIEW = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cline - Authentication Success</title>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Azeret+Mono:wght@300;400;700&display=swap');
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Azeret Mono', monospace;
|
||||
background-color: #ffffff;
|
||||
color: #333333;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 32px;
|
||||
background-color: #f8f8f8;
|
||||
border: 1px solid #e1e1e1;
|
||||
border-radius: 6px;
|
||||
max-width: 480px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background-color: #73c991;
|
||||
margin: 0 auto 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.checkmark::after {
|
||||
content: '✓';
|
||||
font-size: 24px;
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 400;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 24px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.countdown {
|
||||
font-size: 0.8125rem;
|
||||
color: #666666;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #d1d1d1;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.container {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="checkmark"></div>
|
||||
<h1>Authentication Successful</h1>
|
||||
<p>Your authentication token has been securely sent back to your IDE. You can now return to your development environment to continue working.</p>
|
||||
<div class="countdown">Feel free to close this window and continue in your IDE</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
@@ -4,6 +4,8 @@ import * as sinon from "sinon"
|
||||
import { TerminalProcess } from "./TerminalProcess"
|
||||
import * as vscode from "vscode"
|
||||
import { TerminalRegistry } from "./TerminalRegistry"
|
||||
import { DiffViewProviderCreator, HostProvider, WebviewProviderCreator } from "@/hosts/host-provider"
|
||||
import { vscodeHostBridgeClient } from "@/hosts/vscode/hostbridge/client/host-grpc-client"
|
||||
|
||||
declare module "vscode" {
|
||||
// https://github.com/microsoft/vscode/blob/f0417069c62e20f3667506f4b7e53ca0004b4e3e/src/vscode-dts/vscode.d.ts#L7442
|
||||
@@ -36,6 +38,13 @@ describe("TerminalProcess (Integration Tests)", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox({ useFakeTimers: true })
|
||||
HostProvider.reset()
|
||||
HostProvider.initialize(
|
||||
((_) => {}) as WebviewProviderCreator,
|
||||
(() => {}) as DiffViewProviderCreator,
|
||||
vscodeHostBridgeClient,
|
||||
(s: string) => console.log(s),
|
||||
)
|
||||
process = new TerminalProcess()
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { EventEmitter } from "events"
|
||||
import { stripAnsi } from "./ansiUtils"
|
||||
import * as vscode from "vscode"
|
||||
import { Logger } from "@services/logging/Logger"
|
||||
import { getLatestTerminalOutput } from "./get-latest-output"
|
||||
|
||||
export interface TerminalProcessEvents {
|
||||
@@ -28,9 +27,6 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
|
||||
private gracePeriodTimer: NodeJS.Timeout | null = null
|
||||
private hasEmittedCompleted: boolean = false
|
||||
|
||||
// constructor() {
|
||||
// super()
|
||||
|
||||
private async emitCurrentTerminalContents(): Promise<void> {
|
||||
try {
|
||||
const terminalSnapshot = await getLatestTerminalOutput()
|
||||
|
||||
@@ -6,6 +6,7 @@ import { storeSecret } from "@/core/storage/state"
|
||||
import { telemetryService } from "@services/posthog/telemetry/TelemetryService"
|
||||
import { AuthState, UserInfo } from "@shared/proto/cline/account"
|
||||
import { type EmptyRequest, String } from "@shared/proto/cline/common"
|
||||
import { AuthHandler } from "@/hosts/external/AuthHandler"
|
||||
import { FirebaseAuthProvider } from "./providers/FirebaseAuthProvider"
|
||||
import { openExternal } from "@/utils/env"
|
||||
|
||||
@@ -56,7 +57,7 @@ export class AuthService {
|
||||
protected _clineAuthInfo: ClineAuthInfo | null = null
|
||||
protected _provider: { provider: FirebaseAuthProvider } | null = null
|
||||
protected _activeAuthStatusUpdateSubscriptions = new Set<[Controller, StreamingResponseHandler<AuthState>]>()
|
||||
protected _context: vscode.ExtensionContext
|
||||
protected _controller: Controller
|
||||
|
||||
/**
|
||||
* Creates an instance of AuthService.
|
||||
@@ -64,7 +65,7 @@ export class AuthService {
|
||||
* @param authProvider - Optional authentication provider to use.
|
||||
* @param controller - Optional reference to the Controller instance.
|
||||
*/
|
||||
protected constructor(context: vscode.ExtensionContext, config: ServiceConfig, authProvider?: any) {
|
||||
protected constructor(controller: Controller, config: ServiceConfig, authProvider?: any) {
|
||||
const providerName = authProvider || "firebase"
|
||||
this._config = Object.assign({ URI: DefaultClineAccountURI }, config)
|
||||
|
||||
@@ -95,7 +96,7 @@ export class AuthService {
|
||||
|
||||
this._setProvider(authProviders.find((authProvider) => authProvider.name === providerName).name)
|
||||
|
||||
this._context = context
|
||||
this._controller = controller
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,29 +106,29 @@ export class AuthService {
|
||||
* @param controller - Optional reference to the Controller instance.
|
||||
* @returns The singleton instance of AuthService.
|
||||
*/
|
||||
public static getInstance(context?: vscode.ExtensionContext, config?: ServiceConfig, authProvider?: any): AuthService {
|
||||
public static getInstance(controller?: Controller, config?: ServiceConfig, authProvider?: any): AuthService {
|
||||
if (!AuthService.instance) {
|
||||
if (!context) {
|
||||
if (!controller) {
|
||||
console.warn("Extension context was not provided to AuthService.getInstance, using default context")
|
||||
context = {} as vscode.ExtensionContext
|
||||
controller = {} as Controller
|
||||
}
|
||||
if (process.env.E2E_TEST) {
|
||||
// Use require instead of import to avoid circular dependency issues
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { AuthServiceMock } = require("./AuthServiceMock")
|
||||
AuthService.instance = AuthServiceMock.getInstance(context, config || {}, authProvider)
|
||||
AuthService.instance = AuthServiceMock.getInstance(controller, config || {}, authProvider)
|
||||
} else {
|
||||
AuthService.instance = new AuthService(context, config || {}, authProvider)
|
||||
AuthService.instance = new AuthService(controller, config || {}, authProvider)
|
||||
}
|
||||
}
|
||||
if (context !== undefined && AuthService.instance) {
|
||||
AuthService.instance.context = context
|
||||
if (controller !== undefined && AuthService.instance) {
|
||||
AuthService.instance.controller = controller
|
||||
}
|
||||
return AuthService.instance!
|
||||
}
|
||||
|
||||
set context(context: vscode.ExtensionContext) {
|
||||
this._context = context
|
||||
set controller(controller: Controller) {
|
||||
this._controller = controller
|
||||
}
|
||||
|
||||
get authProvider(): any {
|
||||
@@ -195,7 +196,9 @@ export class AuthService {
|
||||
throw new Error("Authentication URI is not configured")
|
||||
}
|
||||
|
||||
const callbackUrl = `${vscode.env.uriScheme || "vscode"}://saoudrizwan.claude-dev/auth`
|
||||
const callbackHost =
|
||||
(await AuthHandler.getInstance().getCallbackUri()) || `${vscode.env.uriScheme || "vscode"}://saoudrizwan.claude-dev`
|
||||
const callbackUrl = `${callbackHost}/auth`
|
||||
|
||||
// Use URL object for more graceful query construction
|
||||
const authUrl = new URL(this._config.URI)
|
||||
@@ -228,7 +231,7 @@ export class AuthService {
|
||||
}
|
||||
|
||||
try {
|
||||
this._clineAuthInfo = await this._provider.provider.signIn(this._context, token, provider)
|
||||
this._clineAuthInfo = await this._provider.provider.signIn(this._controller, token, provider)
|
||||
this._authenticated = true
|
||||
|
||||
if (this._clineAuthInfo) {
|
||||
@@ -248,7 +251,7 @@ export class AuthService {
|
||||
* This is typically called when the user logs out.
|
||||
*/
|
||||
async clearAuthToken(): Promise<void> {
|
||||
await storeSecret(this._context, "clineAccountId", undefined)
|
||||
this._controller.cacheService.setSecret("clineAccountId", undefined)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -261,7 +264,7 @@ export class AuthService {
|
||||
}
|
||||
|
||||
try {
|
||||
this._clineAuthInfo = await this._provider.provider.retrieveClineAuthInfo(this._context)
|
||||
this._clineAuthInfo = await this._provider.provider.retrieveClineAuthInfo(this._controller)
|
||||
if (this._clineAuthInfo) {
|
||||
this._authenticated = true
|
||||
telemetryService.identifyAccount(this._clineAuthInfo.userInfo)
|
||||
|
||||
@@ -4,10 +4,11 @@ import { clineEnvConfig } from "@/config"
|
||||
import { WebviewProvider } from "@/core/webview"
|
||||
import type { UserResponse } from "@/shared/ClineAccount"
|
||||
import { AuthService, type ServiceConfig } from "./AuthService"
|
||||
import { Controller } from "@/core/controller"
|
||||
|
||||
export class AuthServiceMock extends AuthService {
|
||||
protected constructor(context: vscode.ExtensionContext, config: ServiceConfig, authProvider?: any) {
|
||||
super(context, config, authProvider)
|
||||
protected constructor(controller: Controller, config: ServiceConfig, authProvider?: any) {
|
||||
super(controller, config, authProvider)
|
||||
|
||||
if (process?.env?.CLINE_ENVIRONMENT !== "local") {
|
||||
throw new Error("AuthServiceMock should only be used in local environment for testing purposes.")
|
||||
@@ -18,26 +19,22 @@ export class AuthServiceMock extends AuthService {
|
||||
const providerName = "firebase"
|
||||
this._setProvider(providerName)
|
||||
|
||||
this._context = context
|
||||
this._controller = controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the singleton instance of AuthServiceMock.
|
||||
*/
|
||||
public static override getInstance(
|
||||
context?: vscode.ExtensionContext,
|
||||
config?: ServiceConfig,
|
||||
authProvider?: any,
|
||||
): AuthServiceMock {
|
||||
public static override getInstance(controller?: Controller, config?: ServiceConfig, authProvider?: any): AuthServiceMock {
|
||||
if (!AuthServiceMock.instance) {
|
||||
if (!context) {
|
||||
console.warn("Extension context was not provided to AuthServiceMock.getInstance, using default context")
|
||||
context = {} as vscode.ExtensionContext
|
||||
if (!controller) {
|
||||
console.error("Extension controller was not provided to AuthServiceMock.getInstance")
|
||||
throw new Error("Extension controller was not provided to AuthServiceMock.getInstance")
|
||||
}
|
||||
AuthServiceMock.instance = new AuthServiceMock(context, config || {}, authProvider)
|
||||
AuthServiceMock.instance = new AuthServiceMock(controller, config || {}, authProvider)
|
||||
}
|
||||
if (context !== undefined) {
|
||||
AuthServiceMock.instance.context = context
|
||||
if (controller !== undefined) {
|
||||
AuthServiceMock.instance.controller = controller
|
||||
}
|
||||
return AuthServiceMock.instance
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ExtensionContext } from "vscode"
|
||||
import { ClineAccountUserInfo, ClineAuthInfo } from "../AuthService"
|
||||
import { jwtDecode } from "jwt-decode"
|
||||
import { clineEnvConfig } from "@/config"
|
||||
import { Controller } from "@/core/controller"
|
||||
|
||||
export class FirebaseAuthProvider {
|
||||
private _config: any
|
||||
@@ -41,8 +42,8 @@ export class FirebaseAuthProvider {
|
||||
* @returns {Promise<User>} A promise that resolves with the authenticated user.
|
||||
* @throws {Error} Throws an error if the restoration fails.
|
||||
*/
|
||||
async retrieveClineAuthInfo(context: ExtensionContext): Promise<ClineAuthInfo | null> {
|
||||
const userRefreshToken = await getSecret(context, "clineAccountId")
|
||||
async retrieveClineAuthInfo(controller: Controller): Promise<ClineAuthInfo | null> {
|
||||
const userRefreshToken = controller.cacheService.getSecretKey("clineAccountId")
|
||||
if (!userRefreshToken) {
|
||||
console.error("No stored authentication credential found.")
|
||||
return null
|
||||
@@ -100,7 +101,7 @@ export class FirebaseAuthProvider {
|
||||
* @returns {Promise<User>} A promise that resolves with the authenticated user.
|
||||
* @throws {Error} Throws an error if the sign-in fails.
|
||||
*/
|
||||
async signIn(context: ExtensionContext, token: string, provider: string): Promise<ClineAuthInfo | null> {
|
||||
async signIn(controller: Controller, token: string, provider: string): Promise<ClineAuthInfo | null> {
|
||||
try {
|
||||
let credential
|
||||
switch (provider) {
|
||||
@@ -123,7 +124,7 @@ export class FirebaseAuthProvider {
|
||||
|
||||
// store the long-lived refresh token in secret storage
|
||||
try {
|
||||
await storeSecret(context, "clineAccountId", userCredential.refreshToken)
|
||||
controller.cacheService.setSecret("clineAccountId", userCredential.refreshToken)
|
||||
} catch (error) {
|
||||
ErrorService.logMessage("Firebase store token error", "error")
|
||||
ErrorService.logException(error)
|
||||
@@ -131,7 +132,7 @@ export class FirebaseAuthProvider {
|
||||
}
|
||||
|
||||
// userCredential = await this._signInWithCredential(context, credential)
|
||||
return await this.retrieveClineAuthInfo(context)
|
||||
return await this.retrieveClineAuthInfo(controller)
|
||||
} catch (error) {
|
||||
ErrorService.logMessage("Firebase sign-in error", "error")
|
||||
ErrorService.logException(error)
|
||||
|
||||
@@ -41,15 +41,17 @@ export class BrowserSession {
|
||||
private lastConnectionAttempt: number = 0
|
||||
browserSettings: BrowserSettings
|
||||
private isConnectedToRemote: boolean = false
|
||||
private useWebp: boolean
|
||||
|
||||
// Telemetry tracking properties
|
||||
private sessionStartTime: number = 0
|
||||
private browserActions: string[] = []
|
||||
private taskId?: string
|
||||
|
||||
constructor(context: vscode.ExtensionContext, browserSettings: BrowserSettings) {
|
||||
constructor(context: vscode.ExtensionContext, browserSettings: BrowserSettings, useWebp: boolean = true) {
|
||||
this.context = context
|
||||
this.browserSettings = browserSettings
|
||||
this.useWebp = useWebp
|
||||
}
|
||||
|
||||
// Tests remote browser connection
|
||||
@@ -487,14 +489,16 @@ export class BrowserSession {
|
||||
// },
|
||||
}
|
||||
|
||||
const screenshotType = this.useWebp ? "webp" : "png"
|
||||
let screenshotBase64 = await this.page.screenshot({
|
||||
...options,
|
||||
type: "webp",
|
||||
type: screenshotType,
|
||||
})
|
||||
let screenshot = `data:image/webp;base64,${screenshotBase64}`
|
||||
let screenshot = `data:image/${screenshotType};base64,${screenshotBase64}`
|
||||
|
||||
if (!screenshotBase64) {
|
||||
console.info("webp screenshot failed, trying png")
|
||||
// choosing to try screenshot again, regardless of the initial type
|
||||
console.info(`${screenshotType} screenshot failed, trying png`)
|
||||
screenshotBase64 = await this.page.screenshot({
|
||||
...options,
|
||||
type: "png",
|
||||
|
||||
@@ -5,21 +5,15 @@ import { execa } from "execa"
|
||||
import { Logger } from "@services/logging/Logger"
|
||||
import { WebviewProvider } from "@core/webview"
|
||||
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
|
||||
import { TaskServiceClient } from "webview-ui/src/services/grpc-client"
|
||||
import { validateWorkspacePath, initializeGitRepository, getFileChanges, calculateToolSuccessRate } from "./GitHelper"
|
||||
import {
|
||||
updateGlobalState,
|
||||
getAllExtensionState,
|
||||
updateApiConfiguration,
|
||||
storeSecret,
|
||||
updateWorkspaceState,
|
||||
} from "@core/storage/state"
|
||||
import { updateGlobalState, getAllExtensionState, storeSecret } from "@core/storage/state"
|
||||
import { ClineAsk, ExtensionMessage } from "@shared/ExtensionMessage"
|
||||
import { ApiProvider } from "@shared/api"
|
||||
import { HistoryItem } from "@shared/HistoryItem"
|
||||
import { getSavedClineMessages, getSavedApiConversationHistory } from "@core/storage/disk"
|
||||
import { AskResponseRequest } from "@shared/proto/cline/task"
|
||||
import { getCwd } from "@/utils/path"
|
||||
import { askResponse } from "@core/controller/task/askResponse"
|
||||
|
||||
/**
|
||||
* Creates a tracker to monitor tool calls and failures during task execution
|
||||
@@ -268,14 +262,17 @@ export function createTestServer(webviewProvider?: WebviewProvider): http.Server
|
||||
}
|
||||
|
||||
// Store the API key securely
|
||||
await storeSecret(visibleWebview.controller.context, "clineAccountId", apiKey)
|
||||
visibleWebview.controller.cacheService.setSecret("clineAccountId", apiKey)
|
||||
|
||||
// Update the API configuration
|
||||
await updateApiConfiguration(visibleWebview.controller.context, updatedConfig)
|
||||
visibleWebview.controller.cacheService.setApiConfiguration(updatedConfig)
|
||||
|
||||
// Update global state to use cline provider
|
||||
await updateGlobalState(visibleWebview.controller.context, "planModeApiProvider", "cline")
|
||||
await updateGlobalState(visibleWebview.controller.context, "actModeApiProvider", "cline")
|
||||
// Update cache service to use cline provider
|
||||
const currentConfig = visibleWebview.controller.cacheService.getApiConfiguration()
|
||||
visibleWebview.controller.cacheService.setApiConfiguration({
|
||||
...currentConfig,
|
||||
planModeApiProvider: "cline",
|
||||
actModeApiProvider: "cline",
|
||||
})
|
||||
|
||||
// Post state to webview to reflect changes
|
||||
await visibleWebview.controller.postStateToWebview()
|
||||
@@ -624,9 +621,10 @@ async function autoRespondToAsk(webviewProvider: WebviewProvider, askType: Cline
|
||||
// we use the default "yesButtonClicked" to approve the action
|
||||
}
|
||||
|
||||
// Send the response message
|
||||
// Send the response message using the backend controller method
|
||||
try {
|
||||
await TaskServiceClient.askResponse(
|
||||
await askResponse(
|
||||
webviewProvider.controller,
|
||||
AskResponseRequest.create({
|
||||
responseType,
|
||||
text: responseText,
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import * as vscode from "vscode"
|
||||
import { WebviewProvider } from "@/core/webview"
|
||||
|
||||
/**
|
||||
* Shared URI handler that processes both VSCode URI events and HTTP server callbacks
|
||||
*/
|
||||
export class SharedUriHandler {
|
||||
/**
|
||||
* Processes a URI and routes it to the appropriate handler
|
||||
* @param uri The URI to process (can be from VSCode or converted from HTTP)
|
||||
* @returns Promise<boolean> indicating success (true) or failure (false)
|
||||
*/
|
||||
public static async handleUri(uri: vscode.Uri): Promise<boolean> {
|
||||
console.log("SharedUriHandler: Processing URI:", {
|
||||
path: uri.path,
|
||||
query: uri.query,
|
||||
scheme: uri.scheme,
|
||||
})
|
||||
|
||||
const path = uri.path
|
||||
const query = new URLSearchParams(uri.query.replace(/\+/g, "%2B"))
|
||||
const visibleWebview = WebviewProvider.getVisibleInstance()
|
||||
|
||||
if (!visibleWebview) {
|
||||
console.warn("SharedUriHandler: No visible webview found")
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
switch (path) {
|
||||
case "/openrouter": {
|
||||
const code = query.get("code")
|
||||
if (code) {
|
||||
await visibleWebview.controller.handleOpenRouterCallback(code)
|
||||
return true
|
||||
}
|
||||
console.warn("SharedUriHandler: Missing code parameter for OpenRouter callback")
|
||||
return false
|
||||
}
|
||||
case "/auth": {
|
||||
console.log("SharedUriHandler: Auth callback received:", { path: uri.path, provider: query.get("provider") })
|
||||
|
||||
const token = query.get("idToken")
|
||||
const provider = query.get("provider")
|
||||
|
||||
if (token) {
|
||||
await visibleWebview.controller.handleAuthCallback(token, provider)
|
||||
return true
|
||||
}
|
||||
console.warn("SharedUriHandler: Missing idToken parameter for auth callback")
|
||||
return false
|
||||
}
|
||||
default:
|
||||
console.warn(`SharedUriHandler: Unknown path: ${path}`)
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("SharedUriHandler: Error processing URI:", error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an HTTP URL to a vscode.Uri for unified processing
|
||||
* @param httpUrl The HTTP URL to convert
|
||||
* @returns vscode.Uri representation of the URL
|
||||
*/
|
||||
public static convertHttpUrlToUri(httpUrl: string): vscode.Uri {
|
||||
return vscode.Uri.parse(httpUrl)
|
||||
}
|
||||
}
|
||||
+24
-4
@@ -2480,8 +2480,28 @@ export const sambanovaModels = {
|
||||
// Cerebras
|
||||
// https://inference-docs.cerebras.ai/api-reference/models
|
||||
export type CerebrasModelId = keyof typeof cerebrasModels
|
||||
export const cerebrasDefaultModelId: CerebrasModelId = "qwen-3-235b-a22b-instruct-2507"
|
||||
export const cerebrasDefaultModelId: CerebrasModelId = "qwen-3-coder-480b-free"
|
||||
export const cerebrasModels = {
|
||||
"qwen-3-coder-480b-free": {
|
||||
maxTokens: 40000,
|
||||
contextWindow: 64000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description:
|
||||
"SOTA coding model with ~2000 tokens/s ($0 free tier)\n\n• Use this if you don't have a Cerebras subscription\n• 64K context window\n• Rate limits: 150K TPM, 1M TPH/TPD, 10 RPM, 100 RPH/RPD\n\nUpgrade for higher limits: [https://cloud.cerebras.ai/?utm=cline](https://cloud.cerebras.ai/?utm=cline)",
|
||||
},
|
||||
"qwen-3-coder-480b": {
|
||||
maxTokens: 40000,
|
||||
contextWindow: 128000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description:
|
||||
"SOTA coding model with ~2000 tokens/s ($50/$250 paid tiers)\n\n• Use this if you have a Cerebras subscription\n• 131K context window with higher rate limits",
|
||||
},
|
||||
"qwen-3-235b-a22b-instruct-2507": {
|
||||
maxTokens: 64000,
|
||||
contextWindow: 64000,
|
||||
@@ -2509,9 +2529,9 @@ export const cerebrasModels = {
|
||||
outputPrice: 0,
|
||||
description: "SOTA coding performance with ~2500 tokens/s",
|
||||
},
|
||||
"qwen-3-235b-a22b": {
|
||||
maxTokens: 40000,
|
||||
contextWindow: 40000,
|
||||
"qwen-3-235b-a22b-thinking-2507": {
|
||||
maxTokens: 32000,
|
||||
contextWindow: 65000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { activate } from "@/extension"
|
||||
import { Controller } from "@core/controller"
|
||||
import { CacheService } from "@core/storage/CacheService"
|
||||
import { ExternalDiffViewProvider } from "@hosts/external/ExternalDiffviewProvider"
|
||||
import { ExternalWebviewProvider } from "@hosts/external/ExternalWebviewProvider"
|
||||
import { ExternalHostBridgeClientManager } from "@hosts/external/host-bridge-client-manager"
|
||||
@@ -9,6 +10,7 @@ import { v4 as uuidv4 } from "uuid"
|
||||
import { log } from "./utils"
|
||||
import { extensionContext, postMessage } from "./vscode-context"
|
||||
import { startProtobusService } from "./protobus-service"
|
||||
import { AuthHandler } from "@/hosts/external/AuthHandler"
|
||||
import { WebviewProvider } from "@/core/webview"
|
||||
import { DiffViewProvider } from "@/integrations/editor/DiffViewProvider"
|
||||
|
||||
@@ -21,8 +23,13 @@ async function main() {
|
||||
setupGlobalErrorHandlers()
|
||||
|
||||
activate(extensionContext)
|
||||
// Create and initialize cache service
|
||||
|
||||
// Create controller with cache service
|
||||
const controller = new Controller(extensionContext, postMessage, uuidv4())
|
||||
startProtobusService(controller)
|
||||
|
||||
AuthHandler.getInstance().setEnabled(true)
|
||||
}
|
||||
|
||||
function setupHostProvider() {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import type { DiffViewProviderCreator, WebviewProviderCreator } from "@/hosts/host-provider"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { HostBridgeClientProvider } from "@/hosts/host-provider-types"
|
||||
import * as stateModule from "@core/storage/state"
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import { describe, it, beforeEach, afterEach } from "mocha"
|
||||
import * as should from "should"
|
||||
import * as sinon from "sinon"
|
||||
import type { ClineAPI } from "../cline"
|
||||
import { createClineAPI } from "../index"
|
||||
import type { ClineAPI } from "../exports/cline"
|
||||
import { DiffViewProviderCreator, HostProvider, WebviewProviderCreator } from "@/hosts/host-provider"
|
||||
import { vscodeHostBridgeClient } from "@/hosts/vscode/hostbridge/client/host-grpc-client"
|
||||
import * as stateModule from "@core/storage/state"
|
||||
import { createClineAPI } from "@/exports"
|
||||
|
||||
describe("ClineAPI Core Functionality", () => {
|
||||
let api: ClineAPI
|
||||
@@ -15,15 +14,16 @@ describe("ClineAPI Core Functionality", () => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
let getGlobalStateStub: sinon.SinonStub
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Create mock log function
|
||||
mockLogToChannel = sandbox.stub<[string], void>()
|
||||
HostProvider.reset()
|
||||
HostProvider.initialize(
|
||||
((_) => {}) as WebviewProviderCreator,
|
||||
(() => {}) as DiffViewProviderCreator,
|
||||
{} as HostBridgeClientProvider,
|
||||
vscodeHostBridgeClient,
|
||||
mockLogToChannel,
|
||||
)
|
||||
// Stub the getGlobalState function from the state module
|
||||
@@ -33,6 +33,7 @@ describe("ClineAPI Core Functionality", () => {
|
||||
// Create a mock controller that matches what the real createClineAPI expects
|
||||
// We don't import the real Controller to avoid the webview dependencies
|
||||
mockController = {
|
||||
id: "test-controller-id",
|
||||
context: {
|
||||
globalState: {
|
||||
get: sandbox.stub(),
|
||||
@@ -73,10 +74,6 @@ describe("ClineAPI Core Functionality", () => {
|
||||
// Verify task clearing sequence
|
||||
sinon.assert.called(mockController.clearTask)
|
||||
sinon.assert.called(mockController.postStateToWebview)
|
||||
sinon.assert.calledWith(mockController.postMessageToWebview, {
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
sinon.assert.calledWith(mockController.initTask, taskDescription, images)
|
||||
|
||||
// Verify logging - first it logs "Starting new task"
|
||||
@@ -124,7 +124,8 @@ export class E2ETestHelper {
|
||||
}
|
||||
|
||||
public static async runCommandPalette(page: Page, command: string): Promise<void> {
|
||||
await page.locator("li").filter({ hasText: "[Extension Development Host]" }).first().click()
|
||||
const editorMenu = page.locator("li").filter({ hasText: "[Extension Development Host]" }).first()
|
||||
await editorMenu.click()
|
||||
const editorSearchBar = page.getByRole("textbox", {
|
||||
name: "Search files by name (append",
|
||||
})
|
||||
@@ -273,6 +274,7 @@ export const e2e = test
|
||||
.extend({
|
||||
page: async ({ app }, use) => {
|
||||
const page = await app.firstWindow()
|
||||
// Disable notifications before opening sidebar
|
||||
await E2ETestHelper.runCommandPalette(page, "notifications: toggle do not disturb")
|
||||
await use(page)
|
||||
},
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import * as vscode from "vscode"
|
||||
|
||||
/**
|
||||
* Gets the latest announcement ID based on the extension version
|
||||
* Uses major.minor version format (e.g., "1.2" from "1.2.3")
|
||||
*
|
||||
* @param context The VSCode extension context
|
||||
* @returns The announcement ID string (major.minor version) or empty string if unavailable
|
||||
*/
|
||||
export function getLatestAnnouncementId(context: vscode.ExtensionContext): string {
|
||||
return context.extension?.packageJSON?.version?.split(".").slice(0, 2).join(".") ?? ""
|
||||
}
|
||||
@@ -20,6 +20,12 @@ export function isGrok4ModelFamily(api: ApiHandler): boolean {
|
||||
return modelId.includes("grok-4")
|
||||
}
|
||||
|
||||
export function modelDoesntSupportWebp(api: ApiHandler): boolean {
|
||||
const model = api.getModel()
|
||||
const modelId = model.id.toLowerCase()
|
||||
return modelId.includes("grok")
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if reasoning content should be skipped for a given model
|
||||
* Currently skips reasoning for Grok-4 models since they only display "thinking" without useful information
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const tsConfigPaths = require("tsconfig-paths")
|
||||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
const Module = require("module")
|
||||
|
||||
const baseUrl = path.resolve(__dirname)
|
||||
|
||||
@@ -23,3 +24,16 @@ tsConfigPaths.register({
|
||||
baseUrl: baseUrl,
|
||||
paths: outPaths,
|
||||
})
|
||||
|
||||
// Mock the @google/genai module to avoid ESM compatibility issues in tests
|
||||
// The module is ES6 only, but the integration tests are compiled to commonJS.
|
||||
const originalRequire = Module.prototype.require
|
||||
Module.prototype.require = function (id) {
|
||||
// Intercept requires for @google/genai
|
||||
if (id === "@google/genai") {
|
||||
// Return the mock instead
|
||||
const mockPath = path.join(baseUrl, "out/src/api/providers/gemini-mock.test.js")
|
||||
return originalRequire.call(this, mockPath)
|
||||
}
|
||||
return originalRequire.call(this, id)
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ const AppContent = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full">
|
||||
<div className="flex h-screen w-full flex-col">
|
||||
{showSettings && <SettingsView onDone={hideSettings} />}
|
||||
{showHistory && <HistoryView onDone={hideHistory} />}
|
||||
{showMcp && <McpView initialTab={mcpTab} onDone={closeMcpView} />}
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
useScrollBehavior,
|
||||
WelcomeSection,
|
||||
} from "./chat-view"
|
||||
import AutoApproveBar from "./auto-approve-menu/AutoApproveBar"
|
||||
|
||||
interface ChatViewProps {
|
||||
isHidden: boolean
|
||||
@@ -198,8 +199,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
|
||||
// Use message handlers hook
|
||||
const messageHandlers = useMessageHandlers(messages, chatState, isStreaming)
|
||||
const { handleSendMessage, handlePrimaryButtonClick, handleSecondaryButtonClick, handleTaskCloseButtonClick } =
|
||||
messageHandlers
|
||||
|
||||
const { selectedModelInfo } = useMemo(() => {
|
||||
return normalizeApiConfiguration(apiConfiguration, mode)
|
||||
@@ -347,17 +346,19 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
showHistoryView={showHistoryView}
|
||||
/>
|
||||
)}
|
||||
|
||||
{task && (
|
||||
<>
|
||||
<MessagesArea
|
||||
task={task}
|
||||
groupedMessages={groupedMessages}
|
||||
modifiedMessages={modifiedMessages}
|
||||
scrollBehavior={scrollBehavior}
|
||||
chatState={chatState}
|
||||
messageHandlers={messageHandlers}
|
||||
/>
|
||||
<MessagesArea
|
||||
task={task}
|
||||
groupedMessages={groupedMessages}
|
||||
modifiedMessages={modifiedMessages}
|
||||
scrollBehavior={scrollBehavior}
|
||||
chatState={chatState}
|
||||
messageHandlers={messageHandlers}
|
||||
/>
|
||||
)}
|
||||
<footer className="flex-shrink-0 justify-end">
|
||||
<AutoApproveBar />
|
||||
{task && (
|
||||
<ActionButtons
|
||||
chatState={chatState}
|
||||
messageHandlers={messageHandlers}
|
||||
@@ -368,17 +369,16 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
showScrollToBottom: scrollBehavior.showScrollToBottom,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<InputSection
|
||||
chatState={chatState}
|
||||
messageHandlers={messageHandlers}
|
||||
scrollBehavior={scrollBehavior}
|
||||
placeholderText={placeholderText}
|
||||
shouldDisableFilesAndImages={shouldDisableFilesAndImages}
|
||||
selectFilesAndImages={selectFilesAndImages}
|
||||
/>
|
||||
)}
|
||||
<InputSection
|
||||
chatState={chatState}
|
||||
messageHandlers={messageHandlers}
|
||||
scrollBehavior={scrollBehavior}
|
||||
placeholderText={placeholderText}
|
||||
shouldDisableFilesAndImages={shouldDisableFilesAndImages}
|
||||
selectFilesAndImages={selectFilesAndImages}
|
||||
/>
|
||||
</footer>
|
||||
</ChatLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,12 +17,12 @@ export const ChatLayout: React.FC<ChatLayoutProps> = ({ isHidden, children }) =>
|
||||
const ChatLayoutContainer = styled.div.withConfig({
|
||||
shouldForwardProp: (prop) => !["isHidden"].includes(prop),
|
||||
})<{ isHidden: boolean }>`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: ${(props) => (props.isHidden ? "none" : "flex")};
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
`
|
||||
|
||||
@@ -62,8 +62,8 @@ export const MessagesArea: React.FC<MessagesAreaProps> = ({
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ flexGrow: 1, display: "flex" }} ref={scrollContainerRef}>
|
||||
<div className="overflow-hidden flex flex-col h-full">
|
||||
<div className="flex-grow flex" ref={scrollContainerRef}>
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
key={task.ts} // trick to make sure virtuoso re-renders when task changes, and we use initialTopMostItemIndex to start at the bottom
|
||||
@@ -93,7 +93,6 @@ export const MessagesArea: React.FC<MessagesAreaProps> = ({
|
||||
initialTopMostItemIndex={groupedMessages.length - 1}
|
||||
/>
|
||||
</div>
|
||||
<AutoApproveBar />
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,23 +21,14 @@ export const WelcomeSection: React.FC<WelcomeSectionProps> = ({
|
||||
shouldShowQuickWins,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
flex: "1 1 0",
|
||||
minHeight: 0,
|
||||
overflowY: "auto",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
paddingBottom: "10px",
|
||||
}}>
|
||||
<div className="flex flex-col flex-1 w-full h-full p-0 m-0">
|
||||
<div className="overflow-y-auto flex flex-col pb-2.5">
|
||||
{telemetrySetting === "unset" && <TelemetryBanner />}
|
||||
{showAnnouncement && <Announcement version={version} hideAnnouncement={hideAnnouncement} />}
|
||||
<HomeHeader />
|
||||
{!shouldShowQuickWins && taskHistory.length > 0 && <HistoryPreview showHistoryView={showHistoryView} />}
|
||||
</div>
|
||||
<SuggestedTasks shouldShowQuickWins={shouldShowQuickWins} />
|
||||
<AutoApproveBar />
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export const Navbar = () => {
|
||||
return (
|
||||
<nav
|
||||
id="cline-navbar-container"
|
||||
className="fixed top-0 right-2 inline-flex justify-end bg-transparent shadow-sm max-h-[20px] w-full gap-2 mb-1 z-10 border-none items-center">
|
||||
className="flex-none inline-flex justify-end bg-transparent shadow-sm gap-2 mb-1 z-10 border-none items-center mr-4!">
|
||||
{SETTINGS_TABS.map((tab) => (
|
||||
<TabTrigger
|
||||
key={`navbar-trigger-${tab.id}`}
|
||||
|
||||
@@ -26,11 +26,20 @@ https://github.com/gitkraken/vscode-gitlens/blob/b1d71d4844523e8b2ef16f9e007068e
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 1.25;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#root {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body.scrollable,
|
||||
|
||||
Reference in New Issue
Block a user