Compare commits

...

17 Commits

Author SHA1 Message Date
Evan d1408d0792 Cline.ts - Factor out overwriteClineMessages [7/8] (#2363)
* refactor overwriteClineMessages

* changeset
2025-03-24 16:39:58 -07:00
celestial-vault 765843af84 changeset 2025-03-20 14:46:26 -07:00
celestial-vault f8e3b62a17 refactor addToClineMessages 2025-03-20 14:45:58 -07:00
celestial-vault 64fed2c501 changeset 2025-03-20 14:24:43 -07:00
celestial-vault 636c2896bf refactor overwriteApiConversationHistory 2025-03-20 14:24:23 -07:00
celestial-vault e3ca261db5 changeset 2025-03-20 14:07:06 -07:00
celestial-vault e66f77a016 refactor addToApiConversationHistory 2025-03-20 14:06:45 -07:00
celestial-vault b3c71b0bd0 changeset 2025-03-20 13:56:02 -07:00
celestial-vault 4a93058f34 factor out getSavedClineMessages 2025-03-20 13:55:43 -07:00
celestial-vault 754ee766d1 delete extra changeset 2025-03-20 13:50:57 -07:00
celestial-vault d2e393e603 changeset 2025-03-20 13:48:45 -07:00
celestial-vault b0baefc3ba refactor out getSavedApiConversationHistory 2025-03-20 13:48:20 -07:00
celestial-vault 27571b4617 changeset 2025-03-20 13:41:26 -07:00
celestial-vault bf9164e1ae factor out saveApiConversationHistory 2025-03-20 13:40:52 -07:00
celestial-vault eea2c97835 changeset 2025-03-20 13:27:25 -07:00
celestial-vault cc67a77759 merge conflicts 2025-03-20 13:25:44 -07:00
celestial-vault 3d33834ce5 refactor out ensureTaskDirectoryExists 2025-03-20 13:21:58 -07:00
9 changed files with 193 additions and 102 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
refactor
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
refactor
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
refactor
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
refactor
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
refactor
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
refactor
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
refactor
+98 -102
View File
@@ -66,6 +66,12 @@ import { DEFAULT_LANGUAGE_SETTINGS, getLanguageKey, LanguageDisplay, LanguageKey
import { telemetryService } from "../services/telemetry/TelemetryService"
import pTimeout from "p-timeout"
import { GlobalFileNames } from "../global-constants"
import {
ensureTaskDirectoryExists,
getSavedApiConversationHistory,
getSavedClineMessages,
saveApiConversationHistory,
} from "./messages-io"
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
@@ -178,78 +184,11 @@ export class Cline {
// Storing task to disk for history
private async ensureTaskDirectoryExists(): Promise<string> {
const globalStoragePath = this.providerRef.deref()?.context.globalStorageUri.fsPath
if (!globalStoragePath) {
throw new Error("Global storage uri is invalid")
}
const taskDir = path.join(globalStoragePath, "tasks", this.taskId)
await fs.mkdir(taskDir, { recursive: true })
return taskDir
}
private async getSavedApiConversationHistory(): Promise<Anthropic.MessageParam[]> {
const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.apiConversationHistory)
const fileExists = await fileExistsAtPath(filePath)
if (fileExists) {
return JSON.parse(await fs.readFile(filePath, "utf8"))
}
return []
}
private async addToApiConversationHistory(message: Anthropic.MessageParam) {
this.apiConversationHistory.push(message)
await this.saveApiConversationHistory()
}
private async overwriteApiConversationHistory(newHistory: Anthropic.MessageParam[]) {
this.apiConversationHistory = newHistory
await this.saveApiConversationHistory()
}
private async saveApiConversationHistory() {
try {
const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.apiConversationHistory)
await fs.writeFile(filePath, JSON.stringify(this.apiConversationHistory))
} catch (error) {
// in the off chance this fails, we don't want to stop the task
console.error("Failed to save API conversation history:", error)
}
}
private async getSavedClineMessages(): Promise<ClineMessage[]> {
const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.uiMessages)
if (await fileExistsAtPath(filePath)) {
return JSON.parse(await fs.readFile(filePath, "utf8"))
} else {
// check old location
const oldPath = path.join(await this.ensureTaskDirectoryExists(), "claude_messages.json")
if (await fileExistsAtPath(oldPath)) {
const data = JSON.parse(await fs.readFile(oldPath, "utf8"))
await fs.unlink(oldPath) // remove old file
return data
}
}
return []
}
private async addToClineMessages(message: ClineMessage) {
// these values allow us to reconstruct the conversation history at the time this cline message was created
// it's important that apiConversationHistory is initialized before we add cline messages
message.conversationHistoryIndex = this.apiConversationHistory.length - 1 // NOTE: this is the index of the last added message which is the user message, and once the clinemessages have been presented we update the apiconversationhistory with the completed assistant message. This means when resetting to a message, we need to +1 this index to get the correct assistant message that this tool use corresponds to
message.conversationHistoryDeletedRange = this.conversationHistoryDeletedRange
this.clineMessages.push(message)
await this.saveClineMessages()
}
private async overwriteClineMessages(newMessages: ClineMessage[]) {
this.clineMessages = newMessages
await this.saveClineMessages()
}
private async saveClineMessages() {
try {
const taskDir = await this.ensureTaskDirectoryExists()
const globalStoragePath = this.providerRef.deref()?.context.globalStorageUri.fsPath
const taskId = this.taskId
const taskDir = await ensureTaskDirectoryExists(globalStoragePath, taskId)
const filePath = path.join(taskDir, GlobalFileNames.uiMessages)
await fs.writeFile(filePath, JSON.stringify(this.clineMessages))
// combined as they are in ChatView
@@ -336,14 +275,20 @@ export class Cline {
0,
(message.conversationHistoryIndex || 0) + 2,
) // +1 since this index corresponds to the last user message, and another +1 since slice end index is exclusive
await this.overwriteApiConversationHistory(newConversationHistory)
this.apiConversationHistory = newConversationHistory
const globalStoragePath = this.providerRef.deref()?.context.globalStorageUri.fsPath
const taskId = this.taskId
await saveApiConversationHistory(globalStoragePath, taskId, this.apiConversationHistory)
// aggregate deleted api reqs info so we don't lose costs/tokens
const deletedMessages = this.clineMessages.slice(messageIndex + 1)
const deletedApiReqsMetrics = getApiMetrics(combineApiRequests(combineCommandSequences(deletedMessages)))
const newClineMessages = this.clineMessages.slice(0, messageIndex + 1)
await this.overwriteClineMessages(newClineMessages) // calls saveClineMessages which saves historyItem
this.clineMessages = newClineMessages
await this.saveClineMessages()
await this.say(
"deleted_api_reqs",
@@ -619,13 +564,22 @@ export class Cline {
// this.askResponseImages = undefined
askTs = Date.now()
this.lastMessageTs = askTs
await this.addToClineMessages({
// conversationHistoryIndex and conversationHistoryDeletedRange allow us to reconstruct the conversation history at the time this cline message was created
// it's important that apiConversationHistory is initialized before we add cline messages
const message: ClineMessage = {
ts: askTs,
type: "ask",
ask: type,
text,
partial,
})
conversationHistoryIndex: this.apiConversationHistory.length - 1, // NOTE: this is the index of the last added message which is the user message, and once the clinemessages have been presented we update the apiconversationhistory with the completed assistant message. This means when resetting to a message, we need to +1 this index to get the correct assistant message that this tool use corresponds to
conversationHistoryDeletedRange: this.conversationHistoryDeletedRange,
}
this.clineMessages.push(message)
await this.saveClineMessages()
await this.providerRef.deref()?.postStateToWebview()
throw new Error("Current ask promise was ignored 2")
}
@@ -661,12 +615,18 @@ export class Cline {
this.askResponseImages = undefined
askTs = Date.now()
this.lastMessageTs = askTs
await this.addToClineMessages({
const message: ClineMessage = {
ts: askTs,
type: "ask",
ask: type,
text,
})
conversationHistoryIndex: this.apiConversationHistory.length - 1,
conversationHistoryDeletedRange: this.conversationHistoryDeletedRange,
}
this.clineMessages.push(message)
await this.saveClineMessages()
await this.providerRef.deref()?.postStateToWebview()
}
}
@@ -678,12 +638,19 @@ export class Cline {
this.askResponseImages = undefined
askTs = Date.now()
this.lastMessageTs = askTs
await this.addToClineMessages({
const message: ClineMessage = {
ts: askTs,
type: "ask",
ask: type,
text,
})
conversationHistoryIndex: this.apiConversationHistory.length - 1,
conversationHistoryDeletedRange: this.conversationHistoryDeletedRange,
}
this.clineMessages.push(message)
await this.saveClineMessages()
await this.providerRef.deref()?.postStateToWebview()
}
@@ -731,14 +698,19 @@ export class Cline {
// this is a new partial message, so add it with partial state
const sayTs = Date.now()
this.lastMessageTs = sayTs
await this.addToClineMessages({
const message: ClineMessage = {
ts: sayTs,
type: "say",
say: type,
text,
images,
partial,
})
conversationHistoryIndex: this.apiConversationHistory.length - 1,
conversationHistoryDeletedRange: this.conversationHistoryDeletedRange,
}
this.clineMessages.push(message)
await this.saveClineMessages()
await this.providerRef.deref()?.postStateToWebview()
}
} else {
@@ -762,13 +734,19 @@ export class Cline {
// this is a new partial=false message, so add it like normal
const sayTs = Date.now()
this.lastMessageTs = sayTs
await this.addToClineMessages({
const message: ClineMessage = {
ts: sayTs,
type: "say",
say: type,
text,
images,
})
conversationHistoryIndex: this.apiConversationHistory.length - 1,
conversationHistoryDeletedRange: this.conversationHistoryDeletedRange,
}
this.clineMessages.push(message)
await this.saveClineMessages()
await this.providerRef.deref()?.postStateToWebview()
}
}
@@ -776,13 +754,20 @@ export class Cline {
// this is a new non-partial message, so add it like normal
const sayTs = Date.now()
this.lastMessageTs = sayTs
await this.addToClineMessages({
const message: ClineMessage = {
ts: sayTs,
type: "say",
say: type,
text,
images,
})
conversationHistoryIndex: this.apiConversationHistory.length - 1,
conversationHistoryDeletedRange: this.conversationHistoryDeletedRange,
}
this.clineMessages.push(message)
await this.saveClineMessages()
await this.providerRef.deref()?.postStateToWebview()
}
}
@@ -840,8 +825,9 @@ export class Cline {
// if (!doesShadowGitExist) {
// this.checkpointTrackerErrorMessage = "Checkpoints are only available for new tasks"
// }
const modifiedClineMessages = await this.getSavedClineMessages()
const globalStoragePath = this.providerRef.deref()?.context.globalStorageUri.fsPath
const taskId = this.taskId
const modifiedClineMessages = await getSavedClineMessages(globalStoragePath, taskId)
// Remove any resume messages that may have been added before
const lastRelevantMessageIndex = findLastIndex(
@@ -865,12 +851,15 @@ export class Cline {
}
}
await this.overwriteClineMessages(modifiedClineMessages)
this.clineMessages = await this.getSavedClineMessages()
this.clineMessages = modifiedClineMessages
await this.saveClineMessages()
this.clineMessages = await getSavedClineMessages(globalStoragePath, taskId)
// Now present the cline messages to the user and ask if they want to resume (NOTE: we ran into a bug before where the apiconversationhistory wouldnt be initialized when opening a old task, and it was because we were waiting for resume)
// This is important in case the user deletes messages without resuming the task first
this.apiConversationHistory = await this.getSavedApiConversationHistory()
this.apiConversationHistory = await getSavedApiConversationHistory(globalStoragePath, taskId)
const lastClineMessage = this.clineMessages
.slice()
@@ -907,7 +896,10 @@ export class Cline {
// need to make sure that the api conversation history can be resumed by the api, even if it goes out of sync with cline messages
const existingApiConversationHistory: Anthropic.Messages.MessageParam[] = await this.getSavedApiConversationHistory()
const existingApiConversationHistory: Anthropic.Messages.MessageParam[] = await getSavedApiConversationHistory(
globalStoragePath,
taskId,
)
// if the last message is an assistant message, we need to check if there's tool use since every tool use has to have a tool response
// if there's no tool use and only a text block, then we can just add a user message
@@ -1042,7 +1034,9 @@ export class Cline {
newUserContent.push(...formatResponse.imageBlocks(responseImages))
}
await this.overwriteApiConversationHistory(modifiedApiConversationHistory)
this.apiConversationHistory = modifiedApiConversationHistory
await saveApiConversationHistory(globalStoragePath, taskId, this.apiConversationHistory)
await this.initiateTaskLoop(newUserContent, false)
}
@@ -1389,7 +1383,6 @@ export class Cline {
keep,
)
await this.saveClineMessages() // saves task history item which we use to keep track of conversation history deleted range
// await this.overwriteApiConversationHistory(truncatedMessages)
}
}
}
@@ -3158,10 +3151,13 @@ export class Cline {
// add environment details as its own text block, separate from tool results
userContent.push({ type: "text", text: environmentDetails })
await this.addToApiConversationHistory({
this.apiConversationHistory.push({
role: "user",
content: userContent,
})
const globalStoragePath = this.providerRef.deref()?.context.globalStorageUri.fsPath
const taskId = this.taskId
await saveApiConversationHistory(globalStoragePath, taskId, this.apiConversationHistory)
telemetryService.captureConversationTurnEvent(this.taskId, this.apiProvider, this.api.getModel().id, "user")
@@ -3220,7 +3216,8 @@ export class Cline {
}
// Let assistant know their response was interrupted for when task is resumed
await this.addToApiConversationHistory({
this.apiConversationHistory.push({
role: "assistant",
content: [
{
@@ -3235,6 +3232,7 @@ export class Cline {
},
],
})
await saveApiConversationHistory(globalStoragePath, taskId, this.apiConversationHistory)
// update api_req_started to have cancelled and cost, so that we can display the cost of the partial stream
updateApiReqMsg(cancelReason, streamingFailedMessage)
@@ -3386,10 +3384,11 @@ export class Cline {
if (assistantMessage.length > 0) {
telemetryService.captureConversationTurnEvent(this.taskId, this.apiProvider, this.api.getModel().id, "assistant")
await this.addToApiConversationHistory({
this.apiConversationHistory.push({
role: "assistant",
content: [{ type: "text", text: assistantMessage }],
})
await saveApiConversationHistory(globalStoragePath, taskId, this.apiConversationHistory)
// NOTE: this comment is here for future reference - this was a workaround for userMessageContent not getting set to true. It was due to it not recursively calling for partial blocks when didRejectTool, so it would get stuck waiting for a partial block to complete before it could continue.
// in case the content blocks finished
@@ -3421,15 +3420,12 @@ export class Cline {
"error",
"Unexpected API Response: The language model did not provide any assistant messages. This may indicate an issue with the API or the model's output.",
)
await this.addToApiConversationHistory({
this.apiConversationHistory.push({
role: "assistant",
content: [
{
type: "text",
text: "Failure: I did not provide a response.",
},
],
content: [{ type: "text", text: "Failure: I did not provide a response." }],
})
await saveApiConversationHistory(globalStoragePath, taskId, this.apiConversationHistory)
}
return didEndLoop // will always be false for now
+60
View File
@@ -0,0 +1,60 @@
import fs from "fs/promises"
import path from "path"
import { GlobalFileNames } from "../global-constants"
import Anthropic from "@anthropic-ai/sdk"
import { fileExistsAtPath } from "../utils/fs"
import { ClineMessage } from "../shared/ExtensionMessage"
export async function ensureTaskDirectoryExists(globalStoragePath: string | undefined, taskId: string): Promise<string> {
if (!globalStoragePath) {
throw new Error("Global storage uri is invalid")
}
const taskDir = path.join(globalStoragePath, "tasks", taskId)
await fs.mkdir(taskDir, { recursive: true })
return taskDir
}
export async function saveApiConversationHistory(
globalStoragePath: string | undefined,
taskId: string,
apiConversationHistory: Anthropic.MessageParam[],
) {
try {
const filePath = path.join(
await ensureTaskDirectoryExists(globalStoragePath, taskId),
GlobalFileNames.apiConversationHistory,
)
await fs.writeFile(filePath, JSON.stringify(apiConversationHistory))
} catch (error) {
// in the off chance this fails, we don't want to stop the task
console.error("Failed to save API conversation history:", error)
}
}
export async function getSavedApiConversationHistory(
globalStoragePath: string | undefined,
taskId: string,
): Promise<Anthropic.MessageParam[]> {
const filePath = path.join(await ensureTaskDirectoryExists(globalStoragePath, taskId), GlobalFileNames.apiConversationHistory)
const fileExists = await fileExistsAtPath(filePath)
if (fileExists) {
return JSON.parse(await fs.readFile(filePath, "utf8"))
}
return []
}
export async function getSavedClineMessages(globalStoragePath: string | undefined, taskId: string): Promise<ClineMessage[]> {
const filePath = path.join(await ensureTaskDirectoryExists(globalStoragePath, taskId), GlobalFileNames.uiMessages)
if (await fileExistsAtPath(filePath)) {
return JSON.parse(await fs.readFile(filePath, "utf8"))
} else {
// check old location
const oldPath = path.join(await ensureTaskDirectoryExists(globalStoragePath, taskId), "claude_messages.json")
if (await fileExistsAtPath(oldPath)) {
const data = JSON.parse(await fs.readFile(oldPath, "utf8"))
await fs.unlink(oldPath) // remove old file
return data
}
}
return []
}