Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 35 additions & 22 deletions src/core/Cline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,20 @@ type UserContent = Array<
Anthropic.TextBlockParam | Anthropic.ImageBlockParam | Anthropic.ToolUseBlockParam | Anthropic.ToolResultBlockParam
>

export type ClineOptions = {
provider: ClineProvider
apiConfiguration: ApiConfiguration
customInstructions?: string
enableDiff?: boolean
enableCheckpoints?: boolean
fuzzyMatchThreshold?: number
task?: string
images?: string[]
historyItem?: HistoryItem
experiments?: Record<string, boolean>
startTask?: boolean
}

export class Cline {
readonly taskId: string
api: ApiHandler
Expand Down Expand Up @@ -118,19 +132,19 @@ export class Cline {
private didAlreadyUseTool = false
private didCompleteReadingStream = false

constructor(
provider: ClineProvider,
apiConfiguration: ApiConfiguration,
customInstructions?: string,
enableDiff?: boolean,
enableCheckpoints?: boolean,
fuzzyMatchThreshold?: number,
task?: string | undefined,
images?: string[] | undefined,
historyItem?: HistoryItem | undefined,
experiments?: Record<string, boolean>,
constructor({
provider,
apiConfiguration,
customInstructions,
enableDiff,
enableCheckpoints,
fuzzyMatchThreshold,
task,
images,
historyItem,
experiments,
startTask = true,
) {
}: ClineOptions) {
if (startTask && !task && !images && !historyItem) {
throw new Error("Either historyItem or task/images must be provided")
}
Expand Down Expand Up @@ -165,21 +179,20 @@ export class Cline {
}
}

static create(...args: ConstructorParameters<typeof Cline>): [Cline, Promise<void>] {
args[10] = false // startTask
const instance = new Cline(...args)

let task
static create(options: ClineOptions): [Cline, Promise<void>] {
const instance = new Cline({ ...options, startTask: false })
const { images, task, historyItem } = options
let promise

if (args[6] || args[7]) {
task = instance.startTask(args[6], args[7])
} else if (args[8]) {
task = instance.resumeTaskFromHistory()
if (images || task) {
promise = instance.startTask(task, images)
} else if (historyItem) {
promise = instance.resumeTaskFromHistory()
} else {
throw new Error("Either historyItem or task/images must be provided")
}

return [instance, task]
return [instance, promise]
}

// Add method to update diffStrategy
Expand Down
175 changes: 67 additions & 108 deletions src/core/__tests__/Cline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,15 +327,13 @@ describe("Cline", () => {

describe("constructor", () => {
it("should respect provided settings", async () => {
const [cline, task] = Cline.create(
mockProvider,
mockApiConfig,
"custom instructions",
false,
false,
0.95, // 95% threshold
"test task",
)
const [cline, task] = Cline.create({
provider: mockProvider,
apiConfiguration: mockApiConfig,
customInstructions: "custom instructions",
fuzzyMatchThreshold: 0.95,
task: "test task",
})

expect(cline.customInstructions).toBe("custom instructions")
expect(cline.diffEnabled).toBe(false)
Expand All @@ -345,15 +343,14 @@ describe("Cline", () => {
})

it("should use default fuzzy match threshold when not provided", async () => {
const [cline, task] = await Cline.create(
mockProvider,
mockApiConfig,
"custom instructions",
true,
false,
undefined,
"test task",
)
const [cline, task] = await Cline.create({
provider: mockProvider,
apiConfiguration: mockApiConfig,
customInstructions: "custom instructions",
enableDiff: true,
fuzzyMatchThreshold: 0.95,
task: "test task",
})

expect(cline.diffEnabled).toBe(true)
// The diff strategy should be created with default threshold (1.0)
Expand All @@ -366,15 +363,14 @@ describe("Cline", () => {
it("should use provided fuzzy match threshold", async () => {
const getDiffStrategySpy = jest.spyOn(require("../diff/DiffStrategy"), "getDiffStrategy")

const [cline, task] = Cline.create(
mockProvider,
mockApiConfig,
"custom instructions",
true,
false,
0.9, // 90% threshold
"test task",
)
const [cline, task] = await Cline.create({
provider: mockProvider,
apiConfiguration: mockApiConfig,
customInstructions: "custom instructions",
enableDiff: true,
fuzzyMatchThreshold: 0.9,
task: "test task",
})

expect(cline.diffEnabled).toBe(true)
expect(cline.diffStrategy).toBeDefined()
Expand All @@ -389,15 +385,13 @@ describe("Cline", () => {
it("should pass default threshold to diff strategy when not provided", async () => {
const getDiffStrategySpy = jest.spyOn(require("../diff/DiffStrategy"), "getDiffStrategy")

const [cline, task] = Cline.create(
mockProvider,
mockApiConfig,
"custom instructions",
true,
false,
undefined,
"test task",
)
const [cline, task] = Cline.create({
provider: mockProvider,
apiConfiguration: mockApiConfig,
customInstructions: "custom instructions",
enableDiff: true,
task: "test task",
})

expect(cline.diffEnabled).toBe(true)
expect(cline.diffStrategy).toBeDefined()
Expand All @@ -411,15 +405,7 @@ describe("Cline", () => {

it("should require either task or historyItem", () => {
expect(() => {
new Cline(
mockProvider,
mockApiConfig,
undefined, // customInstructions
false, // diffEnabled
false, // checkpointsEnabled
undefined, // fuzzyMatchThreshold
undefined, // task
)
new Cline({ provider: mockProvider, apiConfiguration: mockApiConfig })
}).toThrow("Either historyItem or task/images must be provided")
})
})
Expand Down Expand Up @@ -469,15 +455,11 @@ describe("Cline", () => {
})

it("should include timezone information in environment details", async () => {
const [cline, task] = Cline.create(
mockProvider,
mockApiConfig,
undefined,
false,
false,
undefined,
"test task",
)
const [cline, task] = Cline.create({
provider: mockProvider,
apiConfiguration: mockApiConfig,
task: "test task",
})

const details = await cline["getEnvironmentDetails"](false)

Expand All @@ -493,15 +475,12 @@ describe("Cline", () => {

describe("API conversation handling", () => {
it("should clean conversation history before sending to API", async () => {
const [cline, task] = Cline.create(
mockProvider,
mockApiConfig,
undefined,
false,
false,
undefined,
"test task",
)
const [cline, task] = Cline.create({
provider: mockProvider,
apiConfiguration: mockApiConfig,
task: "test task",
})

cline.abandoned = true
await task

Expand Down Expand Up @@ -611,15 +590,11 @@ describe("Cline", () => {
]

// Test with model that supports images
const [clineWithImages, taskWithImages] = Cline.create(
mockProvider,
configWithImages,
undefined,
false,
false,
undefined,
"test task",
)
const [clineWithImages, taskWithImages] = Cline.create({
provider: mockProvider,
apiConfiguration: configWithImages,
task: "test task",
})

// Mock the model info to indicate image support
jest.spyOn(clineWithImages.api, "getModel").mockReturnValue({
Expand All @@ -638,15 +613,11 @@ describe("Cline", () => {
clineWithImages.apiConversationHistory = conversationHistory

// Test with model that doesn't support images
const [clineWithoutImages, taskWithoutImages] = Cline.create(
mockProvider,
configWithoutImages,
undefined,
false,
false,
undefined,
"test task",
)
const [clineWithoutImages, taskWithoutImages] = Cline.create({
provider: mockProvider,
apiConfiguration: configWithoutImages,
task: "test task",
})

// Mock the model info to indicate no image support
jest.spyOn(clineWithoutImages.api, "getModel").mockReturnValue({
Expand Down Expand Up @@ -742,15 +713,11 @@ describe("Cline", () => {
})

it.skip("should handle API retry with countdown", async () => {
const [cline, task] = Cline.create(
mockProvider,
mockApiConfig,
undefined,
false,
false,
undefined,
"test task",
)
const [cline, task] = Cline.create({
provider: mockProvider,
apiConfiguration: mockApiConfig,
task: "test task",
})

// Mock delay to track countdown timing
const mockDelay = jest.fn().mockResolvedValue(undefined)
Expand Down Expand Up @@ -870,15 +837,11 @@ describe("Cline", () => {
})

it.skip("should not apply retry delay twice", async () => {
const [cline, task] = Cline.create(
mockProvider,
mockApiConfig,
undefined,
false,
false,
undefined,
"test task",
)
const [cline, task] = Cline.create({
provider: mockProvider,
apiConfiguration: mockApiConfig,
task: "test task",
})

// Mock delay to track countdown timing
const mockDelay = jest.fn().mockResolvedValue(undefined)
Expand Down Expand Up @@ -998,15 +961,11 @@ describe("Cline", () => {

describe("loadContext", () => {
it("should process mentions in task and feedback tags", async () => {
const [cline, task] = Cline.create(
mockProvider,
mockApiConfig,
undefined,
false,
false,
undefined,
"test task",
)
const [cline, task] = Cline.create({
provider: mockProvider,
apiConfiguration: mockApiConfig,
task: "test task",
})

// Mock parseMentions to track calls
const mockParseMentions = jest.fn().mockImplementation((text) => `processed: ${text}`)
Expand Down
27 changes: 12 additions & 15 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,18 +413,17 @@ export class ClineProvider implements vscode.WebviewViewProvider {
const modePrompt = customModePrompts?.[mode] as PromptComponent
const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n")

this.cline = new Cline(
this,
this.cline = new Cline({
provider: this,
apiConfiguration,
effectiveInstructions,
diffEnabled,
checkpointsEnabled,
customInstructions: effectiveInstructions,
enableDiff: diffEnabled,
enableCheckpoints: checkpointsEnabled,
fuzzyMatchThreshold,
task,
images,
undefined,
experiments,
)
})
}

public async initClineWithHistoryItem(historyItem: HistoryItem) {
Expand All @@ -444,18 +443,16 @@ export class ClineProvider implements vscode.WebviewViewProvider {
const modePrompt = customModePrompts?.[mode] as PromptComponent
const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n")

this.cline = new Cline(
this,
this.cline = new Cline({
provider: this,
apiConfiguration,
effectiveInstructions,
diffEnabled,
checkpointsEnabled,
customInstructions: effectiveInstructions,
enableDiff: diffEnabled,
enableCheckpoints: checkpointsEnabled,
fuzzyMatchThreshold,
undefined,
undefined,
historyItem,
experiments,
)
})
}

public async postMessageToWebview(message: ExtensionMessage) {
Expand Down
Loading