Compare commits

...

3 Commits

Author SHA1 Message Date
abeatrix ead62eab65 Apply type safe measures and remove unused constructor 2025-09-03 14:06:17 -07:00
abeatrix 7571506c78 Fix all affected files 2025-09-03 13:52:09 -07:00
abeatrix 0bd935f683 dev: apply biome rules to remove unused variables and improve type safety
Apply biome rules: noEmptyPattern, noInvalidPositionAtImportRule, noSwitchDeclarations, noDoubleEquals, noThenProperty, noExplicitAny, noImportAssign, noUselessConstructor, noUselessCatch, noUselessSwitchCase, noStaticOnlyClass

- Remove unused variables and destructured parameters across multiple files
- Add proper type guards for React children in MarkdownBlock component
- Clean up biome.jsonc linting rules by removing noEmptyPattern exception
- Improve error handling and null checks throughout codebase
- Standardize variable usage patterns in API providers and core modules
2025-09-03 13:51:51 -07:00
83 changed files with 356 additions and 390 deletions
+9 -19
View File
@@ -30,14 +30,11 @@
"correctness": {
"useExhaustiveDependencies": "off",
"noUndeclaredVariables": "off",
"noEmptyPattern": "off",
"useJsxKeyInIterable": "off",
"noInnerDeclarations": "off",
"useHookAtTopLevel": "off",
"useYield": "off",
"noConstructorReturn": "off",
"noInvalidPositionAtImportRule": "off",
"noSwitchDeclarations": "off",
"noUnusedImports": "error"
},
"a11y": "off",
@@ -61,25 +58,18 @@
"noUselessElse": "off"
},
"suspicious": {
"noDoubleEquals": "warn",
"noImplicitAnyLet": "info",
"noThenProperty": "off",
"noAsyncPromiseExecutor": "off",
"noImportAssign": "off",
"noExplicitAny": "off",
"noControlCharactersInRegex": "off",
"noShadowRestrictedNames": "off",
"noThenProperty": "warn",
"noAsyncPromiseExecutor": "warn",
"noExplicitAny": "warn",
"noImportAssign": "warn",
"noArrayIndexKey": "info",
"noAssignInExpressions": "warn"
"noAssignInExpressions": "warn",
"noShadowRestrictedNames": "info",
"noControlCharactersInRegex": "warn"
},
"complexity": {
"noUselessConstructor": "off",
"useOptionalChain": "off",
"noBannedTypes": "off",
"useLiteralKeys": "off",
"noUselessCatch": "off",
"noUselessSwitchCase": "off",
"noStaticOnlyClass": "off"
"useOptionalChain": "info",
"useLiteralKeys": "off"
},
"security": {
"noDangerouslySetInnerHtml": "warn"
+1 -1
View File
@@ -200,7 +200,7 @@ async function checkProtos() {
continue
}
// Check message fields
if (def && def.type && def.type.field) {
if (def?.type?.field) {
for (const field of def.type.field) {
if (int64TypeNames.includes(field.type)) {
const name = `${packageName}.${messageName}.${field.name}`
+1 -1
View File
@@ -66,7 +66,7 @@ async function main(): Promise<void> {
}
try {
const apiServer = await ClineApiServerMock.startGlobalServer()
const _apiServer = await ClineApiServerMock.startGlobalServer()
console.log("Cline API Server started in-process")
} catch (error) {
console.error("Failed to start Cline API Server:", error)
+2 -4
View File
@@ -4,13 +4,11 @@ import { ApiHandler } from "../../core/api/index"
import { ApiStream } from "../../core/api/transform/stream"
export class DifyHandler implements ApiHandler {
private options: ApiHandlerOptions
private baseUrl: string
private apiKey: string
private conversationId: string | null = null
constructor(options: ApiHandlerOptions) {
this.options = options
this.apiKey = options.difyApiKey || ""
this.baseUrl = options.difyBaseUrl || ""
@@ -59,7 +57,7 @@ export class DifyHandler implements ApiHandler {
},
body: JSON.stringify(requestBody),
})
} catch (error: any) {
} catch (error) {
console.error("[DIFY DEBUG] Network error during fetch:", error)
// Log more detailed error information if available (e.g., from undici)
const cause = error.cause ? ` | Cause: ${error.cause}` : ""
@@ -236,7 +234,7 @@ export class DifyHandler implements ApiHandler {
console.error("[DIFY DEBUG] Direct JSON Error event:", parsed)
throw new Error(`Dify API error: ${parsed.message || "Unknown error"}`)
}
} catch (e) {
} catch (_e) {
// Not JSON, continue
console.log("[DIFY DEBUG] Line is not direct JSON, continuing")
}
+2 -1
View File
@@ -152,7 +152,7 @@ export class AnthropicHandler implements ApiHandler {
for await (const chunk of stream) {
switch (chunk?.type) {
case "message_start":
case "message_start": {
// tells us cache reads/writes/input/output
const usage = chunk.message.usage
yield {
@@ -163,6 +163,7 @@ export class AnthropicHandler implements ApiHandler {
cacheReadTokens: usage.cache_read_input_tokens || undefined,
}
break
}
case "message_delta":
// tells us stop_reason, stop_sequence, and output tokens along the way and at the end of the message
+1 -1
View File
@@ -861,7 +861,7 @@ export class AwsBedrockHandler implements ApiHandler {
if (item.source.media_type) {
// Extract format from media_type (e.g., "image/jpeg" -> "jpeg")
const formatMatch = item.source.media_type.match(/image\/(\w+)/)
if (formatMatch && formatMatch[1]) {
if (formatMatch?.[1]) {
const extractedFormat = formatMatch[1]
// Ensure format is one of the allowed values
if (["png", "jpeg", "gif", "webp"].includes(extractedFormat)) {
+2 -2
View File
@@ -70,8 +70,8 @@ export class ClaudeCodeHandler implements ApiHandler {
if (message.stop_reason !== null) {
const content = "text" in message.content[0] ? message.content[0] : undefined
const isError = content && content.text.startsWith(`API Error`)
if (isError) {
const isError = content?.text.startsWith(`API Error`)
if (content && isError) {
// Error messages are formatted as: `API Error: <<status code>> <<json>>`
const errorMessageStart = content.text.indexOf("{")
const errorMessage = content.text.slice(errorMessageStart)
+12 -9
View File
@@ -3,6 +3,14 @@ import { ApiHandlerOptions, ModelInfo } from "../../../shared/api"
import { ApiHandler } from "../index"
import { ApiStream } from "../transform/stream"
interface DifyRequestBody {
rating?: "like" | "dislike"
user: string
auto_generate?: boolean
name?: string
content?: string
}
// Dify API Response Types
export interface DifyFileResponse {
id: string
@@ -66,15 +74,11 @@ interface DifyConversationResponse {
}
export class DifyHandler implements ApiHandler {
private options: ApiHandlerOptions
private baseUrl: string
private apiKey: string
private conversationId: string | null = null
private currentTaskId: string | null = null
private abortController: AbortController | null = null
constructor(options: ApiHandlerOptions) {
this.options = options
this.apiKey = options.difyApiKey || ""
this.baseUrl = options.difyBaseUrl || ""
@@ -122,7 +126,7 @@ export class DifyHandler implements ApiHandler {
},
body: JSON.stringify(requestBody),
})
} catch (error: any) {
} catch (error) {
console.error("[DIFY DEBUG] Network error during fetch:", error)
const cause = error.cause ? ` | Cause: ${error.cause}` : ""
throw new Error(`Dify API network error: ${error.message}${cause}`)
@@ -336,7 +340,7 @@ export class DifyHandler implements ApiHandler {
}
hasYieldedContent = true
}
} catch (e) {
} catch (_e) {
// Not JSON, continue
console.log("[DIFY DEBUG] Line is not direct JSON, continuing")
}
@@ -573,7 +577,7 @@ export class DifyHandler implements ApiHandler {
name?: string,
autoGenerate: boolean = false,
): Promise<DifyConversationResponse> {
const body: any = { user, auto_generate: autoGenerate }
const body: DifyRequestBody = { user, auto_generate: autoGenerate }
if (name) {
body.name = name
}
@@ -609,7 +613,7 @@ export class DifyHandler implements ApiHandler {
content?: string,
user: string = "cline-user",
): Promise<void> {
const body: any = { rating, user }
const body: DifyRequestBody = { rating, user }
if (content) {
body.content = content
}
@@ -650,6 +654,5 @@ export class DifyHandler implements ApiHandler {
*/
resetConversation(): void {
this.conversationId = null
this.currentTaskId = null
}
}
@@ -1,10 +1,6 @@
// 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
+1 -1
View File
@@ -230,7 +230,7 @@ export class GeminiHandler implements ApiHandler {
// https://github.com/googleapis/js-genai/blob/v1.11.0/src/_api_client.ts#L758
const response = this.attemptParse(error.message)
if (response && response.error) {
if (response?.error) {
const responseBody = this.attemptParse(response.error.message)
if (responseBody.error) {
+29 -33
View File
@@ -66,47 +66,43 @@ export class HuggingFaceHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
try {
const client = this.ensureClient()
const model = this.getModel()
const client = this.ensureClient()
const model = this.getModel()
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
const requestParams = {
model: model.id,
max_tokens: model.info.maxTokens,
messages: openAiMessages,
stream: true,
stream_options: { include_usage: true },
temperature: 0,
}
const requestParams = {
model: model.id,
max_tokens: model.info.maxTokens,
messages: openAiMessages,
stream: true,
stream_options: { include_usage: true },
temperature: 0,
}
const stream = (await client.chat.completions.create(requestParams)) as any
const stream = (await client.chat.completions.create(requestParams)) as any
let _chunkCount = 0
let _totalContent = ""
let _chunkCount = 0
let _totalContent = ""
for await (const chunk of stream) {
_chunkCount++
const delta = chunk.choices[0]?.delta
if (delta?.content) {
_totalContent += delta.content
for await (const chunk of stream) {
_chunkCount++
const delta = chunk.choices[0]?.delta
if (delta?.content) {
_totalContent += delta.content
yield {
type: "text",
text: delta.content,
}
}
if (chunk.usage) {
yield* this.yieldUsage(model.info, chunk.usage)
yield {
type: "text",
text: delta.content,
}
}
} catch (error: any) {
throw error
if (chunk.usage) {
yield* this.yieldUsage(model.info, chunk.usage)
}
}
}
+2 -1
View File
@@ -106,7 +106,7 @@ export class OpenAiNativeHandler implements ApiHandler {
}
case "gpt-5-2025-08-07":
case "gpt-5-mini-2025-08-07":
case "gpt-5-nano-2025-08-07":
case "gpt-5-nano-2025-08-07": {
const stream = await client.chat.completions.create({
model: model.id,
temperature: 1,
@@ -130,6 +130,7 @@ export class OpenAiNativeHandler implements ApiHandler {
}
}
break
}
default: {
const stream = await client.chat.completions.create({
model: model.id,
+3 -3
View File
@@ -163,7 +163,7 @@ namespace Bedrock {
if (item.source.media_type) {
// Extract format from media_type (e.g., "image/jpeg" -> "jpeg")
const formatMatch = item.source.media_type.match(/image\/(\w+)/)
if (formatMatch && formatMatch[1]) {
if (formatMatch?.[1]) {
const extractedFormat = formatMatch[1]
// Ensure format is one of the allowed values
if (["png", "jpeg", "gif", "webp"].includes(extractedFormat)) {
@@ -257,7 +257,7 @@ namespace Gemini {
}
// Handle content parts for non-thought text
if (data.candidates && data.candidates[0]?.content?.parts) {
if (data.candidates?.[0]?.content?.parts) {
let nonThoughtText = ""
for (const part of data.candidates[0].content.parts) {
if (part.text && !part.thought) {
@@ -902,7 +902,7 @@ export class SapAiCoreHandler implements ApiHandler {
if (data.choices && data.choices.length > 0) {
const choice = data.choices[0]
if (choice.delta && choice.delta.content) {
if (choice.delta?.content) {
yield {
type: "text",
text: choice.delta.content,
+97 -96
View File
@@ -82,40 +82,66 @@ export class VertexHandler implements ApiHandler {
(modelId.includes("3-7") || modelId.includes("sonnet-4") || modelId.includes("opus-4")) &&
budget_tokens !== 0
)
let stream
switch (modelId) {
case "claude-sonnet-4@20250514":
case "claude-opus-4-1@20250805":
case "claude-opus-4@20250514":
case "claude-3-7-sonnet@20250219":
case "claude-3-5-sonnet-v2@20241022":
case "claude-3-5-sonnet@20240620":
case "claude-3-5-haiku@20241022":
case "claude-3-opus@20240229":
case "claude-3-haiku@20240307": {
// Find indices of user messages for cache control
const userMsgIndices = messages.reduce(
(acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc),
[] as number[],
)
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
stream = await clientAnthropic.beta.messages.create(
{
model: modelId,
max_tokens: model.info.maxTokens || 8192,
thinking: reasoningOn ? { type: "enabled", budget_tokens: budget_tokens } : undefined,
temperature: reasoningOn ? undefined : 0,
system: [
{
text: systemPrompt,
type: "text",
cache_control: { type: "ephemeral" },
},
],
messages: messages.map((message, index) => {
if (index === lastUserMsgIndex || index === secondLastMsgUserIndex) {
function getStream() {
switch (modelId) {
case "claude-sonnet-4@20250514":
case "claude-opus-4-1@20250805":
case "claude-opus-4@20250514":
case "claude-3-7-sonnet@20250219":
case "claude-3-5-sonnet-v2@20241022":
case "claude-3-5-sonnet@20240620":
case "claude-3-5-haiku@20241022":
case "claude-3-opus@20240229":
case "claude-3-haiku@20240307": {
// Find indices of user messages for cache control
const userMsgIndices = messages.reduce((acc, msg, index) => {
if (msg.role === "user") {
acc.push(index)
}
return acc
}, [] as number[])
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
return clientAnthropic.beta.messages.create(
{
model: modelId,
max_tokens: model.info.maxTokens || 8192,
thinking: reasoningOn ? { type: "enabled", budget_tokens: budget_tokens } : undefined,
temperature: reasoningOn ? undefined : 0,
system: [
{
text: systemPrompt,
type: "text",
cache_control: { type: "ephemeral" },
},
],
messages: messages.map((message, index) => {
if (index === lastUserMsgIndex || index === secondLastMsgUserIndex) {
return {
...message,
content:
typeof message.content === "string"
? [
{
type: "text",
text: message.content,
cache_control: {
type: "ephemeral",
},
},
]
: message.content.map((content, contentIndex) =>
contentIndex === message.content.length - 1
? {
...content,
cache_control: {
type: "ephemeral",
},
}
: content,
),
}
}
return {
...message,
content:
@@ -124,76 +150,50 @@ export class VertexHandler implements ApiHandler {
{
type: "text",
text: message.content,
cache_control: {
type: "ephemeral",
},
},
]
: message.content.map((content, contentIndex) =>
contentIndex === message.content.length - 1
? {
...content,
cache_control: {
type: "ephemeral",
},
}
: content,
),
: message.content,
}
}
return {
...message,
content:
typeof message.content === "string"
? [
{
type: "text",
text: message.content,
},
]
: message.content,
}
}),
stream: true,
},
{
headers: {},
},
)
break
}
default: {
stream = await clientAnthropic.beta.messages.create({
model: modelId,
max_tokens: model.info.maxTokens || 8192,
temperature: 0,
system: [
{
text: systemPrompt,
type: "text",
}),
stream: true,
},
],
messages: messages.map((message) => ({
...message,
content:
typeof message.content === "string"
? [
{
type: "text",
text: message.content,
},
]
: message.content,
})),
stream: true,
})
break
{
headers: {},
},
)
}
default: {
return clientAnthropic.beta.messages.create({
model: modelId,
max_tokens: model.info.maxTokens || 8192,
temperature: 0,
system: [
{
text: systemPrompt,
type: "text",
},
],
messages: messages.map((message) => ({
...message,
content:
typeof message.content === "string"
? [
{
type: "text",
text: message.content,
},
]
: message.content,
})),
stream: true,
})
}
}
}
for await (const chunk of stream) {
for await (const chunk of await getStream()) {
switch (chunk?.type) {
case "message_start":
case "message_start": {
const usage = chunk.message.usage
yield {
type: "usage",
@@ -203,6 +203,7 @@ export class VertexHandler implements ApiHandler {
cacheReadTokens: usage.cache_read_input_tokens || undefined,
}
break
}
case "message_delta":
yield {
type: "usage",
-1
View File
@@ -388,7 +388,6 @@ export function convertO1ResponseToAnthropicMessage(
return "max_tokens"
case "tool_calls":
return "tool_use"
case "content_filter": // Anthropic doesn't have an exact equivalent
default:
return null
}
-1
View File
@@ -173,7 +173,6 @@ export function convertToAnthropicMessage(completion: OpenAI.Chat.Completions.Ch
return "max_tokens"
case "tool_calls":
return "tool_use"
case "content_filter": // Anthropic doesn't have an exact equivalent
default:
return null
}
+4 -2
View File
@@ -48,7 +48,7 @@ export async function createOpenRouterStream(
case "anthropic/claude-3-haiku":
case "anthropic/claude-3-haiku:beta":
case "anthropic/claude-3-opus":
case "anthropic/claude-3-opus:beta":
case "anthropic/claude-3-opus:beta": {
openAiMessages[0] = {
role: "system",
content: [
@@ -80,6 +80,7 @@ export async function createOpenRouterStream(
}
})
break
}
default:
break
}
@@ -131,7 +132,7 @@ export async function createOpenRouterStream(
case "anthropic/claude-3.7-sonnet:beta":
case "anthropic/claude-3.7-sonnet:thinking":
case "anthropic/claude-3-7-sonnet":
case "anthropic/claude-3-7-sonnet:beta":
case "anthropic/claude-3-7-sonnet:beta": {
const budget_tokens = thinkingBudgetTokens || 0
const reasoningOn = budget_tokens !== 0
if (reasoningOn) {
@@ -139,6 +140,7 @@ export async function createOpenRouterStream(
reasoning = { max_tokens: budget_tokens }
}
break
}
case "cline/sonic":
temperature = 0.7
topP = 0.95
@@ -35,7 +35,7 @@ export async function createVercelAIGatewayStream(
// Find the last text part in the message content
const lastTextPart = msg.content.filter((part) => part.type === "text").pop()
if (lastTextPart && lastTextPart.text && lastTextPart.text.length > 0) {
if (lastTextPart?.text && lastTextPart.text.length > 0) {
// @ts-ignore-next-line
lastTextPart["cache_control"] = { type: "ephemeral" }
}
@@ -16,7 +16,7 @@ class ContextManager {
// If the previous API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request
if (previousApiReqIndex >= 0) {
const previousRequest = clineMessages[previousApiReqIndex]
if (previousRequest && previousRequest.text) {
if (previousRequest?.text) {
const { tokensIn, tokensOut, cacheWrites, cacheReads }: ClineApiReqInfo = JSON.parse(previousRequest.text)
const totalTokens = (tokensIn || 0) + (tokensOut || 0) + (cacheWrites || 0) + (cacheReads || 0)
const { maxAllowedSize } = getContextWindowInfo(api)
@@ -111,7 +111,7 @@ export class ContextManager {
shouldCompactContextWindow(clineMessages: ClineMessage[], api: ApiHandler, previousApiReqIndex: number): boolean {
if (previousApiReqIndex >= 0) {
const previousRequest = clineMessages[previousApiReqIndex]
if (previousRequest && previousRequest.text) {
if (previousRequest?.text) {
const { tokensIn, tokensOut, cacheWrites, cacheReads }: ClineApiReqInfo = JSON.parse(previousRequest.text)
const totalTokens = (tokensIn || 0) + (tokensOut || 0) + (cacheWrites || 0) + (cacheReads || 0)
@@ -150,7 +150,7 @@ export class ContextManager {
if (targetIndex >= 0) {
const targetRequest = clineMessages[targetIndex]
if (targetRequest && targetRequest.text) {
if (targetRequest?.text) {
try {
const { tokensIn, tokensOut, cacheWrites, cacheReads }: ClineApiReqInfo = JSON.parse(targetRequest.text)
const tokensUsed = (tokensIn || 0) + (tokensOut || 0) + (cacheWrites || 0) + (cacheReads || 0)
@@ -187,7 +187,7 @@ export class ContextManager {
// If the previous API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request
if (previousApiReqIndex >= 0) {
const previousRequest = clineMessages[previousApiReqIndex]
if (previousRequest && previousRequest.text) {
if (previousRequest?.text) {
const timestamp = previousRequest.ts
const { tokensIn, tokensOut, cacheWrites, cacheReads }: ClineApiReqInfo = JSON.parse(previousRequest.text)
const totalTokens = (tokensIn || 0) + (tokensOut || 0) + (cacheWrites || 0) + (cacheReads || 0)
@@ -9,27 +9,23 @@ import type { Controller } from "../index"
* @returns User credits data response
*/
export async function getUserOrganizations(controller: Controller, _request: EmptyRequest): Promise<UserOrganizationsResponse> {
try {
if (!controller.accountService) {
throw new Error("Account service not available")
}
// Fetch user organizations from the account service
const organizations = await controller.accountService.fetchUserOrganizationsRPC()
return UserOrganizationsResponse.create({
organizations:
organizations?.map((org) =>
UserOrganization.create({
active: org.active,
memberId: org.memberId,
name: org.name,
organizationId: org.organizationId,
roles: org.roles ? [...org.roles] : [],
}),
) || [],
})
} catch (error) {
throw error
if (!controller.accountService) {
throw new Error("Account service not available")
}
// Fetch user organizations from the account service
const organizations = await controller.accountService.fetchUserOrganizationsRPC()
return UserOrganizationsResponse.create({
organizations:
organizations?.map((org) =>
UserOrganization.create({
active: org.active,
memberId: org.memberId,
name: org.name,
organizationId: org.organizationId,
roles: org.roles ? [...org.roles] : [],
}),
) || [],
})
}
@@ -9,16 +9,12 @@ import type { Controller } from "../index"
* @returns Empty response
*/
export async function setUserOrganization(controller: Controller, request: UserOrganizationUpdateRequest): Promise<Empty> {
try {
if (!controller.accountService) {
throw new Error("Account service not available")
}
// Switch to the specified organization using the account service
await controller.accountService.switchAccount(request.organizationId)
return Empty.create({})
} catch (error) {
throw error
if (!controller.accountService) {
throw new Error("Account service not available")
}
// Switch to the specified organization using the account service
await controller.accountService.switchAccount(request.organizationId)
return Empty.create({})
}
@@ -27,7 +27,7 @@ export async function openFocusChainFile(controller: Controller, request: String
.reverse()
.find((m) => m.say === "task_progress")
if (lastProgressMessage && lastProgressMessage.text) {
if (lastProgressMessage?.text) {
initialFocusChainContent = extractFocusChainListFromText(lastProgressMessage.text) || undefined
}
}
+1 -1
View File
@@ -482,7 +482,7 @@ export class Controller {
let apiKey: string
try {
const response = await axios.post("https://openrouter.ai/api/v1/auth/keys", { code })
if (response.data && response.data.key) {
if (response.data?.key) {
apiKey = response.data.key
} else {
throw new Error("Invalid response from OpenRouter API")
@@ -94,7 +94,7 @@ async function fetchAiCoreModelsAndOrchestration(
* @returns SapAiCoreModelsResponse with model names and orchestration availability
*/
export async function getSapAiCoreModels(
controller: Controller,
_controller: Controller,
request: SapAiCoreModelsRequest,
): Promise<SapAiCoreModelsResponse> {
try {
@@ -81,7 +81,6 @@ export async function getTaskHistory(controller: Controller, request: GetTaskHis
(b.cacheReads || 0) -
((a.tokensIn || 0) + (a.tokensOut || 0) + (a.cacheWrites || 0) + (a.cacheReads || 0))
)
case "newest":
default:
return b.ts - a.ts
}
+4 -4
View File
@@ -28,7 +28,7 @@ export async function initializeWebview(controller: Controller, _request: EmptyR
// Refresh OpenRouter models from API
refreshOpenRouterModels(controller, EmptyRequest.create()).then(async (response) => {
if (response && response.models) {
if (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 = controller.stateManager.getApiConfiguration()
const planActSeparateModelsSetting = controller.stateManager.getGlobalStateKey("planActSeparateModelsSetting")
@@ -74,7 +74,7 @@ export async function initializeWebview(controller: Controller, _request: EmptyR
})
refreshGroqModels(controller, EmptyRequest.create()).then(async (response) => {
if (response && response.models) {
if (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 = controller.stateManager.getApiConfiguration()
const planActSeparateModelsSetting = controller.stateManager.getGlobalStateKey("planActSeparateModelsSetting")
@@ -120,7 +120,7 @@ export async function initializeWebview(controller: Controller, _request: EmptyR
})
refreshBasetenModels(controller, EmptyRequest.create()).then(async (response) => {
if (response && response.models) {
if (response?.models) {
// Update model info in state for Baseten (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 = controller.stateManager.getApiConfiguration()
const planActSeparateModelsSetting = controller.stateManager.getGlobalStateKey("planActSeparateModelsSetting")
@@ -162,7 +162,7 @@ export async function initializeWebview(controller: Controller, _request: EmptyR
// Refresh Vercel AI Gateway models from API
refreshVercelAiGatewayModels(controller, EmptyRequest.create()).then(async (response) => {
if (response && response.models) {
if (response?.models) {
// Update model info in state for Vercel AI Gateway (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 = controller.stateManager.getApiConfiguration()
const planActSeparateModelsSetting = controller.stateManager.getGlobalStateKey("planActSeparateModelsSetting")
@@ -80,15 +80,19 @@ const compareStrings = (expected: string, actual: string): string | null => {
if (differences.length < 10) {
// Limit to first 10 differences for readability
differences.push(`Line ${i + 1}:`)
if (expectedLine)
if (expectedLine) {
differences.push(` - Expected: ${expectedLine.substring(0, 100)}${expectedLine.length > 100 ? "..." : ""}`)
if (actualLine)
}
if (actualLine) {
differences.push(` + Actual: ${actualLine.substring(0, 100)}${actualLine.length > 100 ? "..." : ""}`)
}
}
}
}
if (differences.length === 0) return null
if (differences.length === 0) {
return null
}
const summary = [
`Expected length: ${expected.length} characters`,
@@ -94,7 +94,7 @@ export class PromptBuilder {
.trim() // Remove leading/trailing whitespace
.replace(/====+\s*$/, "") // Remove trailing ==== after trim
.replace(/\n====+\s*\n+\s*====+\n/g, "\n====\n") // Remove empty sections between separators
.replace(/====+\n(?!\n)([^\n])/g, (match, nextChar, offset, string) => {
.replace(/====+\n(?!\n)([^\n])/g, (match, _nextChar, offset, string) => {
// Add extra newline after ====+ if not already followed by a newline
// Exception: preserve single newlines when ====+ appears to be part of diff-like content
// Look for patterns like "SEARCH\n=======\n" or ";\n=======\n" (diff markers)
+1 -1
View File
@@ -192,7 +192,7 @@ export class ToolExecutor {
this.coordinator.register(new ListCodeDefinitionNamesToolHandler(validator))
this.coordinator.register(new SearchFilesToolHandler(validator))
this.coordinator.register(new ExecuteCommandToolHandler(validator))
this.coordinator.register(new ExecuteCommandToolHandler())
this.coordinator.register(new UseMcpToolHandler())
this.coordinator.register(new AccessMcpResourceHandler())
this.coordinator.register(new LoadMcpDocumentationHandler())
+2 -2
View File
@@ -389,12 +389,12 @@ ${listInstrunctionsReminder}\n`
public async updateFCListFromToolResponse(taskProgress: string | undefined) {
try {
// Reset the counter if task_progress was provided
if (taskProgress && taskProgress.trim()) {
if (taskProgress?.trim()) {
this.taskState.apiRequestsSinceLastTodoUpdate = 0
}
// If model provides task_progress update, write it to the markdown file
if (taskProgress && taskProgress.trim()) {
if (taskProgress?.trim()) {
const previousList = this.taskState.currentFocusChainChecklist
this.taskState.currentFocusChainChecklist = taskProgress.trim()
console.debug(
+3 -5
View File
@@ -747,8 +747,7 @@ export class Task {
const lastMessage = clineMessages.at(-1)
const lastMessageIndex = clineMessages.length - 1
const isUpdatingPreviousPartial =
lastMessage && lastMessage.partial && lastMessage.type === "ask" && lastMessage.ask === type
const isUpdatingPreviousPartial = lastMessage?.partial && lastMessage.type === "ask" && lastMessage.ask === type
if (partial) {
if (isUpdatingPreviousPartial) {
// existing partial message, so update it
@@ -872,8 +871,7 @@ export class Task {
if (partial !== undefined) {
const lastMessage = this.messageStateHandler.getClineMessages().at(-1)
const isUpdatingPreviousPartial =
lastMessage && lastMessage.partial && lastMessage.type === "say" && lastMessage.say === type
const isUpdatingPreviousPartial = lastMessage?.partial && lastMessage.type === "say" && lastMessage.say === type
if (partial) {
if (isUpdatingPreviousPartial) {
// existing partial message, so update it
@@ -2284,7 +2282,7 @@ export class Task {
// if last message is a partial we need to update and save it
const lastMessage = this.messageStateHandler.getClineMessages().at(-1)
if (lastMessage && lastMessage.partial) {
if (lastMessage?.partial) {
// lastMessage.ts = Date.now() DO NOT update ts since it is used as a key for virtuoso list
lastMessage.partial = false
// instead of streaming partialMessage events, we do a save and post like normal to persist to disk
+1 -1
View File
@@ -58,7 +58,7 @@ async function getChangedFiles(
lastCheckpointHash: string,
): Promise<ChangedFile[]> {
try {
let changedFiles
let changedFiles: ChangedFile[] | undefined
if (changesSinceLastTaskCompletion) {
changedFiles = await getChangesSinceLastTaskCompletion(
messageStateHandler,
@@ -12,8 +12,6 @@ import { ToolResultUtils } from "../utils/ToolResultUtils"
export class AccessMcpResourceHandler implements IFullyManagedTool {
readonly name = "access_mcp_resource"
constructor() {}
getDescription(block: ToolUse): string {
return `[${block.name} for '${block.params.server_name}']`
}
@@ -15,8 +15,6 @@ import { ToolResultUtils } from "../utils/ToolResultUtils"
export class AttemptCompletionHandler implements IToolHandler, IPartialBlockHandler {
readonly name = "attempt_completion"
constructor() {}
getDescription(block: ToolUse): string {
return `[${block.name}]`
}
@@ -174,7 +174,7 @@ export class BrowserToolHandler implements IFullyManagedTool {
case "click":
case "type":
case "scroll_down":
case "scroll_up":
case "scroll_up": {
await config.callbacks.say("browser_action_result", JSON.stringify(browserActionResult))
const result = formatResponse.toolResult(
`The browser action has been executed. The console logs and screenshot have been captured for your analysis.\n\nConsole logs:\n${
@@ -184,12 +184,14 @@ export class BrowserToolHandler implements IFullyManagedTool {
)
return result
}
case "close":
case "close": {
const closeResult = formatResponse.toolResult(
`The browser has been closed. You may now proceed to using other tools.`,
)
return closeResult
}
}
} catch (error) {
await config.services.browserSession.closeBrowser() // if any error occurs, the browser session is terminated
@@ -12,8 +12,6 @@ import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
export class CondenseHandler implements IToolHandler, IPartialBlockHandler {
readonly name = "condense"
constructor() {}
getDescription(block: ToolUse): string {
return `[${block.name}]`
}
@@ -8,7 +8,6 @@ import { telemetryService } from "@/services/telemetry"
import type { ToolResponse } from "../../index"
import { showNotificationForApprovalIfAutoApprovalEnabled } from "../../utils"
import type { IFullyManagedTool } from "../ToolExecutorCoordinator"
import type { ToolValidator } from "../ToolValidator"
import type { TaskConfig } from "../types/TaskConfig"
import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
import { ToolResultUtils } from "../utils/ToolResultUtils"
@@ -16,8 +15,6 @@ import { ToolResultUtils } from "../utils/ToolResultUtils"
export class ExecuteCommandToolHandler implements IFullyManagedTool {
readonly name = "execute_command"
constructor(_validator: ToolValidator) {}
getDescription(block: ToolUse): string {
return `[${block.name} for '${block.params.command}']`
}
@@ -8,8 +8,6 @@ import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
export class LoadMcpDocumentationHandler implements IToolHandler, IPartialBlockHandler {
readonly name = "load_mcp_documentation"
constructor() {}
getDescription(block: ToolUse): string {
return `[${block.name}]`
}
@@ -19,7 +17,7 @@ export class LoadMcpDocumentationHandler implements IToolHandler, IPartialBlockH
await uiHelpers.say("load_mcp_documentation", "", undefined, undefined, true)
}
async execute(config: TaskConfig, block: ToolUse): Promise<ToolResponse> {
async execute(config: TaskConfig, _block: ToolUse): Promise<ToolResponse> {
// Show loading message at start of execution (self-managed now)
await config.callbacks.say("load_mcp_documentation", "", undefined, undefined, false)
@@ -10,8 +10,6 @@ import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
export class NewTaskHandler implements IToolHandler, IPartialBlockHandler {
readonly name = "new_task"
constructor() {}
getDescription(block: ToolUse): string {
return `[${block.name} for creating a new task]`
}
@@ -11,8 +11,6 @@ import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
export class PlanModeRespondHandler implements IToolHandler, IPartialBlockHandler {
readonly name = "plan_mode_respond"
constructor() {}
getDescription(block: ToolUse): string {
return `[${block.name}]`
}
@@ -14,8 +14,6 @@ import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
export class ReportBugHandler implements IToolHandler, IPartialBlockHandler {
readonly name = "report_bug"
constructor() {}
getDescription(block: ToolUse): string {
return `[${block.name}]`
}
@@ -12,8 +12,6 @@ import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
export class SummarizeTaskHandler implements IToolHandler, IPartialBlockHandler {
readonly name = "summarize_task"
constructor() {}
getDescription(block: ToolUse): string {
return `[${block.name}]`
}
@@ -12,8 +12,6 @@ import { ToolResultUtils } from "../utils/ToolResultUtils"
export class UseMcpToolHandler implements IFullyManagedTool {
readonly name = "use_mcp_tool"
constructor() {}
getDescription(block: ToolUse): string {
return `[${block.name} for '${block.params.server_name}']`
}
+1 -1
View File
@@ -42,7 +42,7 @@ export class ExternalDiffViewProvider extends DiffViewProvider {
})
}
protected async saveDocument(): Promise<Boolean> {
protected async saveDocument(): Promise<boolean> {
if (!this.activeDiffEditorId) {
return false
}
-6
View File
@@ -1,16 +1,10 @@
import * as vscode from "vscode"
import { URI } from "vscode-uri"
import { WebviewProvider } from "@/core/webview"
import { WebviewProviderType } from "@/shared/webview/types"
export class ExternalWebviewProvider extends WebviewProvider {
// This hostname cannot be changed without updating the external webview handler.
private RESOURCE_HOSTNAME: string = "internal.resources"
constructor(context: vscode.ExtensionContext, providerType: WebviewProviderType) {
super(context, providerType)
}
override getWebviewUri(uri: URI) {
if (uri.scheme !== "file") {
return uri
+1 -1
View File
@@ -159,7 +159,7 @@ export class VscodeDiffViewProvider extends DiffViewProvider {
return this.activeDiffEditor.document.getText()
}
protected override async saveDocument(): Promise<Boolean> {
protected override async saveDocument(): Promise<boolean> {
if (!this.activeDiffEditor) {
return false
}
+1 -6
View File
@@ -6,7 +6,6 @@ import { handleGrpcRequest, handleGrpcRequestCancel } from "@/core/controller/gr
import { HostProvider } from "@/hosts/host-provider"
import type { ExtensionMessage } from "@/shared/ExtensionMessage"
import { WebviewMessage } from "@/shared/WebviewMessage"
import type { WebviewProviderType } from "@/shared/webview/types"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -22,10 +21,6 @@ export class VscodeWebviewProvider extends WebviewProvider implements vscode.Web
private webview?: vscode.WebviewView | vscode.WebviewPanel
private disposables: vscode.Disposable[] = []
constructor(context: vscode.ExtensionContext, providerType: WebviewProviderType) {
super(context, providerType)
}
override getWebviewUri(uri: Uri) {
if (!this.webview) {
throw new Error("Webview not initialized")
@@ -128,7 +123,7 @@ export class VscodeWebviewProvider extends WebviewProvider implements vscode.Web
// Listen for configuration changes
vscode.workspace.onDidChangeConfiguration(
async (e) => {
if (e && e.affectsConfiguration("cline.mcpMarketplace.enabled")) {
if (e?.affectsConfiguration("cline.mcpMarketplace.enabled")) {
// Update state when marketplace tab setting changes
await this.controller.postStateToWebview()
}
@@ -14,8 +14,6 @@ const requestRegistry = new GrpcRequestRegistry()
* Handles gRPC requests for the host bridge.
*/
export class GrpcHandler {
constructor() {}
/**
* Handle a gRPC request for the host bridge.
* @param service The service name
@@ -4,7 +4,7 @@ import { SaveOpenDocumentIfDirtyRequest, SaveOpenDocumentIfDirtyResponse } from
export async function saveOpenDocumentIfDirty(request: SaveOpenDocumentIfDirtyRequest): Promise<SaveOpenDocumentIfDirtyResponse> {
const existingDocument = vscode.workspace.textDocuments.find((doc) => arePathsEqual(doc.uri.fsPath, request.filePath))
if (existingDocument && existingDocument.isDirty) {
if (existingDocument?.isDirty) {
await existingDocument.save()
return { wasSaved: true }
}
+1 -1
View File
@@ -85,7 +85,7 @@ export async function* runClaudeCode(options: ClaudeCodeOptions): AsyncGenerator
// We rely on the assistant message. If the output was truncated, it's better having a poorly formatted message
// from which to extract something, than throwing an error/showing the model didn't return any messages.
if (processState.partialData && processState.partialData.startsWith(`{"type":"assistant"`)) {
if (processState.partialData?.startsWith(`{"type":"assistant"`)) {
yield processState.partialData
}
+1 -3
View File
@@ -24,8 +24,6 @@ export abstract class DiffViewProvider {
private streamedLines: string[] = []
private newContent?: string
constructor() {}
public async open(relPath: string): Promise<void> {
this.isEditing = true
this.relPath = relPath
@@ -138,7 +136,7 @@ export abstract class DiffViewProvider {
*
* @returns true if the file was saved.
*/
protected abstract saveDocument(): Promise<Boolean>
protected abstract saveDocument(): Promise<boolean>
/**
* Closes all open diff views.
+2 -1
View File
@@ -61,7 +61,7 @@ export function formatContentBlockToMarkdown(block: Anthropic.ContentBlockParam)
return `[Image]`
case "document":
return `[Document]`
case "tool_use":
case "tool_use": {
let input: string
if (typeof block.input === "object" && block.input !== null) {
input = Object.entries(block.input)
@@ -71,6 +71,7 @@ export function formatContentBlockToMarkdown(block: Anthropic.ContentBlockParam)
input = String(block.input)
}
return `[Tool Use: ${block.name}]\n${input}`
}
case "tool_result":
if (typeof block.content === "string") {
return `[Tool${block.is_error ? " (Error)" : ""}]\n${block.content}`
+2 -1
View File
@@ -50,7 +50,7 @@ export async function callTextExtractionFunctions(filePath: string): Promise<str
return extractTextFromIPYNB(filePath)
case ".xlsx":
return extractTextFromExcel(filePath)
default:
default: {
const fileBuffer = await fs.readFile(filePath)
if (fileBuffer.byteLength > 20 * 1000 * 1024) {
// 20MB limit (20 * 1000 * 1024 bytes, decimal MB)
@@ -58,6 +58,7 @@ export async function callTextExtractionFunctions(filePath: string): Promise<str
}
const encoding = await detectEncoding(fileBuffer, fileExtension)
return iconv.decode(fileBuffer, encoding)
}
}
}
+1 -1
View File
@@ -97,7 +97,7 @@ export async function detectImageUrl(url: string): Promise<boolean> {
})
const contentType = response.headers["content-type"]
return contentType && contentType.startsWith("image/")
return contentType?.startsWith("image/")
} catch (_error) {
// If we can't determine, fall back to checking the file extension
return /\.(jpg|jpeg|png|gif|webp|bmp|svg|tiff|tif|avif)$/i.test(url)
+2 -2
View File
@@ -117,7 +117,7 @@ export class TerminalManager {
try {
const stateChangeDisposable = vscode.window.onDidChangeTerminalState((terminal) => {
const terminalInfo = this.findTerminalInfoByTerminal(terminal)
if (terminalInfo && terminalInfo.pendingCwdChange && terminalInfo.cwdResolved) {
if (terminalInfo?.pendingCwdChange && terminalInfo.cwdResolved) {
// Check if CWD has been updated to match the expected path
if (this.isCwdMatchingExpected(terminalInfo)) {
const resolver = terminalInfo.cwdResolved.resolve
@@ -213,7 +213,7 @@ export class TerminalManager {
.finally(() => {
console.log(`[TerminalManager Test] Proceeding with command execution for terminal ${terminalInfo.id}.`)
const existingProcess = this.processes.get(terminalInfo.id)
if (existingProcess && existingProcess.waitForShellIntegration) {
if (existingProcess?.waitForShellIntegration) {
existingProcess.waitForShellIntegration = false
existingProcess.run(terminalInfo.terminal, command)
}
+2 -2
View File
@@ -29,7 +29,7 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
const returnCurrentTerminalContents = async () => {
try {
const terminalSnapshot = await getLatestTerminalOutput()
if (terminalSnapshot && terminalSnapshot.trim()) {
if (terminalSnapshot?.trim()) {
const fallbackMessage = `The command's output could not be captured due to some technical issue, however it has been executed successfully. Here's the current terminal's content to help you get the command's output:\n\n${terminalSnapshot}`
this.emit("line", fallbackMessage)
}
@@ -38,7 +38,7 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
}
}
if (terminal.shellIntegration && terminal.shellIntegration.executeCommand) {
if (terminal.shellIntegration?.executeCommand) {
const execution = terminal.shellIntegration.executeCommand(command)
const stream = execution.read()
// todo: need to handle errors
@@ -26,7 +26,7 @@ export class FeatureFlagsProviderFactory {
*/
public static createProvider(config: FeatureFlagsProviderConfig): IFeatureFlagsProvider {
switch (config.type) {
case "posthog":
case "posthog": {
// Get the shared PostHog client from PostHogClientProvider
const client = PostHogClientProvider.getClient()
if (client) {
@@ -34,6 +34,7 @@ export class FeatureFlagsProviderFactory {
}
// Fall back to NoOp provider if no client is available
return new NoOpFeatureFlagsProvider()
}
case "none":
return new NoOpFeatureFlagsProvider()
default:
+2 -2
View File
@@ -924,7 +924,7 @@ export class McpHub {
// Update the tools list to reflect the change
const connection = this.connections.find((conn) => conn.server.name === serverName)
if (connection && connection.server.tools) {
if (connection?.server.tools) {
// Update the autoApprove property of each tool in the in-memory server object
connection.server.tools = connection.server.tools.map((tool) => ({
...tool,
@@ -969,7 +969,7 @@ export class McpHub {
// Update the tools list to reflect the change
const connection = this.connections.find((conn) => conn.server.name === serverName)
if (connection && connection.server.tools) {
if (connection?.server.tools) {
// Update the autoApprove property of each tool in the in-memory server object
connection.server.tools = connection.server.tools.map((tool) => ({
...tool,
+1 -1
View File
@@ -322,7 +322,7 @@ export function createTestServer(controller: Controller): http.Server {
}
// Get file changes
let fileChanges
let fileChanges: { created: string[]; modified: string[]; deleted: string[] }
try {
// Get the workspace path using our helper function
const workspacePath = await getCwd()
@@ -215,7 +215,7 @@ describe("Mention Regex", () => {
cases.forEach(([input, expected]) => {
const match = mentionRegex.exec(input)
const actual = match ? match[0] : null
if (expected && expected.includes("41 chars")) {
if (expected?.includes("41 chars")) {
// Special case: should match first 40 chars
expect(actual).to.equal("@abcdef1234567890abcdef1234567890abcdef12")
} else {
@@ -86,7 +86,6 @@ function convertProtoStatusToMcp(status: McpServerStatus): McpServer["status"] {
return "connected"
case McpServerStatus.MCP_SERVER_STATUS_CONNECTING:
return "connecting"
case McpServerStatus.MCP_SERVER_STATUS_DISCONNECTED:
default: // Includes UNSPECIFIED if it were present, maps to disconnected
return "disconnected"
}
+2 -2
View File
@@ -37,10 +37,10 @@ async function asyncIteratorToCallbacks<T>(stream: AsyncIterable<T>, callbacks:
try {
// Process each item in the stream
for await (const response of stream) {
callbacks.onResponse && callbacks.onResponse(response)
callbacks.onResponse?.(response)
}
// Stream completed successfully
callbacks.onComplete && callbacks.onComplete()
callbacks.onComplete?.()
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err))
if (callbacks.onError) {
+8 -11
View File
@@ -9,7 +9,6 @@ import { ClineApiServerMock } from "../fixtures/server"
interface E2ETestDirectories {
workspaceDir: string
userDataDir: string
extensionsDir: string
}
export class E2ETestHelper {
@@ -184,6 +183,7 @@ export class E2ETestHelper {
*/
export const e2e = test
.extend<{ server: ClineApiServerMock | null }>({
// biome-ignore lint/correctness/noEmptyPattern: no dependency
server: async ({}, use) => {
console.log("=== SERVER FIXTURE CALLED ===")
// Start server if it doesn't exist
@@ -198,18 +198,17 @@ export const e2e = test
},
})
.extend<E2ETestDirectories>({
// biome-ignore lint/correctness/noEmptyPattern: no dependency
workspaceDir: async ({}, use) => {
await use(path.join(E2ETestHelper.E2E_TESTS_DIR, "fixtures", "workspace"))
},
// biome-ignore lint/correctness/noEmptyPattern: no dependency
userDataDir: async ({}, use) => {
await use(mkdtempSync(path.join(os.tmpdir(), "vsce")))
},
extensionsDir: async ({}, use) => {
await use(mkdtempSync(path.join(os.tmpdir(), "vsce")))
},
})
.extend<{ openVSCode: () => Promise<ElectronApplication> }>({
openVSCode: async ({ workspaceDir, userDataDir, extensionsDir }, use, testInfo) => {
openVSCode: async ({ workspaceDir, userDataDir }, use, testInfo) => {
const executablePath = await downloadAndUnzipVSCode("stable", undefined, new SilentReporter())
await use(async () => {
@@ -233,7 +232,7 @@ export const e2e = test
"--skip-welcome",
"--skip-release-notes",
`--user-data-dir=${userDataDir}`,
`--extensions-dir=${extensionsDir}`,
`--extensions-dir=${userDataDir}`,
`--install-extension=${path.join(E2ETestHelper.CODEBASE_ROOT_DIR, "dist", "e2e.vsix")}`,
`--extensionDevelopmentPath=${E2ETestHelper.CODEBASE_ROOT_DIR}`,
workspaceDir,
@@ -245,7 +244,7 @@ export const e2e = test
},
})
.extend<{ app: ElectronApplication }>({
app: async ({ openVSCode, userDataDir, extensionsDir }, use) => {
app: async ({ openVSCode, userDataDir }, use) => {
const app = await openVSCode()
try {
@@ -253,14 +252,12 @@ export const e2e = test
} finally {
await app.close()
// Cleanup in parallel
await Promise.allSettled([
E2ETestHelper.rmForRetries(userDataDir, { recursive: true }),
E2ETestHelper.rmForRetries(extensionsDir, { recursive: true }),
])
await Promise.allSettled([E2ETestHelper.rmForRetries(userDataDir, { recursive: true })])
}
},
})
.extend<{ helper: E2ETestHelper }>({
// biome-ignore lint/correctness/noEmptyPattern: no dependency
helper: async ({}, use) => {
const helper = new E2ETestHelper()
await use(helper)
@@ -449,7 +449,7 @@ class StandaloneTerminalManager {
disposeAll() {
// Terminate all processes
for (const [_terminalId, process] of this.processes) {
if (process && process.terminate) {
if (process?.terminate) {
process.terminate()
}
}
@@ -552,7 +552,7 @@ const BrowserSessionRowContent = memo(
</div>
)
case "browser_action":
case "browser_action": {
const browserAction = JSON.parse(message.text || "{}") as ClineSayBrowserAction
return (
<BrowserActionBox
@@ -561,6 +561,7 @@ const BrowserSessionRowContent = memo(
text={browserAction.text}
/>
)
}
default:
return null
+10 -5
View File
@@ -307,7 +307,7 @@ export const ChatRowContent = memo(
),
<span style={{ color: normalColor, fontWeight: "bold" }}>Cline wants to execute this command:</span>,
]
case "use_mcp_server":
case "use_mcp_server": {
const mcpServerUse = JSON.parse(message.text || "{}") as ClineAskUseMcpServer
return [
isMcpServerResponding ? (
@@ -330,6 +330,7 @@ export const ChatRowContent = memo(
MCP server:
</span>,
]
}
case "completion_result":
return [
<span
@@ -444,7 +445,7 @@ export const ChatRowContent = memo(
/>
</>
)
case "readFile":
case "readFile": {
const isImage = isImageFile(tool.path || "")
return (
<>
@@ -511,6 +512,7 @@ export const ChatRowContent = memo(
</div>
</>
)
}
case "listFilesTopLevel":
return (
<>
@@ -1072,7 +1074,7 @@ export const ChatRowContent = memo(
text={message.text}
/>
)
case "user_feedback_diff":
case "user_feedback_diff": {
const tool = JSON.parse(message.text || "{}") as ClineSayTool
return (
<div
@@ -1088,6 +1090,7 @@ export const ChatRowContent = memo(
/>
</div>
)
}
case "error":
return <ErrorRow errorType="error" message={message} />
case "diff_error":
@@ -1111,7 +1114,7 @@ export const ChatRowContent = memo(
Loading MCP documentation
</div>
)
case "completion_result":
case "completion_result": {
const hasChanges = message.text?.endsWith(COMPLETION_RESULT_CHANGES_FLAG) ?? false
const text = hasChanges ? message.text?.slice(0, -COMPLETION_RESULT_CHANGES_FLAG.length) : message.text
return (
@@ -1178,6 +1181,7 @@ export const ChatRowContent = memo(
)}
</>
)
}
case "shell_integration_warning":
return (
<div
@@ -1323,7 +1327,7 @@ export const ChatRowContent = memo(
} else {
return null // Don't render anything when we get a completion_result ask without text
}
case "followup":
case "followup": {
let question: string | undefined
let options: string[] | undefined
let selected: string | undefined
@@ -1373,6 +1377,7 @@ export const ChatRowContent = memo(
</WithCopyButton>
</>
)
}
case "new_task":
return (
<>
@@ -420,7 +420,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
setSearchLoading(true)
// Map ContextMenuOptionType to FileSearchType enum
let searchType
let searchType: FileSearchType | undefined
if (type === ContextMenuOptionType.File) {
searchType = FileSearchType.FILE
} else if (type === ContextMenuOptionType.Folder) {
@@ -1156,8 +1156,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
return `${selectedProvider}:${liteLlmModelId}`
case "requesty":
return `${selectedProvider}:${requestyModelId}`
case "anthropic":
case "openrouter":
default:
return `${selectedProvider}:${selectedModelId}`
}
+1 -6
View File
@@ -205,12 +205,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
value: selectedModelInfo.supportsImages,
}),
)
if (
response &&
response.values1 &&
response.values2 &&
(response.values1.length > 0 || response.values2.length > 0)
) {
if (response?.values1 && response.values2 && (response.values1.length > 0 || response.values2.length > 0)) {
const currentTotal = selectedImages.length + selectedFiles.length
const availableSlots = MAX_IMAGES_AND_FILES_PER_MESSAGE - currentTotal
@@ -38,7 +38,7 @@ const AutoApproveModal: React.FC<AutoApproveModalProps> = ({
useClickAway(modalRef, (e) => {
// Skip if click was on the button that toggles the modal
if (buttonRef.current && buttonRef.current.contains(e.target as Node)) {
if (buttonRef.current?.contains(e.target as Node)) {
return
}
setIsVisible(false)
@@ -40,7 +40,6 @@ const ButtonContainer = styled.div<{ $position?: "top-right" | "bottom-right" }>
switch (props.$position) {
case "bottom-right":
return "bottom: 2px; right: 2px;"
case "top-right":
default:
return "top: 5px; right: 5px;"
}
@@ -59,13 +59,17 @@ const remarkUrlToLink = () => {
visit(tree, "text", (node: any, index, parent) => {
const urlRegex = /https?:\/\/[^\s<>)"]+/g
const matches = node.value.match(urlRegex)
if (!matches) return
if (!matches) {
return
}
const parts = node.value.split(urlRegex)
const children: any[] = []
parts.forEach((part: string, i: number) => {
if (part) children.push({ type: "text", value: part })
if (part) {
children.push({ type: "text", value: part })
}
if (matches[i]) {
children.push({
type: "link",
@@ -96,19 +100,25 @@ const remarkHighlightActMode = () => {
// Added negative lookahead to avoid matching if already followed by the shortcut
const actModeRegex = /\bto\s+Act\s+Mode\b(?!\s*\(⌘⇧A\))/i
if (!node.value.match(actModeRegex)) return
if (!node.value.match(actModeRegex)) {
return
}
// Split the text by the matches
const parts = node.value.split(actModeRegex)
const matches = node.value.match(actModeRegex)
if (!matches || parts.length <= 1) return
if (!matches || parts.length <= 1) {
return
}
const children: any[] = []
parts.forEach((part: string, i: number) => {
// Add the text before the match
if (part) children.push({ type: "text", value: part })
if (part) {
children.push({ type: "text", value: part })
}
// Add the match, but only make "Act Mode" bold (not the "to" part)
if (matches[i]) {
@@ -156,22 +166,32 @@ const remarkPreventBoldFilenames = () => {
return (tree: any) => {
visit(tree, "strong", (node: any, index: number | undefined, parent: any) => {
// Only process if there's a next node (potential file extension)
if (!parent || typeof index === "undefined" || index === parent.children.length - 1) return
if (!parent || typeof index === "undefined" || index === parent.children.length - 1) {
return
}
const nextNode = parent.children[index + 1]
// Check if next node is text and starts with . followed by extension
if (nextNode.type !== "text" || !nextNode.value.match(/^\.[a-zA-Z0-9]+/)) return
if (nextNode.type !== "text" || !nextNode.value.match(/^\.[a-zA-Z0-9]+/)) {
return
}
// If the strong node has multiple children, something weird is happening
if (node.children?.length !== 1) return
if (node.children?.length !== 1) {
return
}
// Get the text content from inside the strong node
const strongContent = node.children?.[0]?.value
if (!strongContent || typeof strongContent !== "string") return
if (!strongContent || typeof strongContent !== "string") {
return
}
// Validate that the strong content is a valid filename
if (!strongContent.match(/^[a-zA-Z0-9_-]+$/)) return
if (!strongContent.match(/^[a-zA-Z0-9_-]+$/)) {
return
}
// Combine into a single text node
const newNode = {
@@ -283,7 +303,9 @@ const PreWithCopyButton = ({ children, ...preProps }: React.HTMLAttributes<HTMLP
const codeElement = preRef.current.querySelector("code")
const textToCopy = codeElement ? codeElement.textContent : preRef.current.textContent
if (!textToCopy) return
if (!textToCopy) {
return
}
return textToCopy
}
return null
@@ -305,7 +327,7 @@ const PreWithCopyButton = ({ children, ...preProps }: React.HTMLAttributes<HTMLP
const remarkFilePathDetection = () => {
return async (tree: Node) => {
const fileNameRegex = /^(?!\/)[\w\-./]+(?<!\/)$/
const inlineCodeNodes: any[] = []
const _inlineCodeNodes: any[] = []
const filePathPromises: Promise<void>[] = []
// Collect all inline code nodes that might be file paths
@@ -399,9 +421,12 @@ const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
// Handle both string children and array of children cases
const childrenText = React.Children.toArray(props.children)
.map((child) => {
if (typeof child === "string") return child
if (typeof child === "object" && "props" in child && child.props.children)
if (typeof child === "string") {
return child
}
if (typeof child === "object" && "props" in child && child.props.children) {
return String(child.props.children)
}
return ""
})
.join("")
@@ -248,7 +248,6 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
case "mostRelevant":
// NOTE: you must never sort directly on object since it will cause members to be reordered
return searchQuery ? 0 : b.ts - a.ts // Keep fuse order if searching, otherwise sort by newest
case "newest":
default:
return b.ts - a.ts
}
@@ -825,7 +824,7 @@ export const highlight = (fuseSearchResult: FuseResult<any>[], highlightClassNam
}
return fuseSearchResult
.filter(({ matches }) => matches && matches.length)
.filter(({ matches }) => matches?.length)
.map(({ item, matches }) => {
const highlightedItem = { ...item }
@@ -98,7 +98,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
value: apiConfiguration?.ollamaBaseUrl || "",
}),
)
if (response && response.values) {
if (response?.values) {
setOllamaModels(response.values)
}
} catch (error) {
@@ -192,7 +192,7 @@ const BasetenModelPicker: React.FC<BasetenModelPickerProps> = ({ isPopup, curren
}
const hasInfo = useMemo(() => {
return selectedModelInfo && selectedModelInfo.description
return selectedModelInfo?.description
}, [selectedModelInfo])
useEffect(() => {
@@ -18,11 +18,11 @@ export const DifyProvider = ({ showModelOptions, isPopup, currentMode }: DifyPro
const { handleFieldChange } = useApiConfigurationHandlers()
// Use debounced input for proper state management
const [baseUrlValue, setBaseUrlValue] = useDebouncedInput(apiConfiguration?.difyBaseUrl || "", (value) =>
const [_baseUrlValue, _setBaseUrlValue] = useDebouncedInput(apiConfiguration?.difyBaseUrl || "", (value) =>
handleFieldChange("difyBaseUrl", value),
)
const [apiKeyValue, setApiKeyValue] = useDebouncedInput(apiConfiguration?.difyApiKey || "", (value) =>
const [_apiKeyValue, _setApiKeyValue] = useDebouncedInput(apiConfiguration?.difyApiKey || "", (value) =>
handleFieldChange("difyApiKey", value),
)
@@ -41,7 +41,7 @@ export const OllamaProvider = ({ showModelOptions, isPopup, currentMode }: Ollam
value: apiConfiguration?.ollamaBaseUrl || "",
}),
)
if (response && response.values) {
if (response?.values) {
setOllamaModels(response.values)
}
} catch (error) {
@@ -25,7 +25,7 @@ export const VSCodeLmProvider = ({ currentMode }: VSCodeLmProviderProps) => {
const requestVsCodeLmModels = useCallback(async () => {
try {
const response = await ModelsServiceClient.getVsCodeLmModels(EmptyRequest.create({}))
if (response && response.models) {
if (response?.models) {
setVsCodeLmModels(response.models)
}
} catch (error) {
@@ -36,7 +36,7 @@ export const VercelAIGatewayProvider = ({ showModelOptions, isPopup, currentMode
setIsLoadingModels(true)
ModelsServiceClient.refreshVercelAiGatewayModels(EmptyRequest.create({}))
.then((response) => {
if (response && response.models) {
if (response?.models) {
setVercelAiGatewayModels(response.models)
}
setIsLoadingModels(false)
@@ -68,7 +68,7 @@ export const XaiProvider = ({ showModelOptions, isPopup, currentMode }: XaiProvi
selectedModelId={selectedModelId}
/>
{selectedModelId && selectedModelId.includes("3-mini") && (
{selectedModelId?.includes("3-mini") && (
<>
<VSCodeCheckbox
checked={reasoningEffortSelected}
@@ -107,7 +107,7 @@ export function normalizeApiConfiguration(
return getProviderData(anthropicModels, anthropicDefaultModelId)
case "claude-code":
return getProviderData(claudeCodeModels, claudeCodeDefaultModelId)
case "bedrock":
case "bedrock": {
const awsBedrockCustomSelected =
currentMode === "plan"
? apiConfiguration?.planModeAwsBedrockCustomSelected
@@ -124,6 +124,7 @@ export function normalizeApiConfiguration(
}
}
return getProviderData(bedrockModels, bedrockDefaultModelId)
}
case "vertex":
return getProviderData(vertexModels, vertexDefaultModelId)
case "gemini":
@@ -132,11 +133,12 @@ export function normalizeApiConfiguration(
return getProviderData(openAiNativeModels, openAiNativeDefaultModelId)
case "deepseek":
return getProviderData(deepSeekModels, deepSeekDefaultModelId)
case "qwen":
case "qwen": {
const qwenModels = apiConfiguration?.qwenApiLine === "china" ? mainlandQwenModels : internationalQwenModels
const qwenDefaultId =
apiConfiguration?.qwenApiLine === "china" ? mainlandQwenDefaultModelId : internationalQwenDefaultModelId
return getProviderData(qwenModels, qwenDefaultId)
}
case "qwen-code":
return getProviderData(qwenCodeModels, qwenCodeDefaultModelId)
case "doubao":
@@ -145,7 +147,7 @@ export function normalizeApiConfiguration(
return getProviderData(mistralModels, mistralDefaultModelId)
case "asksage":
return getProviderData(askSageModels, askSageDefaultModelId)
case "openrouter":
case "openrouter": {
const openRouterModelId =
currentMode === "plan" ? apiConfiguration?.planModeOpenRouterModelId : apiConfiguration?.actModeOpenRouterModelId
const openRouterModelInfo =
@@ -157,7 +159,8 @@ export function normalizeApiConfiguration(
selectedModelId: openRouterModelId || openRouterDefaultModelId,
selectedModelInfo: openRouterModelInfo || openRouterDefaultModelInfo,
}
case "requesty":
}
case "requesty": {
const requestyModelId =
currentMode === "plan" ? apiConfiguration?.planModeRequestyModelId : apiConfiguration?.actModeRequestyModelId
const requestyModelInfo =
@@ -167,7 +170,8 @@ export function normalizeApiConfiguration(
selectedModelId: requestyModelId || requestyDefaultModelId,
selectedModelInfo: requestyModelInfo || requestyDefaultModelInfo,
}
case "cline":
}
case "cline": {
const clineOpenRouterModelId =
(currentMode === "plan"
? apiConfiguration?.planModeOpenRouterModelId
@@ -181,7 +185,8 @@ export function normalizeApiConfiguration(
selectedModelId: clineOpenRouterModelId,
selectedModelInfo: clineOpenRouterModelInfo,
}
case "openai":
}
case "openai": {
const openAiModelId =
currentMode === "plan" ? apiConfiguration?.planModeOpenAiModelId : apiConfiguration?.actModeOpenAiModelId
const openAiModelInfo =
@@ -191,7 +196,8 @@ export function normalizeApiConfiguration(
selectedModelId: openAiModelId || "",
selectedModelInfo: openAiModelInfo || openAiModelInfoSaneDefaults,
}
case "ollama":
}
case "ollama": {
const ollamaModelId =
currentMode === "plan" ? apiConfiguration?.planModeOllamaModelId : apiConfiguration?.actModeOllamaModelId
return {
@@ -202,7 +208,8 @@ export function normalizeApiConfiguration(
contextWindow: Number(apiConfiguration?.ollamaApiOptionsCtxNum ?? 32768),
},
}
case "lmstudio":
}
case "lmstudio": {
const lmStudioModelId =
currentMode === "plan" ? apiConfiguration?.planModeLmStudioModelId : apiConfiguration?.actModeLmStudioModelId
return {
@@ -213,7 +220,8 @@ export function normalizeApiConfiguration(
contextWindow: Number(apiConfiguration?.lmStudioMaxTokens ?? 32768),
},
}
case "vscode-lm":
}
case "vscode-lm": {
const vsCodeLmModelSelector =
currentMode === "plan"
? apiConfiguration?.planModeVsCodeLmModelSelector
@@ -226,7 +234,8 @@ export function normalizeApiConfiguration(
supportsImages: false, // VSCode LM API currently doesn't support images
},
}
case "litellm":
}
case "litellm": {
const liteLlmModelId =
currentMode === "plan" ? apiConfiguration?.planModeLiteLlmModelId : apiConfiguration?.actModeLiteLlmModelId
const liteLlmModelInfo =
@@ -236,11 +245,12 @@ export function normalizeApiConfiguration(
selectedModelId: liteLlmModelId || "",
selectedModelInfo: liteLlmModelInfo || liteLlmModelInfoSaneDefaults,
}
}
case "xai":
return getProviderData(xaiModels, xaiDefaultModelId)
case "moonshot":
return getProviderData(moonshotModels, moonshotDefaultModelId)
case "huggingface":
case "huggingface": {
const huggingFaceModelId =
currentMode === "plan"
? apiConfiguration?.planModeHuggingFaceModelId
@@ -254,13 +264,14 @@ export function normalizeApiConfiguration(
selectedModelId: huggingFaceModelId || huggingFaceDefaultModelId,
selectedModelInfo: huggingFaceModelInfo || huggingFaceModels[huggingFaceDefaultModelId],
}
}
case "nebius":
return getProviderData(nebiusModels, nebiusDefaultModelId)
case "sambanova":
return getProviderData(sambanovaModels, sambanovaDefaultModelId)
case "cerebras":
return getProviderData(cerebrasModels, cerebrasDefaultModelId)
case "groq":
case "groq": {
const groqModelId =
currentMode === "plan" ? apiConfiguration?.planModeGroqModelId : apiConfiguration?.actModeGroqModelId
const groqModelInfo =
@@ -270,7 +281,8 @@ export function normalizeApiConfiguration(
selectedModelId: groqModelId || groqDefaultModelId,
selectedModelInfo: groqModelInfo || groqModels[groqDefaultModelId],
}
case "baseten":
}
case "baseten": {
const basetenModelId =
currentMode === "plan" ? apiConfiguration?.planModeBasetenModelId : apiConfiguration?.actModeBasetenModelId
const basetenModelInfo =
@@ -285,9 +297,10 @@ export function normalizeApiConfiguration(
description: "Baseten model",
},
}
}
case "sapaicore":
return getProviderData(sapAiCoreModels, sapAiCoreDefaultModelId)
case "huawei-cloud-maas":
case "huawei-cloud-maas": {
const huaweiCloudMaasModelId =
currentMode === "plan"
? apiConfiguration?.planModeHuaweiCloudMaasModelId
@@ -301,6 +314,7 @@ export function normalizeApiConfiguration(
selectedModelId: huaweiCloudMaasModelId || huaweiCloudMaasDefaultModelId,
selectedModelInfo: huaweiCloudMaasModelInfo || huaweiCloudMaasModels[huaweiCloudMaasDefaultModelId],
}
}
case "dify":
return {
selectedProvider: provider,
@@ -315,7 +329,7 @@ export function normalizeApiConfiguration(
description: "Dify workflow - model selection is configured in your Dify application",
},
}
case "vercel-ai-gateway":
case "vercel-ai-gateway": {
const vercelAiGatewayModelId =
currentMode === "plan"
? apiConfiguration?.planModeVercelAiGatewayModelId
@@ -329,12 +343,14 @@ export function normalizeApiConfiguration(
selectedModelId: vercelAiGatewayModelId || vercelAiGatewayDefaultModelId,
selectedModelInfo: vercelAiGatewayModelInfo || vercelAiGatewayDefaultModelInfo,
}
case "zai":
}
case "zai": {
const zaiModels = apiConfiguration?.zaiApiLine === "china" ? mainlandZAiModels : internationalZAiModels
const zaiDefaultId =
apiConfiguration?.zaiApiLine === "china" ? mainlandZAiDefaultModelId : internationalZAiDefaultModelId
return getProviderData(zaiModels, zaiDefaultId)
case "fireworks":
}
case "fireworks": {
const fireworksModelId =
currentMode === "plan" ? apiConfiguration?.planModeFireworksModelId : apiConfiguration?.actModeFireworksModelId
return {
@@ -345,6 +361,7 @@ export function normalizeApiConfiguration(
? fireworksModels[fireworksModelId as keyof typeof fireworksModels]
: fireworksModels[fireworksDefaultModelId],
}
}
default:
return getProviderData(anthropicModels, anthropicDefaultModelId)
}
@@ -601,24 +618,6 @@ export async function syncModeConfigurations(
updates.planModeVercelAiGatewayModelInfo = sourceFields.vercelAiGatewayModelInfo
updates.actModeVercelAiGatewayModelInfo = sourceFields.vercelAiGatewayModelInfo
break
// Providers that use apiProvider + apiModelId fields
case "anthropic":
case "claude-code":
case "vertex":
case "gemini":
case "openai-native":
case "deepseek":
case "qwen":
case "doubao":
case "mistral":
case "asksage":
case "xai":
case "nebius":
case "sambanova":
case "cerebras":
case "sapaicore":
case "zai":
default:
updates.planModeApiModelId = sourceFields.apiModelId
updates.actModeApiModelId = sourceFields.apiModelId
+2 -2
View File
@@ -4,13 +4,13 @@
/* @import "tailwindcss/preflight.css" layer(base); */
@import "tailwindcss/utilities.css" layer(utilities);
@config "../tailwind.config.mjs";
/* Import Azeret Mono font from local package */
@import "@fontsource/azeret-mono/300.css";
@import "@fontsource/azeret-mono/400.css";
@import "@fontsource/azeret-mono/700.css";
@config "../tailwind.config.mjs";
textarea:focus {
outline: 1.5px solid var(--vscode-focusBorder, #007fd4);
}
+2 -1
View File
@@ -171,7 +171,7 @@ export function validateModelId(
const { apiProvider, openRouterModelId } = getModeSpecificFields(apiConfiguration, currentMode)
switch (apiProvider) {
case "openrouter":
case "cline":
case "cline": {
const modelId = openRouterModelId || openRouterDefaultModelId // in case the user hasn't changed the model id, it will be undefined by default
if (!modelId) {
return "You must provide a model ID."
@@ -181,6 +181,7 @@ export function validateModelId(
return "The model ID you provided is not available. Please choose a different model."
}
break
}
}
}
return undefined