Refactor claudeMessages

This commit is contained in:
Saoud Rizwan
2024-10-06 05:06:19 -04:00
parent 7612e50a5f
commit 09001fa72a
7 changed files with 86 additions and 124 deletions

View File

@@ -59,7 +59,7 @@ export class Cline {
customInstructions?: string customInstructions?: string
alwaysAllowReadOnly: boolean alwaysAllowReadOnly: boolean
apiConversationHistory: Anthropic.MessageParam[] = [] apiConversationHistory: Anthropic.MessageParam[] = []
claudeMessages: ClineMessage[] = [] clineMessages: ClineMessage[] = []
private askResponse?: ClineAskResponse private askResponse?: ClineAskResponse
private askResponseText?: string private askResponseText?: string
private askResponseImages?: string[] private askResponseImages?: string[]
@@ -149,7 +149,7 @@ export class Cline {
} }
} }
private async getSavedClaudeMessages(): Promise<ClineMessage[]> { private async getSavedClineMessages(): Promise<ClineMessage[]> {
const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.uiMessages) const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.uiMessages)
if (await fileExistsAtPath(filePath)) { if (await fileExistsAtPath(filePath)) {
return JSON.parse(await fs.readFile(filePath, "utf8")) return JSON.parse(await fs.readFile(filePath, "utf8"))
@@ -165,27 +165,27 @@ export class Cline {
return [] return []
} }
private async addToClaudeMessages(message: ClineMessage) { private async addToClineMessages(message: ClineMessage) {
this.claudeMessages.push(message) this.clineMessages.push(message)
await this.saveClaudeMessages() await this.saveClineMessages()
} }
private async overwriteClaudeMessages(newMessages: ClineMessage[]) { private async overwriteClineMessages(newMessages: ClineMessage[]) {
this.claudeMessages = newMessages this.clineMessages = newMessages
await this.saveClaudeMessages() await this.saveClineMessages()
} }
private async saveClaudeMessages() { private async saveClineMessages() {
try { try {
const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.uiMessages) const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.uiMessages)
await fs.writeFile(filePath, JSON.stringify(this.claudeMessages)) await fs.writeFile(filePath, JSON.stringify(this.clineMessages))
// combined as they are in ChatView // combined as they are in ChatView
const apiMetrics = getApiMetrics(combineApiRequests(combineCommandSequences(this.claudeMessages.slice(1)))) const apiMetrics = getApiMetrics(combineApiRequests(combineCommandSequences(this.clineMessages.slice(1))))
const taskMessage = this.claudeMessages[0] // first message is always the task say const taskMessage = this.clineMessages[0] // first message is always the task say
const lastRelevantMessage = const lastRelevantMessage =
this.claudeMessages[ this.clineMessages[
findLastIndex( findLastIndex(
this.claudeMessages, this.clineMessages,
(m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task") (m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task")
) )
] ]
@@ -200,7 +200,7 @@ export class Cline {
totalCost: apiMetrics.totalCost, totalCost: apiMetrics.totalCost,
}) })
} catch (error) { } catch (error) {
console.error("Failed to save claude messages:", error) console.error("Failed to save cline messages:", error)
} }
} }
@@ -218,7 +218,7 @@ export class Cline {
} }
let askTs: number let askTs: number
if (partial !== undefined) { if (partial !== undefined) {
const lastMessage = this.claudeMessages.at(-1) const lastMessage = this.clineMessages.at(-1)
const isUpdatingPreviousPartial = const isUpdatingPreviousPartial =
lastMessage && lastMessage.partial && lastMessage.type === "ask" && lastMessage.ask === type lastMessage && lastMessage.partial && lastMessage.type === "ask" && lastMessage.ask === type
if (partial) { if (partial) {
@@ -227,7 +227,7 @@ export class Cline {
lastMessage.text = text lastMessage.text = text
lastMessage.partial = partial lastMessage.partial = partial
// todo be more efficient about saving and posting only new data or one whole message at a time so ignore partial for saves, and only post parts of partial message instead of whole array in new listener // todo be more efficient about saving and posting only new data or one whole message at a time so ignore partial for saves, and only post parts of partial message instead of whole array in new listener
// await this.saveClaudeMessages() // await this.saveClineMessages()
// await this.providerRef.deref()?.postStateToWebview() // await this.providerRef.deref()?.postStateToWebview()
await this.providerRef await this.providerRef
.deref() .deref()
@@ -240,7 +240,7 @@ export class Cline {
// this.askResponseImages = undefined // this.askResponseImages = undefined
askTs = Date.now() askTs = Date.now()
this.lastMessageTs = askTs this.lastMessageTs = askTs
await this.addToClaudeMessages({ ts: askTs, type: "ask", ask: type, text, partial }) await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text, partial })
await this.providerRef.deref()?.postStateToWebview() await this.providerRef.deref()?.postStateToWebview()
throw new Error("Current ask promise was ignored 2") throw new Error("Current ask promise was ignored 2")
} }
@@ -263,7 +263,7 @@ export class Cline {
// lastMessage.ts = askTs // lastMessage.ts = askTs
lastMessage.text = text lastMessage.text = text
lastMessage.partial = false lastMessage.partial = false
await this.saveClaudeMessages() await this.saveClineMessages()
// await this.providerRef.deref()?.postStateToWebview() // await this.providerRef.deref()?.postStateToWebview()
await this.providerRef await this.providerRef
.deref() .deref()
@@ -275,19 +275,19 @@ export class Cline {
this.askResponseImages = undefined this.askResponseImages = undefined
askTs = Date.now() askTs = Date.now()
this.lastMessageTs = askTs this.lastMessageTs = askTs
await this.addToClaudeMessages({ ts: askTs, type: "ask", ask: type, text }) await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text })
await this.providerRef.deref()?.postStateToWebview() await this.providerRef.deref()?.postStateToWebview()
} }
} }
} else { } else {
// this is a new non-partial message, so add it like normal // this is a new non-partial message, so add it like normal
// const lastMessage = this.claudeMessages.at(-1) // const lastMessage = this.clineMessages.at(-1)
this.askResponse = undefined this.askResponse = undefined
this.askResponseText = undefined this.askResponseText = undefined
this.askResponseImages = undefined this.askResponseImages = undefined
askTs = Date.now() askTs = Date.now()
this.lastMessageTs = askTs this.lastMessageTs = askTs
await this.addToClaudeMessages({ ts: askTs, type: "ask", ask: type, text }) await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text })
await this.providerRef.deref()?.postStateToWebview() await this.providerRef.deref()?.postStateToWebview()
} }
@@ -314,7 +314,7 @@ export class Cline {
} }
if (partial !== undefined) { if (partial !== undefined) {
const lastMessage = this.claudeMessages.at(-1) const lastMessage = this.clineMessages.at(-1)
const isUpdatingPreviousPartial = const isUpdatingPreviousPartial =
lastMessage && lastMessage.partial && lastMessage.type === "say" && lastMessage.say === type lastMessage && lastMessage.partial && lastMessage.type === "say" && lastMessage.say === type
if (partial) { if (partial) {
@@ -330,7 +330,7 @@ export class Cline {
// this is a new partial message, so add it with partial state // this is a new partial message, so add it with partial state
const sayTs = Date.now() const sayTs = Date.now()
this.lastMessageTs = sayTs this.lastMessageTs = sayTs
await this.addToClaudeMessages({ ts: sayTs, type: "say", say: type, text, images, partial }) await this.addToClineMessages({ ts: sayTs, type: "say", say: type, text, images, partial })
await this.providerRef.deref()?.postStateToWebview() await this.providerRef.deref()?.postStateToWebview()
} }
} else { } else {
@@ -344,7 +344,7 @@ export class Cline {
lastMessage.partial = false lastMessage.partial = false
// instead of streaming partialMessage events, we do a save and post like normal to persist to disk // instead of streaming partialMessage events, we do a save and post like normal to persist to disk
await this.saveClaudeMessages() await this.saveClineMessages()
// await this.providerRef.deref()?.postStateToWebview() // await this.providerRef.deref()?.postStateToWebview()
await this.providerRef await this.providerRef
.deref() .deref()
@@ -353,7 +353,7 @@ export class Cline {
// this is a new partial=false message, so add it like normal // this is a new partial=false message, so add it like normal
const sayTs = Date.now() const sayTs = Date.now()
this.lastMessageTs = sayTs this.lastMessageTs = sayTs
await this.addToClaudeMessages({ ts: sayTs, type: "say", say: type, text, images }) await this.addToClineMessages({ ts: sayTs, type: "say", say: type, text, images })
await this.providerRef.deref()?.postStateToWebview() await this.providerRef.deref()?.postStateToWebview()
} }
} }
@@ -361,7 +361,7 @@ export class Cline {
// this is a new non-partial message, so add it like normal // this is a new non-partial message, so add it like normal
const sayTs = Date.now() const sayTs = Date.now()
this.lastMessageTs = sayTs this.lastMessageTs = sayTs
await this.addToClaudeMessages({ ts: sayTs, type: "say", say: type, text, images }) await this.addToClineMessages({ ts: sayTs, type: "say", say: type, text, images })
await this.providerRef.deref()?.postStateToWebview() await this.providerRef.deref()?.postStateToWebview()
} }
} }
@@ -369,7 +369,7 @@ export class Cline {
async sayAndCreateMissingParamError(toolName: ToolUseName, paramName: string, relPath?: string) { async sayAndCreateMissingParamError(toolName: ToolUseName, paramName: string, relPath?: string) {
await this.say( await this.say(
"error", "error",
`Claude tried to use ${toolName}${ `Cline tried to use ${toolName}${
relPath ? ` for '${relPath.toPosix()}'` : "" relPath ? ` for '${relPath.toPosix()}'` : ""
} without value for required parameter '${paramName}'. Retrying...` } without value for required parameter '${paramName}'. Retrying...`
) )
@@ -379,9 +379,9 @@ export class Cline {
// Task lifecycle // Task lifecycle
private async startTask(task?: string, images?: string[]): Promise<void> { private async startTask(task?: string, images?: string[]): Promise<void> {
// conversationHistory (for API) and claudeMessages (for webview) need to be in sync // conversationHistory (for API) and clineMessages (for webview) need to be in sync
// if the extension process were killed, then on restart the claudeMessages might not be empty, so we need to set it to [] when we create a new Cline client (otherwise webview would show stale messages from previous session) // if the extension process were killed, then on restart the clineMessages might not be empty, so we need to set it to [] when we create a new Cline client (otherwise webview would show stale messages from previous session)
this.claudeMessages = [] this.clineMessages = []
this.apiConversationHistory = [] this.apiConversationHistory = []
await this.providerRef.deref()?.postStateToWebview() await this.providerRef.deref()?.postStateToWebview()
@@ -398,52 +398,52 @@ export class Cline {
} }
private async resumeTaskFromHistory() { private async resumeTaskFromHistory() {
const modifiedClaudeMessages = await this.getSavedClaudeMessages() const modifiedClineMessages = await this.getSavedClineMessages()
// Remove any resume messages that may have been added before // Remove any resume messages that may have been added before
const lastRelevantMessageIndex = findLastIndex( const lastRelevantMessageIndex = findLastIndex(
modifiedClaudeMessages, modifiedClineMessages,
(m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task") (m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task")
) )
if (lastRelevantMessageIndex !== -1) { if (lastRelevantMessageIndex !== -1) {
modifiedClaudeMessages.splice(lastRelevantMessageIndex + 1) modifiedClineMessages.splice(lastRelevantMessageIndex + 1)
} }
// since we don't use api_req_finished anymore, we need to check if the last api_req_started has a cost value, if it doesn't and no cancellation reason to present, then we remove it since it indicates an api request without any partial content streamed // since we don't use api_req_finished anymore, we need to check if the last api_req_started has a cost value, if it doesn't and no cancellation reason to present, then we remove it since it indicates an api request without any partial content streamed
const lastApiReqStartedIndex = findLastIndex( const lastApiReqStartedIndex = findLastIndex(
modifiedClaudeMessages, modifiedClineMessages,
(m) => m.type === "say" && m.say === "api_req_started" (m) => m.type === "say" && m.say === "api_req_started"
) )
if (lastApiReqStartedIndex !== -1) { if (lastApiReqStartedIndex !== -1) {
const lastApiReqStarted = modifiedClaudeMessages[lastApiReqStartedIndex] const lastApiReqStarted = modifiedClineMessages[lastApiReqStartedIndex]
const { cost, cancelReason }: ClineApiReqInfo = JSON.parse(lastApiReqStarted.text || "{}") const { cost, cancelReason }: ClineApiReqInfo = JSON.parse(lastApiReqStarted.text || "{}")
if (cost === undefined && cancelReason === undefined) { if (cost === undefined && cancelReason === undefined) {
modifiedClaudeMessages.splice(lastApiReqStartedIndex, 1) modifiedClineMessages.splice(lastApiReqStartedIndex, 1)
} }
} }
await this.overwriteClaudeMessages(modifiedClaudeMessages) await this.overwriteClineMessages(modifiedClineMessages)
this.claudeMessages = await this.getSavedClaudeMessages() this.clineMessages = await this.getSavedClineMessages()
// Now present the claude messages to the user and ask if they want to resume // Now present the cline messages to the user and ask if they want to resume
const lastClaudeMessage = this.claudeMessages const lastClineMessage = this.clineMessages
.slice() .slice()
.reverse() .reverse()
.find((m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task")) // could be multiple resume tasks .find((m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task")) // could be multiple resume tasks
// const lastClaudeMessage = this.claudeMessages[lastClaudeMessageIndex] // const lastClineMessage = this.clineMessages[lastClineMessageIndex]
// could be a completion result with a command // could be a completion result with a command
// const secondLastClaudeMessage = this.claudeMessages // const secondLastClineMessage = this.clineMessages
// .slice() // .slice()
// .reverse() // .reverse()
// .find( // .find(
// (m, index) => // (m, index) =>
// index !== lastClaudeMessageIndex && !(m.ask === "resume_task" || m.ask === "resume_completed_task") // index !== lastClineMessageIndex && !(m.ask === "resume_task" || m.ask === "resume_completed_task")
// ) // )
// (lastClaudeMessage?.ask === "command" && secondLastClaudeMessage?.ask === "completion_result") // (lastClineMessage?.ask === "command" && secondLastClineMessage?.ask === "completion_result")
let askType: ClineAsk let askType: ClineAsk
if (lastClaudeMessage?.ask === "completion_result") { if (lastClineMessage?.ask === "completion_result") {
askType = "resume_completed_task" askType = "resume_completed_task"
} else { } else {
askType = "resume_task" askType = "resume_task"
@@ -458,7 +458,7 @@ export class Cline {
responseImages = images responseImages = images
} }
// need to make sure that the api conversation history can be resumed by the api, even if it goes out of sync with claude messages // 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
// 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 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 // if there's no tool use and only a text block, then we can just add a user message
@@ -546,7 +546,7 @@ export class Cline {
let newUserContent: UserContent = [...modifiedOldUserContent] let newUserContent: UserContent = [...modifiedOldUserContent]
const agoText = (() => { const agoText = (() => {
const timestamp = lastClaudeMessage?.ts ?? Date.now() const timestamp = lastClineMessage?.ts ?? Date.now()
const now = Date.now() const now = Date.now()
const diff = now - timestamp const diff = now - timestamp
const minutes = Math.floor(diff / 60000) const minutes = Math.floor(diff / 60000)
@@ -586,11 +586,11 @@ export class Cline {
let nextUserContent = userContent let nextUserContent = userContent
let includeFileDetails = true let includeFileDetails = true
while (!this.abort) { while (!this.abort) {
const didEndLoop = await this.recursivelyMakeClaudeRequests(nextUserContent, includeFileDetails) const didEndLoop = await this.recursivelyMakeClineRequests(nextUserContent, includeFileDetails)
includeFileDetails = false // we only need file details the first time includeFileDetails = false // we only need file details the first time
// The way this agentic loop works is that claude will be given a task that he then calls tools to complete. unless there's an attempt_completion call, we keep responding back to him with his tool's responses until he either attempt_completion or does not use anymore tools. If he does not use anymore tools, we ask him to consider if he's completed the task and then call attempt_completion, otherwise proceed with completing the task. // The way this agentic loop works is that cline will be given a task that he then calls tools to complete. unless there's an attempt_completion call, we keep responding back to him with his tool's responses until he either attempt_completion or does not use anymore tools. If he does not use anymore tools, we ask him to consider if he's completed the task and then call attempt_completion, otherwise proceed with completing the task.
// There is a MAX_REQUESTS_PER_TASK limit to prevent infinite requests, but Claude is prompted to finish the task as efficiently as he can. // There is a MAX_REQUESTS_PER_TASK limit to prevent infinite requests, but Cline is prompted to finish the task as efficiently as he can.
//const totalCost = this.calculateApiCost(totalInputTokens, totalOutputTokens) //const totalCost = this.calculateApiCost(totalInputTokens, totalOutputTokens)
if (didEndLoop) { if (didEndLoop) {
@@ -600,7 +600,7 @@ export class Cline {
} else { } else {
// this.say( // this.say(
// "tool", // "tool",
// "Claude responded with only text blocks but has not called attempt_completion yet. Forcing him to continue with task..." // "Cline responded with only text blocks but has not called attempt_completion yet. Forcing him to continue with task..."
// ) // )
nextUserContent = [ nextUserContent = [
{ {
@@ -708,7 +708,7 @@ export class Cline {
// If the previous API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request // If the previous API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request
if (previousApiReqIndex >= 0) { if (previousApiReqIndex >= 0) {
const previousRequest = this.claudeMessages[previousApiReqIndex] const previousRequest = this.clineMessages[previousApiReqIndex]
if (previousRequest && previousRequest.text) { if (previousRequest && previousRequest.text) {
const { tokensIn, tokensOut, cacheWrites, cacheReads }: ClineApiReqInfo = JSON.parse( const { tokensIn, tokensOut, cacheWrites, cacheReads }: ClineApiReqInfo = JSON.parse(
previousRequest.text previousRequest.text
@@ -1395,7 +1395,7 @@ export class Cline {
let resultToSend = result let resultToSend = result
if (command) { if (command) {
await this.say("completion_result", resultToSend) await this.say("completion_result", resultToSend)
// TODO: currently we don't handle if this command fails, it could be useful to let claude know and retry // TODO: currently we don't handle if this command fails, it could be useful to let cline know and retry
const [didUserReject, commandResult] = await this.executeCommand(command, true) const [didUserReject, commandResult] = await this.executeCommand(command, true)
// if we received non-empty string, the command was rejected or failed // if we received non-empty string, the command was rejected or failed
if (commandResult) { if (commandResult) {
@@ -1413,13 +1413,13 @@ export class Cline {
const result: string | undefined = block.params.result const result: string | undefined = block.params.result
const command: string | undefined = block.params.command const command: string | undefined = block.params.command
try { try {
const lastMessage = this.claudeMessages.at(-1) const lastMessage = this.clineMessages.at(-1)
if (block.partial) { if (block.partial) {
if (command) { if (command) {
// the attempt_completion text is done, now we're getting command // the attempt_completion text is done, now we're getting command
// remove the previous partial attempt_completion ask, replace with say, post state to webview, then stream command // remove the previous partial attempt_completion ask, replace with say, post state to webview, then stream command
// const secondLastMessage = this.claudeMessages.at(-2) // const secondLastMessage = this.clineMessages.at(-2)
if (lastMessage && lastMessage.ask === "command") { if (lastMessage && lastMessage.ask === "command") {
// update command // update command
await this.ask( await this.ask(
@@ -1555,7 +1555,7 @@ export class Cline {
} }
} }
async recursivelyMakeClaudeRequests( async recursivelyMakeClineRequests(
userContent: UserContent, userContent: UserContent,
includeFileDetails: boolean = false includeFileDetails: boolean = false
): Promise<boolean> { ): Promise<boolean> {
@@ -1585,7 +1585,7 @@ export class Cline {
} }
// get previous api req's index to check token usage and determine if we need to truncate conversation history // get previous api req's index to check token usage and determine if we need to truncate conversation history
const previousApiReqIndex = findLastIndex(this.claudeMessages, (m) => m.say === "api_req_started") const previousApiReqIndex = findLastIndex(this.clineMessages, (m) => m.say === "api_req_started")
// getting verbose details is an expensive operation, it uses globby to top-down build file structure of project which for large projects can take a few seconds // getting verbose details is an expensive operation, it uses globby to top-down build file structure of project which for large projects can take a few seconds
// for the best UX we show a placeholder api_req_started message with a loading spinner as this happens // for the best UX we show a placeholder api_req_started message with a loading spinner as this happens
@@ -1605,11 +1605,11 @@ export class Cline {
await this.addToApiConversationHistory({ role: "user", content: userContent }) await this.addToApiConversationHistory({ role: "user", content: userContent })
// since we sent off a placeholder api_req_started message to update the webview while waiting to actually start the API request (to load potential details for example), we need to update the text of that message // since we sent off a placeholder api_req_started message to update the webview while waiting to actually start the API request (to load potential details for example), we need to update the text of that message
const lastApiReqIndex = findLastIndex(this.claudeMessages, (m) => m.say === "api_req_started") const lastApiReqIndex = findLastIndex(this.clineMessages, (m) => m.say === "api_req_started")
this.claudeMessages[lastApiReqIndex].text = JSON.stringify({ this.clineMessages[lastApiReqIndex].text = JSON.stringify({
request: userContent.map((block) => formatContentBlockToMarkdown(block)).join("\n\n"), request: userContent.map((block) => formatContentBlockToMarkdown(block)).join("\n\n"),
} satisfies ClineApiReqInfo) } satisfies ClineApiReqInfo)
await this.saveClaudeMessages() await this.saveClineMessages()
await this.providerRef.deref()?.postStateToWebview() await this.providerRef.deref()?.postStateToWebview()
try { try {
@@ -1623,8 +1623,8 @@ export class Cline {
// fortunately api_req_finished was always parsed out for the gui anyways, so it remains solely for legacy purposes to keep track of prices in tasks from history // fortunately api_req_finished was always parsed out for the gui anyways, so it remains solely for legacy purposes to keep track of prices in tasks from history
// (it's worth removing a few months from now) // (it's worth removing a few months from now)
const updateApiReqMsg = (cancelReason?: ClineApiReqCancelReason, streamingFailedMessage?: string) => { const updateApiReqMsg = (cancelReason?: ClineApiReqCancelReason, streamingFailedMessage?: string) => {
this.claudeMessages[lastApiReqIndex].text = JSON.stringify({ this.clineMessages[lastApiReqIndex].text = JSON.stringify({
...JSON.parse(this.claudeMessages[lastApiReqIndex].text || "{}"), ...JSON.parse(this.clineMessages[lastApiReqIndex].text || "{}"),
tokensIn: inputTokens, tokensIn: inputTokens,
tokensOut: outputTokens, tokensOut: outputTokens,
cacheWrites: cacheWriteTokens, cacheWrites: cacheWriteTokens,
@@ -1649,13 +1649,13 @@ export class Cline {
} }
// if last message is a partial we need to update and save it // if last message is a partial we need to update and save it
const lastMessage = this.claudeMessages.at(-1) const lastMessage = this.clineMessages.at(-1)
if (lastMessage && lastMessage.partial) { if (lastMessage && lastMessage.partial) {
// lastMessage.ts = Date.now() DO NOT update ts since it is used as a key for virtuoso list // lastMessage.ts = Date.now() DO NOT update ts since it is used as a key for virtuoso list
lastMessage.partial = false lastMessage.partial = false
// instead of streaming partialMessage events, we do a save and post like normal to persist to disk // instead of streaming partialMessage events, we do a save and post like normal to persist to disk
console.log("updating partial message", lastMessage) console.log("updating partial message", lastMessage)
// await this.saveClaudeMessages() // await this.saveClineMessages()
} }
// Let assistant know their response was interrupted for when task is resumed // Let assistant know their response was interrupted for when task is resumed
@@ -1677,7 +1677,7 @@ export class Cline {
// update api_req_started to have cancelled and cost, so that we can display the cost of the partial stream // update api_req_started to have cancelled and cost, so that we can display the cost of the partial stream
updateApiReqMsg(cancelReason, streamingFailedMessage) updateApiReqMsg(cancelReason, streamingFailedMessage)
await this.saveClaudeMessages() await this.saveClineMessages()
// signals to provider that it can retrieve the saved messages from disk, as abortTask can not be awaited on in nature // signals to provider that it can retrieve the saved messages from disk, as abortTask can not be awaited on in nature
this.didFinishAborting = true this.didFinishAborting = true
@@ -1761,7 +1761,7 @@ export class Cline {
} }
updateApiReqMsg() updateApiReqMsg()
await this.saveClaudeMessages() await this.saveClineMessages()
await this.providerRef.deref()?.postStateToWebview() await this.providerRef.deref()?.postStateToWebview()
// now add to apiconversationhistory // now add to apiconversationhistory
@@ -1793,7 +1793,7 @@ export class Cline {
this.consecutiveMistakeCount++ this.consecutiveMistakeCount++
} }
const recDidEndLoop = await this.recursivelyMakeClaudeRequests(this.userMessageContent) const recDidEndLoop = await this.recursivelyMakeClineRequests(this.userMessageContent)
didEndLoop = recDidEndLoop didEndLoop = recDidEndLoop
} else { } else {
// if there's no assistant_responses, that means we got no text or tool_use content blocks from API which we should assume is an error // if there's no assistant_responses, that means we got no text or tool_use content blocks from API which we should assume is an error
@@ -1863,7 +1863,7 @@ export class Cline {
async getEnvironmentDetails(includeFileDetails: boolean = false) { async getEnvironmentDetails(includeFileDetails: boolean = false) {
let details = "" let details = ""
// It could be useful for claude to know if the user went from one or no file to another between messages, so we always include this context // It could be useful for cline to know if the user went from one or no file to another between messages, so we always include this context
details += "\n\n# VSCode Visible Files" details += "\n\n# VSCode Visible Files"
const visibleFiles = vscode.window.visibleTextEditors const visibleFiles = vscode.window.visibleTextEditors
?.map((editor) => editor.document?.uri?.fsPath) ?.map((editor) => editor.document?.uri?.fsPath)
@@ -1911,7 +1911,7 @@ export class Cline {
// we want to get diagnostics AFTER terminal cools down for a few reasons: terminal could be scaffolding a project, dev servers (compilers like webpack) will first re-compile and then send diagnostics, etc // we want to get diagnostics AFTER terminal cools down for a few reasons: terminal could be scaffolding a project, dev servers (compilers like webpack) will first re-compile and then send diagnostics, etc
/* /*
let diagnosticsDetails = "" let diagnosticsDetails = ""
const diagnostics = await this.diagnosticsMonitor.getCurrentDiagnostics(this.didEditFile || terminalWasBusy) // if claude ran a command (ie npm install) or edited the workspace then wait a bit for updated diagnostics const diagnostics = await this.diagnosticsMonitor.getCurrentDiagnostics(this.didEditFile || terminalWasBusy) // if cline ran a command (ie npm install) or edited the workspace then wait a bit for updated diagnostics
for (const [uri, fileDiagnostics] of diagnostics) { for (const [uri, fileDiagnostics] of diagnostics) {
const problems = fileDiagnostics.filter((d) => d.severity === vscode.DiagnosticSeverity.Error) const problems = fileDiagnostics.filter((d) => d.severity === vscode.DiagnosticSeverity.Error)
if (problems.length > 0) { if (problems.length > 0) {

View File

@@ -180,9 +180,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// if the extension is starting a new session, clear previous task state // if the extension is starting a new session, clear previous task state
this.clearTask() this.clearTask()
// Clear previous version's (0.0.6) claudeMessage cache from workspace state. We now store in global state with a unique identifier for each provider instance. We need to store globally rather than per workspace to eventually implement task history
this.updateWorkspaceState("claudeMessages", undefined)
this.outputChannel.appendLine("Webview view resolved") this.outputChannel.appendLine("Webview view resolved")
} }
@@ -719,7 +716,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
customInstructions, customInstructions,
alwaysAllowReadOnly, alwaysAllowReadOnly,
uriScheme: vscode.env.uriScheme, uriScheme: vscode.env.uriScheme,
claudeMessages: this.cline?.claudeMessages || [], clineMessages: this.cline?.clineMessages || [],
taskHistory: (taskHistory || []).filter((item) => item.ts && item.task).sort((a, b) => b.ts - a.ts), taskHistory: (taskHistory || []).filter((item) => item.ts && item.task).sort((a, b) => b.ts - a.ts),
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId, shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
} }
@@ -740,41 +737,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
We need to use a unique identifier for each ClineProvider instance's message cache since we could be running several instances of the extension outside of just the sidebar i.e. in editor panels. We need to use a unique identifier for each ClineProvider instance's message cache since we could be running several instances of the extension outside of just the sidebar i.e. in editor panels.
For now since we don't need to store task history, we'll just use an identifier unique to this provider instance (since there can be several provider instances open at once).
However in the future when we implement task history, we'll need to use a unique identifier for each task. As well as manage a data structure that keeps track of task history with their associated identifiers and the task message itself, to present in a 'Task History' view.
Task history is a significant undertaking as it would require refactoring how we wait for ask responses--it would need to be a hidden claudeMessage, so that user's can resume tasks that ended with an ask.
*/
// private providerInstanceIdentifier = Date.now()
// getClaudeMessagesStateKey() {
// return `claudeMessages-${this.providerInstanceIdentifier}`
// }
// getApiConversationHistoryStateKey() {
// return `apiConversationHistory-${this.providerInstanceIdentifier}`
// }
// claude messages to present in the webview
// getClaudeMessages(): ClaudeMessage[] {
// // const messages = (await this.getGlobalState(this.getClaudeMessagesStateKey())) as ClaudeMessage[]
// // return messages || []
// return this.claudeMessages
// }
// setClaudeMessages(messages: ClaudeMessage[] | undefined) {
// // await this.updateGlobalState(this.getClaudeMessagesStateKey(), messages)
// this.claudeMessages = messages || []
// }
// addClaudeMessage(message: ClaudeMessage): ClaudeMessage[] {
// // const messages = await this.getClaudeMessages()
// // messages.push(message)
// // await this.setClaudeMessages(messages)
// // return messages
// this.claudeMessages.push(message)
// return this.claudeMessages
// }
// conversation history to send in API requests // conversation history to send in API requests
/* /*

View File

@@ -32,7 +32,7 @@ export interface ExtensionState {
customInstructions?: string customInstructions?: string
alwaysAllowReadOnly?: boolean alwaysAllowReadOnly?: boolean
uriScheme?: string uriScheme?: string
claudeMessages: ClineMessage[] clineMessages: ClineMessage[]
taskHistory: HistoryItem[] taskHistory: HistoryItem[]
shouldShowAnnouncement: boolean shouldShowAnnouncement: boolean
} }

View File

@@ -1,14 +1,14 @@
import { ClineMessage } from "./ExtensionMessage" import { ClineMessage } from "./ExtensionMessage"
/** /**
* Combines API request start and finish messages in an array of ClaudeMessages. * Combines API request start and finish messages in an array of ClineMessages.
* *
* This function looks for pairs of 'api_req_started' and 'api_req_finished' messages. * This function looks for pairs of 'api_req_started' and 'api_req_finished' messages.
* When it finds a pair, it combines them into a single 'api_req_combined' message. * When it finds a pair, it combines them into a single 'api_req_combined' message.
* The JSON data in the text fields of both messages are merged. * The JSON data in the text fields of both messages are merged.
* *
* @param messages - An array of ClaudeMessage objects to process. * @param messages - An array of ClineMessage objects to process.
* @returns A new array of ClaudeMessage objects with API requests combined. * @returns A new array of ClineMessage objects with API requests combined.
* *
* @example * @example
* const messages = [ * const messages = [

View File

@@ -1,18 +1,18 @@
import { ClineMessage } from "./ExtensionMessage" import { ClineMessage } from "./ExtensionMessage"
/** /**
* Combines sequences of command and command_output messages in an array of ClaudeMessages. * Combines sequences of command and command_output messages in an array of ClineMessages.
* *
* This function processes an array of ClaudeMessage objects, looking for sequences * This function processes an array of ClineMessages objects, looking for sequences
* where a 'command' message is followed by one or more 'command_output' messages. * where a 'command' message is followed by one or more 'command_output' messages.
* When such a sequence is found, it combines them into a single message, merging * When such a sequence is found, it combines them into a single message, merging
* their text contents. * their text contents.
* *
* @param messages - An array of ClaudeMessage objects to process. * @param messages - An array of ClineMessage objects to process.
* @returns A new array of ClaudeMessage objects with command sequences combined. * @returns A new array of ClineMessage objects with command sequences combined.
* *
* @example * @example
* const messages: ClaudeMessage[] = [ * const messages: ClineMessage[] = [
* { type: 'ask', ask: 'command', text: 'ls', ts: 1625097600000 }, * { type: 'ask', ask: 'command', text: 'ls', ts: 1625097600000 },
* { type: 'ask', ask: 'command_output', text: 'file1.txt', ts: 1625097601000 }, * { type: 'ask', ask: 'command_output', text: 'file1.txt', ts: 1625097601000 },
* { type: 'ask', ask: 'command_output', text: 'file2.txt', ts: 1625097602000 } * { type: 'ask', ask: 'command_output', text: 'file2.txt', ts: 1625097602000 }

View File

@@ -28,7 +28,7 @@ interface ChatViewProps {
export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryView }: ChatViewProps) => { const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryView }: ChatViewProps) => {
const { version, claudeMessages: messages, taskHistory, apiConfiguration } = useExtensionState() const { version, clineMessages: messages, taskHistory, apiConfiguration } = useExtensionState()
//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined //const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined
const task = useMemo(() => messages.at(0), [messages]) // leaving this less safe version here since if the first message is not a task, then the extension is in a bad state and needs to be debugged (see Cline.abort) const task = useMemo(() => messages.at(0), [messages]) // leaving this less safe version here since if the first message is not a task, then the extension is in a bad state and needs to be debugged (see Cline.abort)

View File

@@ -23,7 +23,7 @@ const ExtensionStateContext = createContext<ExtensionStateContextType | undefine
export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [state, setState] = useState<ExtensionState>({ const [state, setState] = useState<ExtensionState>({
version: "", version: "",
claudeMessages: [], clineMessages: [],
taskHistory: [], taskHistory: [],
shouldShowAnnouncement: false, shouldShowAnnouncement: false,
}) })
@@ -69,11 +69,11 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
const partialMessage = message.partialMessage! const partialMessage = message.partialMessage!
setState((prevState) => { setState((prevState) => {
// worth noting it will never be possible for a more up-to-date message to be sent here or in normal messages post since the presentAssistantContent function uses lock // worth noting it will never be possible for a more up-to-date message to be sent here or in normal messages post since the presentAssistantContent function uses lock
const lastIndex = findLastIndex(prevState.claudeMessages, (msg) => msg.ts === partialMessage.ts) const lastIndex = findLastIndex(prevState.clineMessages, (msg) => msg.ts === partialMessage.ts)
if (lastIndex !== -1) { if (lastIndex !== -1) {
const newClaudeMessages = [...prevState.claudeMessages] const newClineMessages = [...prevState.clineMessages]
newClaudeMessages[lastIndex] = partialMessage newClineMessages[lastIndex] = partialMessage
return { ...prevState, claudeMessages: newClaudeMessages } return { ...prevState, clineMessages: newClineMessages }
} }
return prevState return prevState
}) })