remove unused code/files

fix broken webview build

remove unused code/files

fix broken webview build
This commit is contained in:
Max Paulus 🥪
2026-06-24 14:11:30 +09:00
committed by Dominic Cooney
parent 1f31738b32
commit 78b1f30133
181 changed files with 409 additions and 3887 deletions
+32 -21
View File
@@ -1,23 +1,34 @@
{
"entry": [
"src/extension.ts",
"src/standalone/cline-core.ts",
"src/generated/hosts/standalone/protobus-server-setup.ts",
"src/generated/hosts/standalone/host-bridge-clients.ts",
"src/generated/hosts/vscode/protobus-services.ts",
"src/generated/hosts/vscode/hostbridge-grpc-service-config.ts"
],
"project": [
"src/**/*.ts"
],
"ignore": [
"out/**",
"node_modules/**",
"*.d.ts",
"**/*.test.ts",
"**/__tests__",
"src/test/**",
"src/shared/**"
],
"vite": true
"$schema": "https://unpkg.com/knip@5/schema.json",
"workspaces": {
".": {
"entry": [
"src/extension.ts",
"src/standalone/cline-core.ts",
"src/generated/hosts/standalone/protobus-server-setup.ts",
"src/generated/hosts/standalone/host-bridge-clients.ts",
"src/generated/hosts/vscode/protobus-services.ts",
"src/generated/hosts/vscode/hostbridge-grpc-service-config.ts",
"src/**/*.test.ts",
"src/**/__tests__/**/*.ts",
"src/test/**/*.ts"
],
"project": [
"src/**/*.ts"
]
},
"webview-ui": {
"entry": [
"src/services/grpc-client.ts",
"src/**/*.test.{ts,tsx}",
"src/**/*.spec.{ts,tsx}",
"src/**/__tests__/**/*.{ts,tsx}"
],
"project": [
"src/**/*.{ts,tsx}",
"*.ts"
],
"vite": true
}
}
}
+5 -1
View File
@@ -363,6 +363,10 @@
"check-types": "npm run protos && npx tsc --noEmit && cd webview-ui && npx tsc --noEmit",
"lint": "biome lint --config-path ./biome.jsonc --no-errors-on-unmatched --files-ignore-unknown=true --diagnostic-level=error && npm run lint:proto",
"lint:proto": "bash ./scripts/proto-lint.sh",
"analyze:unused": "npx --yes knip --config knip.json --include files,exports,types,enumMembers,duplicates",
"analyze:unused:prod": "npx --yes knip --config knip.json --production --include files,exports,types,enumMembers,dependencies",
"analyze:unused:fix-exports": "node scripts/remove-unused-exports.mjs --apply",
"analyze:unused:fix-exports:dry": "node scripts/remove-unused-exports.mjs",
"format": "biome format --config-path ./biome.jsonc --changed --since main --no-errors-on-unmatched --files-ignore-unknown=true --diagnostic-level=error --semicolons=as-needed",
"format:fix": "biome check --config-path ./biome.jsonc --changed --since main --no-errors-on-unmatched --files-ignore-unknown=true --write --semicolons=as-needed",
"fix:all": "biome check --config-path ./biome.jsonc --no-errors-on-unmatched --files-ignore-unknown=true --write --diagnostic-level=error --unsafe --semicolons=as-needed",
@@ -401,7 +405,7 @@
"lint-staged": {
"src/shared/storage/state-keys.ts": [
"node scripts/generate-state-proto.mjs",
"git add proto/cline/state.proto"
"git add apps/vscode/proto/cline/state.proto"
],
"*": [
"biome check --write --staged --no-errors-on-unmatched --files-ignore-unknown=true --semicolons=as-needed"
+3 -4
View File
@@ -4,12 +4,11 @@ import * as path from "path"
import { Environment, type EnvironmentConfig } from "./shared/config-types"
import { Logger } from "./shared/services/Logger"
export { Environment, type EnvironmentConfig }
/**
export { Environment } /**
* Schema for the endpoints.json configuration file used in on-premise deployments.
* All fields are required and must be valid URLs.
*/
interface EndpointsFileSchema {
appBaseUrl: string
apiBaseUrl: string
@@ -36,7 +35,7 @@ class ClineEndpoint {
private onPremiseConfig: EndpointsFileSchema | null = null
private environment: Environment = Environment.production
// Track if config came from bundled file (enterprise distribution)
private isBundled: boolean = false
private isBundled = false
private constructor() {
// Set environment at module load. Use override if provided.
+1 -25
View File
@@ -1,9 +1,5 @@
import { type ApiHandler as SdkApiHandler, type ApiStreamChunk as SdkApiStreamChunk } from "@cline/llms"
import { ApiConfiguration, ModelInfo } from "@shared/api"
import { ModelInfo } from "@shared/api"
import { Mode } from "@shared/storage/types"
import { ClineStorageMessage } from "@/shared/messages/content"
import { ClineTool } from "@/shared/tools"
import { ApiStream, ApiStreamUsageChunk } from "./transform/stream"
// buildApiHandler now routes inference through the Cline SDK. It lives in
// apps/vscode/src/sdk/sdk-api-handler.ts and callers import it directly from
@@ -13,22 +9,6 @@ import { ApiStream, ApiStreamUsageChunk } from "./transform/stream"
// at module-eval time (which can break extension activation). Keep this file
// types-only.
// Re-export the SDK inference contracts so callers can depend on the SDK types
// through the existing @core/api entry point. These are the canonical handler
// and stream types going forward; the local interfaces below remain for the
// classic provider classes until they are removed.
export type { SdkApiHandler, SdkApiStreamChunk }
export type CommonApiHandlerOptions = {
onRetryAttempt?: ApiConfiguration["onRetryAttempt"]
}
export interface ApiHandler {
createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: ClineTool[], useResponseApi?: boolean): ApiStream
getModel(): ApiHandlerModel
getApiStreamUsage?(): Promise<ApiStreamUsageChunk | undefined>
abort?(): void
}
export interface ApiHandlerModel {
id: string
info: ModelInfo
@@ -41,7 +21,3 @@ export interface ApiProviderInfo {
mode: Mode
customPrompt?: string // "compact"
}
export interface SingleCompletionHandler {
completePrompt(prompt: string): Promise<string>
}
@@ -1,98 +0,0 @@
export type ApiStream = AsyncGenerator<ApiStreamChunk> & { id?: string }
export type ApiStreamChunk = ApiStreamTextChunk | ApiStreamThinkingChunk | ApiStreamUsageChunk | ApiStreamToolCallsChunk
export interface ApiStreamTextChunk {
type: "text"
/**
* Text content generated by the model
*/
text: string
/**
* The response ID associated with this chunk
*/
id?: string
/**
* The thought signature associated with this chunk used by Gemini
*/
signature?: string
}
export interface ApiStreamUsageChunk {
type: "usage"
inputTokens: number
outputTokens: number
cacheWriteTokens?: number
cacheReadTokens?: number
thoughtsTokenCount?: number // openrouter
totalCost?: number // openrouter
/**
* The response ID associated with this response
*/
id?: string
}
export interface ApiStreamToolCallsChunk {
type: "tool_calls"
/**
* The tool call information
*/
tool_call: ApiStreamToolCall
/**
* The response ID associated with this chunk
*/
id?: string
/**
* The thought signature associated with this chunk used by Gemini
*/
signature?: string
}
export interface ApiStreamToolCall {
/**
* The call ID associated with this tool call
*/
call_id?: string
// Information about the tool being called
function: {
/**
* The tool call ID
*/
id?: string
/**
* Name of the tool
*/
name?: string
/**
* The arguments passed to the tool execution
*/
arguments?: any
}
}
export interface ApiStreamThinkingChunk {
type: "reasoning"
/**
* The reasoning text generated by the model.
* Redacted reasoning block will have this field set to "[REDACTED]" or an empty string.
*/
reasoning: string
/**
* openrouter has various properties that we can pass back unmodified in api requests to preserve reasoning traces
* This is also where we store the summary details for OpenAI.
*/
details?: unknown
/**
* It's used when sending the thinking block back to the API.
* API expects this in completed form, not as array of deltas.
* Also used by Gemini for thought signature associated with this chunk
*/
signature?: string
/**
* redacted data
*/
redacted_data?: string
/**
* The response ID associated with this chunk
*/
id?: string
}
@@ -8,14 +8,14 @@ export interface FileMetadataEntry {
user_edit_date?: number | null
}
export interface ModelMetadataEntry {
interface ModelMetadataEntry {
ts: number
model_id: string
model_provider_id: string
mode: string
}
export interface EnvironmentMetadataEntry {
interface EnvironmentMetadataEntry {
ts: number
os_name: string
os_version: string
@@ -1,150 +1,8 @@
import {
ActivatedConditionalRule,
getRemoteRulesTotalContentWithMetadata,
getRuleFilesTotalContentWithMetadata,
RULE_SOURCE_PREFIX,
RuleLoadResultWithInstructions,
synchronizeRuleToggles,
} from "@core/context/instructions/user-instructions/rule-helpers"
import { formatResponse } from "@core/prompts/responses"
import { synchronizeRuleToggles } from "@core/context/instructions/user-instructions/rule-helpers"
import { ensureRulesDirectoryExists, GlobalFileNames } from "@core/storage/disk"
import { StateManager } from "@core/storage/StateManager"
import { ClineRulesToggles } from "@shared/cline-rules"
import { fileExistsAtPath, isDirectory, readDirectory } from "@utils/fs"
import fs from "fs/promises"
import path from "path"
import { Controller } from "@/core/controller"
import { Logger } from "@/shared/services/Logger"
import { parseYamlFrontmatter } from "./frontmatter"
import { evaluateRuleConditionals, type RuleEvaluationContext } from "./rule-conditionals"
export const getGlobalClineRules = async (
globalClineRulesFilePath: string,
toggles: ClineRulesToggles,
opts?: { evaluationContext?: RuleEvaluationContext },
): Promise<RuleLoadResultWithInstructions> => {
let combinedContent = ""
const activatedConditionalRules: ActivatedConditionalRule[] = []
// 1. Get file-based rules
if (await fileExistsAtPath(globalClineRulesFilePath)) {
if (await isDirectory(globalClineRulesFilePath)) {
try {
const rulesFilePaths = await readDirectory(globalClineRulesFilePath)
// Note: ruleNamePrefix explicitly set to "global" for clarity (matches the default)
const rulesFilesTotal = await getRuleFilesTotalContentWithMetadata(
rulesFilePaths,
globalClineRulesFilePath,
toggles,
{
evaluationContext: opts?.evaluationContext,
ruleNamePrefix: "global",
},
)
if (rulesFilesTotal.content) {
combinedContent = rulesFilesTotal.content
activatedConditionalRules.push(...rulesFilesTotal.activatedConditionalRules)
}
} catch {
Logger.error(`Failed to read .clinerules directory at ${globalClineRulesFilePath}`)
}
} else {
Logger.error(`${globalClineRulesFilePath} is not a directory`)
}
}
// 2. Append remote config rules
const stateManager = StateManager.get()
const remoteConfigSettings = stateManager.getRemoteConfigSettings()
const remoteRules = remoteConfigSettings.remoteGlobalRules || []
const remoteToggles = stateManager.getGlobalStateKey("remoteRulesToggles") || {}
const remoteResult = getRemoteRulesTotalContentWithMetadata(remoteRules, remoteToggles, {
evaluationContext: opts?.evaluationContext,
})
if (remoteResult.content) {
if (combinedContent) combinedContent += "\n\n"
combinedContent += remoteResult.content
activatedConditionalRules.push(...remoteResult.activatedConditionalRules)
}
// 3. Return formatted instructions
if (!combinedContent) {
return { instructions: undefined, activatedConditionalRules: [] }
}
return {
instructions: formatResponse.clineRulesGlobalDirectoryInstructions(globalClineRulesFilePath, combinedContent),
activatedConditionalRules,
}
}
export const getLocalClineRules = async (
cwd: string,
toggles: ClineRulesToggles,
opts?: { evaluationContext?: RuleEvaluationContext },
): Promise<RuleLoadResultWithInstructions> => {
const clineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
let instructions: string | undefined
const activatedConditionalRules: ActivatedConditionalRule[] = []
if (await fileExistsAtPath(clineRulesFilePath)) {
if (await isDirectory(clineRulesFilePath)) {
try {
const rulesFilePaths = await readDirectory(clineRulesFilePath, [
[".clinerules", "workflows"],
[".clinerules", "hooks"],
[".clinerules", "skills"],
])
const rulesFilesTotal = await getRuleFilesTotalContentWithMetadata(rulesFilePaths, cwd, toggles, {
evaluationContext: opts?.evaluationContext,
ruleNamePrefix: "workspace",
})
if (rulesFilesTotal.content) {
instructions = formatResponse.clineRulesLocalDirectoryInstructions(cwd, rulesFilesTotal.content)
activatedConditionalRules.push(...rulesFilesTotal.activatedConditionalRules)
}
} catch {
Logger.error(`Failed to read .clinerules directory at ${clineRulesFilePath}`)
}
} else {
try {
if (clineRulesFilePath in toggles && toggles[clineRulesFilePath] !== false) {
const raw = (await fs.readFile(clineRulesFilePath, "utf8")).trim()
if (raw) {
// Keep single-file .clinerules behavior consistent with directory/remote rules:
// - Parse YAML frontmatter (fail-open on parse errors)
// - Evaluate conditionals against the request's evaluation context
const parsed = parseYamlFrontmatter(raw)
if (parsed.hadFrontmatter && parsed.parseError) {
// Fail-open: preserve the raw contents so the LLM can still see the author's intent.
instructions = formatResponse.clineRulesLocalFileInstructions(cwd, raw)
} else {
const { passed, matchedConditions } = evaluateRuleConditionals(
parsed.data,
opts?.evaluationContext ?? {},
)
if (passed) {
instructions = formatResponse.clineRulesLocalFileInstructions(cwd, parsed.body.trim())
if (parsed.hadFrontmatter && Object.keys(matchedConditions).length > 0) {
activatedConditionalRules.push({
name: `${RULE_SOURCE_PREFIX.workspace}:${GlobalFileNames.clineRules}`,
matchedConditions,
})
}
}
}
}
}
} catch {
Logger.error(`Failed to read .clinerules file at ${clineRulesFilePath}`)
}
}
}
return { instructions, activatedConditionalRules }
}
export async function refreshClineRulesToggles(
controller: Controller,
@@ -1,18 +1,8 @@
import {
combineRuleToggles,
getRuleFilesTotalContent,
readDirectoryRecursive,
synchronizeRuleToggles,
} from "@core/context/instructions/user-instructions/rule-helpers"
import { formatResponse } from "@core/prompts/responses"
import { combineRuleToggles, synchronizeRuleToggles } from "@core/context/instructions/user-instructions/rule-helpers"
import { GlobalFileNames } from "@core/storage/disk"
import { listFiles } from "@services/glob/list-files"
import { ClineRulesToggles } from "@shared/cline-rules"
import { fileExistsAtPath, isDirectory } from "@utils/fs"
import fs from "fs/promises"
import path from "path"
import { Controller } from "@/core/controller"
import { Logger } from "@/shared/services/Logger"
/**
* Refreshes the toggles for windsurf, cursor, and agents rules
@@ -57,146 +47,3 @@ export async function refreshExternalRulesToggles(
agentsLocalToggles: updatedLocalAgentsToggles,
}
}
/**
* Gather formatted windsurf rules
*/
export const getLocalWindsurfRules = async (cwd: string, toggles: ClineRulesToggles) => {
const windsurfRulesFilePath = path.resolve(cwd, GlobalFileNames.windsurfRules)
let windsurfRulesFileInstructions: string | undefined
if (await fileExistsAtPath(windsurfRulesFilePath)) {
if (!(await isDirectory(windsurfRulesFilePath))) {
try {
if (windsurfRulesFilePath in toggles && toggles[windsurfRulesFilePath] !== false) {
const ruleFileContent = (await fs.readFile(windsurfRulesFilePath, "utf8")).trim()
if (ruleFileContent) {
windsurfRulesFileInstructions = formatResponse.windsurfRulesLocalFileInstructions(cwd, ruleFileContent)
}
}
} catch {
Logger.error(`Failed to read .windsurfrules file at ${windsurfRulesFilePath}`)
}
}
}
return windsurfRulesFileInstructions
}
/**
* Gather formatted cursor rules, which can come from two sources
*/
export const getLocalCursorRules = async (cwd: string, toggles: ClineRulesToggles) => {
// we first check for the .cursorrules file
const cursorRulesFilePath = path.resolve(cwd, GlobalFileNames.cursorRulesFile)
let cursorRulesFileInstructions: string | undefined
if (await fileExistsAtPath(cursorRulesFilePath)) {
if (!(await isDirectory(cursorRulesFilePath))) {
try {
if (cursorRulesFilePath in toggles && toggles[cursorRulesFilePath] !== false) {
const ruleFileContent = (await fs.readFile(cursorRulesFilePath, "utf8")).trim()
if (ruleFileContent) {
cursorRulesFileInstructions = formatResponse.cursorRulesLocalFileInstructions(cwd, ruleFileContent)
}
}
} catch {
Logger.error(`Failed to read .cursorrules file at ${cursorRulesFilePath}`)
}
}
}
// we then check for the .cursor/rules dir
const cursorRulesDirPath = path.resolve(cwd, GlobalFileNames.cursorRulesDir)
let cursorRulesDirInstructions: string | undefined
if (await fileExistsAtPath(cursorRulesDirPath)) {
if (await isDirectory(cursorRulesDirPath)) {
try {
const rulesFilePaths = await readDirectoryRecursive(cursorRulesDirPath, ".mdc")
const rulesFilesTotalContent = await getRuleFilesTotalContent(rulesFilePaths, cwd, toggles)
if (rulesFilesTotalContent) {
cursorRulesDirInstructions = formatResponse.cursorRulesLocalDirectoryInstructions(cwd, rulesFilesTotalContent)
}
} catch {
Logger.error(`Failed to read .cursor/rules directory at ${cursorRulesDirPath}`)
}
}
}
return [cursorRulesFileInstructions, cursorRulesDirInstructions]
}
/**
* Helper function to find all agents.md files recursively (case-insensitive)
* Only searches if a top-level agents.md file exists
*/
async function findAgentsMdFiles(cwd: string): Promise<string[]> {
try {
// First check if top-level agents.md exists
const topLevelAgentsPath = path.resolve(cwd, GlobalFileNames.agentsRulesFile)
const topLevelExists = await fileExistsAtPath(topLevelAgentsPath)
// Only search recursively if top-level agents.md exists
if (!topLevelExists) {
return []
}
// Search recursively for all agents.md files
const [allFiles] = await listFiles(cwd, true, 500)
return allFiles.filter((filePath) => {
const basename = path.basename(filePath).toLowerCase()
return basename === GlobalFileNames.agentsRulesFile.toLowerCase()
})
} catch (error) {
Logger.error(`Failed to find agents.md files in ${cwd}:`, error)
return []
}
}
/**
* Gather formatted agents rules - searches recursively and combines all agents.md files
*/
export const getLocalAgentsRules = async (cwd: string, toggles: ClineRulesToggles) => {
const agentsRulesFilePath = path.resolve(cwd, GlobalFileNames.agentsRulesFile)
// Check if the top-level agents.md file is enabled
if (agentsRulesFilePath in toggles && toggles[agentsRulesFilePath] === false) {
return undefined
}
try {
const agentsMdFiles = await findAgentsMdFiles(cwd)
if (agentsMdFiles.length === 0) {
return undefined
}
// Read and combine all agents.md files
const combinedContent = await Promise.all(
agentsMdFiles.map(async (filePath) => {
try {
const fullPath = path.resolve(cwd, filePath)
const content = (await fs.readFile(fullPath, "utf8")).trim()
if (content) {
const relativePath = path.relative(cwd, fullPath)
return `## ${relativePath}\n\n${content}`
}
return null
} catch (error) {
Logger.error(`Failed to read agents.md file at ${filePath}:`, error)
return null
}
}),
).then((contents) => contents.filter(Boolean).join("\n\n"))
if (combinedContent) {
return formatResponse.agentsRulesLocalFileInstructions(cwd, combinedContent)
}
} catch (error) {
Logger.error("Failed to read agents.md files:", error)
}
return undefined
}
@@ -9,7 +9,6 @@
* - The `paths` conditional matches if any candidate path matches any glob pattern.
* - Candidate paths are expected to be workspace-root-relative POSIX paths.
*/
import * as path from "path"
import picomatch from "picomatch"
export type RuleEvaluationContext = {
@@ -20,8 +19,6 @@ export type RuleEvaluationContext = {
paths?: string[]
}
export type ConditionalEvaluator = (frontmatterValue: unknown, context: RuleEvaluationContext) => boolean
type MatchedConditions = Record<string, string[]>
type ConditionalEvaluatorResult = {
@@ -154,14 +151,3 @@ export function extractPathLikeStrings(text: string): string[] {
}
return result
}
/**
* Normalize an absolute filesystem path to a workspace-root-relative POSIX path.
* Returns undefined if the absolute path is not within the given root.
*/
export function toWorkspaceRelativePosixPath(absPath: string, workspaceRoot: string): string | undefined {
const rel = path.relative(workspaceRoot, absPath)
// Outside the root
if (rel.startsWith("..") || path.isAbsolute(rel)) return undefined
return toPosix(rel)
}
@@ -12,7 +12,7 @@ import { evaluateRuleConditionals, RuleEvaluationContext } from "./rule-conditio
/**
* Recursively traverses directory and finds all files, including checking for optional whitelisted file extension
*/
export async function readDirectoryRecursive(
async function readDirectoryRecursive(
directoryPath: string,
allowedFileExtension: string,
excludedPaths: string[][] = [],
@@ -145,11 +145,11 @@ export function combineRuleToggles(toggles1: ClineRulesToggles, toggles2: ClineR
/**
* Read the content of rules files
*/
export const getRuleFilesTotalContent = async (rulesFilePaths: string[], basePath: string, toggles: ClineRulesToggles) => {
const getRuleFilesTotalContent = async (rulesFilePaths: string[], basePath: string, toggles: ClineRulesToggles) => {
return (await getRuleFilesTotalContentWithMetadata(rulesFilePaths, basePath, toggles)).content
}
export type ActivatedConditionalRule = {
type ActivatedConditionalRule = {
name: string
matchedConditions: Record<string, string[]>
}
@@ -171,7 +171,7 @@ export type RuleLoadResult = {
* Result type for rule loading functions that return formatted instructions.
* Used by getGlobalClineRules and getLocalClineRules.
*/
export type RuleLoadResultWithInstructions = {
type RuleLoadResultWithInstructions = {
instructions?: string
activatedConditionalRules: ActivatedConditionalRule[]
}
@@ -231,7 +231,7 @@ export const getRuleFilesTotalContentWithMetadata = async (
}
}
export function getRemoteRulesTotalContentWithMetadata(
function getRemoteRulesTotalContentWithMetadata(
remoteRules: GlobalInstructionsFile[],
remoteToggles: ClineRulesToggles,
opts?: { evaluationContext?: RuleEvaluationContext },
@@ -275,7 +275,7 @@ export function getRemoteRulesTotalContentWithMetadata(
* Doesn't do anything if the dir already exists or doesn't exist
* Returns whether there are any uncaught errors
*/
export async function ensureLocalClineDirExists(clinerulePath: string, defaultRuleFilename: string): Promise<boolean> {
async function ensureLocalClineDirExists(clinerulePath: string, defaultRuleFilename: string): Promise<boolean> {
try {
const exists = await fileExistsAtPath(clinerulePath)
@@ -18,13 +18,6 @@ export interface ValidatedRemoteSkill {
contents: string
}
export interface SkillToggleState {
globalSkillsToggles?: Record<string, boolean>
localSkillsToggles?: Record<string, boolean>
remoteSkillsToggles?: Record<string, boolean>
remoteSkillEntries?: GlobalInstructionsFile[]
}
/**
* Parse and validate remote skill entries from GlobalInstructionsFile[].
*
@@ -202,34 +195,6 @@ export function getAvailableSkills(skills: SkillMetadata[]): SkillMetadata[] {
return result
}
export function filterEnabledSkills(skills: SkillMetadata[], toggleState: SkillToggleState = {}): SkillMetadata[] {
const globalSkillsToggles = toggleState.globalSkillsToggles ?? {}
const localSkillsToggles = toggleState.localSkillsToggles ?? {}
const remoteSkillsToggles = toggleState.remoteSkillsToggles ?? {}
const remoteSkillMap = new Map(
parseRemoteSkillEntries(toggleState.remoteSkillEntries || []).map((entry) => [entry.name, entry]),
)
return skills.filter((skill) => {
if (skill.path.startsWith("remote:")) {
const name = skill.path.replace("remote:", "")
const entry = remoteSkillMap.get(name)
if (entry?.alwaysEnabled) {
return true
}
return remoteSkillsToggles[name] !== false
}
const toggles = skill.source === "global" ? globalSkillsToggles : localSkillsToggles
return toggles[skill.path] !== false
})
}
export async function discoverAvailableSkills(cwd: string, toggleState: SkillToggleState = {}): Promise<SkillMetadata[]> {
const allSkills = await discoverSkills(cwd, toggleState.remoteSkillEntries)
return filterEnabledSkills(getAvailableSkills(allSkills), toggleState)
}
/**
* Get full skill content including instructions.
* For remote skills, pass remoteSkillEntries so content can be loaded without disk I/O.
@@ -1,20 +1,7 @@
import { CheckpointEvent, CheckpointEvent_OperationType, CheckpointSubscriptionRequest } from "@shared/proto/cline/checkpoints"
import { Timestamp } from "@shared/proto/google/protobuf/timestamp"
import { Logger } from "@/shared/services/Logger"
import { CheckpointEvent, CheckpointSubscriptionRequest } from "@shared/proto/cline/checkpoints"
import { getRequestRegistry, StreamingResponseHandler } from "../grpc-handler"
import { Controller } from "../index"
/**
* Parameters for creating a checkpoint event
*/
export interface CheckpointEventData {
operation: keyof typeof CheckpointEvent_OperationType
cwdHash: string
isActive: boolean
taskId?: string
commitHash?: string
}
/**
* Track active checkpoint subscriptions per workspace.
* Map structure: cwdHash -> Set of response streams
@@ -72,47 +59,3 @@ export async function subscribeToCheckpoints(
)
}
}
/**
* Send a checkpoint event to all subscribers of the specified workspace.
*
* @param eventData The checkpoint event to send
*/
export async function sendCheckpointEvent(eventData: CheckpointEventData): Promise<void> {
const { cwdHash } = eventData
const subscriptions = activeCheckpointSubscriptions.get(cwdHash)
if (!subscriptions || subscriptions.size === 0) {
return
}
const now = new Date()
const timestamp: Timestamp = {
seconds: Math.trunc(now.getTime() / 1_000),
nanos: (now.getTime() % 1_000) * 1_000_000,
}
const event: CheckpointEvent = {
operation: CheckpointEvent_OperationType[eventData.operation],
cwdHash: eventData.cwdHash,
isActive: eventData.isActive,
timestamp,
taskId: eventData.taskId,
commitHash: eventData.commitHash,
}
// Send the event to all active subscribers for this workspace
const promises = Array.from(subscriptions).map(async (responseStream) => {
try {
await responseStream(event, false) // Not the last message
} catch (error) {
Logger.error("Error sending checkpoint event:", error)
subscriptions.delete(responseStream)
if (subscriptions.size === 0) {
activeCheckpointSubscriptions.delete(cwdHash)
}
}
})
await Promise.all(promises)
}
@@ -2,7 +2,7 @@ import { FALLBACK_CLINE_RECOMMENDED_MODELS, fetchClineRecommendedModels } from "
import { ClineEnv } from "@/config"
import { fetch } from "@/shared/net"
export interface ClineRecommendedModelData {
interface ClineRecommendedModelData {
id: string
name: string
description: string
@@ -325,7 +325,7 @@ const CLINE_STEALTH_MODELS: Record<string, ModelInfo> = {
},
}
export function appendClineStealthModels(currentModels: Record<string, ModelInfo>): Record<string, ModelInfo> {
function appendClineStealthModels(currentModels: Record<string, ModelInfo>): Record<string, ModelInfo> {
// Create a shallow clone of the current models to avoid mutating the original object
const cloned = { ...currentModels }
for (const [modelId, modelInfo] of Object.entries(CLINE_STEALTH_MODELS)) {
@@ -8,7 +8,7 @@ import { Controller } from "../index"
const activePartialMessageSubscriptions = new Set<StreamingResponseHandler<ClineMessage>>()
// Keep track of callback-based subscriptions (for CLI and other non-gRPC consumers)
export type PartialMessageCallback = (message: ClineMessage) => void
type PartialMessageCallback = (message: ClineMessage) => void
const callbackSubscriptions = new Set<PartialMessageCallback>()
/**
@@ -38,18 +38,6 @@ export async function subscribeToPartialMessage(
}
}
/**
* Register a callback to receive partial message events (for CLI and non-gRPC consumers)
* @param callback The callback function to receive messages
* @returns A function to unsubscribe
*/
export function registerPartialMessageCallback(callback: PartialMessageCallback): () => void {
callbackSubscriptions.add(callback)
return () => {
callbackSubscriptions.delete(callback)
}
}
/**
* Send a partial message event to all active subscribers
* @param partialMessage The ClineMessage to send
@@ -1,5 +1,4 @@
import { Empty, EmptyRequest } from "@shared/proto/cline/common"
import { Logger } from "@/shared/services/Logger"
import { getRequestRegistry, StreamingResponseHandler } from "../grpc-handler"
import { Controller } from "../index"
@@ -32,25 +31,3 @@ export async function subscribeToRelinquishControl(
getRequestRegistry().registerRequest(requestId, cleanup, { type: "relinquish_control_subscription" }, responseStream)
}
}
/**
* Send a relinquish control event to all active subscribers
*/
export async function sendRelinquishControlEvent(): Promise<void> {
// Send the event to all active subscribers
const promises = Array.from(activeRelinquishControlSubscriptions).map(async (responseStream) => {
try {
const event = Empty.create({})
await responseStream(
event,
false, // Not the last message
)
} catch (error) {
Logger.error("Error sending relinquish control event:", error)
// Remove the subscription if there was an error
activeRelinquishControlSubscriptions.delete(responseStream)
}
})
await Promise.all(promises)
}
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* Types of errors that can occur during hook execution
*/
export enum HookErrorType {
enum HookErrorType {
/** Hook execution exceeded the timeout limit */
TIMEOUT = "timeout",
/** Hook output failed JSON validation */
+1 -1
View File
@@ -129,7 +129,7 @@ export interface Hooks {
}
}
export interface HookModelInputContext {
interface HookModelInputContext {
provider?: string
slug?: string
}
+1 -17
View File
@@ -1,4 +1,4 @@
export type LockType = "file" | "instance" | "folder"
type LockType = "file" | "instance" | "folder"
export interface LockRow {
id: number
@@ -12,19 +12,3 @@ export interface SqliteLockManagerOptions {
dbPath: string
instanceAddress: string // cline core address
}
export interface FolderLockOptions {
lockTarget: string // The cwdHash of the folder to lock
heldBy: string // taskId of the locking task
}
export interface FolderLockResult {
acquired: boolean // success or failure
conflictingLock?: LockRow // conflicting lock if available
}
export interface FolderLockWithRetryResult {
acquired: boolean // success or failure
skipped?: boolean // lock attempt was skipped (VS Code expected behavior)
conflictingLock?: LockRow // conflicting lock if available
}
+3 -198
View File
@@ -1,5 +1,5 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { EnvironmentMetadataEntry, TaskMetadata } from "@core/context/context-tracking/ContextTrackerTypes"
import { TaskMetadata } from "@core/context/context-tracking/ContextTrackerTypes"
import { execa } from "@packages/execa"
import { RemoteConfig } from "@shared/remote-config/schema"
import { GlobalState, Settings } from "@shared/storage/state-keys"
@@ -8,35 +8,10 @@ import fs from "fs/promises"
import os from "os"
import * as path from "path"
import { HostProvider } from "@/hosts/host-provider"
import { ExtensionRegistryInfo } from "@/registry"
import { McpMarketplaceCatalog } from "@/shared/mcp"
import { Logger } from "@/shared/services/Logger"
import { syncWorker } from "@/shared/services/worker/sync"
import { StateManager } from "./StateManager"
/**
* Atomically write data to a file using temp file + rename pattern.
* This prevents readers from seeing partial/incomplete data by writing to a temporary
* file first, then renaming it to the target location. The rename operation is atomic
* in most cases on modern systems, though behavior may vary across platforms and filesystems.
*
* @param filePath - The target file path
* @param data - The data to write
*/
async function atomicWriteFile(filePath: string, data: string): Promise<void> {
const tmpPath = `${filePath}.tmp.${Date.now()}.${Math.random().toString(36).substring(7)}.json`
try {
// Write to temporary file first
await fs.writeFile(tmpPath, data, "utf8")
// Rename temp file to target (atomic in most cases)
await fs.rename(tmpPath, filePath)
} catch (error) {
// Clean up temp file if it exists
fs.unlink(tmpPath).catch(() => {})
throw error
}
}
export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
contextHistory: "context_history.json",
@@ -109,7 +84,7 @@ export async function getDocumentsPath(): Promise<string> {
*
* This is intended to eventually replace ~/Documents/Cline as the global config location.
*/
export function getClineHomePath(): string {
function getClineHomePath(): string {
return path.join(os.homedir(), ".cline")
}
@@ -237,54 +212,6 @@ export async function getSavedApiConversationHistory(taskId: string): Promise<An
return []
}
export async function saveApiConversationHistory(taskId: string, apiConversationHistory: Anthropic.MessageParam[]) {
try {
if (apiConversationHistory.length > 0) {
const fileName = GlobalFileNames.apiConversationHistory
const data = JSON.stringify(apiConversationHistory)
// Queue for remote sync without blocking
syncWorker().enqueue(taskId, fileName, data)
// Store locally
const filePath = path.join(await ensureTaskDirectoryExists(taskId), fileName)
await atomicWriteFile(filePath, data)
}
} catch (error) {
// in the off chance this fails, we don't want to stop the task
Logger.error("Failed to save API conversation history:", error)
}
}
/**
* Collects environment metadata for the current system and host.
* This information is used for debugging and task portability.
* Returns metadata without timestamp - timestamp is added by EnvironmentContextTracker.
*/
export async function collectEnvironmentMetadata(): Promise<Omit<EnvironmentMetadataEntry, "ts">> {
try {
const hostVersion = await HostProvider.env.getHostVersion({})
return {
os_name: os.platform(),
os_version: os.release(),
os_arch: os.arch(),
host_name: hostVersion.platform || "Unknown",
host_version: hostVersion.version || "Unknown",
cline_version: ExtensionRegistryInfo.version,
}
} catch (error) {
Logger.error("Failed to collect environment metadata:", error)
// Return fallback values if collection fails
return {
os_name: os.platform(),
os_version: os.release(),
os_arch: os.arch(),
host_name: "Unknown",
host_version: "Unknown",
cline_version: "Unknown",
}
}
}
export async function getTaskMetadata(taskId: string): Promise<TaskMetadata> {
const filePath = path.join(await ensureTaskDirectoryExists(taskId), GlobalFileNames.taskMetadata)
try {
@@ -307,10 +234,6 @@ export async function saveTaskMetadata(taskId: string, metadata: TaskMetadata) {
}
}
export async function ensureStateDirectoryExists(): Promise<string> {
return getGlobalStorageDir("state")
}
export async function ensureCacheDirectoryExists(): Promise<string> {
return getGlobalStorageDir("cache")
}
@@ -422,7 +345,7 @@ export async function deleteRemoteConfigFromCache(organizationId: string): Promi
* Gets the path to the global hooks directory if it exists.
* Returns undefined if the directory doesn't exist.
*/
export async function getGlobalHooksDir(): Promise<string | undefined> {
async function getGlobalHooksDir(): Promise<string | undefined> {
const globalHooksDir = await ensureHooksDirectoryExists()
return (await isDirectory(globalHooksDir)) ? globalHooksDir : undefined
}
@@ -491,121 +414,3 @@ export async function getWorkspaceHooksDirs(): Promise<string[]> {
)
).filter((path): path is string => Boolean(path))
}
/**
* Writes the conversation history to a temporary JSON file for PreCompact hook consumption.
* The file is created in the task's directory with a unique timestamp-based name.
* Returns the absolute path to the created file.
*
* @param taskId The task ID
* @param apiConversationHistory The conversation history to write
* @param timestamp Optional timestamp to use for the filename (defaults to Date.now())
* @returns The absolute path to the temporary file
*/
export async function writeConversationHistoryJson(
taskId: string,
apiConversationHistory: Anthropic.MessageParam[],
timestamp?: number,
): Promise<string> {
const taskDir = await ensureTaskDirectoryExists(taskId)
const fileTimestamp = timestamp ?? Date.now()
const tempFileName = `conversation_history_${fileTimestamp}.json`
const tempFilePath = path.join(taskDir, tempFileName)
try {
await atomicWriteFile(tempFilePath, JSON.stringify(apiConversationHistory, null, 2))
return tempFilePath
} catch (error) {
Logger.error("Failed to write conversation history JSON for hook:", error)
throw error
}
}
/**
* Cleans up a temporary conversation history file created for hook execution.
* Silently handles errors (file already deleted, permissions, etc.)
*
* @param filePath The path to the temporary file to delete
*/
export async function cleanupConversationHistoryFile(filePath: string): Promise<void> {
try {
if (await fileExistsAtPath(filePath)) {
await fs.unlink(filePath)
}
} catch (error) {
// Silently handle errors - this is cleanup, not critical
Logger.debug("Failed to cleanup conversation history file:", filePath, error)
}
}
/**
* Writes the conversation history in human-readable text format to a temporary file for PreCompact hook consumption.
* This formats the conversation history (user and assistant messages) in a readable text format,
* making it easy to analyze the conversation flow without parsing JSON.
*
* @param taskId The task ID
* @param conversationHistory The conversation history messages
* @param timestamp Optional timestamp to use for the filename (defaults to Date.now())
* @returns The absolute path to the temporary file
*/
export async function writeConversationHistoryText(
taskId: string,
conversationHistory: Anthropic.MessageParam[],
timestamp?: number,
): Promise<string> {
const taskDir = await ensureTaskDirectoryExists(taskId)
const fileTimestamp = timestamp ?? Date.now()
const tempFileName = `conversation_history_${fileTimestamp}.txt`
const tempFilePath = path.join(taskDir, tempFileName)
try {
// Build the formatted conversation history (excluding system prompt)
let fullContext = "=== CONVERSATION HISTORY ===\n\n"
// Format each message in the conversation
for (let i = 0; i < conversationHistory.length; i++) {
const message = conversationHistory[i]
fullContext += `--- Message ${i + 1} (${message.role.toUpperCase()}) ---\n`
// Handle content which can be a string or array
if (typeof message.content === "string") {
fullContext += message.content
} else if (Array.isArray(message.content)) {
for (const block of message.content) {
if (block.type === "text") {
fullContext += block.text
} else if (block.type === "image") {
fullContext += `[IMAGE: ${block.source?.type || "unknown"}]`
} else if (block.type === "tool_use") {
fullContext += `[TOOL USE: ${block.name}]\n`
fullContext += `Input: ${JSON.stringify(block.input, null, 2)}`
} else if (block.type === "tool_result") {
fullContext += `[TOOL RESULT: ${block.tool_use_id}]\n`
if (typeof block.content === "string") {
fullContext += block.content
} else if (Array.isArray(block.content)) {
for (const resultBlock of block.content) {
if (resultBlock.type === "text") {
fullContext += resultBlock.text
} else if (resultBlock.type === "image") {
fullContext += `[IMAGE]`
}
}
}
}
fullContext += "\n\n"
}
}
fullContext += "\n"
}
fullContext += "=== END OF CONTEXT ===\n"
await atomicWriteFile(tempFilePath, fullContext)
return tempFilePath
} catch (error) {
Logger.error("Failed to write conversation history text for hook:", error)
throw error
}
}
@@ -20,7 +20,7 @@ async function ensureGlobalRemoteConfigWorkspacePath(): Promise<string> {
return workspacePath
}
export async function getRemoteConfigWorkspacePath(workspacePath?: string): Promise<string> {
async function getRemoteConfigWorkspacePath(workspacePath?: string): Promise<string> {
const trimmed = workspacePath?.trim()
if (trimmed) {
return trimmed
@@ -70,29 +70,6 @@ export async function migrateTaskHistoryToFile(_context: vscode.ExtensionContext
// TODO migrate to sdk location
}
export async function migrateMcpMarketplaceEnableSetting(mcpMarketplaceEnabledRaw: boolean | undefined): Promise<boolean> {
const config = vscode.workspace.getConfiguration("cline")
const mcpMarketplaceEnabled = config.get<boolean>("mcpMarketplace.enabled")
if (mcpMarketplaceEnabled !== undefined) {
// Remove from VSCode configuration
await config.update("mcpMarketplace.enabled", undefined, true)
return !mcpMarketplaceEnabled
}
return mcpMarketplaceEnabledRaw ?? true
}
export async function migrateEnableCheckpointsSetting(enableCheckpointsSettingRaw: boolean | undefined): Promise<boolean> {
const config = vscode.workspace.getConfiguration("cline")
const enableCheckpoints = config.get<boolean>("enableCheckpoints")
if (enableCheckpoints !== undefined) {
// Remove from VSCode configuration
await config.update("enableCheckpoints", undefined, true)
return enableCheckpoints
}
return enableCheckpointsSettingRaw ?? true
}
export async function migrateCustomInstructionsToGlobalRules(context: vscode.ExtensionContext) {
try {
const customInstructions = (await context.globalState.get("customInstructions")) as string | undefined
@@ -138,375 +115,6 @@ export async function migrateCustomInstructionsToGlobalRules(context: vscode.Ext
}
}
export async function migrateLegacyApiConfigurationToModeSpecific(context: vscode.ExtensionContext) {
try {
// Check if migration is needed - if planModeApiProvider already exists, skip migration
const planModeApiProvider = await context.globalState.get("planModeApiProvider")
if (planModeApiProvider !== undefined) {
Logger.log("Legacy API configuration migration already completed, skipping...")
return
}
Logger.log("Starting legacy API configuration migration to mode-specific keys...")
// Get the planActSeparateModelsSetting to determine migration strategy
const planActSeparateModelsSetting = (await context.globalState.get("planActSeparateModelsSetting")) as
| boolean
| undefined
// Read legacy values directly
const apiProvider = await context.globalState.get("apiProvider")
const apiModelId = await context.globalState.get("apiModelId")
const thinkingBudgetTokens = await context.globalState.get("thinkingBudgetTokens")
const reasoningEffort = await context.globalState.get("reasoningEffort")
const vsCodeLmModelSelector = await context.globalState.get("vsCodeLmModelSelector")
const awsBedrockCustomSelected = await context.globalState.get("awsBedrockCustomSelected")
const awsBedrockCustomModelBaseId = await context.globalState.get("awsBedrockCustomModelBaseId")
const openRouterModelId = await context.globalState.get("openRouterModelId")
const openRouterModelInfo = await context.globalState.get("openRouterModelInfo")
const openAiModelId = await context.globalState.get("openAiModelId")
const openAiModelInfo = await context.globalState.get("openAiModelInfo")
const ollamaModelId = await context.globalState.get("ollamaModelId")
const lmStudioModelId = await context.globalState.get("lmStudioModelId")
const liteLlmModelId = await context.globalState.get("liteLlmModelId")
const liteLlmModelInfo = await context.globalState.get("liteLlmModelInfo")
const requestyModelId = await context.globalState.get("requestyModelId")
const requestyModelInfo = await context.globalState.get("requestyModelInfo")
const togetherModelId = await context.globalState.get("togetherModelId")
const fireworksModelId = await context.globalState.get("fireworksModelId")
const sapAiCoreModelId = await context.globalState.get("sapAiCoreModelId")
const groqModelId = await context.globalState.get("groqModelId")
const groqModelInfo = await context.globalState.get("groqModelInfo")
const huggingFaceModelId = await context.globalState.get("huggingFaceModelId")
const huggingFaceModelInfo = await context.globalState.get("huggingFaceModelInfo")
// Read previous mode values
const previousModeApiProvider = await context.globalState.get("previousModeApiProvider")
const previousModeModelId = await context.globalState.get("previousModeModelId")
const previousModeModelInfo = await context.globalState.get("previousModeModelInfo")
const previousModeVsCodeLmModelSelector = await context.globalState.get("previousModeVsCodeLmModelSelector")
const previousModeThinkingBudgetTokens = await context.globalState.get("previousModeThinkingBudgetTokens")
const previousModeReasoningEffort = await context.globalState.get("previousModeReasoningEffort")
const previousModeAwsBedrockCustomSelected = await context.globalState.get("previousModeAwsBedrockCustomSelected")
const previousModeAwsBedrockCustomModelBaseId = await context.globalState.get("previousModeAwsBedrockCustomModelBaseId")
const previousModeSapAiCoreModelId = await context.globalState.get("previousModeSapAiCoreModelId")
// Migrate based on planActSeparateModelsSetting
if (planActSeparateModelsSetting === false) {
Logger.log("Migrating with separate models DISABLED - using current values for both modes")
// Use current values for both plan and act modes
if (apiProvider !== undefined) {
await context.globalState.update("planModeApiProvider", apiProvider)
await context.globalState.update("actModeApiProvider", apiProvider)
}
if (apiModelId !== undefined) {
await context.globalState.update("planModeApiModelId", apiModelId)
await context.globalState.update("actModeApiModelId", apiModelId)
}
if (thinkingBudgetTokens !== undefined) {
await context.globalState.update("planModeThinkingBudgetTokens", thinkingBudgetTokens)
await context.globalState.update("actModeThinkingBudgetTokens", thinkingBudgetTokens)
}
if (reasoningEffort !== undefined) {
await context.globalState.update("planModeReasoningEffort", reasoningEffort)
await context.globalState.update("actModeReasoningEffort", reasoningEffort)
}
if (vsCodeLmModelSelector !== undefined) {
await context.globalState.update("planModeVsCodeLmModelSelector", vsCodeLmModelSelector)
await context.globalState.update("actModeVsCodeLmModelSelector", vsCodeLmModelSelector)
}
if (awsBedrockCustomSelected !== undefined) {
await context.globalState.update("planModeAwsBedrockCustomSelected", awsBedrockCustomSelected)
await context.globalState.update("actModeAwsBedrockCustomSelected", awsBedrockCustomSelected)
}
if (awsBedrockCustomModelBaseId !== undefined) {
await context.globalState.update("planModeAwsBedrockCustomModelBaseId", awsBedrockCustomModelBaseId)
await context.globalState.update("actModeAwsBedrockCustomModelBaseId", awsBedrockCustomModelBaseId)
}
if (openRouterModelId !== undefined) {
await context.globalState.update("planModeOpenRouterModelId", openRouterModelId)
await context.globalState.update("actModeOpenRouterModelId", openRouterModelId)
}
if (openRouterModelInfo !== undefined) {
await context.globalState.update("planModeOpenRouterModelInfo", openRouterModelInfo)
await context.globalState.update("actModeOpenRouterModelInfo", openRouterModelInfo)
}
if (openAiModelId !== undefined) {
await context.globalState.update("planModeOpenAiModelId", openAiModelId)
await context.globalState.update("actModeOpenAiModelId", openAiModelId)
}
if (openAiModelInfo !== undefined) {
await context.globalState.update("planModeOpenAiModelInfo", openAiModelInfo)
await context.globalState.update("actModeOpenAiModelInfo", openAiModelInfo)
}
if (ollamaModelId !== undefined) {
await context.globalState.update("planModeOllamaModelId", ollamaModelId)
await context.globalState.update("actModeOllamaModelId", ollamaModelId)
}
if (lmStudioModelId !== undefined) {
await context.globalState.update("planModeLmStudioModelId", lmStudioModelId)
await context.globalState.update("actModeLmStudioModelId", lmStudioModelId)
}
if (liteLlmModelId !== undefined) {
await context.globalState.update("planModeLiteLlmModelId", liteLlmModelId)
await context.globalState.update("actModeLiteLlmModelId", liteLlmModelId)
}
if (liteLlmModelInfo !== undefined) {
await context.globalState.update("planModeLiteLlmModelInfo", liteLlmModelInfo)
await context.globalState.update("actModeLiteLlmModelInfo", liteLlmModelInfo)
}
if (requestyModelId !== undefined) {
await context.globalState.update("planModeRequestyModelId", requestyModelId)
await context.globalState.update("actModeRequestyModelId", requestyModelId)
}
if (requestyModelInfo !== undefined) {
await context.globalState.update("planModeRequestyModelInfo", requestyModelInfo)
await context.globalState.update("actModeRequestyModelInfo", requestyModelInfo)
}
if (togetherModelId !== undefined) {
await context.globalState.update("planModeTogetherModelId", togetherModelId)
await context.globalState.update("actModeTogetherModelId", togetherModelId)
}
if (fireworksModelId !== undefined) {
await context.globalState.update("planModeFireworksModelId", fireworksModelId)
await context.globalState.update("actModeFireworksModelId", fireworksModelId)
}
if (sapAiCoreModelId !== undefined) {
await context.globalState.update("planModeSapAiCoreModelId", sapAiCoreModelId)
await context.globalState.update("actModeSapAiCoreModelId", sapAiCoreModelId)
}
if (groqModelId !== undefined) {
await context.globalState.update("planModeGroqModelId", groqModelId)
await context.globalState.update("actModeGroqModelId", groqModelId)
}
if (groqModelInfo !== undefined) {
await context.globalState.update("planModeGroqModelInfo", groqModelInfo)
await context.globalState.update("actModeGroqModelInfo", groqModelInfo)
}
if (huggingFaceModelId !== undefined) {
await context.globalState.update("planModeHuggingFaceModelId", huggingFaceModelId)
await context.globalState.update("actModeHuggingFaceModelId", huggingFaceModelId)
}
if (huggingFaceModelInfo !== undefined) {
await context.globalState.update("planModeHuggingFaceModelInfo", huggingFaceModelInfo)
await context.globalState.update("actModeHuggingFaceModelInfo", huggingFaceModelInfo)
}
} else {
Logger.log("Migrating with separate models ENABLED - using current->plan, previous->act")
// Use current values for plan mode
if (apiProvider !== undefined) {
await context.globalState.update("planModeApiProvider", apiProvider)
}
if (apiModelId !== undefined) {
await context.globalState.update("planModeApiModelId", apiModelId)
}
if (thinkingBudgetTokens !== undefined) {
await context.globalState.update("planModeThinkingBudgetTokens", thinkingBudgetTokens)
}
if (reasoningEffort !== undefined) {
await context.globalState.update("planModeReasoningEffort", reasoningEffort)
}
if (vsCodeLmModelSelector !== undefined) {
await context.globalState.update("planModeVsCodeLmModelSelector", vsCodeLmModelSelector)
}
if (awsBedrockCustomSelected !== undefined) {
await context.globalState.update("planModeAwsBedrockCustomSelected", awsBedrockCustomSelected)
}
if (awsBedrockCustomModelBaseId !== undefined) {
await context.globalState.update("planModeAwsBedrockCustomModelBaseId", awsBedrockCustomModelBaseId)
}
if (openRouterModelId !== undefined) {
await context.globalState.update("planModeOpenRouterModelId", openRouterModelId)
}
if (openRouterModelInfo !== undefined) {
await context.globalState.update("planModeOpenRouterModelInfo", openRouterModelInfo)
}
if (openAiModelId !== undefined) {
await context.globalState.update("planModeOpenAiModelId", openAiModelId)
}
if (openAiModelInfo !== undefined) {
await context.globalState.update("planModeOpenAiModelInfo", openAiModelInfo)
}
if (ollamaModelId !== undefined) {
await context.globalState.update("planModeOllamaModelId", ollamaModelId)
}
if (lmStudioModelId !== undefined) {
await context.globalState.update("planModeLmStudioModelId", lmStudioModelId)
}
if (liteLlmModelId !== undefined) {
await context.globalState.update("planModeLiteLlmModelId", liteLlmModelId)
}
if (liteLlmModelInfo !== undefined) {
await context.globalState.update("planModeLiteLlmModelInfo", liteLlmModelInfo)
}
if (requestyModelId !== undefined) {
await context.globalState.update("planModeRequestyModelId", requestyModelId)
}
if (requestyModelInfo !== undefined) {
await context.globalState.update("planModeRequestyModelInfo", requestyModelInfo)
}
if (togetherModelId !== undefined) {
await context.globalState.update("planModeTogetherModelId", togetherModelId)
}
if (fireworksModelId !== undefined) {
await context.globalState.update("planModeFireworksModelId", fireworksModelId)
}
if (sapAiCoreModelId !== undefined) {
await context.globalState.update("planModeSapAiCoreModelId", sapAiCoreModelId)
}
if (groqModelId !== undefined) {
await context.globalState.update("planModeGroqModelId", groqModelId)
}
if (groqModelInfo !== undefined) {
await context.globalState.update("planModeGroqModelInfo", groqModelInfo)
}
if (huggingFaceModelId !== undefined) {
await context.globalState.update("planModeHuggingFaceModelId", huggingFaceModelId)
}
if (huggingFaceModelInfo !== undefined) {
await context.globalState.update("planModeHuggingFaceModelInfo", huggingFaceModelInfo)
}
// Use previous values for act mode (with fallback to current values)
if (previousModeApiProvider !== undefined) {
await context.globalState.update("actModeApiProvider", previousModeApiProvider)
} else if (apiProvider !== undefined) {
await context.globalState.update("actModeApiProvider", apiProvider)
}
if (previousModeModelId !== undefined) {
await context.globalState.update("actModeApiModelId", previousModeModelId)
} else if (apiModelId !== undefined) {
await context.globalState.update("actModeApiModelId", apiModelId)
}
if (previousModeThinkingBudgetTokens !== undefined) {
await context.globalState.update("actModeThinkingBudgetTokens", previousModeThinkingBudgetTokens)
} else if (thinkingBudgetTokens !== undefined) {
await context.globalState.update("actModeThinkingBudgetTokens", thinkingBudgetTokens)
}
if (previousModeReasoningEffort !== undefined) {
await context.globalState.update("actModeReasoningEffort", previousModeReasoningEffort)
} else if (reasoningEffort !== undefined) {
await context.globalState.update("actModeReasoningEffort", reasoningEffort)
}
if (previousModeVsCodeLmModelSelector !== undefined) {
await context.globalState.update("actModeVsCodeLmModelSelector", previousModeVsCodeLmModelSelector)
} else if (vsCodeLmModelSelector !== undefined) {
await context.globalState.update("actModeVsCodeLmModelSelector", vsCodeLmModelSelector)
}
if (previousModeAwsBedrockCustomSelected !== undefined) {
await context.globalState.update("actModeAwsBedrockCustomSelected", previousModeAwsBedrockCustomSelected)
} else if (awsBedrockCustomSelected !== undefined) {
await context.globalState.update("actModeAwsBedrockCustomSelected", awsBedrockCustomSelected)
}
if (previousModeAwsBedrockCustomModelBaseId !== undefined) {
await context.globalState.update("actModeAwsBedrockCustomModelBaseId", previousModeAwsBedrockCustomModelBaseId)
} else if (awsBedrockCustomModelBaseId !== undefined) {
await context.globalState.update("actModeAwsBedrockCustomModelBaseId", awsBedrockCustomModelBaseId)
}
if (previousModeSapAiCoreModelId !== undefined) {
await context.globalState.update("actModeSapAiCoreModelId", previousModeSapAiCoreModelId)
} else if (sapAiCoreModelId !== undefined) {
await context.globalState.update("actModeSapAiCoreModelId", sapAiCoreModelId)
}
// For fields without previous variants, use current values for act mode
if (previousModeModelInfo !== undefined) {
await context.globalState.update("actModeOpenRouterModelInfo", previousModeModelInfo)
} else if (openRouterModelInfo !== undefined) {
await context.globalState.update("actModeOpenRouterModelInfo", openRouterModelInfo)
}
if (openRouterModelId !== undefined) {
await context.globalState.update("actModeOpenRouterModelId", openRouterModelId)
}
if (openAiModelId !== undefined) {
await context.globalState.update("actModeOpenAiModelId", openAiModelId)
}
if (openAiModelInfo !== undefined) {
await context.globalState.update("actModeOpenAiModelInfo", openAiModelInfo)
}
if (ollamaModelId !== undefined) {
await context.globalState.update("actModeOllamaModelId", ollamaModelId)
}
if (lmStudioModelId !== undefined) {
await context.globalState.update("actModeLmStudioModelId", lmStudioModelId)
}
if (liteLlmModelId !== undefined) {
await context.globalState.update("actModeLiteLlmModelId", liteLlmModelId)
}
if (liteLlmModelInfo !== undefined) {
await context.globalState.update("actModeLiteLlmModelInfo", liteLlmModelInfo)
}
if (requestyModelId !== undefined) {
await context.globalState.update("actModeRequestyModelId", requestyModelId)
}
if (requestyModelInfo !== undefined) {
await context.globalState.update("actModeRequestyModelInfo", requestyModelInfo)
}
if (togetherModelId !== undefined) {
await context.globalState.update("actModeTogetherModelId", togetherModelId)
}
if (fireworksModelId !== undefined) {
await context.globalState.update("actModeFireworksModelId", fireworksModelId)
}
if (groqModelId !== undefined) {
await context.globalState.update("actModeGroqModelId", groqModelId)
}
if (groqModelInfo !== undefined) {
await context.globalState.update("actModeGroqModelInfo", groqModelInfo)
}
if (huggingFaceModelId !== undefined) {
await context.globalState.update("actModeHuggingFaceModelId", huggingFaceModelId)
}
if (huggingFaceModelInfo !== undefined) {
await context.globalState.update("actModeHuggingFaceModelInfo", huggingFaceModelInfo)
}
}
// Clean up legacy keys after successful migration
Logger.log("Cleaning up legacy keys...")
await context.globalState.update("apiProvider", undefined)
await context.globalState.update("apiModelId", undefined)
await context.globalState.update("thinkingBudgetTokens", undefined)
await context.globalState.update("reasoningEffort", undefined)
await context.globalState.update("vsCodeLmModelSelector", undefined)
await context.globalState.update("awsBedrockCustomSelected", undefined)
await context.globalState.update("awsBedrockCustomModelBaseId", undefined)
await context.globalState.update("openRouterModelId", undefined)
await context.globalState.update("openRouterModelInfo", undefined)
await context.globalState.update("openAiModelId", undefined)
await context.globalState.update("openAiModelInfo", undefined)
await context.globalState.update("ollamaModelId", undefined)
await context.globalState.update("lmStudioModelId", undefined)
await context.globalState.update("liteLlmModelId", undefined)
await context.globalState.update("liteLlmModelInfo", undefined)
await context.globalState.update("requestyModelId", undefined)
await context.globalState.update("requestyModelInfo", undefined)
await context.globalState.update("togetherModelId", undefined)
await context.globalState.update("fireworksModelId", undefined)
await context.globalState.update("sapAiCoreModelId", undefined)
await context.globalState.update("groqModelId", undefined)
await context.globalState.update("groqModelInfo", undefined)
await context.globalState.update("huggingFaceModelId", undefined)
await context.globalState.update("huggingFaceModelInfo", undefined)
await context.globalState.update("previousModeApiProvider", undefined)
await context.globalState.update("previousModeModelId", undefined)
await context.globalState.update("previousModeModelInfo", undefined)
await context.globalState.update("previousModeVsCodeLmModelSelector", undefined)
await context.globalState.update("previousModeThinkingBudgetTokens", undefined)
await context.globalState.update("previousModeReasoningEffort", undefined)
await context.globalState.update("previousModeAwsBedrockCustomSelected", undefined)
await context.globalState.update("previousModeAwsBedrockCustomModelBaseId", undefined)
await context.globalState.update("previousModeSapAiCoreModelId", undefined)
Logger.log("Successfully migrated legacy API configuration to mode-specific keys")
} catch (error) {
Logger.error("Failed to migrate legacy API configuration to mode-specific keys:", error)
// Continue execution - migration failure shouldn't break extension startup
}
}
export async function migrateWelcomeViewCompleted(context: vscode.ExtensionContext) {
try {
// Check if welcomeViewCompleted is already set
@@ -6,14 +6,14 @@ import { ensureTaskDirectoryExists } from "../../storage/disk"
/**
* Generate the standard file path for a task's focusChain markdown file
*/
export function getFocusChainFilePath(taskDir: string, taskId: string): string {
function getFocusChainFilePath(taskDir: string, taskId: string): string {
return path.join(taskDir, `focus_chain_taskid_${taskId}.md`)
}
/**
* Create the standard markdown content structure for a focusChain file
*/
export function createFocusChainMarkdownContent(taskId: string, focusChainList: string): string {
function createFocusChainMarkdownContent(taskId: string, focusChainList: string): string {
return `# Focus Chain List for Task ${taskId}
<!-- Edit this markdown file to update your focus chain list -->
@@ -28,7 +28,7 @@ ${focusChainList}
* Extract focusChain items from text content (markdown or message text)
* Returns array of lines that match focusChain item format
*/
export function extractFocusChainItemsFromText(text: string): string[] {
function extractFocusChainItemsFromText(text: string): string[] {
const lines = text.split("\n")
return lines.filter((line) => {
const trimmed = line.trim()
@@ -9,7 +9,7 @@ import { z } from "zod"
import { buildSubagentToolName } from "./SubagentToolName"
/** Default Directory for agent configurations: ~/Documents/Cline/Agents */
export const AGENTS_CONFIG_DIRECTORY_NAME = "Agents"
const AGENTS_CONFIG_DIRECTORY_NAME = "Agents"
const SUBAGENT_DYNAMIC_TOOL_NAMESPACE = "subagent"
const AgentBaseConfigSchema = z.object({
@@ -77,7 +77,7 @@ function parseSkills(skills: string | string[] | undefined): string[] | undefine
return Array.from(new Set(rawSkills.map(normalizeSkillName)))
}
export function parseAgentConfigFromYaml(content: string): AgentBaseConfig {
function parseAgentConfigFromYaml(content: string): AgentBaseConfig {
const { data, body, hadFrontmatter, parseError } = parseYamlFrontmatter(content)
if (parseError) {
throw new Error(`Failed to parse YAML frontmatter: ${parseError}`)
@@ -102,7 +102,7 @@ export function parseAgentConfigFromYaml(content: string): AgentBaseConfig {
}) as AgentBaseConfig
}
export function getAgentsConfigPath(homeDir = os.homedir()): string {
function getAgentsConfigPath(homeDir = os.homedir()): string {
return path.join(homeDir, "Documents", "Cline", AGENTS_CONFIG_DIRECTORY_NAME)
}
@@ -114,7 +114,7 @@ function isYamlFile(filePath: string): boolean {
return /\.(yaml|yml)$/i.test(filePath)
}
export async function readAgentConfigsFromDisk(homeDir = os.homedir()): Promise<Map<string, AgentBaseConfig>> {
async function readAgentConfigsFromDisk(homeDir = os.homedir()): Promise<Map<string, AgentBaseConfig>> {
const agentsDirectoryPath = getAgentsConfigPath(homeDir)
const configs = new Map<string, AgentBaseConfig>()
@@ -39,7 +39,3 @@ export function buildSubagentToolName(agentName: string): string {
const body = sanitized.slice(0, Math.max(1, maxBodyLength))
return trimToolNameToMax(`${SUBAGENT_TOOL_NAME_PREFIX}${body}_${hashSuffix}`)
}
export function isSubagentToolName(toolName: string): boolean {
return toolName.startsWith(SUBAGENT_TOOL_NAME_PREFIX)
}
@@ -327,13 +327,6 @@ export function resolveWorkspacePath(
}
}
/**
* Helper to check if we're in trace mode
*/
export function isWorkspaceTraceEnabled(): boolean {
return process.env.MULTI_ROOT_TRACE === "true" || process.env.NODE_ENV === "development"
}
/**
* Phase 0: Convenience function for path.basename with tracking
* This is what we'll use to replace existing path.basename() calls
@@ -16,9 +16,9 @@ export interface WorkspaceContext {
export class WorkspaceRootManager {
private roots: WorkspaceRoot[] = []
private primaryIndex: number = 0
private primaryIndex = 0
constructor(roots: WorkspaceRoot[] = [], primaryIndex: number = 0) {
constructor(roots: WorkspaceRoot[] = [], primaryIndex = 0) {
this.roots = roots
this.primaryIndex = Math.min(primaryIndex, Math.max(0, roots.length - 1))
}
@@ -247,12 +247,3 @@ export class WorkspaceRootManager {
return JSON.stringify({ workspaces }, null, 2)
}
}
// Export for use in Task and Controller
export function createLegacyWorkspaceRoot(cwd: string): WorkspaceRoot {
return {
path: cwd,
name: path.basename(cwd),
vcs: VcsType.None, // Will be detected properly during initialization
}
}
+2 -20
View File
@@ -3,25 +3,7 @@
*/
// Export workspace path parsing utilities
export type { ParsedWorkspacePath } from "./utils/parseWorkspaceInlinePath"
export {
addWorkspaceHint,
hasWorkspaceHint,
parseMultipleWorkspacePaths,
parseWorkspaceInlinePath,
removeWorkspaceHint,
} from "./utils/parseWorkspaceInlinePath"
export type { WorkspaceAdapterConfig } from "./WorkspacePathAdapter"
export { createWorkspacePathAdapter, WorkspacePathAdapter } from "./WorkspacePathAdapter"
export {
getWorkspaceBasename,
isWorkspaceTraceEnabled,
resolveWorkspacePath,
WorkspaceResolver,
workspaceResolver,
} from "./WorkspaceResolver"
export type { WorkspaceContext } from "./WorkspaceRootManager"
export { createLegacyWorkspaceRoot, WorkspaceRootManager } from "./WorkspaceRootManager"
export { getWorkspaceBasename, workspaceResolver } from "./WorkspaceResolver"
export { WorkspaceRootManager } from "./WorkspaceRootManager"
// Re-export convenience function at module level for easier imports
// Usage: import { resolveWorkspacePath } from "@core/workspace"
@@ -1,28 +0,0 @@
import { HostProvider } from "@/hosts/host-provider"
import { EmptyRequest } from "@/shared/proto/cline/common"
import { Logger } from "@/shared/services/Logger"
/**
* Checks if the current workspace has multiple root folders open.
* This is a lightweight check that only counts workspace folders,
* independent of feature flags or internal multi-root implementation status.
*
* Use this when you need to know the actual workspace state (e.g., for telemetry,
* headers, or UI display), not whether multi-root features are enabled.
*
* @returns true if 2 or more workspace folders are open, false otherwise
* @example
* ```typescript
* const isMultiRoot = await isMultiRootWorkspace()
* Logger.log(`User has ${isMultiRoot ? 'multiple' : 'single'} workspace folders open`)
* ```
*/
export async function isMultiRootWorkspace(): Promise<boolean> {
try {
const workspacePaths = await HostProvider.workspace.getWorkspacePaths(EmptyRequest.create({}))
return workspacePaths.paths.length > 1
} catch (error) {
Logger.error("Failed to detect multi-root workspace", error)
return false
}
}
+1 -1
View File
@@ -36,7 +36,7 @@ export type GrpcStreamingResponseHandlerWrapper = <TRequest, TResponse>(
controller: Controller,
) => grpc.handleServerStreamingCall<TRequest, TResponse>
export type StreamingResponseWriter<TResponse> = (response: TResponse, isLast?: boolean, sequenceNumber?: number) => Promise<void>
type StreamingResponseWriter<TResponse> = (response: TResponse, isLast?: boolean, sequenceNumber?: number) => Promise<void>
/**
* Abstract base class for type-safe gRPC client implementations.
@@ -173,11 +173,3 @@ export interface HostServiceHandlerConfig {
requestId?: string,
) => Promise<void>
}
/**
* Get the request registry instance
* This allows other parts of the code to access the registry
*/
export function getRequestRegistry(): GrpcRequestRegistry {
return requestRegistry
}
@@ -20,7 +20,7 @@ export interface MethodMetadata {
/**
* Generic service registry for gRPC services
*/
export class ServiceRegistry {
class ServiceRegistry {
private serviceName: string
private methodRegistry: Record<string, ServiceMethodHandler> = {}
private streamingMethodRegistry: Record<string, StreamingMethodHandler> = {}
@@ -1,4 +1,4 @@
export function ansiRegex({ onlyFirst = false } = {}) {
function ansiRegex({ onlyFirst = false } = {}) {
// Valid string terminator sequences are BEL, ESC\, and 0x9c
const ST = "(?:\\u0007|\\u001B\\u005C|\\u009C)"
const pattern = [
@@ -1,99 +1,3 @@
import { access, constants, mkdir } from "fs/promises"
import os from "os"
import * as path from "path"
import { HostProvider } from "@/hosts/host-provider"
import { getCwd, getDesktopDir } from "@/utils/path"
/**
* Gets the path to the shadow Git repository in globalStorage.
*
* Checkpoints path structure:
* globalStorage/
* checkpoints/
* {cwdHash}/
* .git/
*
* @param cwdHash - Hash of the working directory path
* @returns Promise<string> The absolute path to the shadow git directory
* @throws Error if global storage path is invalid
*/
export async function getShadowGitPath(cwdHash: string): Promise<string> {
const checkpointsDir = path.join(HostProvider.get().globalStorageFsPath, "checkpoints", cwdHash)
await mkdir(checkpointsDir, { recursive: true })
const gitPath = path.join(checkpointsDir, ".git")
return gitPath
}
/**
* Validates that a workspace path is safe for checkpoints.
* Checks that checkpoints are not being used in protected directories
* like home, Desktop, Documents, or Downloads. Also confirms that the workspace
* is accessible and that we will not encounter breaking permissions issues when
* creating checkpoints.
*
* Protected directories:
* - User's home directory
* - Desktop
* - Documents
* - Downloads
*
* @param workspacePath - The absolute path to the workspace directory to validate
* @returns Promise<void> Resolves if the path is valid
* @throws Error if the path is in a protected directory or if no read access
*/
export async function validateWorkspacePath(workspacePath: string): Promise<void> {
// Check if directory exists and we have read permissions
try {
await access(workspacePath, constants.R_OK)
} catch (error) {
throw new Error(
`Cannot access workspace directory. Please ensure VS Code has permission to access your workspace. Error: ${error instanceof Error ? error.message : String(error)}`,
)
}
const homedir = os.homedir()
const desktopPath = getDesktopDir()
const documentsPath = path.join(homedir, "Documents")
const downloadsPath = path.join(homedir, "Downloads")
switch (workspacePath) {
case homedir:
throw new Error("Cannot use checkpoints in home directory")
case desktopPath:
throw new Error("Cannot use checkpoints in Desktop directory")
case documentsPath:
throw new Error("Cannot use checkpoints in Documents directory")
case downloadsPath:
throw new Error("Cannot use checkpoints in Downloads directory")
}
}
/**
* Gets the current working directory from the VS Code workspace.
* Validates that checkpoints are not being used in protected directories
* like home, Desktop, Documents, or Downloads. Checks to confirm that the workspace
* is accessible and that we will not encounter breaking permissions issues when
* creating checkpoints.
*
* Protected directories:
* - User's home directory
* - Desktop
* - Documents
* - Downloads
*
* @returns Promise<string> The absolute path to the current working directory
* @throws Error if no workspace is detected, if in a protected directory, or if no read access
*/
export async function getWorkingDirectory(): Promise<string> {
const cwd = await getCwd()
if (!cwd) {
throw new Error("No workspace detected. Please open Cline in a workspace to use checkpoints.")
}
await validateWorkspacePath(cwd)
return cwd
}
/**
* Hashes the current working directory to a 13-character numeric hash.
* @param workingDir - The absolute path to the working directory
@@ -15,17 +15,17 @@ export async function detectEncoding(fileBuffer: Buffer, fileExtension?: string)
const detected = chardet.detect(fileBuffer)
if (typeof detected === "string") {
return detected
} else if (detected && (detected as any).encoding) {
return (detected as any).encoding
} else {
if (fileExtension) {
const isBinary = await isBinaryFile(fileBuffer).catch(() => false)
if (isBinary) {
throw new Error(`Cannot read text for file type: ${fileExtension}`)
}
}
return "utf8"
}
if (detected && (detected as any).encoding) {
return (detected as any).encoding
}
if (fileExtension) {
const isBinary = await isBinaryFile(fileBuffer).catch(() => false)
if (isBinary) {
throw new Error(`Cannot read text for file type: ${fileExtension}`)
}
}
return "utf8"
}
export async function extractTextFromFile(filePath: string): Promise<string> {
@@ -42,7 +42,7 @@ export async function extractTextFromFile(filePath: string): Promise<string> {
* Expects the fs.access call to have already been performed prior to calling.
* Content is automatically truncated if it exceeds 400KB to prevent context overflow.
*/
export async function callTextExtractionFunctions(filePath: string): Promise<string> {
async function callTextExtractionFunctions(filePath: string): Promise<string> {
const fileExtension = path.extname(filePath).toLowerCase()
let content: string
@@ -131,9 +131,8 @@ function formatCellValue(cell: ExcelJS.Cell): string {
if (typeof value === "object" && "formula" in value) {
if ("result" in value && value.result !== undefined && value.result !== null) {
return value.result.toString()
} else {
return `[Formula: ${value.formula}]`
}
return `[Formula: ${value.formula}]`
}
return value.toString()
@@ -15,7 +15,7 @@ const IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/gif", "image/svg+xml
* @param cell A notebook cell object
* @returns The cell with sanitized outputs
*/
export function sanitizeCellOutputs(cell: Record<string, unknown>): Record<string, unknown> {
function sanitizeCellOutputs(cell: Record<string, unknown>): Record<string, unknown> {
if (cell.cell_type !== "code" || !cell.outputs || !Array.isArray(cell.outputs)) {
return cell
}
@@ -62,28 +62,27 @@ export async function selectFiles(imagesAllowed: boolean): Promise<{ images: str
const mimeType = getMimeType(filePath)
return { type: "image", data: `data:${mimeType};base64,${base64}` }
} else {
// for standard models we will check the size of the file to ensure its not too large
try {
const stats = await fs.stat(filePath)
if (stats.size > 20 * 1000 * 1024) {
Logger.warn(`File too large, skipping: ${filePath}`)
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: `File too large: ${path.basename(filePath)} was skipped (size exceeds 20MB).`,
})
return null
}
} catch (error) {
Logger.error(`Error checking file size for ${filePath}:`, error)
}
// for standard models we will check the size of the file to ensure its not too large
try {
const stats = await fs.stat(filePath)
if (stats.size > 20 * 1000 * 1024) {
Logger.warn(`File too large, skipping: ${filePath}`)
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: `Could not check file size for ${path.basename(filePath)}, skipping.`,
message: `File too large: ${path.basename(filePath)} was skipped (size exceeds 20MB).`,
})
return null
}
return { type: "file", data: filePath }
} catch (error) {
Logger.error(`Error checking file size for ${filePath}:`, error)
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: `Could not check file size for ${path.basename(filePath)}, skipping.`,
})
return null
}
return { type: "file", data: filePath }
})
const dataUrlsWithNulls = await Promise.all(processFilesPromises)
@@ -103,7 +102,7 @@ export async function selectFiles(imagesAllowed: boolean): Promise<{ images: str
return { images, files }
}
export function getMimeType(filePath: string): string {
function getMimeType(filePath: string): string {
const ext = path.extname(filePath).toLowerCase()
switch (ext) {
case ".png":
@@ -16,7 +16,7 @@ import { Logger } from "@/shared/services/Logger"
* - Fixed callback port: 1455
* - Codex-specific params: codex_cli_simplified_flow=true, originator=cline
*/
export const OPENAI_CODEX_OAUTH_CONFIG = {
const OPENAI_CODEX_OAUTH_CONFIG = {
authorizationEndpoint: "https://auth.openai.com/oauth/authorize",
tokenEndpoint: "https://auth.openai.com/oauth/token",
clientId: "app_EMoamEEZ73f0CkXaXp7hrann",
@@ -25,9 +25,6 @@ export const OPENAI_CODEX_OAUTH_CONFIG = {
callbackPort: 1455,
} as const
// Token storage key - must match the key in SECRETS_KEYS (state-keys.ts)
const OPENAI_CODEX_CREDENTIALS_KEY = "openai-codex-oauth-credentials"
type ProviderSettingsManager = {
getProviderSettings(providerId: string):
| {
@@ -78,7 +75,7 @@ const openAiCodexCredentialsSchema = z.object({
accountId: z.string().optional(),
})
export type OpenAiCodexCredentials = z.infer<typeof openAiCodexCredentialsSchema>
type OpenAiCodexCredentials = z.infer<typeof openAiCodexCredentialsSchema>
function providerSettingsToCredentials(
settings: NonNullable<ReturnType<ProviderSettingsManager["getProviderSettings"]>>,
@@ -287,7 +284,7 @@ function parseOAuthErrorDetails(errorText: string): { errorCode?: string; errorM
* Generates a cryptographically random PKCE code verifier
* Must be 43-128 characters long using unreserved characters
*/
export function generateCodeVerifier(): string {
function generateCodeVerifier(): string {
const buffer = crypto.randomBytes(32)
return buffer.toString("base64url")
}
@@ -295,7 +292,7 @@ export function generateCodeVerifier(): string {
/**
* Generates the PKCE code challenge from the verifier using S256 method
*/
export function generateCodeChallenge(verifier: string): string {
function generateCodeChallenge(verifier: string): string {
const hash = crypto.createHash("sha256").update(verifier).digest()
return hash.toString("base64url")
}
@@ -303,7 +300,7 @@ export function generateCodeChallenge(verifier: string): string {
/**
* Generates a random state parameter for CSRF protection
*/
export function generateState(): string {
function generateState(): string {
return crypto.randomBytes(16).toString("hex")
}
@@ -311,7 +308,7 @@ export function generateState(): string {
* Builds the authorization URL for OpenAI Codex OAuth flow
* Includes Codex-specific parameters per the implementation guide
*/
export function buildAuthorizationUrl(codeChallenge: string, state: string): string {
function buildAuthorizationUrl(codeChallenge: string, state: string): string {
const params = new URLSearchParams({
client_id: OPENAI_CODEX_OAUTH_CONFIG.clientId,
redirect_uri: OPENAI_CODEX_OAUTH_CONFIG.redirectUri,
@@ -333,7 +330,7 @@ export function buildAuthorizationUrl(codeChallenge: string, state: string): str
* Important: Uses application/x-www-form-urlencoded (not JSON)
* Important: state must NOT be included in token exchange body
*/
export async function exchangeCodeForTokens(code: string, codeVerifier: string): Promise<OpenAiCodexCredentials> {
async function exchangeCodeForTokens(code: string, codeVerifier: string): Promise<OpenAiCodexCredentials> {
// Per the implementation guide: use application/x-www-form-urlencoded
// and do NOT include state in the body (OpenAI returns error if included)
const body = new URLSearchParams({
@@ -388,7 +385,7 @@ export async function exchangeCodeForTokens(code: string, codeVerifier: string):
* Refreshes the access token using the refresh token
* Uses application/x-www-form-urlencoded (not JSON)
*/
export async function refreshAccessToken(credentials: OpenAiCodexCredentials): Promise<OpenAiCodexCredentials> {
async function refreshAccessToken(credentials: OpenAiCodexCredentials): Promise<OpenAiCodexCredentials> {
const body = new URLSearchParams({
grant_type: "refresh_token",
client_id: OPENAI_CODEX_OAUTH_CONFIG.clientId,
@@ -441,7 +438,7 @@ export async function refreshAccessToken(credentials: OpenAiCodexCredentials): P
* Checks if the credentials are expired (with 5 minute buffer)
* Per the implementation guide: expires is in milliseconds since epoch
*/
export function isTokenExpired(credentials: OpenAiCodexCredentials): boolean {
function isTokenExpired(credentials: OpenAiCodexCredentials): boolean {
const bufferMs = 5 * 60 * 1000 // 5 minutes buffer
return Date.now() >= credentials.expires - bufferMs
}
@@ -449,7 +446,7 @@ export function isTokenExpired(credentials: OpenAiCodexCredentials): boolean {
/**
* OpenAiCodexOAuthManager - Handles OAuth flow and token management
*/
export class OpenAiCodexOAuthManager {
class OpenAiCodexOAuthManager {
private credentials: OpenAiCodexCredentials | null = null
private refreshPromise: Promise<OpenAiCodexCredentials> | null = null
private pendingAuth: {
@@ -1,270 +0,0 @@
/**
* CommandExecutor - Unified command execution for all terminal modes.
*
* This class handles command execution for both VSCode terminal mode and
* standalone/CLI mode. It uses the shared CommandOrchestrator for the
* common orchestration logic (buffering, user interaction, result formatting).
*
* The differentiation between modes happens at the TerminalManager level:
* - VscodeTerminalManager → VscodeTerminalProcess (shell integration)
* - StandaloneTerminalManager → StandaloneTerminalProcess (child_process)
*
* IMPORTANT: Background execution mode uses StandaloneTerminalManager to run
* commands in hidden terminals without cluttering the visible terminal.
*/
import { findLastIndex } from "@shared/array"
import { ClineToolResponseContent } from "@shared/messages"
import { Logger } from "@/shared/services/Logger"
import { orchestrateCommandExecution } from "./CommandOrchestrator"
import { StandaloneTerminalManager } from "./standalone/StandaloneTerminalManager"
import type {
CommandExecutionOptions,
CommandExecutorCallbacks,
CommandExecutorConfig,
ITerminalManager,
ShellIntegrationWarningTracker,
TerminalProcessResultPromise,
} from "./types"
/**
* CommandExecutor - Unified command executor for all terminal modes.
*
* Uses the shared CommandOrchestrator for common logic and delegates
* process management to the appropriate TerminalManager.
*/
export class CommandExecutor {
private cwd: string
private taskId: string
private ulid: string
private terminalExecutionMode: "vscodeTerminal" | "backgroundExec"
private terminalManager: ITerminalManager
private standaloneManager: StandaloneTerminalManager
private callbacks: CommandExecutorCallbacks
// Track the currently executing foreground process for cancellation
private currentProcess: TerminalProcessResultPromise | null = null
// Flag to track if the current command was cancelled externally
private wasCancelledExternally = false
// Track shell integration warnings to determine when to show background terminal suggestion
private shellIntegrationWarningTracker: ShellIntegrationWarningTracker = {
timestamps: [],
lastSuggestionShown: undefined,
}
constructor(config: CommandExecutorConfig, callbacks: CommandExecutorCallbacks) {
this.cwd = config.cwd
this.taskId = config.taskId
this.ulid = config.ulid
this.terminalExecutionMode = config.terminalExecutionMode
this.terminalManager = config.terminalManager
this.callbacks = callbacks
// When in backgroundExec mode, the terminalManager is already a StandaloneTerminalManager
// created by Task. We should reuse it so that Task.getEnvironmentDetails() can see
// the terminals and processes we create (for isHot logic, busy terminals, etc.)
if (config.terminalExecutionMode === "backgroundExec" && config.terminalManager instanceof StandaloneTerminalManager) {
// Reuse the same instance that Task is using
this.standaloneManager = config.terminalManager
Logger.info(`[CommandExecutor] Reusing Task's StandaloneTerminalManager for backgroundExec mode`)
} else {
// Create a standalone manager for background execution support.
this.standaloneManager = new StandaloneTerminalManager()
Logger.info(`[CommandExecutor] Created new StandaloneTerminalManager`)
// Copy settings from the provided terminalManager to ensure consistency
if ("shellIntegrationTimeout" in config.terminalManager) {
const tm = config.terminalManager as any
this.standaloneManager.setShellIntegrationTimeout(tm.shellIntegrationTimeout || 4000)
this.standaloneManager.setTerminalReuseEnabled(tm.terminalReuseEnabled ?? true)
this.standaloneManager.setTerminalOutputLineLimit(tm.terminalOutputLineLimit || 500)
}
}
}
/**
* Execute a command in the terminal.
*
* Routing logic:
* 1. Background mode commands use StandaloneTerminalManager
* 2. Regular commands use the configured terminal manager
*
* @param command The command to execute
* @param timeoutSeconds Optional timeout in seconds
* @returns [userRejected, result] tuple
*/
async execute(
command: string,
timeoutSeconds: number | undefined,
options?: CommandExecutionOptions,
): Promise<[boolean, ClineToolResponseContent]> {
// Strip leading `cd` to workspace from command
const workspaceCdPrefix = `cd ${this.cwd} && `
if (command.startsWith(workspaceCdPrefix)) {
command = command.substring(workspaceCdPrefix.length)
}
// Select the appropriate terminal manager
const useStandalone = options?.useBackgroundExecution || this.terminalExecutionMode === "backgroundExec"
const manager = useStandalone ? this.standaloneManager : this.terminalManager
Logger.info(
`Executing command in ${useStandalone ? "standalone" : "VSCode"} terminal (mode=${this.terminalExecutionMode}, managerCtor=${manager?.constructor?.name}): ${command}`,
)
// Get terminal and run command
const terminalInfo = await manager.getOrCreateTerminal(this.cwd)
terminalInfo.terminal.show()
const process = manager.runCommand(terminalInfo, command)
// Reset cancellation flag and track the current process
this.wasCancelledExternally = false
this.currentProcess = process
const clearCurrentProcess = () => {
this.currentProcess = null
}
process.once("completed", clearCurrentProcess)
process.once("error", clearCurrentProcess)
// Use shared orchestration logic
// The StandaloneTerminalManager handles background command tracking internally
const result = await orchestrateCommandExecution(process, manager, this.callbacks, {
command,
timeoutSeconds,
suppressUserInteraction: options?.suppressUserInteraction,
// When "Proceed While Running" is triggered, track the command in the manager
// Returns the log file path so the orchestrator can send it to the UI
// existingOutput contains all output lines captured so far
onProceedWhileRunning: useStandalone
? (existingOutput: string[]) => {
const backgroundCmd = this.standaloneManager.trackBackgroundCommand(process, command, existingOutput)
return { logFilePath: backgroundCmd.logFilePath }
}
: undefined,
showShellIntegrationSuggestion: this.shouldShowBackgroundTerminalSuggestion(),
terminalType: useStandalone ? "standalone" : "vscode",
})
// If the command was cancelled externally (via cancel button), return a clear cancellation message
// This ensures the AI agent knows the command was cancelled by the user
if (this.wasCancelledExternally) {
const outputSoFar =
result.outputLines.length > 0
? `\nOutput captured before cancellation:\n${manager.processOutput(result.outputLines)}`
: ""
return [true, `Command was cancelled by the user.${outputSoFar}`]
}
return [result.userRejected, result.result]
}
/**
* Cancel all running commands (both foreground and background).
*
* This method cancels:
* 1. All detached background commands (those that were "proceeded while running")
* 2. The current foreground process (if one is actively running)
*
* @returns true if any commands were cancelled, false otherwise
*/
async cancelBackgroundCommand(): Promise<boolean> {
let cancelled = false
// 1. Cancel all detached background commands
const runningCommands = this.standaloneManager.getRunningBackgroundCommands()
for (const cmd of runningCommands) {
if (this.standaloneManager.cancelBackgroundCommand(cmd.id)) {
cancelled = true
Logger.info(`Cancelled background command: ${cmd.command}`)
}
}
// 2. Cancel the current foreground process (if any)
if (this.currentProcess && typeof (this.currentProcess as any).terminate === "function") {
// Set flag so execute() knows the command was cancelled externally
this.wasCancelledExternally = true
;(this.currentProcess as any).terminate()
this.currentProcess = null
cancelled = true
Logger.info("Cancelled foreground command")
}
// 3. Update UI state and notify user by modifying existing message
// We modify the previous command_output message instead of sending a new say()
// to avoid interfering with any pending ask() dialogs (which would cause
// "Current ask promise was ignored" errors)
if (cancelled) {
this.callbacks.updateBackgroundCommandState(false)
// Wait for terminal buffers to flush before updating the message
// This prevents the cancellation notice from appearing in the middle of output
await new Promise((resolve) => setTimeout(resolve, 300))
// Find the last command_output message and update it
const messages = this.callbacks.getClineMessages()
const lastCommandOutputIndex = findLastIndex(messages, (m) => m.ask === "command_output")
if (lastCommandOutputIndex !== -1) {
const existingText = messages[lastCommandOutputIndex].text || ""
const cancellationNotice = "\n\nCommand(s) cancelled by user."
await this.callbacks.updateClineMessage(lastCommandOutputIndex, {
text: existingText + cancellationNotice,
})
}
}
return cancelled
}
/**
* Check if there are any active background commands.
* Delegates to StandaloneTerminalManager.
*/
hasActiveBackgroundCommand(): boolean {
return this.standaloneManager.hasActiveBackgroundCommands()
}
/**
* Get a summary of background commands for environment details.
* Delegates to StandaloneTerminalManager which tracks multiple commands.
*/
getBackgroundCommandSummary(): string | undefined {
const summary = this.standaloneManager.getBackgroundCommandsSummary()
return summary || undefined
}
/**
* Determines whether to show the background terminal suggestion.
* Shows suggestion if there have been 3+ shell integration warnings in the last hour,
* and we haven't shown the suggestion in the last hour.
*
* @returns true if the suggestion should be shown, false otherwise
*/
private shouldShowBackgroundTerminalSuggestion(): boolean {
const oneHourAgo = Date.now() - 60 * 60 * 1000
// Clean old timestamps (older than 1 hour)
this.shellIntegrationWarningTracker.timestamps = this.shellIntegrationWarningTracker.timestamps.filter(
(ts) => ts > oneHourAgo,
)
// Add current warning
this.shellIntegrationWarningTracker.timestamps.push(Date.now())
// Check if we've shown suggestion recently (within last hour)
if (
this.shellIntegrationWarningTracker.lastSuggestionShown &&
Date.now() - this.shellIntegrationWarningTracker.lastSuggestionShown < 60 * 60 * 1000
) {
return false
}
// Show suggestion if 3+ warnings in last hour
if (this.shellIntegrationWarningTracker.timestamps.length >= 3) {
this.shellIntegrationWarningTracker.lastSuggestionShown = Date.now()
return true
}
return false
}
}
@@ -675,7 +675,7 @@ export async function orchestrateCommandExecution(
/**
* Helper to find last index matching a predicate
*/
export function findLastIndex<T>(array: T[], predicate: (item: T) => boolean): number {
function findLastIndex<T>(array: T[], predicate: (item: T) => boolean): number {
for (let i = array.length - 1; i >= 0; i--) {
if (predicate(array[i])) {
return i
@@ -82,10 +82,10 @@ export const BACKGROUND_COMMAND_TIMEOUT_MS = 10 * 60 * 1000
// Used to detect if a command is compiling/building
/** Markers that indicate compilation is starting */
export const COMPILING_MARKERS = ["compiling", "building", "bundling", "transpiling", "generating", "starting"]
const COMPILING_MARKERS = ["compiling", "building", "bundling", "transpiling", "generating", "starting"]
/** Markers that indicate compilation is done (nullify extended timeout) */
export const COMPILING_NULLIFIERS = [
const COMPILING_NULLIFIERS = [
"compiled",
"success",
"finish",
+1 -29
View File
@@ -20,36 +20,8 @@
*/
// Export unified command executor
export { CommandExecutor } from "./CommandExecutor"
// Export command orchestrator (shared logic)
export { findLastIndex, orchestrateCommandExecution } from "./CommandOrchestrator"
// Export standalone terminal implementations
export { StandaloneTerminal } from "./standalone/StandaloneTerminal"
export { StandaloneTerminalManager } from "./standalone/StandaloneTerminalManager"
export { StandaloneTerminalProcess } from "./standalone/StandaloneTerminalProcess"
export { StandaloneTerminalRegistry } from "./standalone/StandaloneTerminalRegistry"
// Export all types from types.ts
export type {
// Command Executor types
ActiveBackgroundCommand,
AskResponse,
CommandExecutionOptions,
CommandExecutorCallbacks,
CommandExecutorConfig,
FullCommandExecutorConfig,
// Terminal types
ITerminal,
ITerminalManager,
ITerminalProcess,
ITerminalProcessResult,
// Command Orchestrator types
OrchestrationOptions,
OrchestrationResult,
StandaloneTerminalOptions,
TerminalInfo,
TerminalProcessEvents,
TerminalProcessResultPromise,
} from "./types"
export type { ITerminalManager } from "./types"
+1 -67
View File
@@ -131,13 +131,6 @@ export interface ITerminal {
dispose(): void
}
/**
* Terminal process result interface.
* @deprecated Use ITerminalProcess instead.
* This is kept for backwards compatibility.
*/
export type ITerminalProcessResult = ITerminalProcess
/**
* Promise-like interface for terminal process results.
* Combines Promise<void> with ITerminalProcess for flexible usage.
@@ -281,34 +274,10 @@ export interface BackgroundCommand {
// Command Executor Types
// =============================================================================
/**
* Tracker for shell integration warnings to determine when to show background terminal suggestion.
* Used internally by CommandExecutor to track warning frequency.
*/
export interface ShellIntegrationWarningTracker {
/** Timestamps of recent shell integration warnings */
timestamps: number[]
/** Timestamp when the suggestion was last shown */
lastSuggestionShown?: number
}
/**
* Represents an active background command that can be cancelled
* @deprecated Use BackgroundCommand instead
*/
export interface ActiveBackgroundCommand {
process: {
terminate?: () => void
continue?: () => void
}
command: string
outputLines: string[]
}
/**
* Response from an ask() call
*/
export interface AskResponse {
interface AskResponse {
response: string // "yesButtonClicked" | "noButtonClicked" | "messageResponse"
text?: string
images?: string[]
@@ -343,41 +312,6 @@ export interface CommandExecutorCallbacks {
addToUserMessageContent: (content: { type: string; text: string }) => void
}
/**
* Optional per-command execution behavior overrides.
*/
export interface CommandExecutionOptions {
/**
* Force command execution in standalone/background terminal mode for this command.
* This is useful for subagent runs and headless-style execution flows.
*/
useBackgroundExecution?: boolean
/**
* Suppress command interaction/output UI messages (ask/say) for this command execution.
* Command output is still captured and returned as the tool result.
*/
suppressUserInteraction?: boolean
}
/**
* Configuration for CommandExecutor
*/
export interface CommandExecutorConfig {
/** Working directory for command execution */
cwd: string
/** Task ID for tracking */
taskId: string
/** Unique task identifier */
ulid: string
/** Terminal execution mode */
terminalExecutionMode: "vscodeTerminal" | "backgroundExec"
/** The primary terminal manager (VSCode or Standalone) */
terminalManager: ITerminalManager
}
/** Alias for backwards compatibility */
export type FullCommandExecutorConfig = CommandExecutorConfig
// =============================================================================
// Command Orchestrator Types
// =============================================================================
-1
View File
@@ -15,7 +15,6 @@ export { Controller } from "./SdkController"
export * from "./sdk-followup-coordinator"
export * from "./sdk-interaction-coordinator"
export * from "./sdk-mcp-coordinator"
export type { SessionEventListener } from "./sdk-message-coordinator"
export * from "./sdk-message-coordinator"
export * from "./sdk-mode-coordinator"
export * from "./sdk-session-config-builder"
+9 -9
View File
@@ -31,47 +31,47 @@ export function resolveDataDir(override?: string): string {
}
/** Path to globalState.json */
export function globalStatePath(dataDir?: string): string {
function globalStatePath(dataDir?: string): string {
return path.join(resolveDataDir(dataDir), "globalState.json")
}
/** Path to secrets.json */
export function secretsPath(dataDir?: string): string {
function secretsPath(dataDir?: string): string {
return path.join(resolveDataDir(dataDir), "secrets.json")
}
/** Path to taskHistory.json (stored in state/ subdirectory) */
export function taskHistoryPath(dataDir?: string): string {
function taskHistoryPath(dataDir?: string): string {
return path.join(resolveDataDir(dataDir), "state", "taskHistory.json")
}
/** Path to MCP settings file */
export function mcpSettingsPath(dataDir?: string): string {
function mcpSettingsPath(dataDir?: string): string {
return path.join(resolveDataDir(dataDir), "settings", "cline_mcp_settings.json")
}
/** Path to a task directory */
export function taskDirPath(taskId: string, dataDir?: string): string {
function taskDirPath(taskId: string, dataDir?: string): string {
return path.join(resolveDataDir(dataDir), "tasks", taskId)
}
/** Path to api_conversation_history.json for a task */
export function apiConversationHistoryPath(taskId: string, dataDir?: string): string {
function apiConversationHistoryPath(taskId: string, dataDir?: string): string {
return path.join(taskDirPath(taskId, dataDir), "api_conversation_history.json")
}
/** Path to ui_messages.json for a task */
export function uiMessagesPath(taskId: string, dataDir?: string): string {
function uiMessagesPath(taskId: string, dataDir?: string): string {
return path.join(taskDirPath(taskId, dataDir), "ui_messages.json")
}
/** Path to context_history.json for a task */
export function contextHistoryPath(taskId: string, dataDir?: string): string {
function contextHistoryPath(taskId: string, dataDir?: string): string {
return path.join(taskDirPath(taskId, dataDir), "context_history.json")
}
/** Path to task_metadata.json for a task */
export function taskMetadataPath(taskId: string, dataDir?: string): string {
function taskMetadataPath(taskId: string, dataDir?: string): string {
return path.join(taskDirPath(taskId, dataDir), "task_metadata.json")
}
+1 -1
View File
@@ -445,7 +445,7 @@ export class MessageTranslatorState {
* ask_question/ask_followup_question → (not a visual tool — handled by askQuestion executor in SdkController)
* MCP tools (serverName__toolName) → (handled before reaching sdkToolToClineSayTool — emitted as say="use_mcp_server")
*/
export function sdkToolToClineSayTool(toolName: string, input?: unknown): ClineSayTool {
function sdkToolToClineSayTool(toolName: string, input?: unknown): ClineSayTool {
// Parse input if it's a string (some SDK tools pass stringified JSON)
const parsedInput = parseToolInput(input)
@@ -106,7 +106,7 @@ export interface EffectiveProviderConfig {
* Empty patches are allowed and are no-ops. A field present with value
* `null` means "clear this field"; an absent field means "leave unchanged."
*/
export interface ProviderReasoningPatch {
interface ProviderReasoningPatch {
readonly enabled?: boolean
readonly effort?: string // "none" | "low" | "medium" | "high" | "xhigh"
readonly budgetTokens?: number
@@ -230,7 +230,7 @@ export interface ProviderListing {
* Source of a model-list result. Observability counterpart to the cache
* record. Logged but not used for control flow.
*/
export type CatalogSource = "sdk-dynamic" | "sdk-bundled" | "extension-dynamic" | "legacy-static" | "host-adapter"
type CatalogSource = "sdk-dynamic" | "sdk-bundled" | "extension-dynamic" | "legacy-static" | "host-adapter"
/**
* Structured error from `resolveModels`. Carries enough information to
+1 -5
View File
@@ -40,11 +40,7 @@ export interface BuildApiHandlerOptions {
* BOTH. We therefore send at most one — preferring an explicit thinking budget
* over effort — or none when reasoning is disabled.
*/
export function buildSdkProviderConfig(
configuration: ApiConfiguration,
mode: Mode,
options?: BuildApiHandlerOptions,
): ProviderConfig {
function buildSdkProviderConfig(configuration: ApiConfiguration, mode: Mode, options?: BuildApiHandlerOptions): ProviderConfig {
const providerId = (mode === "plan" ? configuration.planModeApiProvider : configuration.actModeApiProvider) ?? "cline"
const apiKey = resolveApiKey(providerId, configuration)
+2 -2
View File
@@ -8,8 +8,8 @@ import { buildToolPolicies } from "./sdk-tool-policies"
import type { SdkSessionHost } from "./session-host"
import { VscodeSessionHost } from "./vscode-session-host"
export type RequestToolApprovalHandler = NonNullable<Parameters<typeof VscodeSessionHost.create>[0]["requestToolApproval"]>
export type AskQuestionHandler = NonNullable<Parameters<typeof VscodeSessionHost.create>[0]["askQuestion"]>
type RequestToolApprovalHandler = NonNullable<Parameters<typeof VscodeSessionHost.create>[0]["requestToolApproval"]>
type AskQuestionHandler = NonNullable<Parameters<typeof VscodeSessionHost.create>[0]["askQuestion"]>
export interface SdkSessionLifecycleOptions {
mcpHub: McpHub
-10
View File
@@ -14,16 +14,6 @@ import { sdkMessagesToClineMessages } from "./message-translator"
import type { SdkSessionLifecycle } from "./sdk-session-lifecycle"
import type { VscodeSessionHost } from "./vscode-session-host"
export interface TaskWithId {
historyItem: HistoryItem
taskDirPath: string
apiConversationHistoryFilePath: string
uiMessagesFilePath: string
contextHistoryFilePath: string
taskMetadataFilePath: string
apiConversationHistory: unknown[]
}
export interface TaskUsage {
tokensIn: number
tokensOut: number
+4 -4
View File
@@ -54,7 +54,7 @@ export interface MessageStateHandlerEvents {
* Change event for message updates.
* Mirrors ClineMessageChange from src/core/task/message-state.ts.
*/
export interface ClineMessageChange {
interface ClineMessageChange {
type: "add" | "update" | "set" | "delete"
/** The full array after the change */
messages: ClineMessage[]
@@ -117,7 +117,7 @@ export class MessageStateHandler extends EventEmitter<MessageStateHandlerEvents>
* The `api` property is settable — updateSettings() replaces it when the
* user switches models/providers.
*/
export interface TaskProxyApi {
interface TaskProxyApi {
getModel: () => { id: string }
}
@@ -126,7 +126,7 @@ export interface TaskProxyApi {
* The updateSettings handler calls setDefaultTerminalProfile() which
* returns { closedCount, busyTerminals }. This stub safely no-ops.
*/
export interface TaskProxyTerminalManager {
interface TaskProxyTerminalManager {
setDefaultTerminalProfile: (profileId: string) => { closedCount: number; busyTerminals: never[] }
setShellIntegrationTimeout: (timeout: number) => void
setTerminalReuseEnabled: (enabled: boolean) => void
@@ -137,7 +137,7 @@ export interface TaskProxyTerminalManager {
* Task state tracking — mirrors the subset of classic TaskState
* that handlers reference.
*/
export interface TaskProxyState {
interface TaskProxyState {
askResponse?: ClineAskResponse
autoRetryAttempts?: number
/** Checkpoint manager initialized flag (stub — checkpoints removed) */
+1 -17
View File
@@ -1,11 +1,10 @@
import { isMultiRootWorkspace } from "@/core/workspace/utils/workspace-detection"
import { HostProvider } from "@/hosts/host-provider"
import { ExtensionRegistryInfo } from "@/registry"
import { EmptyRequest } from "@/shared/proto/cline/common"
import { Logger } from "@/shared/services/Logger"
// Canonical header names for extra client/host context
export const ClineHeaders = {
const ClineHeaders = {
PLATFORM: "X-PLATFORM",
PLATFORM_VERSION: "X-PLATFORM-VERSION",
CLIENT_VERSION: "X-CLIENT-VERSION",
@@ -13,7 +12,6 @@ export const ClineHeaders = {
CORE_VERSION: "X-CORE-VERSION",
IS_MULTIROOT: "X-IS-MULTIROOT",
} as const
export type ClineHeaderName = (typeof ClineHeaders)[keyof typeof ClineHeaders]
export function buildExternalBasicHeaders(): Record<string, string> {
return {
@@ -40,17 +38,3 @@ export async function buildBasicClineHeaders(): Promise<Record<string, string>>
return headers
}
export async function buildClineExtraHeaders(): Promise<Record<string, string>> {
const headers = await buildBasicClineHeaders()
try {
const isMultiRoot = await isMultiRootWorkspace()
headers[ClineHeaders.IS_MULTIROOT] = isMultiRoot ? "true" : "false"
} catch (error) {
Logger.log("Failed to detect multi-root workspace", error)
headers[ClineHeaders.IS_MULTIROOT] = "false"
}
return headers
}
+2 -3
View File
@@ -4,11 +4,10 @@
// All modules that import AuthService from this path continue to work
// because the SDK AuthService exposes the same interface.
export { AuthService, type ClineAccountOrganization, type ClineAccountUserInfo, type ClineAuthInfo } from "@/sdk/auth-service"
export { AuthService, type ClineAccountUserInfo } from "@/sdk/auth-service" // ServiceConfig was defined in the classic AuthService but never imported
// ServiceConfig was defined in the classic AuthService but never imported
// by any other module. Re-export a compatible type for safety.
export type ServiceConfig = {
type ServiceConfig = {
URI?: string
[key: string]: any
}
@@ -14,7 +14,7 @@ type PkceState = {
redirect_uri: string
}
export class OcaRefreshError extends Error {
class OcaRefreshError extends Error {
status?: number
code?: string
invalidGrant?: boolean
@@ -14,8 +14,6 @@ export const DEFAULT_EXTERNAL_IDCS_URL = "https://login-ext.identity.oraclecloud
export const DEFAULT_EXTERNAL_IDSC_SCOPES = "openid offline_access"
export const DEFAULT_EXTERNAL_OCA_BASE_URL = "https://code.aiservice.us-chicago-1.oci.oraclecloud.com/20250206/app/litellm"
export const OCI_HEADER_OPC_REQUEST_ID = "opc-request-id"
export const CHAT_COMPLETIONS_API = "CHAT_COMPLETIONS"
export const RESPONSES_API = "RESPONSES"
export const MESSAGES_API = "MESSAGES"
@@ -93,7 +93,7 @@ export function pkceChallengeFromVerifier(verifier: string): string {
*
* Use: Send this single value as the opc-request-id header.
*/
export async function generateOpcRequestId(taskId: string, token: string): Promise<string> {
async function generateOpcRequestId(taskId: string, token: string): Promise<string> {
async function hash8(str: string): Promise<string> {
const data = new TextEncoder().encode(str)
const hash = await crypto.subtle.digest("SHA-256", data)
@@ -142,7 +142,7 @@ export async function createOcaHeaders(accessToken: string, taskId: string): Pro
* Use only for non-security, informational, or display purposes.
* @param token JWT string
*/
export function parseJwtPayload<T extends JwtPayload>(token: string): T | null {
function parseJwtPayload<T extends JwtPayload>(token: string): T | null {
try {
const payload = jwtDecode<T>(token)
return payload
@@ -42,7 +42,7 @@ export async function isPortOpen(host: string, port: number, timeout = 1000): Pr
/**
* Try to connect to Chrome at a specific IP address
*/
export async function tryConnect(ipAddress: string): Promise<{ endpoint: string; ip: string } | null> {
async function tryConnect(ipAddress: string): Promise<{ endpoint: string; ip: string } | null> {
try {
const response = await axios.get(`http://${ipAddress}:9222/json/version`, { timeout: 1000 })
const data = response.data
+53 -106
View File
@@ -1,9 +1,8 @@
import { serializeError } from "serialize-error";
import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "../../shared/ClineAccount";
import { serializeError } from "serialize-error"
import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "../../shared/ClineAccount"
export enum ClineErrorType {
Auth = "auth",
Network = "network",
RateLimit = "rateLimit",
Balance = "balance",
SpendLimit = "spendLimit",
@@ -16,50 +15,42 @@ interface ErrorDetails {
/**
* The HTTP status code of the error, if applicable.
*/
status?: number;
status?: number
/**
* The request ID associated with the error, if available.
* This can be useful for debugging and support.
*/
request_id?: string;
request_id?: string
/**
* Specific error code provided by the API or service.
*/
code?: string;
code?: string
/**
* The model ID associated with the error, if applicable.
* This is useful for identifying which model the error relates to.
*/
modelId?: string;
modelId?: string
/**
* The provider ID associated with the error, if applicable.
* This is useful for identifying which provider the error relates to.
*/
providerId?: string;
providerId?: string
/**
* The error message associated with the error, if applicable.
*/
message?: string;
message?: string
// Additional details that might be present in the error
// This can include things like current balance, error messages, etc.
details?: any;
details?: any
}
const RATE_LIMIT_PATTERNS = [
/status code 429/i,
/rate limit/i,
/too many requests/i,
/quota exceeded/i,
/resource exhausted/i,
];
const ORG_CLINE_PASS_RESTRICTION_MESSAGE =
"organization accounts cannot use individual model inference subscriptions";
const ORG_CLINE_PASS_RESTRICTION_USER_MESSAGE =
"organization accounts cannot use clinepass subscriptions";
const RATE_LIMIT_PATTERNS = [/status code 429/i, /rate limit/i, /too many requests/i, /quota exceeded/i, /resource exhausted/i]
const ORG_CLINE_PASS_RESTRICTION_MESSAGE = "organization accounts cannot use individual model inference subscriptions"
const ORG_CLINE_PASS_RESTRICTION_USER_MESSAGE = "organization accounts cannot use clinepass subscriptions"
export class ClineError extends Error {
readonly title = "ClineError";
readonly _error: ErrorDetails;
readonly title = "ClineError"
readonly _error: ErrorDetails
// Error details per providers:
// Cline: error?.error
@@ -70,19 +61,15 @@ export class ClineError extends Error {
public readonly modelId?: string,
public readonly providerId?: string,
) {
const error = serializeError(raw);
const error = serializeError(raw)
const message =
error.message ||
error?.response?.message ||
String(error) ||
error?.cause?.means;
super(message);
const message = error.message || error?.response?.message || String(error) || error?.cause?.means
super(message)
// Extract status from multiple possible locations
const status = error.status || error.statusCode || error.response?.status;
this.modelId = modelId || error.modelId;
this.providerId = providerId || error.providerId;
const status = error.status || error.statusCode || error.response?.status
this.modelId = modelId || error.modelId
this.providerId = providerId || error.providerId
// Construct the error details object to includes relevant information
// And ensure it has a consistent structure
@@ -100,7 +87,7 @@ export class ClineError extends Error {
providerId: this.providerId,
details: error.details || error.error, // Additional details provided by the server
stack: undefined, // Avoid serializing stack trace to keep the error object clean
};
}
}
/**
@@ -116,7 +103,7 @@ export class ClineError extends Error {
modelId: this.modelId,
providerId: this.providerId,
details: this._error.details,
});
})
}
/**
@@ -124,33 +111,29 @@ export class ClineError extends Error {
*/
static parse(errorStr?: string, modelId?: string): ClineError | undefined {
if (!errorStr || typeof errorStr !== "string") {
return undefined;
return undefined
}
return ClineError.transform(errorStr, modelId);
return ClineError.transform(errorStr, modelId)
}
/**
* Transforms any object into a ClineError instance.
* Always returns a ClineError, even if the input is not a valid error object.
*/
static transform(
error: any,
modelId?: string,
providerId?: string,
): ClineError {
static transform(error: any, modelId?: string, providerId?: string): ClineError {
try {
// If already a ClineError, return it directly to prevent infinite recursion
if (error instanceof ClineError) {
return error;
return error
}
return new ClineError(JSON.parse(error), modelId, providerId);
return new ClineError(JSON.parse(error), modelId, providerId)
} catch {
return new ClineError(error, modelId, providerId);
return new ClineError(error, modelId, providerId)
}
}
public isErrorType(type: ClineErrorType): boolean {
return ClineError.getErrorType(this) === type;
return ClineError.getErrorType(this) === type
}
/**
@@ -158,102 +141,66 @@ export class ClineError extends Error {
* This is useful for determining how to handle the error in the UI or logic.
*/
static getErrorType(err: ClineError): ClineErrorType | undefined {
const { code, status, details } = err._error;
const message = (
err._error?.message ||
err.message ||
JSON.stringify(err._error)
)?.toLowerCase();
const { code, status, details } = err._error
const message = (err._error?.message || err.message || JSON.stringify(err._error))?.toLowerCase()
// Check balance error first (most specific)
if (
code === "insufficient_credits" &&
typeof details?.current_balance === "number"
) {
return ClineErrorType.Balance;
if (code === "insufficient_credits" && typeof details?.current_balance === "number") {
return ClineErrorType.Balance
}
// Check spend limit exceeded (org-enforced budget cap, 429 SPEND_LIMIT_EXCEEDED)
// Must be checked before the generic rate-limit check since both use 429
if (
code === "SPEND_LIMIT_EXCEEDED" ||
details?.code === "SPEND_LIMIT_EXCEEDED"
) {
return ClineErrorType.SpendLimit;
if (code === "SPEND_LIMIT_EXCEEDED" || details?.code === "SPEND_LIMIT_EXCEEDED") {
return ClineErrorType.SpendLimit
}
// ClinePass entitlement errors are user-actionable and should not fall through to generic 403 auth.
// The organization-account variant gets separate copy because subscribing is not the right action.
const isEntitlementCode =
code === "ENTITLEMENT_ERROR" || details?.code === "ENTITLEMENT_ERROR";
const entitlementText =
`${message ?? ""} ${details?.message ?? ""}`.toLowerCase();
const isEntitlementCode = code === "ENTITLEMENT_ERROR" || details?.code === "ENTITLEMENT_ERROR"
const entitlementText = `${message ?? ""} ${details?.message ?? ""}`.toLowerCase()
if (
isEntitlementCode &&
(entitlementText.includes(ORG_CLINE_PASS_RESTRICTION_MESSAGE) ||
entitlementText.includes(ORG_CLINE_PASS_RESTRICTION_USER_MESSAGE))
) {
return ClineErrorType.OrgClinePassRestriction;
return ClineErrorType.OrgClinePassRestriction
}
if (
isEntitlementCode &&
entitlementText.includes("not subscribed to required model plan")
) {
return ClineErrorType.Entitlement;
if (isEntitlementCode && entitlementText.includes("not subscribed to required model plan")) {
return ClineErrorType.Entitlement
}
// Check auth errors
const isAuthStatus = status !== undefined && status > 400 && status < 429;
if (
code === "ERR_BAD_REQUEST" ||
err instanceof AuthInvalidTokenError ||
isAuthStatus
) {
return ClineErrorType.Auth;
const isAuthStatus = status !== undefined && status > 400 && status < 429
if (code === "ERR_BAD_REQUEST" || err instanceof AuthInvalidTokenError || isAuthStatus) {
return ClineErrorType.Auth
}
if (code === "INFERENCE_CAP_ERROR") {
return ClineErrorType.QuotaExceeded;
return ClineErrorType.QuotaExceeded
}
if (message) {
// Check for specific error codes/messages if applicable
const authErrorRegex = [
/(?:in)?valid[-_ ]?(?:api )?(?:token|key)/i,
/authentication[-_ ]?failed/i,
/unauthorized/i,
];
if (
message?.includes(CLINE_ACCOUNT_AUTH_ERROR_MESSAGE) ||
authErrorRegex.some((regex) => regex.test(message))
) {
return ClineErrorType.Auth;
const authErrorRegex = [/(?:in)?valid[-_ ]?(?:api )?(?:token|key)/i, /authentication[-_ ]?failed/i, /unauthorized/i]
if (message?.includes(CLINE_ACCOUNT_AUTH_ERROR_MESSAGE) || authErrorRegex.some((regex) => regex.test(message))) {
return ClineErrorType.Auth
}
// Check rate limit patterns
const lowerMessage = message.toLowerCase();
const lowerMessage = message.toLowerCase()
if (RATE_LIMIT_PATTERNS.some((pattern) => pattern.test(lowerMessage))) {
return ClineErrorType.RateLimit;
return ClineErrorType.RateLimit
}
}
return undefined;
return undefined
}
}
export class AuthNetworkError extends Error {
constructor(
message: string,
override readonly cause?: Error,
) {
super(message);
this.name = ClineErrorType.Network;
}
}
export class AuthInvalidTokenError extends Error {
class AuthInvalidTokenError extends Error {
constructor(message: string) {
super(message);
this.name = ClineErrorType.Auth;
super(message)
this.name = ClineErrorType.Auth
}
}
@@ -8,7 +8,7 @@ import { PostHogErrorProvider } from "./providers/PostHogErrorProvider"
/**
* Supported error provider types
*/
export type ErrorProviderType = "posthog" | "no-op"
type ErrorProviderType = "posthog" | "no-op"
/**
* Configuration for error providers
+1 -5
View File
@@ -1,11 +1,7 @@
import { ErrorSettings } from "./providers/IErrorProvider"
export { ClineError, ClineErrorType } from "./ClineError"
export { type ErrorProviderConfig, ErrorProviderFactory, type ErrorProviderType } from "./ErrorProviderFactory"
export { ErrorService } from "./ErrorService"
export type { ErrorSettings, IErrorProvider } from "./providers/IErrorProvider"
export { PostHogErrorProvider } from "./providers/PostHogErrorProvider"
export type { ErrorSettings } from "./providers/IErrorProvider"
export function getErrorLevelFromString(level: string | undefined): ErrorSettings["level"] {
switch (level) {
case "disabled":
@@ -8,7 +8,7 @@ import { PostHogFeatureFlagsProvider } from "./providers/PostHogFeatureFlagsProv
/**
* Supported feature flags provider types
*/
export type FeatureFlagsProviderType = "posthog" | "no-op"
type FeatureFlagsProviderType = "posthog" | "no-op"
/**
* Configuration for feature flags providers
@@ -1,11 +1,4 @@
export {
type FeatureFlagsProviderConfig,
FeatureFlagsProviderFactory,
type FeatureFlagsProviderType,
} from "./FeatureFlagsProviderFactory"
export { FeatureFlagsService } from "./FeatureFlagsService"
export type { FeatureFlagsSettings, IFeatureFlagsProvider } from "./providers/IFeatureFlagsProvider"
export { PostHogFeatureFlagsProvider } from "./providers/PostHogFeatureFlagsProvider"
import { FeatureFlagsProviderFactory } from "./FeatureFlagsProviderFactory"
import { FeatureFlagsService } from "./FeatureFlagsService"
@@ -25,13 +18,6 @@ export function getFeatureFlagsService(): FeatureFlagsService {
return _featureFlagsServiceInstance
}
/**
* Reset the feature flags service instance (useful for testing)
*/
export function resetFeatureFlagsService(): void {
_featureFlagsServiceInstance = null
}
export const featureFlagsService = new Proxy({} as FeatureFlagsService, {
get(_target, prop, _receiver) {
const service = getFeatureFlagsService()
@@ -20,7 +20,7 @@ import { shouldStartNewOAuthFlow } from "./mcpOAuthFlow"
* "Authenticate"; 10 minutes leaves room to create an account, while still
* guaranteeing a stale flow eventually expires so the system makes progress.
*/
export const MCP_OAUTH_STATE_EXPIRY_MS = 10 * 60 * 1000 // 10 minutes
const MCP_OAUTH_STATE_EXPIRY_MS = 10 * 60 * 1000 // 10 minutes
/**
* Structure for all OAuth data stored in the single mcpOAuthSecrets JSON
+1 -1
View File
@@ -2,7 +2,7 @@ import { DEFAULT_MCP_TIMEOUT_SECONDS, MIN_MCP_TIMEOUT_SECONDS } from "@shared/mc
import { z } from "zod"
import { TYPE_ERROR_MESSAGE } from "./constants"
export const AutoApproveSchema = z.array(z.string()).default([])
const AutoApproveSchema = z.array(z.string()).default([])
export const BaseConfigSchema = z.object({
autoApprove: AutoApproveSchema.optional(),
-2
View File
@@ -16,6 +16,4 @@ export type McpConnection = {
authProvider?: OAuthClientProvider
}
export type McpTransportType = "stdio" | "sse" | "http"
export type McpServerConfig = z.infer<typeof ServerConfigSchema>
@@ -20,7 +20,7 @@ import { getBinaryLocation } from "@/utils/fs"
export type FileSearchSource = "host_index" | "ripgrep"
// Wrapper function for childProcess.spawn
export type SpawnFunction = typeof childProcess.spawn
type SpawnFunction = typeof childProcess.spawn
export const getSpawnFunction = (): SpawnFunction => childProcess.spawn
/** Thrown when ripgrep fails to spawn or exits non-zero. */
@@ -12,11 +12,6 @@ import { OpenTelemetryTelemetryProvider } from "./providers/opentelemetry/OpenTe
import { PostHogClientProvider } from "./providers/posthog/PostHogClientProvider"
import { PostHogTelemetryProvider } from "./providers/posthog/PostHogTelemetryProvider"
/**
* Supported telemetry provider types
*/
export type TelemetryProviderType = "posthog" | "no-op" | "opentelemetry"
/**
* Configuration for telemetry providers
*/
@@ -46,7 +46,6 @@ export type TerminalOutputMethod = VscodeOutputMethod | StandaloneOutputMethod
export enum TerminalOutputFailureReason {
TIMEOUT = "timeout",
NO_SHELL_INTEGRATION = "no_shell_integration",
CLIPBOARD_FAILED = "clipboard_failed",
}
/**
@@ -54,7 +53,6 @@ export enum TerminalOutputFailureReason {
*/
export enum TerminalUserInterventionAction {
PROCESS_WHILE_RUNNING = "process_while_running",
MANUAL_PASTE = "manual_paste",
CANCELLED = "cancelled",
}
@@ -64,7 +62,6 @@ export enum TerminalUserInterventionAction {
export enum TerminalHangStage {
WAITING_FOR_COMPLETION = "waiting_for_completion",
BUFFER_STUCK = "buffer_stuck",
STREAM_TIMEOUT = "stream_timeout",
}
export type TelemetryMetadata = {
+2 -30
View File
@@ -1,26 +1,6 @@
export type {
ITelemetryProvider,
TelemetrySettings,
} from "./providers/ITelemetryProvider"
export { PostHogTelemetryProvider } from "./providers/posthog/PostHogTelemetryProvider"
export {
type TelemetryProviderConfig,
TelemetryProviderFactory,
type TelemetryProviderType,
} from "./TelemetryProviderFactory"
// Export terminal type definitions for type-safe telemetry
export type {
StandaloneOutputMethod,
TerminalOutputMethod,
TerminalType,
VscodeOutputMethod,
} from "./TelemetryService"
// Export the enums and types for terminal telemetry
export {
TerminalHangStage,
TerminalOutputFailureReason,
TerminalUserInterventionAction,
} from "./TelemetryService"
export { TerminalHangStage, TerminalOutputFailureReason, TerminalUserInterventionAction } from "./TelemetryService"
// Create a singleton instance for easy access throughout the application
import { TelemetryService } from "./TelemetryService"
@@ -33,7 +13,7 @@ let _initializationPromise: Promise<TelemetryService> | null = null
* @param distinctId Optional distinct ID for the telemetry provider
* @returns TelemetryService instance
*/
export async function getTelemetryService(): Promise<TelemetryService> {
async function getTelemetryService(): Promise<TelemetryService> {
if (_telemetryServiceInstance) {
return _telemetryServiceInstance
}
@@ -59,14 +39,6 @@ export async function getTelemetryService(): Promise<TelemetryService> {
return _initializationPromise
}
/**
* Reset the telemetry service instance (useful for testing)
*/
export function resetTelemetryService(): void {
_telemetryServiceInstance = null
_initializationPromise = null
}
export const telemetryService = new Proxy({} as TelemetryService, {
get(_target, prop, _receiver) {
// Return a function that will call the method on the actual service
@@ -8,23 +8,23 @@ import type { ClineAccountUserInfo } from "../../auth/AuthService"
/**
* JSON-serializable primitive types for telemetry properties
*/
export type TelemetryPrimitive = string | number | boolean | null | undefined
type TelemetryPrimitive = string | number | boolean | null | undefined
/**
* JSON-serializable value types for telemetry properties
* Ensures all telemetry data can be properly serialized
*/
export type TelemetryValue = TelemetryPrimitive | TelemetryObject | TelemetryArray
type TelemetryValue = TelemetryPrimitive | TelemetryObject | TelemetryArray
/**
* JSON-serializable object for telemetry properties
*/
export type TelemetryObject = { [key: string]: TelemetryValue }
type TelemetryObject = { [key: string]: TelemetryValue }
/**
* JSON-serializable array for telemetry properties
*/
export type TelemetryArray = Array<TelemetryValue>
type TelemetryArray = Array<TelemetryValue>
/**
* Properties that can be safely passed to telemetry providers
@@ -24,7 +24,7 @@ export function createConsoleLogExporter(): ConsoleLogRecordExporter {
return new ConsoleLogRecordExporter()
}
export function ensurePathSuffix(url: URL, suffix: string): void {
function ensurePathSuffix(url: URL, suffix: string): void {
const pathname = url.pathname
const normalizedPathname = pathname.endsWith("/") ? pathname.slice(0, -1) : pathname
url.pathname = normalizedPathname
+1 -1
View File
@@ -36,7 +36,7 @@ export async function validateWorkspacePath(workspacePath: string): Promise<void
* Cleans up any existing Git repository in the specified workspace path
* @param workspacePath The workspace path to clean up
*/
export async function cleanupPreviousGit(workspacePath: string): Promise<void> {
async function cleanupPreviousGit(workspacePath: string): Promise<void> {
const gitDir = path.join(workspacePath, ".git")
try {
+2 -2
View File
@@ -18,7 +18,7 @@ let isTestMode = false
* Sets the test mode state
* @param value Whether test mode is enabled
*/
export function setTestMode(value: boolean): void {
function setTestMode(value: boolean): void {
isTestMode = value
}
@@ -26,7 +26,7 @@ export function setTestMode(value: boolean): void {
* Checks if the extension is running in test mode
* @returns True if in test mode, false otherwise
*/
export function isInTestMode(): boolean {
function isInTestMode(): boolean {
return isTestMode
}
+1 -1
View File
@@ -83,7 +83,7 @@ export interface OrganizationUsageTransaction {
userId: string
}
export interface UserRemoteConfigOrganization {
interface UserRemoteConfigOrganization {
organizationId: string
name: string
}
+3 -26
View File
@@ -2,8 +2,8 @@
* Banner message types for Cline extension
*/
export type BannerSeverity = "info" | "success" | "warning"
export type BannerPlacement = "top" | "bottom" | "welcome"
type BannerSeverity = "info" | "success" | "warning"
type BannerPlacement = "top" | "bottom" | "welcome"
export interface Banner {
id: string
@@ -37,7 +37,7 @@ export interface BannersResponse {
/**
* Audience targeting options
*/
export type BannerAudience = "all" | "team_admin_only" | "team_members" | "personal_only"
type BannerAudience = "all" | "team_admin_only" | "team_members" | "personal_only"
/**
* Rules that can be evaluated for banner targeting
@@ -60,26 +60,3 @@ export interface BannerRules {
/** Optional actions embedded in rules JSON for banners */
actions?: BannerAction[]
}
/**
* Banner event types for telemetry
*/
export type BannerEventType = "dismiss"
/**
* Banner event payload sent to the telemetry API
*/
export interface BannerEventPayload {
banner_id: string
instance_id: string
surface: "vscode" | "jetbrains" | "cli"
event_type: BannerEventType
}
/**
* Tracks when a banner was dismissed by a user
*/
export interface BannerDismissal {
bannerId: string
dismissedAt: number // timestamp
}
+2 -41
View File
@@ -273,43 +273,8 @@ export interface ClineSayTool {
readLineEnd?: number
}
export interface ClineSayHook {
hookName: string // Name of the hook (e.g., "PreToolUse", "PostToolUse")
toolName?: string // Tool name if applicable (for PreToolUse/PostToolUse)
status: "running" | "completed" | "failed" | "cancelled" // Execution status
exitCode?: number // Exit code when completed
hasJsonResponse?: boolean // Whether a JSON response was parsed
// Pending tool information (only present during PreToolUse "running" status)
pendingToolInfo?: {
tool: string // Tool name (e.g., "write_to_file", "execute_command")
path?: string // File path for file operations
command?: string // Command for execute_command
content?: string // Content preview (first 200 chars)
diff?: string // Diff preview (first 200 chars)
regex?: string // Regex pattern for search_files
url?: string // URL for web_fetch or browser_action
mcpTool?: string // MCP tool name
mcpServer?: string // MCP server name
resourceUri?: string // MCP resource URI
}
// Structured error information (only present when status is "failed")
error?: {
type: "timeout" | "validation" | "execution" | "cancellation" // Type of error
message: string // User-friendly error message
details?: string // Technical details for expansion
scriptPath?: string // Path to the hook script
}
}
export type HookOutputStreamMeta = {
/** Which hook configuration the script originated from (global vs workspace). */
source: "global" | "workspace"
/** Full path to the hook script that emitted the output. */
scriptPath: string
}
// must keep in sync with system prompt
export const browserActions = ["launch", "click", "type", "scroll_down", "scroll_up", "close"] as const
const browserActions = ["launch", "click", "type", "scroll_down", "scroll_up", "close"] as const
export type BrowserAction = (typeof browserActions)[number]
export interface ClineSayBrowserAction {
@@ -382,10 +347,6 @@ export interface ClineAskQuestion {
selected?: string
}
export interface ClineAskNewTask {
context: string
}
export interface ClineApiReqInfo {
request?: string
tokensIn?: number
@@ -412,6 +373,6 @@ export interface ClineSubagentUsageInfo {
cost: number
}
export type ClineApiReqCancelReason = "streaming_failed" | "user_cancelled" | "retries_exhausted"
type ClineApiReqCancelReason = "streaming_failed" | "user_cancelled" | "retries_exhausted"
export const COMPLETION_RESULT_CHANGES_FLAG = "HAS_CHANGES"
+1 -1
View File
@@ -40,7 +40,7 @@ export type LanguageDisplay =
export const DEFAULT_LANGUAGE_SETTINGS: LanguageKey = "en"
export const languageOptions: { key: LanguageKey; display: LanguageDisplay }[] = [
const languageOptions: { key: LanguageKey; display: LanguageDisplay }[] = [
{ key: "en", display: "English" },
{ key: "ar", display: "Arabic - العربية" },
{ key: "pt-BR", display: "Portuguese - Português (Brasil)" },
+1 -238
View File
@@ -111,29 +111,12 @@ export interface OcaModelInfo extends OpenAiCompatibleModelInfo {
reasoningEffortOptions: string[]
}
export interface HicapCompatibleModelInfo extends ModelInfo {
temperature?: number
}
export const hicapModelInfoSafeDefaults: HicapCompatibleModelInfo = {
maxTokens: -1,
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 0,
outputPrice: 0,
temperature: 1,
}
// Anthropic
// https://docs.anthropic.com/en/docs/about-claude/models // prices updated 2025-01-02
export type AnthropicModelId = string
export const ANTHROPIC_MIN_THINKING_BUDGET = 1_024
export const ANTHROPIC_MAX_THINKING_BUDGET = 6_000
// Claude Code
export type ClaudeCodeModelId = string
// AWS Bedrock
// https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html
export type BedrockModelId = string
@@ -154,18 +137,6 @@ export const openRouterDefaultModelInfo: ModelInfo = {
"Claude Sonnet 4.5 is an Anthropic model for coding, agentic search, and AI agent workflows. It supports planning and implementation tasks across the software development lifecycle.\n\nRead more in the [blog post here](https://www.anthropic.com/claude/sonnet)",
}
// Cline custom model - Devstral
export const clineDevstralModelInfo: ModelInfo = {
contextWindow: 256000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
cacheReadsPrice: 0,
cacheWritesPrice: 0,
description: "A stealth model for agentic coding tasks",
}
export type ClinePassModelId = keyof typeof clinePassModels
export const clinePassDefaultModelId = "cline-pass/glm-5.1"
export const clinePassModelInfoSaneDefaults: ModelInfo = {
@@ -218,97 +189,6 @@ export function resolveClinePassModelInfo(modelId: string, modelInfoByName?: Rec
)
}
export const OPENROUTER_PROVIDER_PREFERENCES: Record<string, { order: string[]; allow_fallbacks: boolean }> = {
// Exacto Providers
"moonshotai/kimi-k2:exacto": {
order: ["groq", "moonshotai"],
allow_fallbacks: false,
},
"z-ai/glm-4.6:exacto": {
order: ["z-ai", "novita"],
allow_fallbacks: false,
},
"deepseek/deepseek-v3.1-terminus:exacto": {
order: ["novita", "deepinfra"],
allow_fallbacks: false,
},
"qwen/qwen3-coder:exacto": {
order: ["baseten"],
allow_fallbacks: false,
},
"openai/gpt-oss-120b:exacto": {
order: ["groq", "novita"],
allow_fallbacks: false,
},
// Normal Providers
"moonshotai/kimi-k2": {
order: ["groq", "fireworks", "baseten", "parasail", "novita", "deepinfra"],
allow_fallbacks: false,
},
"qwen/qwen3-coder": {
order: ["nebius", "baseten", "fireworks", "together", "deepinfra"],
allow_fallbacks: false,
},
"qwen/qwen3-235b-a22b-thinking-2507": {
order: ["nebius", "baseten", "fireworks", "together", "deepinfra"],
allow_fallbacks: false,
},
"qwen/qwen3-235b-a22b-07-25": {
order: ["nebius", "baseten", "fireworks", "together", "deepinfra"],
allow_fallbacks: false,
},
"qwen/qwen3-30b-a3b-thinking-2507": {
order: ["nebius", "baseten", "fireworks", "together", "deepinfra"],
allow_fallbacks: false,
},
"qwen/qwen3-30b-a3b-instruct-2507": {
order: ["nebius", "baseten", "fireworks", "together", "deepinfra"],
allow_fallbacks: false,
},
"qwen/qwen3-30b-a3b:free": {
order: ["nebius", "baseten", "fireworks", "together", "deepinfra"],
allow_fallbacks: false,
},
"qwen/qwen3-next-80b-a3b-thinking": {
order: ["nebius", "baseten", "fireworks", "together", "deepinfra"],
allow_fallbacks: false,
},
"qwen/qwen3-next-80b-a3b-instruct": {
order: ["nebius", "baseten", "fireworks", "together", "deepinfra"],
allow_fallbacks: false,
},
"qwen/qwen3-max": {
order: ["nebius", "baseten", "fireworks", "together", "deepinfra"],
allow_fallbacks: false,
},
"deepseek/deepseek-v3.2-exp": {
order: ["deepseek", "novita", "fireworks", "nebius"],
allow_fallbacks: false,
},
"z-ai/glm-4.6": {
order: ["z-ai", "novita", "baseten", "fireworks", "chutes"],
allow_fallbacks: false,
},
"z-ai/glm-4.5v": {
order: ["z-ai", "novita", "baseten", "fireworks", "chutes"],
allow_fallbacks: false,
},
"z-ai/glm-4.5": {
order: ["z-ai", "novita", "baseten", "fireworks", "chutes"],
allow_fallbacks: false,
},
"z-ai/glm-4.5-air": {
order: ["z-ai", "novita", "baseten", "fireworks", "chutes"],
allow_fallbacks: false,
},
}
// Vertex AI
// https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude
// https://cloud.google.com/vertex-ai/generative-ai/pricing#partner-models
export type VertexModelId = string
export const openAiModelInfoSafeDefaults: OpenAiCompatibleModelInfo = {
maxTokens: -1,
contextWindow: 128_000,
@@ -320,36 +200,17 @@ export const openAiModelInfoSafeDefaults: OpenAiCompatibleModelInfo = {
temperature: 0,
}
// Gemini
// https://ai.google.dev/gemini-api/docs/models/gemini
export type GeminiModelId = string
// OpenAI Native
// https://openai.com/api/pricing/
export type OpenAiNativeModelId = string
// OpenAI Codex (ChatGPT Plus/Pro subscription)
// Uses OAuth authentication via ChatGPT, routes to chatgpt.com/backend-api/codex/responses
// Subscription-based pricing (all costs are $0).
//
// The Codex catalog and default model id are sourced from the `@cline/llms`
// SDK; the re-export here lets callers continue importing them from
// `@shared/api` without duplicating model parameters in the extension.
// Edit `openai-codex-models.ts` to change adapter behavior.
export { getOpenAiCodexDefaultModelId, type OpenAiCodexModelId, openAiCodexModels } from "./openai-codex-models"
// SDK.
// Azure OpenAI
// https://learn.microsoft.com/en-us/azure/ai-services/openai/api-version-deprecation
// https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#api-specs
export const azureOpenAiDefaultApiVersion = "2024-08-01-preview"
// DeepSeek model list and default are sourced from the SDK provider catalog;
// see src/sdk/model-catalog/ and webview-ui/.../providers/DeepSeekProvider.tsx.
// Hugging Face Inference Providers
// https://huggingface.co/docs/inference-providers/en/index
export type HuggingFaceModelId = string
// Qwen
// https://bailian.console.aliyun.com/
// The first model in the list is used as the default model for each region
@@ -358,64 +219,11 @@ export enum QwenApiRegions {
CHINA = "china",
INTERNATIONAL = "international",
}
export type MainlandQwenModelId = string
export type InternationalQwenModelId = string
// Doubao
// https://www.volcengine.com/docs/82379/1298459
// https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement
export type DoubaoModelId = string
// Mistral
// https://docs.mistral.ai/getting-started/models/models_overview/
export type MistralModelId = string
// LiteLLM
// https://docs.litellm.ai/docs/
export type LiteLLMModelId = string
export const liteLlmDefaultModelId = "anthropic/claude-3-7-sonnet-20250219"
export interface LiteLLMModelInfo extends ModelInfo {
temperature?: number
}
export const liteLlmModelInfoSafeDefaults: LiteLLMModelInfo = {
maxTokens: -1,
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 0,
outputPrice: 0,
cacheWritesPrice: 0,
cacheReadsPrice: 0,
temperature: 0,
}
// AskSage Models
// https://docs.asksage.ai/
export type AskSageModelId = string
export const askSageDefaultURL: string = "https://api.asksage.ai/server"
export type NebiusModelId = string
// W&B Inference by CoreWeave
// https://docs.wandb.ai/inference/models
export type WandbModelId = string
// X AI
// https://docs.x.ai/docs/api-reference
export type XAIModelId = string
// SambaNova
// https://docs.sambanova.ai/cloud/docs/get-started/supported-models
export type SambanovaModelId = string
// Cerebras
// https://inference-docs.cerebras.ai/api-reference/models
export type CerebrasModelId = string
// Groq
// https://console.groq.com/docs/models
// https://groq.com/pricing/
export type GroqModelId = string
// Requesty
// https://requesty.ai/models
export const requestyDefaultModelId = "anthropic/claude-3-7-sonnet-latest"
@@ -431,48 +239,3 @@ export const requestyDefaultModelInfo: ModelInfo = {
cacheReadsPrice: 0.3,
description: "Anthropic's most intelligent model. Highest level of intelligence and capability.",
}
// SAP AI Core
export type SapAiCoreModelId = string
// Moonshot AI Studio
// https://platform.moonshot.ai/docs/pricing/chat
export type MoonshotModelId = string
// Huawei Cloud MaaS
// Dify.ai - No model selection needed, models are configured in Dify workflows
export type HuaweiCloudMaasModelId = string
// Baseten
// https://baseten.co/products/model-apis/
// Extended ModelInfo to include supportedFeatures, like tools
export interface BasetenModelInfo extends ModelInfo {
supportedFeatures?: string[]
}
export type BasetenModelId = string
// Z AI
// https://docs.z.ai/guides/llm/glm-5.1
// https://docs.z.ai/guides/llm/glm-5
// https://docs.z.ai/guides/overview/pricing
export type internationalZAiModelId = string
export type mainlandZAiModelId = string
// Fireworks AI
export type FireworksModelId = string
// Qwen Code
// https://chat.qwen.ai/
export type QwenCodeModelId = string
// Minimax
// https://www.minimax.io/platform/document/text_api_intro
// https://www.minimax.io/platform/document/pricing
export type MinimaxModelId = string
// NousResearch
// https://inference-api.nousResearch.com
export type NousResearchModelId = string
-34
View File
@@ -20,37 +20,3 @@ export function findLast<T>(array: Array<T>, predicate: (value: T, index: number
const index = findLastIndex(array, predicate)
return index === -1 ? undefined : array[index]
}
/**
* Converts a partial or complete stringified array into an actual array.
* Handles both complete JSON strings and incomplete array strings.
* Splits on the specific tokens: [" ", " "]
* @param arrayString A string representation of an array, which may be incomplete
* @returns Array of strings parsed from the input
*/
export function parsePartialArrayString(arrayString: string): string[] {
try {
// Try parsing as complete JSON first
return JSON.parse(arrayString)
} catch {
// If JSON parsing fails, handle as partial string
const trimmed = arrayString.trim()
if (!trimmed.startsWith('["')) {
return []
}
// Remove leading ["
let content = trimmed.slice(2)
// Remove trailing "] if it exists
content = content.replace(/"]$/, "")
if (!content) {
return []
}
// Split on ", " token and handle the parts
return content
.split('", "')
.map((item) => item.trim())
.filter(Boolean)
}
}
+1 -6
View File
@@ -5,9 +5,8 @@ type URLType = "router" | "app" | "api"
const replaceCname = (baseUrl: string, type: URLType): string => {
if (type === "router") {
return baseUrl
} else {
return baseUrl.replace("router", type).replace("v1", "")
}
return baseUrl.replace("router", type).replace("v1", "")
}
export const toRequestyServiceUrl = (baseUrl?: string, service: URLType = "router"): URL | undefined => {
@@ -19,7 +18,3 @@ export const toRequestyServiceUrl = (baseUrl?: string, service: URLType = "route
return undefined
}
}
export const toRequestyServiceStringUrl = (baseUrl?: string, service: URLType = "router"): string | undefined => {
return toRequestyServiceUrl(baseUrl, service)?.toString()
}
+1 -1
View File
@@ -147,7 +147,7 @@ export interface ClineExtensionContext {
readonly extension: Extension<any>
}
export enum ExtensionKind {
enum ExtensionKind {
/**
* Extension runs where the UI runs.
*/
-2
View File
@@ -4,6 +4,4 @@ export * from "./onboarding"
export enum ClineClient {
VSCode = "VSCode Extension",
JetBrains = "JetBrains",
Cli = "CLI",
}
@@ -1,4 +1,4 @@
export interface ClineRecommendedModel {
interface ClineRecommendedModel {
id: string
name: string
description: string
@@ -371,4 +371,4 @@ export function combineHookSequences(messages: ClineMessage[]): ClineMessage[] {
return reordered
}
export const HOOK_OUTPUT_STRING = "__HOOK_OUTPUT__"
const HOOK_OUTPUT_STRING = "__HOOK_OUTPUT__"
+2 -2
View File
@@ -4,12 +4,12 @@
*/
/** Maximum content size in bytes (400KB) */
export const MAX_CONTENT_SIZE_BYTES = 400 * 1024
const MAX_CONTENT_SIZE_BYTES = 400 * 1024
/**
* Format bytes into a human-readable string (e.g., "1.5 MB", "400 KB").
*/
export function formatBytes(bytes: number): string {
function formatBytes(bytes: number): string {
if (bytes < 1024) {
return `${bytes} B`
}
@@ -11,33 +11,3 @@
export function isFocusChainItem(line: string): boolean {
return line.startsWith("- [ ]") || line.startsWith("- [x]") || line.startsWith("- [X]")
}
/**
* Checks if a trimmed line is a completed focus chain item
* @param line The trimmed line to check
* @returns true if the line is a completed focus chain item (- [x] or - [X])
*/
export function isCompletedFocusChainItem(line: string): boolean {
return line.startsWith("- [x]") || line.startsWith("- [X]")
}
/**
* Flexible regex pattern for matching focus chain items with spacing variations
* Matches patterns like "- [x] text", "- [X] text", "- [ ] text", "- [ ] text", etc.
*/
export const FOCUS_CHAIN_ITEM_REGEX = /^-\s*\[([ xX])\]\s*(.+)$/
/**
* Parse focus chain item using flexible regex (allows spacing variations)
* @param line The trimmed line to parse
* @returns Object with checked status and text, or null if not a focus chain item
*/
export function parseFocusChainItem(line: string): { checked: boolean; text: string } | null {
const match = line.match(FOCUS_CHAIN_ITEM_REGEX)
if (match) {
const checked = match[1] === "x" || match[1] === "X"
const text = match[2].trim()
return { checked, text }
}
return null
}
+1 -1
View File
@@ -7,7 +7,7 @@ const CLINE_TRUSTED_TESTER_DOMAINS = ["fibilabs.tech"]
* Checks if the given email belongs to a Cline bot user.
* E.g. Emails ending with @cline.bot
*/
export function isClineBotUser(email: string): boolean {
function isClineBotUser(email: string): boolean {
return email.endsWith("@cline.bot")
}
+2 -15
View File
@@ -3,10 +3,8 @@
* where each tool name is the combination of the server name + identifier + tool name.
* This enables to uniquely identify which MCP server a tool belongs to.
*/
export const CLINE_MCP_TOOL_IDENTIFIER = "0mcp0"
export const DEFAULT_MCP_TIMEOUT_SECONDS = 60 // matches Anthropic's default timeout in their MCP SDK
export const MIN_MCP_TIMEOUT_SECONDS = 1
export type McpMode = "full" | "server-use-only" | "off"
export type McpServer = {
name: string
@@ -60,7 +58,7 @@ export type McpPrompt = {
arguments?: McpPromptArgument[]
}
export type McpPromptMessageContent =
type McpPromptMessageContent =
| {
type: "text"
text: string
@@ -85,7 +83,7 @@ export type McpPromptMessageContent =
}
}
export type McpPromptMessage = {
type McpPromptMessage = {
role: "user" | "assistant"
content: McpPromptMessageContent
}
@@ -167,15 +165,4 @@ export interface McpMarketplaceCatalog {
items: McpMarketplaceItem[]
}
export interface McpDownloadResponse {
mcpId: string
githubUrl: string
name: string
author: string
description: string
readmeContent: string
llmsInstallationContent: string
requiresApiKey: boolean
}
export type McpViewTab = "marketplace" | "addRemote" | "configure"
+9 -27
View File
@@ -25,42 +25,30 @@ export const REASONING_DETAILS_PROVIDERS = ["cline", "openrouter"];
* This ensures backward compatibility where the messages were stored in Anthropic format with additional
* fields unknown to Anthropic SDK.
*/
export interface ClineTextContentBlock
extends Anthropic.TextBlockParam,
ClineSharedMessageParam {
export interface ClineTextContentBlock extends Anthropic.TextBlockParam, ClineSharedMessageParam {
// reasoning_details only exists for providers listed in REASONING_DETAILS_PROVIDERS
reasoning_details?: ClineReasoningDetailParam[];
// Thought Signature associates with Gemini
signature?: string;
}
export interface ClineImageContentBlock
extends Anthropic.ImageBlockParam,
ClineSharedMessageParam {}
export interface ClineImageContentBlock extends Anthropic.ImageBlockParam, ClineSharedMessageParam {}
export interface ClineDocumentContentBlock
extends Anthropic.DocumentBlockParam,
ClineSharedMessageParam {}
export interface ClineDocumentContentBlock extends Anthropic.DocumentBlockParam, ClineSharedMessageParam {}
export interface ClineUserToolResultContentBlock
extends Anthropic.ToolResultBlockParam,
ClineSharedMessageParam {}
export interface ClineUserToolResultContentBlock extends Anthropic.ToolResultBlockParam, ClineSharedMessageParam {}
/**
* Assistant only content types
*/
export interface ClineAssistantToolUseBlock
extends Anthropic.ToolUseBlockParam,
ClineSharedMessageParam {
export interface ClineAssistantToolUseBlock extends Anthropic.ToolUseBlockParam, ClineSharedMessageParam {
// reasoning_details only exists for providers listed in REASONING_DETAILS_PROVIDERS
reasoning_details?: unknown[] | ClineReasoningDetailParam[];
// Thought Signature associates with Gemini
signature?: string;
}
export interface ClineAssistantThinkingBlock
extends Anthropic.ThinkingBlock,
ClineSharedMessageParam {
export interface ClineAssistantThinkingBlock extends Anthropic.ThinkingBlock, ClineSharedMessageParam {
// The summary items returned by OpenAI response API
// The reasoning details that will be moved to the text block when finalized
summary?: unknown[] | ClineReasoningDetailParam[];
@@ -136,9 +124,7 @@ export function convertClineStorageToAnthropicMessage(
}
// Removes thinking block that has no signature (invalid thinking block that's incompatible with Anthropic API)
const filteredContent = content.filter(
(b) => b.type !== "thinking" || !!b.signature,
);
const filteredContent = content.filter((b) => b.type !== "thinking" || !!b.signature);
// Handle array content - strip Cline-specific fields for non-reasoning_details providers
const shouldCleanContent = !REASONING_DETAILS_PROVIDERS.includes(provider);
@@ -155,9 +141,7 @@ export function convertClineStorageToAnthropicMessage(
* narrows to the base64 variant for the transform layer. URL sources are not produced by Cline,
* so they degrade to empty values rather than throwing.
*/
export function getBase64ImageSource(
source: Anthropic.ImageBlockParam["source"],
): { mediaType: string; data: string } {
export function getBase64ImageSource(source: Anthropic.ImageBlockParam["source"]): { mediaType: string; data: string } {
if (source.type === "base64") {
return { mediaType: source.media_type, data: source.data };
}
@@ -167,9 +151,7 @@ export function getBase64ImageSource(
/**
* Builds a base64 data URL from an image block's source. See getBase64ImageSource.
*/
export function getImageDataUrl(
source: Anthropic.ImageBlockParam["source"],
): string {
export function getImageDataUrl(source: Anthropic.ImageBlockParam["source"]): string {
const { mediaType, data } = getBase64ImageSource(source);
return `data:${mediaType};base64,${data}`;
}
+2 -19
View File
@@ -1,20 +1,3 @@
// Core content types
export type {
ClineAssistantContent,
ClineAssistantRedactedThinkingBlock,
ClineAssistantThinkingBlock,
ClineAssistantToolUseBlock,
ClineContent,
ClineDocumentContentBlock,
ClineImageContentBlock,
ClineMessageRole,
ClinePromptInputContent,
ClineReasoningDetailParam,
ClineStorageMessage,
ClineTextContentBlock,
ClineToolResponseContent,
ClineUserContent,
ClineUserToolResultContentBlock,
} from "./content"
export { cleanContentBlock, convertClineStorageToAnthropicMessage, REASONING_DETAILS_PROVIDERS } from "./content"
export type { ClineMessageMetricsInfo, ClineMessageModelInfo } from "./metrics"
export type { ClineToolResponseContent } from "./content"
export type { ClineMessageModelInfo } from "./metrics"
@@ -1,92 +0,0 @@
/**
* OpenAI Codex (ChatGPT Plus/Pro subscription) model catalog.
*
* Single source of truth: the `@cline/llms` SDK. The SDK's
* {@link filterOpenAICodexModels} produces the canonical Codex allowlist by
* filtering the generated `openai-native` catalog, and the SDK's builtin
* provider registry exposes the canonical default model id.
*
* The SDK reports real per-token pricing for these models. The Codex
* subscription bills per ChatGPT Plus/Pro plan rather than per token, so
* `OpenAiCodexProvider.tsx` passes `hideUsageCost` to {@link ModelInfoView}
* to suppress the price strip in the settings UI. The runtime handler
* separately hard-codes `totalCost: 0` in
* {@link OpenAiCodexHandler.normalizeUsage}, so users never see a charge
* either way; the SDK pricing is just informational metadata.
*/
import { getProviderCollectionSync } from "@cline/llms"
// Relative path: this module is imported by both the extension and
// webview-ui builds, which carry different `@/*` path mappings. A
// relative import resolves the same way in both contexts.
import { adaptSdkModelInfo } from "../sdk/model-catalog/shape-adapter"
import type { ModelInfo } from "./api"
const CODEX_PROVIDER_ID = "openai-codex"
function buildCodexCatalog(): {
models: Record<string, ModelInfo>
defaultModelId: string
} {
const collection = getProviderCollectionSync(CODEX_PROVIDER_ID)
if (!collection) {
// SDK has no Codex provider registered. In production the SDK
// always carries it; this branch is reachable today only under
// the test stubs in `apps/vscode/src/test/requires.ts` and
// `apps/vscode/test-setup.js`, which return undefined. Surface
// an empty catalog so callers see "nothing to offer" rather
// than a fabricated value.
return { models: {}, defaultModelId: "" }
}
const models: Record<string, ModelInfo> = {}
for (const info of Object.values(collection.models)) {
models[info.id] = adaptSdkModelInfo(info)
}
return { models, defaultModelId: collection.provider.defaultModelId }
}
// Memoized so the SDK catalog is built at most once, on first access, rather
// than at module-evaluation time. Building eagerly here would run during the
// `@shared/api` ↔ `shape-adapter` import cycle and touch `shape-adapter`
// module constants before they finish initializing (a temporal-dead-zone
// crash).
let codexCatalog: { models: Record<string, ModelInfo>; defaultModelId: string } | undefined
function getCodexCatalog(): { models: Record<string, ModelInfo>; defaultModelId: string } {
if (!codexCatalog) {
codexCatalog = buildCodexCatalog()
}
return codexCatalog
}
/**
* Canonical OpenAI Codex model catalog, derived from the `@cline/llms` SDK.
*
* Keys are SDK model ids (e.g. `"gpt-5.4"`, `"gpt-5.3-codex"`). The set of
* keys is determined by {@link filterOpenAICodexModels} in the SDK.
*
* Lazily built on first access (see {@link getCodexCatalog}).
*/
export const openAiCodexModels: Record<string, ModelInfo> = new Proxy({} as Record<string, ModelInfo>, {
get: (_target, prop: string) => getCodexCatalog().models[prop],
has: (_target, prop: string) => prop in getCodexCatalog().models,
ownKeys: () => Reflect.ownKeys(getCodexCatalog().models),
getOwnPropertyDescriptor: (_target, prop: string) => Object.getOwnPropertyDescriptor(getCodexCatalog().models, prop),
})
/**
* Default Codex model id, sourced from the SDK's builtin provider metadata.
* Lazily resolved on first access.
*/
export function getOpenAiCodexDefaultModelId(): string {
return getCodexCatalog().defaultModelId
}
/**
* OpenAI Codex model id. Plain `string` because the SDK provides Codex
* model ids as opaque strings. Use {@link openAiCodexModels} for
* membership checks.
*/
export type OpenAiCodexModelId = string
@@ -13,17 +13,3 @@ export function convertSearchResultsToProtoFileInfos(
workspaceName: result.workspaceName,
}))
}
/**
* Converts proto FileInfo objects to domain search result objects
*/
export function convertProtoFileInfosToSearchResults(
protoResults: FileInfo[],
): { path: string; type: "file" | "folder"; label?: string; workspaceName?: string }[] {
return protoResults.map((protoResult) => ({
path: protoResult.path,
type: protoResult.type as "file" | "folder",
label: protoResult.label,
workspaceName: protoResult.workspaceName,
}))
}
@@ -1,22 +1,6 @@
import { TelemetrySettingEnum } from "@shared/proto/cline/state"
import { TelemetrySetting } from "../../TelemetrySetting"
/**
* Converts a domain TelemetrySetting string to a proto TelemetrySettingEnum
*/
export function convertDomainTelemetrySettingToProto(setting: TelemetrySetting): TelemetrySettingEnum {
switch (setting) {
case "unset":
return TelemetrySettingEnum.UNSET
case "enabled":
return TelemetrySettingEnum.ENABLED
case "disabled":
return TelemetrySettingEnum.DISABLED
default:
return TelemetrySettingEnum.UNSET
}
}
/**
* Converts a proto TelemetrySettingEnum to a domain TelemetrySetting string
*/
+28 -28
View File
@@ -14,7 +14,7 @@
import { z } from "zod"
// OpenAI Compatible model schema with per-model settings
export const OpenAiCompatibleModelSchema = z.object({
const OpenAiCompatibleModelSchema = z.object({
id: z.string(), // The model ID is required
temperature: z.number().optional(),
isR1FormatRequired: z.boolean().optional(),
@@ -37,13 +37,13 @@ export const OpenAiCompatibleSchema = z.object({
})
// AWS Bedrock model schema with per-model settings
export const AwsBedrockModelSchema = z.object({
const AwsBedrockModelSchema = z.object({
id: z.string(), // The model ID is required
thinkingBudgetTokens: z.number().optional(),
})
// AWS Bedrock custom model schema (separate from regular models)
export const AwsBedrockCustomModelSchema = z.object({
const AwsBedrockCustomModelSchema = z.object({
name: z.string(), // The model name is required
baseModelId: z.string(), // The base model ID is required
thinkingBudgetTokens: z.number().optional(),
@@ -64,7 +64,7 @@ export const AwsBedrockSettingsSchema = z.object({
})
// Cline Provider model schema with per-model settings
export const ClineModelSchema = z.object({
const ClineModelSchema = z.object({
id: z.string(), // The model ID is required
})
@@ -75,36 +75,36 @@ export const ClineSettingsSchema = z.object({
})
// Vertex Provider model schema with per-model settings
export const VertexModelSchema = z.object({
const VertexModelSchema = z.object({
id: z.string(), // The model ID is required
thinkingBudgetTokens: z.number().optional(),
})
// GCP Vertex Provider specific settings
export const VertexSettingsSchema = z.object({
const VertexSettingsSchema = z.object({
// A list of the allowed models with their settings
models: z.array(VertexModelSchema).optional(),
vertexProjectId: z.string().optional(),
vertexRegion: z.string().optional(),
})
export const LiteLLMModelSchema = z.object({
const LiteLLMModelSchema = z.object({
id: z.string(),
thinkingBudgetTokens: z.number().optional(),
promptCachingEnabled: z.boolean().optional(),
})
export const LiteLLMSchema = z.object({
const LiteLLMSchema = z.object({
models: z.array(LiteLLMModelSchema).optional(),
baseUrl: z.string().optional(),
})
export const AnthropicModelSchema = z.object({
const AnthropicModelSchema = z.object({
id: z.string(),
thinkingBudgetTokens: z.number().optional(),
})
export const AnthropicSchema = z.object({
const AnthropicSchema = z.object({
models: z.array(AnthropicModelSchema).optional(),
baseUrl: z.string().optional(),
})
@@ -120,12 +120,12 @@ const ProviderSettingsSchema = z.object({
Anthropic: AnthropicSchema.optional(),
})
export const AllowedMCPServerSchema = z.object({
const AllowedMCPServerSchema = z.object({
// The ID of the MCP is the URL for their github repo.
id: z.string(),
})
export const RemoteMCPServerSchema = z.object({
const RemoteMCPServerSchema = z.object({
// The name of the MCP server
name: z.string(),
// The URL of the MCP server
@@ -137,7 +137,7 @@ export const RemoteMCPServerSchema = z.object({
})
// Settings for a global cline rules or workflow file.
export const GlobalInstructionsFileSchema = z.object({
const GlobalInstructionsFileSchema = z.object({
// When this is enabled, the user cannot turn off this rule or workflow.
alwaysEnabled: z.boolean(),
// The name of the rules or workflow file.
@@ -232,30 +232,30 @@ export const APIKeySchema = z.record(z.string(), z.string())
// Type inference from schemas
export type RemoteConfig = z.infer<typeof RemoteConfigSchema>
export type MCPServer = z.infer<typeof AllowedMCPServerSchema>
type MCPServer = z.infer<typeof AllowedMCPServerSchema>
export type RemoteMCPServer = z.infer<typeof RemoteMCPServerSchema>
export type GlobalInstructionsFile = z.infer<typeof GlobalInstructionsFileSchema>
export type ProviderSettings = z.infer<typeof ProviderSettingsSchema>
type ProviderSettings = z.infer<typeof ProviderSettingsSchema>
export type OpenAiCompatible = z.infer<typeof OpenAiCompatibleSchema>
export type OpenAiCompatibleModel = z.infer<typeof OpenAiCompatibleModelSchema>
type OpenAiCompatible = z.infer<typeof OpenAiCompatibleSchema>
type OpenAiCompatibleModel = z.infer<typeof OpenAiCompatibleModelSchema>
export type AwsBedrockSettings = z.infer<typeof AwsBedrockSettingsSchema>
export type AwsBedrockModel = z.infer<typeof AwsBedrockModelSchema>
export type AwsBedrockCustomModel = z.infer<typeof AwsBedrockCustomModelSchema>
type AwsBedrockSettings = z.infer<typeof AwsBedrockSettingsSchema>
type AwsBedrockModel = z.infer<typeof AwsBedrockModelSchema>
type AwsBedrockCustomModel = z.infer<typeof AwsBedrockCustomModelSchema>
export type VertexSettings = z.infer<typeof VertexSettingsSchema>
export type VertexModel = z.infer<typeof VertexModelSchema>
type VertexSettings = z.infer<typeof VertexSettingsSchema>
type VertexModel = z.infer<typeof VertexModelSchema>
export type LiteLLMSettings = z.infer<typeof LiteLLMSchema>
export type LiteLLMModel = z.infer<typeof LiteLLMModelSchema>
type LiteLLMSettings = z.infer<typeof LiteLLMSchema>
type LiteLLMModel = z.infer<typeof LiteLLMModelSchema>
export type AnthropicSettings = z.infer<typeof AnthropicSchema>
export type AnthropicModel = z.infer<typeof AnthropicModelSchema>
type AnthropicSettings = z.infer<typeof AnthropicSchema>
type AnthropicModel = z.infer<typeof AnthropicModelSchema>
export type APIKeySettings = z.infer<typeof APIKeySchema>
export type EnterpriseTelemetry = z.infer<typeof EnterpriseTelemetrySchema>
export type PromptUploading = z.infer<typeof PromptUploadingSchema>
type EnterpriseTelemetry = z.infer<typeof EnterpriseTelemetrySchema>
type PromptUploading = z.infer<typeof PromptUploadingSchema>
export type S3AccessKeySettings = z.infer<typeof S3AccessKeySettingsSchema>
@@ -14,7 +14,7 @@ import { getTaskTimestamp } from "./utils"
/**
* Result of a backfill operation for a single task.
*/
export interface BackfillTaskResult {
interface BackfillTaskResult {
taskId: string
success: boolean
filesQueued: string[]
@@ -56,7 +56,7 @@ async function listTaskIds(_before?: string, _after?: string): Promise<string[]>
*
* @param taskId Task identifier
*/
export async function backfillTask(taskId: string): Promise<BackfillTaskResult> {
async function backfillTask(taskId: string): Promise<BackfillTaskResult> {
const result: BackfillTaskResult = {
taskId,
success: false,
@@ -6,7 +6,7 @@ import { SEVEN_DAYS_MS } from "./worker"
/**
* Sync queue item status.
*/
export type SyncQueueStatus = "pending" | "synced" | "failed"
type SyncQueueStatus = "pending" | "synced" | "failed"
/**
* Represents an item in the sync queue.
@@ -209,7 +209,7 @@ export class SyncQueue {
* @param key File key
* @param remove Whether to remove the item after marking synced (default: false)
*/
markSynced(taskId: string, key: string, remove: boolean = false): void {
markSynced(taskId: string, key: string, remove = false): void {
const id = `${taskId}/${key}`
if (remove) {
delete this.data.items[id]
@@ -322,7 +322,7 @@ export class SyncQueue {
* @param maxAgeMs Maximum age for failed items in milliseconds (default: 7 days)
* @returns Number of items cleaned up
*/
cleanupFailedItems(maxRetries: number = 5, maxAgeMs: number = SEVEN_DAYS_MS): number {
cleanupFailedItems(maxRetries = 5, maxAgeMs: number = SEVEN_DAYS_MS): number {
const cutoff = Date.now() - maxAgeMs
let count = 0
Object.entries(this.data.items).forEach(([key, item]) => {
@@ -344,7 +344,7 @@ export class SyncQueue {
* @param maxSize Maximum number of items to keep (default: 1000)
* @returns Number of items evicted
*/
enforceMaxSize(maxSize: number = 1000): number {
enforceMaxSize(maxSize = 1000): number {
const items = Object.entries(this.data.items)
if (items.length <= maxSize) {
return 0
@@ -18,12 +18,7 @@ import { SyncQueue } from "./queue"
import type { SyncWorkerOptions } from "./worker"
import { disposeSyncWorker, initSyncWorker, SyncWorker } from "./worker"
export type { SyncQueueItem, SyncQueueStatus } from "./queue"
// Re-export types and functions
export { SyncQueue } from "./queue"
export type { SyncWorkerEvent, SyncWorkerOptions } from "./worker"
export { disposeSyncWorker, SyncWorker } from "./worker"
let syncQueueInstance: SyncQueue | null = null
/**
-52
View File
@@ -40,55 +40,3 @@ export const BASE_SLASH_COMMANDS: SlashCommand[] = [
// VS Code-only slash commands
export const VSCODE_ONLY_COMMANDS: SlashCommand[] = []
// CLI-only slash commands (handled locally, not sent to backend)
export const CLI_ONLY_COMMANDS: SlashCommand[] = [
{
name: "help",
description: "Learn how to use Cline CLI",
section: "default",
cliCompatible: true,
},
{
name: "settings",
description: "Change API provider, auto-approve, and feature settings",
section: "default",
cliCompatible: true,
},
{
name: "models",
description: "Change the model used for the current mode",
section: "default",
cliCompatible: true,
},
{
name: "history",
description: "Browse and search task history",
section: "default",
cliCompatible: true,
},
{
name: "clear",
description: "Clear the current task and start fresh",
section: "default",
cliCompatible: true,
},
{
name: "exit",
description: "Alternative to Ctrl+C",
section: "default",
cliCompatible: true,
},
{
name: "q",
description: "Alternative to Ctrl+C",
section: "default",
cliCompatible: true,
},
{
name: "skills",
description: "View and manage installed skills",
section: "default",
cliCompatible: true,
},
]
@@ -9,7 +9,7 @@ export type { BlobStoreSettings } from "./types"
* S3/R2/Azure blob storage implementation of ClineStorage.
* Uses AWS S3, Cloudflare R2, or Azure Blob Storage as the backend storage.
*/
export class ClineBlobStorage extends ClineStorage {
class ClineBlobStorage extends ClineStorage {
override name = "ClineBlobStorage"
private static store: ClineBlobStorage | null = null
+1 -23
View File
@@ -1,6 +1,6 @@
import { Logger } from "../services/Logger"
export interface ClineStorageChangeEvent {
interface ClineStorageChangeEvent {
readonly key: string
}
@@ -210,25 +210,3 @@ export abstract class ClineSyncStorage<T = any> {
protected abstract _delete(key: string): void
protected abstract _keys(): readonly string[]
}
/**
* A simple in-memory implementation of ClineStorage using a Map.
*/
export class ClineInMemoryStorage extends ClineStorage {
/**
* A simple in-memory cache to store key-value pairs.
*/
private readonly _cache = new Map<string, string>()
protected async _get(key: string): Promise<string | undefined> {
return this._cache.get(key)
}
protected async _store(key: string, value: string): Promise<void> {
this._cache.set(key, value)
}
protected async _delete(key: string): Promise<void> {
this._cache.delete(key)
}
}

Some files were not shown because too many files have changed in this diff Show More