|
|
@@ -4,26 +4,24 @@ import crypto from "crypto"
|
|
|
import EventEmitter from "events"
|
|
|
|
|
|
import { Anthropic } from "@anthropic-ai/sdk"
|
|
|
-import cloneDeep from "clone-deep"
|
|
|
import delay from "delay"
|
|
|
import pWaitFor from "p-wait-for"
|
|
|
import { serializeError } from "serialize-error"
|
|
|
-import * as vscode from "vscode"
|
|
|
|
|
|
// schemas
|
|
|
-import { TokenUsage, ToolUsage, ToolName } from "../schemas"
|
|
|
+import { TokenUsage, ToolUsage, ToolName } from "../../schemas"
|
|
|
|
|
|
// api
|
|
|
-import { ApiHandler, buildApiHandler } from "../api"
|
|
|
-import { ApiStream } from "../api/transform/stream"
|
|
|
+import { ApiHandler, buildApiHandler } from "../../api"
|
|
|
+import { ApiStream } from "../../api/transform/stream"
|
|
|
|
|
|
-import { t } from "../i18n" // kilocode_change
|
|
|
+import { t } from "../../i18n" // kilocode_change
|
|
|
|
|
|
// shared
|
|
|
-import { ApiConfiguration } from "../shared/api"
|
|
|
-import { findLastIndex } from "../shared/array"
|
|
|
-import { combineApiRequests } from "../shared/combineApiRequests"
|
|
|
-import { combineCommandSequences } from "../shared/combineCommandSequences"
|
|
|
+import { ProviderSettings } from "../../shared/api"
|
|
|
+import { findLastIndex } from "../../shared/array"
|
|
|
+import { combineApiRequests } from "../../shared/combineApiRequests"
|
|
|
+import { combineCommandSequences } from "../../shared/combineCommandSequences"
|
|
|
import {
|
|
|
ClineApiReqCancelReason,
|
|
|
ClineApiReqInfo,
|
|
|
@@ -31,71 +29,53 @@ import {
|
|
|
ClineMessage,
|
|
|
ClineSay,
|
|
|
ToolProgressStatus,
|
|
|
-} from "../shared/ExtensionMessage"
|
|
|
-import { getApiMetrics } from "../shared/getApiMetrics"
|
|
|
-import { HistoryItem } from "../shared/HistoryItem"
|
|
|
-import { ClineAskResponse } from "../shared/WebviewMessage"
|
|
|
-import { defaultModeSlug, getModeBySlug } from "../shared/modes"
|
|
|
-import { ToolParamName, ToolResponse, DiffStrategy } from "../shared/tools"
|
|
|
+} from "../../shared/ExtensionMessage"
|
|
|
+import { getApiMetrics } from "../../shared/getApiMetrics"
|
|
|
+import { HistoryItem } from "../../shared/HistoryItem"
|
|
|
+import { ClineAskResponse } from "../../shared/WebviewMessage"
|
|
|
+import { defaultModeSlug } from "../../shared/modes"
|
|
|
+import { DiffStrategy } from "../../shared/tools"
|
|
|
|
|
|
// services
|
|
|
-import { UrlContentFetcher } from "../services/browser/UrlContentFetcher"
|
|
|
-import { BrowserSession } from "../services/browser/BrowserSession"
|
|
|
-import { McpHub } from "../services/mcp/McpHub"
|
|
|
-import { ToolRepetitionDetector } from "./ToolRepetitionDetector"
|
|
|
-import { McpServerManager } from "../services/mcp/McpServerManager"
|
|
|
-import { CheckpointServiceOptions, RepoPerTaskCheckpointService } from "../services/checkpoints"
|
|
|
+import { UrlContentFetcher } from "../../services/browser/UrlContentFetcher"
|
|
|
+import { BrowserSession } from "../../services/browser/BrowserSession"
|
|
|
+import { McpHub } from "../../services/mcp/McpHub"
|
|
|
+import { McpServerManager } from "../../services/mcp/McpServerManager"
|
|
|
+import { RepoPerTaskCheckpointService } from "../../services/checkpoints"
|
|
|
|
|
|
// integrations
|
|
|
-import { DIFF_VIEW_URI_SCHEME, DiffViewProvider } from "../integrations/editor/DiffViewProvider"
|
|
|
-import { findToolName, formatContentBlockToMarkdown } from "../integrations/misc/export-markdown"
|
|
|
-import { RooTerminalProcess } from "../integrations/terminal/types"
|
|
|
-import { TerminalRegistry } from "../integrations/terminal/TerminalRegistry"
|
|
|
+import { DiffViewProvider } from "../../integrations/editor/DiffViewProvider"
|
|
|
+import { findToolName, formatContentBlockToMarkdown } from "../../integrations/misc/export-markdown"
|
|
|
+import { RooTerminalProcess } from "../../integrations/terminal/types"
|
|
|
+import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry"
|
|
|
|
|
|
// utils
|
|
|
-import { calculateApiCostAnthropic } from "../utils/cost"
|
|
|
-import { getWorkspacePath } from "../utils/path"
|
|
|
-
|
|
|
-// tools
|
|
|
-import { fetchInstructionsTool } from "./tools/fetchInstructionsTool"
|
|
|
-import { listFilesTool } from "./tools/listFilesTool"
|
|
|
-import { readFileTool } from "./tools/readFileTool"
|
|
|
-import { writeToFileTool } from "./tools/writeToFileTool"
|
|
|
-import { applyDiffTool } from "./tools/applyDiffTool"
|
|
|
-import { insertContentTool } from "./tools/insertContentTool"
|
|
|
-import { searchAndReplaceTool } from "./tools/searchAndReplaceTool"
|
|
|
-import { listCodeDefinitionNamesTool } from "./tools/listCodeDefinitionNamesTool"
|
|
|
-import { searchFilesTool } from "./tools/searchFilesTool"
|
|
|
-import { browserActionTool } from "./tools/browserActionTool"
|
|
|
-import { executeCommandTool } from "./tools/executeCommandTool"
|
|
|
-import { useMcpToolTool } from "./tools/useMcpToolTool"
|
|
|
-import { accessMcpResourceTool } from "./tools/accessMcpResourceTool"
|
|
|
-import { askFollowupQuestionTool } from "./tools/askFollowupQuestionTool"
|
|
|
-import { switchModeTool } from "./tools/switchModeTool"
|
|
|
-import { attemptCompletionTool } from "./tools/attemptCompletionTool"
|
|
|
-import { newTaskTool } from "./tools/newTaskTool"
|
|
|
+import { calculateApiCostAnthropic } from "../../utils/cost"
|
|
|
+import { getWorkspacePath } from "../../utils/path"
|
|
|
|
|
|
// prompts
|
|
|
-import { formatResponse } from "./prompts/responses"
|
|
|
-import { SYSTEM_PROMPT } from "./prompts/system"
|
|
|
-
|
|
|
-// ... everything else
|
|
|
-import { parseMentions } from "./mentions"
|
|
|
-import { FileContextTracker } from "./context-tracking/FileContextTracker"
|
|
|
-import { RooIgnoreController } from "./ignore/RooIgnoreController"
|
|
|
-import { type AssistantMessageContent, parseAssistantMessage } from "./assistant-message"
|
|
|
-import { truncateConversationIfNeeded } from "./sliding-window"
|
|
|
-import { ClineProvider } from "./webview/ClineProvider"
|
|
|
-import { validateToolUse } from "./mode-validator"
|
|
|
-import { MultiSearchReplaceDiffStrategy } from "./diff/strategies/multi-search-replace"
|
|
|
-import { readApiMessages, saveApiMessages, readTaskMessages, saveTaskMessages, taskMetadata } from "./task-persistence"
|
|
|
-import { getEnvironmentDetails } from "./environment/getEnvironmentDetails"
|
|
|
-
|
|
|
-// kilocode_change begin
|
|
|
-import { parseSlashCommands } from "./slash-commands"
|
|
|
-// kilocode_change end
|
|
|
-
|
|
|
-type UserContent = Array<Anthropic.Messages.ContentBlockParam>
|
|
|
+import { formatResponse } from "../prompts/responses"
|
|
|
+import { SYSTEM_PROMPT } from "../prompts/system"
|
|
|
+
|
|
|
+// core modules
|
|
|
+import { ToolRepetitionDetector } from "../tools/ToolRepetitionDetector"
|
|
|
+import { FileContextTracker } from "../context-tracking/FileContextTracker"
|
|
|
+import { RooIgnoreController } from "../ignore/RooIgnoreController"
|
|
|
+import { type AssistantMessageContent, parseAssistantMessage, presentAssistantMessage } from "../assistant-message"
|
|
|
+import { truncateConversationIfNeeded } from "../sliding-window"
|
|
|
+import { ClineProvider } from "../webview/ClineProvider"
|
|
|
+import { MultiSearchReplaceDiffStrategy } from "../diff/strategies/multi-search-replace"
|
|
|
+import { readApiMessages, saveApiMessages, readTaskMessages, saveTaskMessages, taskMetadata } from "../task-persistence"
|
|
|
+import { getEnvironmentDetails } from "../environment/getEnvironmentDetails"
|
|
|
+import {
|
|
|
+ type CheckpointDiffOptions,
|
|
|
+ type CheckpointRestoreOptions,
|
|
|
+ getCheckpointService,
|
|
|
+ checkpointSave,
|
|
|
+ checkpointRestore,
|
|
|
+ checkpointDiff,
|
|
|
+} from "../checkpoints"
|
|
|
+import { processUserContentMentions } from "../mentions/processUserContentMentions"
|
|
|
|
|
|
export type ClineEvents = {
|
|
|
message: [{ action: "created" | "updated"; message: ClineMessage }]
|
|
|
@@ -111,9 +91,9 @@ export type ClineEvents = {
|
|
|
taskToolFailed: [taskId: string, tool: ToolName, error: string]
|
|
|
}
|
|
|
|
|
|
-export type ClineOptions = {
|
|
|
+export type TaskOptions = {
|
|
|
provider: ClineProvider
|
|
|
- apiConfiguration: ApiConfiguration
|
|
|
+ apiConfiguration: ProviderSettings
|
|
|
customInstructions?: string
|
|
|
enableDiff?: boolean
|
|
|
enableCheckpoints?: boolean
|
|
|
@@ -124,89 +104,87 @@ export type ClineOptions = {
|
|
|
historyItem?: HistoryItem
|
|
|
experiments?: Record<string, boolean>
|
|
|
startTask?: boolean
|
|
|
- rootTask?: Cline
|
|
|
- parentTask?: Cline
|
|
|
+ rootTask?: Task
|
|
|
+ parentTask?: Task
|
|
|
taskNumber?: number
|
|
|
- onCreated?: (cline: Cline) => void
|
|
|
+ onCreated?: (cline: Task) => void
|
|
|
}
|
|
|
|
|
|
-export class Cline extends EventEmitter<ClineEvents> {
|
|
|
+export class Task extends EventEmitter<ClineEvents> {
|
|
|
readonly taskId: string
|
|
|
readonly instanceId: string
|
|
|
|
|
|
- readonly rootTask: Cline | undefined = undefined
|
|
|
- readonly parentTask: Cline | undefined = undefined
|
|
|
+ readonly rootTask: Task | undefined = undefined
|
|
|
+ readonly parentTask: Task | undefined = undefined
|
|
|
readonly taskNumber: number
|
|
|
readonly workspacePath: string
|
|
|
|
|
|
+ providerRef: WeakRef<ClineProvider>
|
|
|
+ private readonly globalStoragePath: string
|
|
|
+ abort: boolean = false
|
|
|
+ didFinishAbortingStream = false
|
|
|
+ abandoned = false
|
|
|
+ isInitialized = false
|
|
|
isPaused: boolean = false
|
|
|
pausedModeSlug: string = defaultModeSlug
|
|
|
private pauseInterval: NodeJS.Timeout | undefined
|
|
|
+ customInstructions?: string
|
|
|
|
|
|
- readonly apiConfiguration: ApiConfiguration
|
|
|
+ // API
|
|
|
+ readonly apiConfiguration: ProviderSettings
|
|
|
api: ApiHandler
|
|
|
private promptCacheKey: string
|
|
|
+ private lastApiRequestTime?: number
|
|
|
|
|
|
+ toolRepetitionDetector: ToolRepetitionDetector
|
|
|
rooIgnoreController?: RooIgnoreController
|
|
|
fileContextTracker: FileContextTracker
|
|
|
- private urlContentFetcher: UrlContentFetcher
|
|
|
+ urlContentFetcher: UrlContentFetcher
|
|
|
+ terminalProcess?: RooTerminalProcess
|
|
|
+
|
|
|
+ // Computer User
|
|
|
browserSession: BrowserSession
|
|
|
- didEditFile: boolean = false
|
|
|
- customInstructions?: string
|
|
|
|
|
|
+ // Editing
|
|
|
+ diffViewProvider: DiffViewProvider
|
|
|
diffStrategy?: DiffStrategy
|
|
|
diffEnabled: boolean = false
|
|
|
fuzzyMatchThreshold: number
|
|
|
+ didEditFile: boolean = false
|
|
|
|
|
|
+ // LLM Messages & Chat Messages
|
|
|
apiConversationHistory: (Anthropic.MessageParam & { ts?: number })[] = []
|
|
|
clineMessages: ClineMessage[] = []
|
|
|
|
|
|
+ // Ask
|
|
|
private askResponse?: ClineAskResponse
|
|
|
private askResponseText?: string
|
|
|
private askResponseImages?: string[]
|
|
|
public lastMessageTs?: number
|
|
|
|
|
|
- // Not private since it needs to be accessible by tools.
|
|
|
+ // Tool Use
|
|
|
consecutiveMistakeCount: number = 0
|
|
|
consecutiveMistakeLimit: number
|
|
|
consecutiveMistakeCountForApplyDiff: Map<string, number> = new Map()
|
|
|
+ private toolUsage: ToolUsage = {}
|
|
|
|
|
|
- // For tracking identical consecutive tool calls
|
|
|
- private toolRepetitionDetector: ToolRepetitionDetector
|
|
|
-
|
|
|
- // Not private since it needs to be accessible by tools.
|
|
|
- providerRef: WeakRef<ClineProvider>
|
|
|
- private readonly globalStoragePath: string
|
|
|
- private abort: boolean = false
|
|
|
- didFinishAbortingStream = false
|
|
|
- abandoned = false
|
|
|
- diffViewProvider: DiffViewProvider
|
|
|
- private lastApiRequestTime?: number
|
|
|
- isInitialized = false
|
|
|
-
|
|
|
- // checkpoints
|
|
|
- private enableCheckpoints: boolean
|
|
|
- private checkpointService?: RepoPerTaskCheckpointService
|
|
|
- private checkpointServiceInitializing = false
|
|
|
+ // Checkpoints
|
|
|
+ enableCheckpoints: boolean
|
|
|
+ checkpointService?: RepoPerTaskCheckpointService
|
|
|
+ checkpointServiceInitializing = false
|
|
|
|
|
|
- // streaming
|
|
|
+ // Streaming
|
|
|
isWaitingForFirstChunk = false
|
|
|
isStreaming = false
|
|
|
- private currentStreamingContentIndex = 0
|
|
|
- private assistantMessageContent: AssistantMessageContent[] = []
|
|
|
- private presentAssistantMessageLocked = false
|
|
|
- private presentAssistantMessageHasPendingUpdates = false
|
|
|
+ currentStreamingContentIndex = 0
|
|
|
+ assistantMessageContent: AssistantMessageContent[] = []
|
|
|
+ presentAssistantMessageLocked = false
|
|
|
+ presentAssistantMessageHasPendingUpdates = false
|
|
|
userMessageContent: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[] = []
|
|
|
- private userMessageContentReady = false
|
|
|
+ userMessageContentReady = false
|
|
|
didRejectTool = false
|
|
|
- private didAlreadyUseTool = false
|
|
|
- private didCompleteReadingStream = false
|
|
|
-
|
|
|
- // metrics
|
|
|
- private toolUsage: ToolUsage = {}
|
|
|
-
|
|
|
- // terminal
|
|
|
- public terminalProcess?: RooTerminalProcess
|
|
|
+ didAlreadyUseTool = false
|
|
|
+ didCompleteReadingStream = false
|
|
|
|
|
|
constructor({
|
|
|
provider,
|
|
|
@@ -224,7 +202,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|
|
parentTask,
|
|
|
taskNumber = -1,
|
|
|
onCreated,
|
|
|
- }: ClineOptions) {
|
|
|
+ }: TaskOptions) {
|
|
|
super()
|
|
|
|
|
|
if (startTask && !task && !images && !historyItem) {
|
|
|
@@ -281,8 +259,8 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- static create(options: ClineOptions): [Cline, Promise<void>] {
|
|
|
- const instance = new Cline({ ...options, startTask: false })
|
|
|
+ static create(options: TaskOptions): [Task, Promise<void>] {
|
|
|
+ const instance = new Task({ ...options, startTask: false })
|
|
|
const { images, task, historyItem } = options
|
|
|
let promise
|
|
|
|
|
|
@@ -732,7 +710,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|
|
|
|
|
// if the last message is a user message, we can need to get the assistant message before it to see if it made tool calls, and if so, fill in the remaining tool responses with 'interrupted'
|
|
|
|
|
|
- let modifiedOldUserContent: UserContent // either the last message if its user message, or the user message before the last (assistant) message
|
|
|
+ let modifiedOldUserContent: Anthropic.Messages.ContentBlockParam[] // either the last message if its user message, or the user message before the last (assistant) message
|
|
|
let modifiedApiConversationHistory: Anthropic.Messages.MessageParam[] // need to remove the last user message to replace with new modified user message
|
|
|
if (existingApiConversationHistory.length > 0) {
|
|
|
const lastMessage = existingApiConversationHistory[existingApiConversationHistory.length - 1]
|
|
|
@@ -762,7 +740,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|
|
const previousAssistantMessage: Anthropic.Messages.MessageParam | undefined =
|
|
|
existingApiConversationHistory[existingApiConversationHistory.length - 2]
|
|
|
|
|
|
- const existingUserContent: UserContent = Array.isArray(lastMessage.content)
|
|
|
+ const existingUserContent: Anthropic.Messages.ContentBlockParam[] = Array.isArray(lastMessage.content)
|
|
|
? lastMessage.content
|
|
|
: [{ type: "text", text: lastMessage.content }]
|
|
|
if (previousAssistantMessage && previousAssistantMessage.role === "assistant") {
|
|
|
@@ -806,7 +784,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|
|
throw new Error("Unexpected: No existing API conversation history")
|
|
|
}
|
|
|
|
|
|
- let newUserContent: UserContent = [...modifiedOldUserContent]
|
|
|
+ let newUserContent: Anthropic.Messages.ContentBlockParam[] = [...modifiedOldUserContent]
|
|
|
|
|
|
const agoText = ((): string => {
|
|
|
const timestamp = lastClineMessage?.ts ?? Date.now()
|
|
|
@@ -914,9 +892,9 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|
|
|
|
|
// Task Loop
|
|
|
|
|
|
- private async initiateTaskLoop(userContent: UserContent): Promise<void> {
|
|
|
+ private async initiateTaskLoop(userContent: Anthropic.Messages.ContentBlockParam[]): Promise<void> {
|
|
|
// Kicks off the checkpoints initialization process in the background.
|
|
|
- this.getCheckpointService()
|
|
|
+ getCheckpointService(this)
|
|
|
|
|
|
let nextUserContent = userContent
|
|
|
let includeFileDetails = true
|
|
|
@@ -950,7 +928,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|
|
}
|
|
|
|
|
|
public async recursivelyMakeClineRequests(
|
|
|
- userContent: UserContent,
|
|
|
+ userContent: Anthropic.Messages.ContentBlockParam[],
|
|
|
includeFileDetails: boolean = false,
|
|
|
): Promise<boolean> {
|
|
|
if (this.abort) {
|
|
|
@@ -1021,7 +999,13 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|
|
}),
|
|
|
)
|
|
|
|
|
|
- const parsedUserContent = await this.parseUserContent(userContent)
|
|
|
+ const parsedUserContent = await processUserContentMentions({
|
|
|
+ userContent,
|
|
|
+ cwd: this.cwd,
|
|
|
+ urlContentFetcher: this.urlContentFetcher,
|
|
|
+ fileContextTracker: this.fileContextTracker,
|
|
|
+ })
|
|
|
+
|
|
|
const environmentDetails = await getEnvironmentDetails(this, includeFileDetails)
|
|
|
|
|
|
// Add environment details as its own text block, separate from tool
|
|
|
@@ -1177,7 +1161,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|
|
}
|
|
|
|
|
|
// Present content to user.
|
|
|
- this.presentAssistantMessage()
|
|
|
+ presentAssistantMessage(this)
|
|
|
break
|
|
|
}
|
|
|
|
|
|
@@ -1268,7 +1252,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|
|
// `pWaitFor` before making the next request. All this is really
|
|
|
// doing is presenting the last partial message that we just set
|
|
|
// to complete.
|
|
|
- this.presentAssistantMessage()
|
|
|
+ presentAssistantMessage(this)
|
|
|
}
|
|
|
|
|
|
updateApiReqMsg()
|
|
|
@@ -1624,816 +1608,28 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|
|
yield* iterator
|
|
|
}
|
|
|
|
|
|
- public async presentAssistantMessage() {
|
|
|
- if (this.abort) {
|
|
|
- throw new Error(`[Cline#presentAssistantMessage] task ${this.taskId}.${this.instanceId} aborted`)
|
|
|
- }
|
|
|
-
|
|
|
- if (this.presentAssistantMessageLocked) {
|
|
|
- this.presentAssistantMessageHasPendingUpdates = true
|
|
|
- return
|
|
|
- }
|
|
|
- this.presentAssistantMessageLocked = true
|
|
|
- this.presentAssistantMessageHasPendingUpdates = false
|
|
|
-
|
|
|
- if (this.currentStreamingContentIndex >= this.assistantMessageContent.length) {
|
|
|
- // this may happen if the last content block was completed before streaming could finish. if streaming is finished, and we're out of bounds then this means we already presented/executed the last content block and are ready to continue to next request
|
|
|
- if (this.didCompleteReadingStream) {
|
|
|
- this.userMessageContentReady = true
|
|
|
- }
|
|
|
- // console.log("no more content blocks to stream! this shouldn't happen?")
|
|
|
- this.presentAssistantMessageLocked = false
|
|
|
- return
|
|
|
- //throw new Error("No more content blocks to stream! This shouldn't happen...") // remove and just return after testing
|
|
|
- }
|
|
|
-
|
|
|
- const block = cloneDeep(this.assistantMessageContent[this.currentStreamingContentIndex]) // need to create copy bc while stream is updating the array, it could be updating the reference block properties too
|
|
|
-
|
|
|
- switch (block.type) {
|
|
|
- case "text": {
|
|
|
- if (this.didRejectTool || this.didAlreadyUseTool) {
|
|
|
- break
|
|
|
- }
|
|
|
- let content = block.content
|
|
|
- if (content) {
|
|
|
- // (have to do this for partial and complete since sending content in thinking tags to markdown renderer will automatically be removed)
|
|
|
- // Remove end substrings of <thinking or </thinking (below xml parsing is only for opening tags)
|
|
|
- // (this is done with the xml parsing below now, but keeping here for reference)
|
|
|
- // content = content.replace(/<\/?t(?:h(?:i(?:n(?:k(?:i(?:n(?:g)?)?)?$/, "")
|
|
|
- // Remove all instances of <thinking> (with optional line break after) and </thinking> (with optional line break before)
|
|
|
- // - Needs to be separate since we dont want to remove the line break before the first tag
|
|
|
- // - Needs to happen before the xml parsing below
|
|
|
- content = content.replace(/<thinking>\s?/g, "")
|
|
|
- content = content.replace(/\s?<\/thinking>/g, "")
|
|
|
-
|
|
|
- // Remove partial XML tag at the very end of the content (for tool use and thinking tags)
|
|
|
- // (prevents scrollview from jumping when tags are automatically removed)
|
|
|
- const lastOpenBracketIndex = content.lastIndexOf("<")
|
|
|
- if (lastOpenBracketIndex !== -1) {
|
|
|
- const possibleTag = content.slice(lastOpenBracketIndex)
|
|
|
- // Check if there's a '>' after the last '<' (i.e., if the tag is complete) (complete thinking and tool tags will have been removed by now)
|
|
|
- const hasCloseBracket = possibleTag.includes(">")
|
|
|
- if (!hasCloseBracket) {
|
|
|
- // Extract the potential tag name
|
|
|
- let tagContent: string
|
|
|
- if (possibleTag.startsWith("</")) {
|
|
|
- tagContent = possibleTag.slice(2).trim()
|
|
|
- } else {
|
|
|
- tagContent = possibleTag.slice(1).trim()
|
|
|
- }
|
|
|
- // Check if tagContent is likely an incomplete tag name (letters and underscores only)
|
|
|
- const isLikelyTagName = /^[a-zA-Z_]+$/.test(tagContent)
|
|
|
- // Preemptively remove < or </ to keep from these artifacts showing up in chat (also handles closing thinking tags)
|
|
|
- const isOpeningOrClosing = possibleTag === "<" || possibleTag === "</"
|
|
|
- // If the tag is incomplete and at the end, remove it from the content
|
|
|
- if (isOpeningOrClosing || isLikelyTagName) {
|
|
|
- content = content.slice(0, lastOpenBracketIndex).trim()
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- await this.say("text", content, undefined, block.partial)
|
|
|
- break
|
|
|
- }
|
|
|
- case "tool_use":
|
|
|
- const toolDescription = (): string => {
|
|
|
- switch (block.name) {
|
|
|
- case "execute_command":
|
|
|
- return `[${block.name} for '${block.params.command}']`
|
|
|
- case "read_file":
|
|
|
- return `[${block.name} for '${block.params.path}']`
|
|
|
- case "fetch_instructions":
|
|
|
- return `[${block.name} for '${block.params.task}']`
|
|
|
- case "write_to_file":
|
|
|
- return `[${block.name} for '${block.params.path}']`
|
|
|
- case "apply_diff":
|
|
|
- return `[${block.name} for '${block.params.path}']`
|
|
|
- case "search_files":
|
|
|
- return `[${block.name} for '${block.params.regex}'${
|
|
|
- block.params.file_pattern ? ` in '${block.params.file_pattern}'` : ""
|
|
|
- }]`
|
|
|
- case "insert_content":
|
|
|
- return `[${block.name} for '${block.params.path}']`
|
|
|
- case "search_and_replace":
|
|
|
- return `[${block.name} for '${block.params.path}']`
|
|
|
- case "list_files":
|
|
|
- return `[${block.name} for '${block.params.path}']`
|
|
|
- case "list_code_definition_names":
|
|
|
- return `[${block.name} for '${block.params.path}']`
|
|
|
- case "browser_action":
|
|
|
- return `[${block.name} for '${block.params.action}']`
|
|
|
- case "use_mcp_tool":
|
|
|
- return `[${block.name} for '${block.params.server_name}']`
|
|
|
- case "access_mcp_resource":
|
|
|
- return `[${block.name} for '${block.params.server_name}']`
|
|
|
- case "ask_followup_question":
|
|
|
- return `[${block.name} for '${block.params.question}']`
|
|
|
- case "attempt_completion":
|
|
|
- return `[${block.name}]`
|
|
|
- case "switch_mode":
|
|
|
- return `[${block.name} to '${block.params.mode_slug}'${block.params.reason ? ` because: ${block.params.reason}` : ""}]`
|
|
|
- case "new_task": {
|
|
|
- const mode = block.params.mode ?? defaultModeSlug
|
|
|
- const message = block.params.message ?? "(no message)"
|
|
|
- const modeName = getModeBySlug(mode, customModes)?.name ?? mode
|
|
|
- return `[${block.name} in ${modeName} mode: '${message}']`
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (this.didRejectTool) {
|
|
|
- // ignore any tool content after user has rejected tool once
|
|
|
- if (!block.partial) {
|
|
|
- this.userMessageContent.push({
|
|
|
- type: "text",
|
|
|
- text: `Skipping tool ${toolDescription()} due to user rejecting a previous tool.`,
|
|
|
- })
|
|
|
- } else {
|
|
|
- // partial tool after user rejected a previous tool
|
|
|
- this.userMessageContent.push({
|
|
|
- type: "text",
|
|
|
- text: `Tool ${toolDescription()} was interrupted and not executed due to user rejecting a previous tool.`,
|
|
|
- })
|
|
|
- }
|
|
|
- break
|
|
|
- }
|
|
|
-
|
|
|
- if (this.didAlreadyUseTool) {
|
|
|
- // ignore any content after a tool has already been used
|
|
|
- this.userMessageContent.push({
|
|
|
- type: "text",
|
|
|
- text: `Tool [${block.name}] was not executed because a tool has already been used in this message. Only one tool may be used per message. You must assess the first tool's result before proceeding to use the next tool.`,
|
|
|
- })
|
|
|
- break
|
|
|
- }
|
|
|
-
|
|
|
- const pushToolResult = (content: ToolResponse) => {
|
|
|
- this.userMessageContent.push({
|
|
|
- type: "text",
|
|
|
- text: `${toolDescription()} Result:`,
|
|
|
- })
|
|
|
- if (typeof content === "string") {
|
|
|
- this.userMessageContent.push({
|
|
|
- type: "text",
|
|
|
- text: content || "(tool did not return anything)",
|
|
|
- })
|
|
|
- } else {
|
|
|
- this.userMessageContent.push(...content)
|
|
|
- }
|
|
|
- // once a tool result has been collected, ignore all other tool uses since we should only ever present one tool result per message
|
|
|
- this.didAlreadyUseTool = true
|
|
|
-
|
|
|
- // Flag a checkpoint as possible since we've used a tool
|
|
|
- // which may have changed the file system.
|
|
|
- }
|
|
|
-
|
|
|
- const askApproval = async (
|
|
|
- type: ClineAsk,
|
|
|
- partialMessage?: string,
|
|
|
- progressStatus?: ToolProgressStatus,
|
|
|
- ) => {
|
|
|
- const { response, text, images } = await this.ask(type, partialMessage, false, progressStatus)
|
|
|
- if (response !== "yesButtonClicked") {
|
|
|
- // Handle both messageResponse and noButtonClicked with text
|
|
|
- if (text) {
|
|
|
- await this.say("user_feedback", text, images)
|
|
|
- pushToolResult(
|
|
|
- formatResponse.toolResult(formatResponse.toolDeniedWithFeedback(text), images),
|
|
|
- )
|
|
|
- } else {
|
|
|
- pushToolResult(formatResponse.toolDenied())
|
|
|
- }
|
|
|
- this.didRejectTool = true
|
|
|
- return false
|
|
|
- }
|
|
|
- // Handle yesButtonClicked with text
|
|
|
- if (text) {
|
|
|
- await this.say("user_feedback", text, images)
|
|
|
- pushToolResult(formatResponse.toolResult(formatResponse.toolApprovedWithFeedback(text), images))
|
|
|
- }
|
|
|
- return true
|
|
|
- }
|
|
|
-
|
|
|
- const askFinishSubTaskApproval = async () => {
|
|
|
- // ask the user to approve this task has completed, and he has reviewd it, and we can declare task is finished
|
|
|
- // and return control to the parent task to continue running the rest of the sub-tasks
|
|
|
- const toolMessage = JSON.stringify({
|
|
|
- tool: "finishTask",
|
|
|
- })
|
|
|
-
|
|
|
- return await askApproval("tool", toolMessage)
|
|
|
- }
|
|
|
-
|
|
|
- const handleError = async (action: string, error: Error) => {
|
|
|
- const errorString = `Error ${action}: ${JSON.stringify(serializeError(error))}`
|
|
|
- await this.say(
|
|
|
- "error",
|
|
|
- `Error ${action}:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`,
|
|
|
- )
|
|
|
- // this.toolResults.push({
|
|
|
- // type: "tool_result",
|
|
|
- // tool_use_id: toolUseId,
|
|
|
- // content: await this.formatToolError(errorString),
|
|
|
- // })
|
|
|
- pushToolResult(formatResponse.toolError(errorString))
|
|
|
- }
|
|
|
-
|
|
|
- // If block is partial, remove partial closing tag so its not presented to user
|
|
|
- const removeClosingTag = (tag: ToolParamName, text?: string): string => {
|
|
|
- if (!block.partial) {
|
|
|
- return text || ""
|
|
|
- }
|
|
|
- if (!text) {
|
|
|
- return ""
|
|
|
- }
|
|
|
- // This regex dynamically constructs a pattern to match the closing tag:
|
|
|
- // - Optionally matches whitespace before the tag
|
|
|
- // - Matches '<' or '</' optionally followed by any subset of characters from the tag name
|
|
|
- const tagRegex = new RegExp(
|
|
|
- `\\s?<\/?${tag
|
|
|
- .split("")
|
|
|
- .map((char) => `(?:${char})?`)
|
|
|
- .join("")}$`,
|
|
|
- "g",
|
|
|
- )
|
|
|
- return text.replace(tagRegex, "")
|
|
|
- }
|
|
|
-
|
|
|
- if (block.name !== "browser_action") {
|
|
|
- await this.browserSession.closeBrowser()
|
|
|
- }
|
|
|
-
|
|
|
- if (!block.partial) {
|
|
|
- this.recordToolUsage(block.name)
|
|
|
- }
|
|
|
-
|
|
|
- // Validate tool use before execution
|
|
|
- const { mode, customModes } = (await this.providerRef.deref()?.getState()) ?? {}
|
|
|
- try {
|
|
|
- validateToolUse(
|
|
|
- block.name as ToolName,
|
|
|
- mode ?? defaultModeSlug,
|
|
|
- customModes ?? [],
|
|
|
- {
|
|
|
- apply_diff: this.diffEnabled,
|
|
|
- },
|
|
|
- block.params,
|
|
|
- )
|
|
|
- } catch (error) {
|
|
|
- this.consecutiveMistakeCount++
|
|
|
- pushToolResult(formatResponse.toolError(error.message))
|
|
|
- break
|
|
|
- }
|
|
|
-
|
|
|
- // Check for identical consecutive tool calls
|
|
|
- if (!block.partial) {
|
|
|
- // Use the detector to check for repetition, passing the ToolUse block directly
|
|
|
- const repetitionCheck = this.toolRepetitionDetector.check(block)
|
|
|
-
|
|
|
- // If execution is not allowed, notify user and break
|
|
|
- if (!repetitionCheck.allowExecution && repetitionCheck.askUser) {
|
|
|
- // Handle repetition similar to mistake_limit_reached pattern
|
|
|
- const { response, text, images } = await this.ask(
|
|
|
- repetitionCheck.askUser.messageKey as ClineAsk,
|
|
|
- repetitionCheck.askUser.messageDetail.replace("{toolName}", block.name),
|
|
|
- )
|
|
|
-
|
|
|
- if (response === "messageResponse") {
|
|
|
- // Add user feedback to userContent
|
|
|
- this.userMessageContent.push(
|
|
|
- {
|
|
|
- type: "text" as const,
|
|
|
- text: `Tool repetition limit reached. User feedback: ${text}`,
|
|
|
- },
|
|
|
- ...formatResponse.imageBlocks(images),
|
|
|
- )
|
|
|
-
|
|
|
- // Add user feedback to chat
|
|
|
- await this.say("user_feedback", text, images)
|
|
|
-
|
|
|
- // Track tool repetition in telemetry
|
|
|
- // telemetryService.captureConsecutiveMistakeError(this.taskId) // Using existing telemetry method
|
|
|
- }
|
|
|
-
|
|
|
- // Return tool result message about the repetition
|
|
|
- pushToolResult(
|
|
|
- formatResponse.toolError(
|
|
|
- `Tool call repetition limit reached for ${block.name}. Please try a different approach.`,
|
|
|
- ),
|
|
|
- )
|
|
|
- break
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- switch (block.name) {
|
|
|
- case "write_to_file":
|
|
|
- await writeToFileTool(this, block, askApproval, handleError, pushToolResult, removeClosingTag)
|
|
|
- break
|
|
|
- case "apply_diff":
|
|
|
- await applyDiffTool(this, block, askApproval, handleError, pushToolResult, removeClosingTag)
|
|
|
- break
|
|
|
- case "insert_content":
|
|
|
- await insertContentTool(this, block, askApproval, handleError, pushToolResult, removeClosingTag)
|
|
|
- break
|
|
|
- case "search_and_replace":
|
|
|
- await searchAndReplaceTool(
|
|
|
- this,
|
|
|
- block,
|
|
|
- askApproval,
|
|
|
- handleError,
|
|
|
- pushToolResult,
|
|
|
- removeClosingTag,
|
|
|
- )
|
|
|
- break
|
|
|
- case "read_file":
|
|
|
- await readFileTool(this, block, askApproval, handleError, pushToolResult, removeClosingTag)
|
|
|
-
|
|
|
- break
|
|
|
- case "fetch_instructions":
|
|
|
- await fetchInstructionsTool(this, block, askApproval, handleError, pushToolResult)
|
|
|
- break
|
|
|
- case "list_files":
|
|
|
- await listFilesTool(this, block, askApproval, handleError, pushToolResult, removeClosingTag)
|
|
|
- break
|
|
|
- case "list_code_definition_names":
|
|
|
- await listCodeDefinitionNamesTool(
|
|
|
- this,
|
|
|
- block,
|
|
|
- askApproval,
|
|
|
- handleError,
|
|
|
- pushToolResult,
|
|
|
- removeClosingTag,
|
|
|
- )
|
|
|
- break
|
|
|
- case "search_files":
|
|
|
- await searchFilesTool(this, block, askApproval, handleError, pushToolResult, removeClosingTag)
|
|
|
- break
|
|
|
- case "browser_action":
|
|
|
- await browserActionTool(this, block, askApproval, handleError, pushToolResult, removeClosingTag)
|
|
|
- break
|
|
|
- case "execute_command":
|
|
|
- await executeCommandTool(
|
|
|
- this,
|
|
|
- block,
|
|
|
- askApproval,
|
|
|
- handleError,
|
|
|
- pushToolResult,
|
|
|
- removeClosingTag,
|
|
|
- )
|
|
|
- break
|
|
|
- case "use_mcp_tool":
|
|
|
- await useMcpToolTool(this, block, askApproval, handleError, pushToolResult, removeClosingTag)
|
|
|
- break
|
|
|
- case "access_mcp_resource":
|
|
|
- await accessMcpResourceTool(
|
|
|
- this,
|
|
|
- block,
|
|
|
- askApproval,
|
|
|
- handleError,
|
|
|
- pushToolResult,
|
|
|
- removeClosingTag,
|
|
|
- )
|
|
|
- break
|
|
|
- case "ask_followup_question":
|
|
|
- await askFollowupQuestionTool(
|
|
|
- this,
|
|
|
- block,
|
|
|
- askApproval,
|
|
|
- handleError,
|
|
|
- pushToolResult,
|
|
|
- removeClosingTag,
|
|
|
- )
|
|
|
- break
|
|
|
- case "switch_mode":
|
|
|
- await switchModeTool(this, block, askApproval, handleError, pushToolResult, removeClosingTag)
|
|
|
- break
|
|
|
- case "new_task":
|
|
|
- await newTaskTool(this, block, askApproval, handleError, pushToolResult, removeClosingTag)
|
|
|
- break
|
|
|
- case "attempt_completion":
|
|
|
- await attemptCompletionTool(
|
|
|
- this,
|
|
|
- block,
|
|
|
- askApproval,
|
|
|
- handleError,
|
|
|
- pushToolResult,
|
|
|
- removeClosingTag,
|
|
|
- toolDescription,
|
|
|
- askFinishSubTaskApproval,
|
|
|
- )
|
|
|
- break
|
|
|
- }
|
|
|
-
|
|
|
- break
|
|
|
- }
|
|
|
-
|
|
|
- const recentlyModifiedFiles = this.fileContextTracker.getAndClearCheckpointPossibleFile()
|
|
|
-
|
|
|
- if (recentlyModifiedFiles.length > 0) {
|
|
|
- // TODO: We can track what file changes were made and only
|
|
|
- // checkpoint those files, this will be save storage.
|
|
|
- await this.checkpointSave()
|
|
|
- }
|
|
|
-
|
|
|
- /*
|
|
|
- Seeing out of bounds is fine, it means that the next too call is being built up and ready to add to assistantMessageContent to present.
|
|
|
- When you see the UI inactive during this, it means that a tool is breaking without presenting any UI. For example the write_to_file tool was breaking when relpath was undefined, and for invalid relpath it never presented UI.
|
|
|
- */
|
|
|
- this.presentAssistantMessageLocked = false // this needs to be placed here, if not then calling this.presentAssistantMessage below would fail (sometimes) since it's locked
|
|
|
- // NOTE: when tool is rejected, iterator stream is interrupted and it waits for userMessageContentReady to be true. Future calls to present will skip execution since didRejectTool and iterate until contentIndex is set to message length and it sets userMessageContentReady to true itself (instead of preemptively doing it in iterator)
|
|
|
- if (!block.partial || this.didRejectTool || this.didAlreadyUseTool) {
|
|
|
- // block is finished streaming and executing
|
|
|
- if (this.currentStreamingContentIndex === this.assistantMessageContent.length - 1) {
|
|
|
- // its okay that we increment if !didCompleteReadingStream, it'll just return bc out of bounds and as streaming continues it will call presentAssitantMessage if a new block is ready. if streaming is finished then we set userMessageContentReady to true when out of bounds. This gracefully allows the stream to continue on and all potential content blocks be presented.
|
|
|
- // last block is complete and it is finished executing
|
|
|
- this.userMessageContentReady = true // will allow pwaitfor to continue
|
|
|
- }
|
|
|
-
|
|
|
- // call next block if it exists (if not then read stream will call it when its ready)
|
|
|
- this.currentStreamingContentIndex++ // need to increment regardless, so when read stream calls this function again it will be streaming the next block
|
|
|
-
|
|
|
- if (this.currentStreamingContentIndex < this.assistantMessageContent.length) {
|
|
|
- // there are already more content blocks to stream, so we'll call this function ourselves
|
|
|
- // await this.presentAssistantContent()
|
|
|
-
|
|
|
- this.presentAssistantMessage()
|
|
|
- return
|
|
|
- }
|
|
|
- }
|
|
|
- // block is partial, but the read stream may have finished
|
|
|
- if (this.presentAssistantMessageHasPendingUpdates) {
|
|
|
- this.presentAssistantMessage()
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Transform
|
|
|
-
|
|
|
- public async parseUserContent(userContent: UserContent) {
|
|
|
- // Process userContent array, which contains various block types:
|
|
|
- // TextBlockParam, ImageBlockParam, ToolUseBlockParam, and ToolResultBlockParam.
|
|
|
- // We need to apply parseMentions() to:
|
|
|
- // 1. All TextBlockParam's text (first user message with task)
|
|
|
- // 2. ToolResultBlockParam's content/context text arrays if it contains
|
|
|
- // "<feedback>" (see formatToolDeniedFeedback, attemptCompletion,
|
|
|
- // executeCommand, and consecutiveMistakeCount >= 3) or "<answer>"
|
|
|
- // (see askFollowupQuestion), we place all user generated content in
|
|
|
- // these tags so they can effectively be used as markers for when we
|
|
|
- // should parse mentions).
|
|
|
- return Promise.all(
|
|
|
- userContent.map(async (block) => {
|
|
|
- const shouldProcessMentions = (text: string) => text.includes("<task>") || text.includes("<feedback>")
|
|
|
-
|
|
|
- if (block.type === "text") {
|
|
|
- if (shouldProcessMentions(block.text)) {
|
|
|
- // kilocode_change begin: pull slash commands from Cline
|
|
|
- let parsedText = await parseMentions(
|
|
|
- block.text,
|
|
|
- this.cwd,
|
|
|
- this.urlContentFetcher,
|
|
|
- this.fileContextTracker,
|
|
|
- )
|
|
|
-
|
|
|
- // when parsing slash commands, we still want to allow the user to provide their desired context
|
|
|
- parsedText = parseSlashCommands(parsedText)
|
|
|
-
|
|
|
- return {
|
|
|
- ...block,
|
|
|
- text: parsedText,
|
|
|
- }
|
|
|
- // kilocode_change end
|
|
|
- }
|
|
|
-
|
|
|
- return block
|
|
|
- } else if (block.type === "tool_result") {
|
|
|
- if (typeof block.content === "string") {
|
|
|
- if (shouldProcessMentions(block.content)) {
|
|
|
- return {
|
|
|
- ...block,
|
|
|
- content: await parseMentions(
|
|
|
- block.content,
|
|
|
- this.cwd,
|
|
|
- this.urlContentFetcher,
|
|
|
- this.fileContextTracker,
|
|
|
- ),
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return block
|
|
|
- } else if (Array.isArray(block.content)) {
|
|
|
- const parsedContent = await Promise.all(
|
|
|
- block.content.map(async (contentBlock) => {
|
|
|
- if (contentBlock.type === "text" && shouldProcessMentions(contentBlock.text)) {
|
|
|
- return {
|
|
|
- ...contentBlock,
|
|
|
- text: await parseMentions(
|
|
|
- contentBlock.text,
|
|
|
- this.cwd,
|
|
|
- this.urlContentFetcher,
|
|
|
- this.fileContextTracker,
|
|
|
- ),
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return contentBlock
|
|
|
- }),
|
|
|
- )
|
|
|
-
|
|
|
- return { ...block, content: parsedContent }
|
|
|
- }
|
|
|
-
|
|
|
- return block
|
|
|
- }
|
|
|
-
|
|
|
- return block
|
|
|
- }),
|
|
|
- )
|
|
|
- }
|
|
|
-
|
|
|
// Checkpoints
|
|
|
|
|
|
- private getCheckpointService() {
|
|
|
- if (!this.enableCheckpoints) {
|
|
|
- return undefined
|
|
|
- }
|
|
|
-
|
|
|
- if (this.checkpointService) {
|
|
|
- return this.checkpointService
|
|
|
- }
|
|
|
-
|
|
|
- if (this.checkpointServiceInitializing) {
|
|
|
- console.log("[Cline#getCheckpointService] checkpoint service is still initializing")
|
|
|
- return undefined
|
|
|
- }
|
|
|
-
|
|
|
- const log = (message: string) => {
|
|
|
- console.log(message)
|
|
|
-
|
|
|
- try {
|
|
|
- this.providerRef.deref()?.log(message)
|
|
|
- } catch (err) {
|
|
|
- // NO-OP
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- console.log("[Cline#getCheckpointService] initializing checkpoints service")
|
|
|
-
|
|
|
- try {
|
|
|
- const workspaceDir = getWorkspacePath()
|
|
|
-
|
|
|
- if (!workspaceDir) {
|
|
|
- log("[Cline#getCheckpointService] workspace folder not found, disabling checkpoints")
|
|
|
- this.enableCheckpoints = false
|
|
|
- return undefined
|
|
|
- }
|
|
|
-
|
|
|
- const globalStorageDir = this.providerRef.deref()?.context.globalStorageUri.fsPath
|
|
|
-
|
|
|
- if (!globalStorageDir) {
|
|
|
- log("[Cline#getCheckpointService] globalStorageDir not found, disabling checkpoints")
|
|
|
- this.enableCheckpoints = false
|
|
|
- return undefined
|
|
|
- }
|
|
|
-
|
|
|
- const options: CheckpointServiceOptions = {
|
|
|
- taskId: this.taskId,
|
|
|
- workspaceDir,
|
|
|
- shadowDir: globalStorageDir,
|
|
|
- log,
|
|
|
- }
|
|
|
-
|
|
|
- const service = RepoPerTaskCheckpointService.create(options)
|
|
|
-
|
|
|
- this.checkpointServiceInitializing = true
|
|
|
-
|
|
|
- service.on("initialize", () => {
|
|
|
- log("[Cline#getCheckpointService] service initialized")
|
|
|
-
|
|
|
- try {
|
|
|
- const isCheckpointNeeded =
|
|
|
- typeof this.clineMessages.find(({ say }) => say === "checkpoint_saved") === "undefined"
|
|
|
-
|
|
|
- this.checkpointService = service
|
|
|
- this.checkpointServiceInitializing = false
|
|
|
-
|
|
|
- if (isCheckpointNeeded) {
|
|
|
- log("[Cline#getCheckpointService] no checkpoints found, saving initial checkpoint")
|
|
|
- this.checkpointSave()
|
|
|
- }
|
|
|
- } catch (err) {
|
|
|
- log("[Cline#getCheckpointService] caught error in on('initialize'), disabling checkpoints")
|
|
|
- this.enableCheckpoints = false
|
|
|
- }
|
|
|
- })
|
|
|
-
|
|
|
- service.on("checkpoint", ({ isFirst, fromHash: from, toHash: to }) => {
|
|
|
- try {
|
|
|
- this.providerRef.deref()?.postMessageToWebview({ type: "currentCheckpointUpdated", text: to })
|
|
|
-
|
|
|
- this.say("checkpoint_saved", to, undefined, undefined, { isFirst, from, to }).catch((err) => {
|
|
|
- log("[Cline#getCheckpointService] caught unexpected error in say('checkpoint_saved')")
|
|
|
- console.error(err)
|
|
|
- })
|
|
|
- } catch (err) {
|
|
|
- log(
|
|
|
- "[Cline#getCheckpointService] caught unexpected error in on('checkpoint'), disabling checkpoints",
|
|
|
- )
|
|
|
- console.error(err)
|
|
|
- this.enableCheckpoints = false
|
|
|
- }
|
|
|
- })
|
|
|
-
|
|
|
- log("[Cline#getCheckpointService] initializing shadow git")
|
|
|
-
|
|
|
- service.initShadowGit().catch((err) => {
|
|
|
- log(
|
|
|
- `[Cline#getCheckpointService] caught unexpected error in initShadowGit, disabling checkpoints (${err.message})`,
|
|
|
- )
|
|
|
- console.error(err)
|
|
|
- this.enableCheckpoints = false
|
|
|
- })
|
|
|
-
|
|
|
- return service
|
|
|
- } catch (err) {
|
|
|
- log("[Cline#getCheckpointService] caught unexpected error, disabling checkpoints")
|
|
|
- this.enableCheckpoints = false
|
|
|
- return undefined
|
|
|
- }
|
|
|
+ public async checkpointSave() {
|
|
|
+ return checkpointSave(this)
|
|
|
}
|
|
|
|
|
|
- private async getInitializedCheckpointService({
|
|
|
- interval = 250,
|
|
|
- timeout = 15_000,
|
|
|
- }: { interval?: number; timeout?: number } = {}) {
|
|
|
- const service = this.getCheckpointService()
|
|
|
-
|
|
|
- if (!service || service.isInitialized) {
|
|
|
- return service
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- await pWaitFor(
|
|
|
- () => {
|
|
|
- console.log("[Cline#getCheckpointService] waiting for service to initialize")
|
|
|
- return service.isInitialized
|
|
|
- },
|
|
|
- { interval, timeout },
|
|
|
- )
|
|
|
-
|
|
|
- return service
|
|
|
- } catch (err) {
|
|
|
- return undefined
|
|
|
- }
|
|
|
+ public async checkpointRestore(options: CheckpointRestoreOptions) {
|
|
|
+ return checkpointRestore(this, options)
|
|
|
}
|
|
|
|
|
|
- public async checkpointDiff({
|
|
|
- ts,
|
|
|
- previousCommitHash,
|
|
|
- commitHash,
|
|
|
- mode,
|
|
|
- }: {
|
|
|
- ts: number
|
|
|
- previousCommitHash?: string
|
|
|
- commitHash: string
|
|
|
- mode: "full" | "checkpoint"
|
|
|
- }) {
|
|
|
- const service = await this.getInitializedCheckpointService()
|
|
|
-
|
|
|
- if (!service) {
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- if (!previousCommitHash && mode === "checkpoint") {
|
|
|
- const previousCheckpoint = this.clineMessages
|
|
|
- .filter(({ say }) => say === "checkpoint_saved")
|
|
|
- .sort((a, b) => b.ts - a.ts)
|
|
|
- .find((message) => message.ts < ts)
|
|
|
-
|
|
|
- previousCommitHash = previousCheckpoint?.text
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- const changes = await service.getDiff({ from: previousCommitHash, to: commitHash })
|
|
|
-
|
|
|
- if (!changes?.length) {
|
|
|
- vscode.window.showInformationMessage("No changes found.")
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- await vscode.commands.executeCommand(
|
|
|
- "vscode.changes",
|
|
|
- mode === "full" ? "Changes since task started" : "Changes since previous checkpoint",
|
|
|
- changes.map((change) => [
|
|
|
- vscode.Uri.file(change.paths.absolute),
|
|
|
- vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${change.paths.relative}`).with({
|
|
|
- query: Buffer.from(change.content.before ?? "").toString("base64"),
|
|
|
- }),
|
|
|
- vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${change.paths.relative}`).with({
|
|
|
- query: Buffer.from(change.content.after ?? "").toString("base64"),
|
|
|
- }),
|
|
|
- ]),
|
|
|
- )
|
|
|
- } catch (err) {
|
|
|
- this.providerRef.deref()?.log("[checkpointDiff] disabling checkpoints for this task")
|
|
|
- this.enableCheckpoints = false
|
|
|
- }
|
|
|
+ public async checkpointDiff(options: CheckpointDiffOptions) {
|
|
|
+ return checkpointDiff(this, options)
|
|
|
}
|
|
|
|
|
|
- public async checkpointSave() {
|
|
|
- const service = this.getCheckpointService()
|
|
|
-
|
|
|
- if (!service) {
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- if (!service.isInitialized) {
|
|
|
- this.providerRef
|
|
|
- .deref()
|
|
|
- ?.log("[checkpointSave] checkpoints didn't initialize in time, disabling checkpoints for this task")
|
|
|
-
|
|
|
- this.enableCheckpoints = false
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- // Start the checkpoint process in the background.
|
|
|
- return service.saveCheckpoint(`Task: ${this.taskId}, Time: ${Date.now()}`).catch((err) => {
|
|
|
- console.error("[Cline#checkpointSave] caught unexpected error, disabling checkpoints", err)
|
|
|
- this.enableCheckpoints = false
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- public async checkpointRestore({
|
|
|
- ts,
|
|
|
- commitHash,
|
|
|
- mode,
|
|
|
- }: {
|
|
|
- ts: number
|
|
|
- commitHash: string
|
|
|
- mode: "preview" | "restore"
|
|
|
- }) {
|
|
|
- const service = await this.getInitializedCheckpointService()
|
|
|
-
|
|
|
- if (!service) {
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- const index = this.clineMessages.findIndex((m) => m.ts === ts)
|
|
|
-
|
|
|
- if (index === -1) {
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- await service.restoreCheckpoint(commitHash)
|
|
|
-
|
|
|
- await this.providerRef.deref()?.postMessageToWebview({ type: "currentCheckpointUpdated", text: commitHash })
|
|
|
-
|
|
|
- if (mode === "restore") {
|
|
|
- await this.overwriteApiConversationHistory(
|
|
|
- this.apiConversationHistory.filter((m) => !m.ts || m.ts < ts),
|
|
|
- )
|
|
|
-
|
|
|
- const deletedMessages = this.clineMessages.slice(index + 1)
|
|
|
-
|
|
|
- const { totalTokensIn, totalTokensOut, totalCacheWrites, totalCacheReads, totalCost } = getApiMetrics(
|
|
|
- combineApiRequests(combineCommandSequences(deletedMessages)),
|
|
|
- )
|
|
|
-
|
|
|
- await this.overwriteClineMessages(this.clineMessages.slice(0, index + 1))
|
|
|
-
|
|
|
- // TODO: Verify that this is working as expected.
|
|
|
- await this.say(
|
|
|
- "api_req_deleted",
|
|
|
- JSON.stringify({
|
|
|
- tokensIn: totalTokensIn,
|
|
|
- tokensOut: totalTokensOut,
|
|
|
- cacheWrites: totalCacheWrites,
|
|
|
- cacheReads: totalCacheReads,
|
|
|
- cost: totalCost,
|
|
|
- } satisfies ClineApiReqInfo),
|
|
|
- )
|
|
|
- }
|
|
|
+ // Metrics
|
|
|
|
|
|
- // The task is already cancelled by the provider beforehand, but we
|
|
|
- // need to re-init to get the updated messages.
|
|
|
- //
|
|
|
- // This was take from Cline's implementation of the checkpoints
|
|
|
- // feature. The cline instance will hang if we don't cancel twice,
|
|
|
- // so this is currently necessary, but it seems like a complicated
|
|
|
- // and hacky solution to a problem that I don't fully understand.
|
|
|
- // I'd like to revisit this in the future and try to improve the
|
|
|
- // task flow and the communication between the webview and the
|
|
|
- // Cline instance.
|
|
|
- this.providerRef.deref()?.cancelTask()
|
|
|
- } catch (err) {
|
|
|
- this.providerRef.deref()?.log("[checkpointRestore] disabling checkpoints for this task")
|
|
|
- this.enableCheckpoints = false
|
|
|
- }
|
|
|
+ public combineMessages(messages: ClineMessage[]) {
|
|
|
+ return combineApiRequests(combineCommandSequences(messages))
|
|
|
}
|
|
|
|
|
|
- // Metrics
|
|
|
-
|
|
|
public getTokenUsage() {
|
|
|
- return getApiMetrics(combineApiRequests(combineCommandSequences(this.clineMessages.slice(1))))
|
|
|
+ return getApiMetrics(this.combineMessages(this.clineMessages.slice(1)))
|
|
|
}
|
|
|
|
|
|
public recordToolUsage(toolName: ToolName) {
|