ClaudeDev.ts 75 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896
  1. import { Anthropic } from "@anthropic-ai/sdk"
  2. import cloneDeep from "clone-deep"
  3. import delay from "delay"
  4. import fs from "fs/promises"
  5. import os from "os"
  6. import pWaitFor from "p-wait-for"
  7. import * as path from "path"
  8. import { serializeError } from "serialize-error"
  9. import * as vscode from "vscode"
  10. import { ApiHandler, buildApiHandler } from "../api"
  11. import { ApiStream } from "../api/transform/stream"
  12. import { DiffViewProvider } from "../integrations/editor/DiffViewProvider"
  13. import { formatContentBlockToMarkdown } from "../integrations/misc/export-markdown"
  14. import { extractTextFromFile } from "../integrations/misc/extract-text"
  15. import { TerminalManager } from "../integrations/terminal/TerminalManager"
  16. import { UrlContentFetcher } from "../services/browser/UrlContentFetcher"
  17. import { listFiles } from "../services/glob/list-files"
  18. import { regexSearchFiles } from "../services/ripgrep"
  19. import { parseSourceCodeForDefinitionsTopLevel } from "../services/tree-sitter"
  20. import { ApiConfiguration } from "../shared/api"
  21. import { findLastIndex } from "../shared/array"
  22. import { combineApiRequests } from "../shared/combineApiRequests"
  23. import { combineCommandSequences } from "../shared/combineCommandSequences"
  24. import { ClaudeAsk, ClaudeMessage, ClaudeSay, ClaudeSayTool } from "../shared/ExtensionMessage"
  25. import { getApiMetrics } from "../shared/getApiMetrics"
  26. import { HistoryItem } from "../shared/HistoryItem"
  27. import { ToolName } from "../shared/Tool"
  28. import { ClaudeAskResponse } from "../shared/WebviewMessage"
  29. import { calculateApiCost } from "../utils/cost"
  30. import { fileExistsAtPath } from "../utils/fs"
  31. import { arePathsEqual, getReadablePath } from "../utils/path"
  32. import { parseMentions } from "./mentions"
  33. import {
  34. AssistantMessageContent,
  35. TextContent,
  36. ToolParamName,
  37. toolParamNames,
  38. ToolUse,
  39. ToolUseName,
  40. toolUseNames,
  41. } from "./prompts/AssistantMessage"
  42. import { formatResponse } from "./prompts/responses"
  43. import { addCustomInstructions, SYSTEM_PROMPT } from "./prompts/system"
  44. import { truncateHalfConversation } from "./sliding-window"
  45. import { ClaudeDevProvider, GlobalFileNames } from "./webview/ClaudeDevProvider"
  46. const cwd =
  47. 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
  48. type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam>
  49. type UserContent = Array<
  50. Anthropic.TextBlockParam | Anthropic.ImageBlockParam | Anthropic.ToolUseBlockParam | Anthropic.ToolResultBlockParam
  51. >
  52. export class ClaudeDev {
  53. readonly taskId: string
  54. api: ApiHandler
  55. private terminalManager: TerminalManager
  56. private urlContentFetcher: UrlContentFetcher
  57. private didEditFile: boolean = false
  58. customInstructions?: string
  59. alwaysAllowReadOnly: boolean
  60. apiConversationHistory: Anthropic.MessageParam[] = []
  61. claudeMessages: ClaudeMessage[] = []
  62. private askResponse?: ClaudeAskResponse
  63. private askResponseText?: string
  64. private askResponseImages?: string[]
  65. private lastMessageTs?: number
  66. private consecutiveMistakeCount: number = 0
  67. private providerRef: WeakRef<ClaudeDevProvider>
  68. private abort: boolean = false
  69. private diffViewProvider: DiffViewProvider
  70. // streaming
  71. private currentStreamingContentIndex = 0
  72. private assistantMessageContent: AssistantMessageContent[] = []
  73. private presentAssistantMessageLocked = false
  74. private presentAssistantMessageHasPendingUpdates = false
  75. private userMessageContent: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[] = []
  76. private userMessageContentReady = false
  77. private didRejectTool = false
  78. private didCompleteReadingStream = false
  79. constructor(
  80. provider: ClaudeDevProvider,
  81. apiConfiguration: ApiConfiguration,
  82. customInstructions?: string,
  83. alwaysAllowReadOnly?: boolean,
  84. task?: string,
  85. images?: string[],
  86. historyItem?: HistoryItem
  87. ) {
  88. this.providerRef = new WeakRef(provider)
  89. this.api = buildApiHandler(apiConfiguration)
  90. this.terminalManager = new TerminalManager()
  91. this.urlContentFetcher = new UrlContentFetcher(provider.context)
  92. this.diffViewProvider = new DiffViewProvider(cwd)
  93. this.customInstructions = customInstructions
  94. this.alwaysAllowReadOnly = alwaysAllowReadOnly ?? false
  95. if (historyItem) {
  96. this.taskId = historyItem.id
  97. this.resumeTaskFromHistory()
  98. } else if (task || images) {
  99. this.taskId = Date.now().toString()
  100. this.startTask(task, images)
  101. } else {
  102. throw new Error("Either historyItem or task/images must be provided")
  103. }
  104. }
  105. // Storing task to disk for history
  106. private async ensureTaskDirectoryExists(): Promise<string> {
  107. const globalStoragePath = this.providerRef.deref()?.context.globalStorageUri.fsPath
  108. if (!globalStoragePath) {
  109. throw new Error("Global storage uri is invalid")
  110. }
  111. const taskDir = path.join(globalStoragePath, "tasks", this.taskId)
  112. await fs.mkdir(taskDir, { recursive: true })
  113. return taskDir
  114. }
  115. private async getSavedApiConversationHistory(): Promise<Anthropic.MessageParam[]> {
  116. const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.apiConversationHistory)
  117. const fileExists = await fileExistsAtPath(filePath)
  118. if (fileExists) {
  119. return JSON.parse(await fs.readFile(filePath, "utf8"))
  120. }
  121. return []
  122. }
  123. private async addToApiConversationHistory(message: Anthropic.MessageParam) {
  124. this.apiConversationHistory.push(message)
  125. await this.saveApiConversationHistory()
  126. }
  127. private async overwriteApiConversationHistory(newHistory: Anthropic.MessageParam[]) {
  128. this.apiConversationHistory = newHistory
  129. await this.saveApiConversationHistory()
  130. }
  131. private async saveApiConversationHistory() {
  132. try {
  133. const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.apiConversationHistory)
  134. await fs.writeFile(filePath, JSON.stringify(this.apiConversationHistory))
  135. } catch (error) {
  136. // in the off chance this fails, we don't want to stop the task
  137. console.error("Failed to save API conversation history:", error)
  138. }
  139. }
  140. private async getSavedClaudeMessages(): Promise<ClaudeMessage[]> {
  141. const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.claudeMessages)
  142. const fileExists = await fileExistsAtPath(filePath)
  143. if (fileExists) {
  144. return JSON.parse(await fs.readFile(filePath, "utf8"))
  145. }
  146. return []
  147. }
  148. private async addToClaudeMessages(message: ClaudeMessage) {
  149. this.claudeMessages.push(message)
  150. await this.saveClaudeMessages()
  151. }
  152. private async overwriteClaudeMessages(newMessages: ClaudeMessage[]) {
  153. this.claudeMessages = newMessages
  154. await this.saveClaudeMessages()
  155. }
  156. private async saveClaudeMessages() {
  157. try {
  158. const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.claudeMessages)
  159. await fs.writeFile(filePath, JSON.stringify(this.claudeMessages))
  160. // combined as they are in ChatView
  161. const apiMetrics = getApiMetrics(combineApiRequests(combineCommandSequences(this.claudeMessages.slice(1))))
  162. const taskMessage = this.claudeMessages[0] // first message is always the task say
  163. const lastRelevantMessage =
  164. this.claudeMessages[
  165. findLastIndex(
  166. this.claudeMessages,
  167. (m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task")
  168. )
  169. ]
  170. await this.providerRef.deref()?.updateTaskHistory({
  171. id: this.taskId,
  172. ts: lastRelevantMessage.ts,
  173. task: taskMessage.text ?? "",
  174. tokensIn: apiMetrics.totalTokensIn,
  175. tokensOut: apiMetrics.totalTokensOut,
  176. cacheWrites: apiMetrics.totalCacheWrites,
  177. cacheReads: apiMetrics.totalCacheReads,
  178. totalCost: apiMetrics.totalCost,
  179. })
  180. } catch (error) {
  181. console.error("Failed to save claude messages:", error)
  182. }
  183. }
  184. // Communicate with webview
  185. // partial has three valid states true (partial message), false (completion of partial message), undefined (individual complete message)
  186. async ask(
  187. type: ClaudeAsk,
  188. text?: string,
  189. partial?: boolean
  190. ): Promise<{ response: ClaudeAskResponse; text?: string; images?: string[] }> {
  191. // If this ClaudeDev instance was aborted by the provider, then the only thing keeping us alive is a promise still running in the background, in which case we don't want to send its result to the webview as it is attached to a new instance of ClaudeDev now. So we can safely ignore the result of any active promises, and this class will be deallocated. (Although we set claudeDev = undefined in provider, that simply removes the reference to this instance, but the instance is still alive until this promise resolves or rejects.)
  192. if (this.abort) {
  193. throw new Error("ClaudeDev instance aborted")
  194. }
  195. let askTs: number
  196. if (partial !== undefined) {
  197. const lastMessage = this.claudeMessages.at(-1)
  198. const isUpdatingPreviousPartial =
  199. lastMessage && lastMessage.partial && lastMessage.type === "ask" && lastMessage.ask === type
  200. if (partial) {
  201. if (isUpdatingPreviousPartial) {
  202. // existing partial message, so update it
  203. lastMessage.text = text
  204. lastMessage.partial = partial
  205. // 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
  206. // await this.saveClaudeMessages()
  207. // await this.providerRef.deref()?.postStateToWebview()
  208. await this.providerRef
  209. .deref()
  210. ?.postMessageToWebview({ type: "partialMessage", partialMessage: lastMessage })
  211. throw new Error("Current ask promise was ignored 1")
  212. } else {
  213. // this is a new partial message, so add it with partial state
  214. // this.askResponse = undefined
  215. // this.askResponseText = undefined
  216. // this.askResponseImages = undefined
  217. // askTs = Date.now()
  218. // this.lastMessageTs = askTs
  219. await this.addToClaudeMessages({ ts: Date.now(), type: "ask", ask: type, text, partial })
  220. await this.providerRef.deref()?.postStateToWebview()
  221. throw new Error("Current ask promise was ignored 2")
  222. }
  223. } else {
  224. // partial=false means its a complete version of a previously partial message
  225. if (isUpdatingPreviousPartial) {
  226. // this is the complete version of a previously partial message, so replace the partial with the complete version
  227. this.askResponse = undefined
  228. this.askResponseText = undefined
  229. this.askResponseImages = undefined
  230. askTs = Date.now()
  231. this.lastMessageTs = askTs
  232. lastMessage.ts = askTs
  233. lastMessage.text = text
  234. lastMessage.partial = false
  235. await this.saveClaudeMessages()
  236. await this.providerRef.deref()?.postStateToWebview()
  237. } else {
  238. // this is a new partial=false message, so add it like normal
  239. this.askResponse = undefined
  240. this.askResponseText = undefined
  241. this.askResponseImages = undefined
  242. askTs = Date.now()
  243. this.lastMessageTs = askTs
  244. await this.addToClaudeMessages({ ts: askTs, type: "ask", ask: type, text })
  245. await this.providerRef.deref()?.postStateToWebview()
  246. }
  247. }
  248. } else {
  249. // this is a new non-partial message, so add it like normal
  250. // const lastMessage = this.claudeMessages.at(-1)
  251. this.askResponse = undefined
  252. this.askResponseText = undefined
  253. this.askResponseImages = undefined
  254. askTs = Date.now()
  255. this.lastMessageTs = askTs
  256. await this.addToClaudeMessages({ ts: askTs, type: "ask", ask: type, text })
  257. await this.providerRef.deref()?.postStateToWebview()
  258. }
  259. await pWaitFor(() => this.askResponse !== undefined || this.lastMessageTs !== askTs, { interval: 100 })
  260. if (this.lastMessageTs !== askTs) {
  261. throw new Error("Current ask promise was ignored") // could happen if we send multiple asks in a row i.e. with command_output. It's important that when we know an ask could fail, it is handled gracefully
  262. }
  263. const result = { response: this.askResponse!, text: this.askResponseText, images: this.askResponseImages }
  264. this.askResponse = undefined
  265. this.askResponseText = undefined
  266. this.askResponseImages = undefined
  267. return result
  268. }
  269. async handleWebviewAskResponse(askResponse: ClaudeAskResponse, text?: string, images?: string[]) {
  270. this.askResponse = askResponse
  271. this.askResponseText = text
  272. this.askResponseImages = images
  273. }
  274. async say(type: ClaudeSay, text?: string, images?: string[], partial?: boolean): Promise<undefined> {
  275. if (this.abort) {
  276. throw new Error("ClaudeDev instance aborted")
  277. }
  278. if (partial !== undefined) {
  279. const lastMessage = this.claudeMessages.at(-1)
  280. const isUpdatingPreviousPartial =
  281. lastMessage && lastMessage.partial && lastMessage.type === "say" && lastMessage.say === type
  282. if (partial) {
  283. if (isUpdatingPreviousPartial) {
  284. // existing partial message, so update it
  285. lastMessage.text = text
  286. lastMessage.images = images
  287. lastMessage.partial = partial
  288. await this.providerRef
  289. .deref()
  290. ?.postMessageToWebview({ type: "partialMessage", partialMessage: lastMessage })
  291. } else {
  292. // this is a new partial message, so add it with partial state
  293. await this.addToClaudeMessages({ ts: Date.now(), type: "say", say: type, text, images, partial })
  294. await this.providerRef.deref()?.postStateToWebview()
  295. }
  296. } else {
  297. // partial=false means its a complete version of a previously partial message
  298. if (isUpdatingPreviousPartial) {
  299. // this is the complete version of a previously partial message, so replace the partial with the complete version
  300. const sayTs = Date.now()
  301. this.lastMessageTs = sayTs
  302. lastMessage.ts = sayTs
  303. lastMessage.text = text
  304. lastMessage.images = images
  305. lastMessage.partial = false
  306. // instead of streaming partialMessage events, we do a save and post like normal to persist to disk
  307. await this.saveClaudeMessages()
  308. await this.providerRef.deref()?.postStateToWebview()
  309. } else {
  310. // this is a new partial=false message, so add it like normal
  311. const sayTs = Date.now()
  312. this.lastMessageTs = sayTs
  313. await this.addToClaudeMessages({ ts: sayTs, type: "say", say: type, text, images })
  314. await this.providerRef.deref()?.postStateToWebview()
  315. }
  316. }
  317. } else {
  318. // this is a new non-partial message, so add it like normal
  319. const sayTs = Date.now()
  320. this.lastMessageTs = sayTs
  321. await this.addToClaudeMessages({ ts: sayTs, type: "say", say: type, text, images })
  322. await this.providerRef.deref()?.postStateToWebview()
  323. }
  324. }
  325. async sayAndCreateMissingParamError(toolName: ToolName, paramName: string, relPath?: string) {
  326. await this.say(
  327. "error",
  328. `Claude tried to use ${toolName}${
  329. relPath ? ` for '${relPath.toPosix()}'` : ""
  330. } without value for required parameter '${paramName}'. Retrying...`
  331. )
  332. return formatResponse.toolError(formatResponse.missingToolParameterError(paramName))
  333. }
  334. // Task lifecycle
  335. private async startTask(task?: string, images?: string[]): Promise<void> {
  336. // conversationHistory (for API) and claudeMessages (for webview) need to be in sync
  337. // 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 ClaudeDev client (otherwise webview would show stale messages from previous session)
  338. this.claudeMessages = []
  339. this.apiConversationHistory = []
  340. await this.providerRef.deref()?.postStateToWebview()
  341. await this.say("text", task, images)
  342. let imageBlocks: Anthropic.ImageBlockParam[] = formatResponse.imageBlocks(images)
  343. await this.initiateTaskLoop([
  344. {
  345. type: "text",
  346. text: `<task>\n${task}\n</task>`,
  347. },
  348. ...imageBlocks,
  349. ])
  350. }
  351. private async resumeTaskFromHistory() {
  352. const modifiedClaudeMessages = await this.getSavedClaudeMessages()
  353. // Need to modify claude messages for good ux, i.e. if the last message is an api_request_started, then remove it otherwise the user will think the request is still loading
  354. const lastApiReqStartedIndex = modifiedClaudeMessages.reduce(
  355. (lastIndex, m, index) => (m.type === "say" && m.say === "api_req_started" ? index : lastIndex),
  356. -1
  357. )
  358. const lastApiReqFinishedIndex = modifiedClaudeMessages.reduce(
  359. (lastIndex, m, index) => (m.type === "say" && m.say === "api_req_finished" ? index : lastIndex),
  360. -1
  361. )
  362. if (lastApiReqStartedIndex > lastApiReqFinishedIndex && lastApiReqStartedIndex !== -1) {
  363. modifiedClaudeMessages.splice(lastApiReqStartedIndex, 1)
  364. }
  365. // Remove any resume messages that may have been added before
  366. const lastRelevantMessageIndex = findLastIndex(
  367. modifiedClaudeMessages,
  368. (m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task")
  369. )
  370. if (lastRelevantMessageIndex !== -1) {
  371. modifiedClaudeMessages.splice(lastRelevantMessageIndex + 1)
  372. }
  373. await this.overwriteClaudeMessages(modifiedClaudeMessages)
  374. this.claudeMessages = await this.getSavedClaudeMessages()
  375. // Now present the claude messages to the user and ask if they want to resume
  376. const lastClaudeMessage = this.claudeMessages
  377. .slice()
  378. .reverse()
  379. .find((m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task")) // could be multiple resume tasks
  380. // const lastClaudeMessage = this.claudeMessages[lastClaudeMessageIndex]
  381. // could be a completion result with a command
  382. // const secondLastClaudeMessage = this.claudeMessages
  383. // .slice()
  384. // .reverse()
  385. // .find(
  386. // (m, index) =>
  387. // index !== lastClaudeMessageIndex && !(m.ask === "resume_task" || m.ask === "resume_completed_task")
  388. // )
  389. // (lastClaudeMessage?.ask === "command" && secondLastClaudeMessage?.ask === "completion_result")
  390. let askType: ClaudeAsk
  391. if (lastClaudeMessage?.ask === "completion_result") {
  392. askType = "resume_completed_task"
  393. } else {
  394. askType = "resume_task"
  395. }
  396. const { response, text, images } = await this.ask(askType) // calls poststatetowebview
  397. let responseText: string | undefined
  398. let responseImages: string[] | undefined
  399. if (response === "messageResponse") {
  400. await this.say("user_feedback", text, images)
  401. responseText = text
  402. responseImages = images
  403. }
  404. // 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
  405. // 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
  406. // if there's no tool use and only a text block, then we can just add a user message
  407. // (note this isn't relevant anymore since we use custom tool prompts instead of tool use blocks, but this is here for legacy purposes in case users resume old tasks)
  408. // 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'
  409. const existingApiConversationHistory: Anthropic.Messages.MessageParam[] =
  410. await this.getSavedApiConversationHistory()
  411. let modifiedOldUserContent: UserContent // either the last message if its user message, or the user message before the last (assistant) message
  412. let modifiedApiConversationHistory: Anthropic.Messages.MessageParam[] // need to remove the last user message to replace with new modified user message
  413. if (existingApiConversationHistory.length > 0) {
  414. const lastMessage = existingApiConversationHistory[existingApiConversationHistory.length - 1]
  415. if (lastMessage.role === "assistant") {
  416. const content = Array.isArray(lastMessage.content)
  417. ? lastMessage.content
  418. : [{ type: "text", text: lastMessage.content }]
  419. const hasToolUse = content.some((block) => block.type === "tool_use")
  420. if (hasToolUse) {
  421. const toolUseBlocks = content.filter(
  422. (block) => block.type === "tool_use"
  423. ) as Anthropic.Messages.ToolUseBlock[]
  424. const toolResponses: Anthropic.ToolResultBlockParam[] = toolUseBlocks.map((block) => ({
  425. type: "tool_result",
  426. tool_use_id: block.id,
  427. content: "Task was interrupted before this tool call could be completed.",
  428. }))
  429. modifiedApiConversationHistory = [...existingApiConversationHistory] // no changes
  430. modifiedOldUserContent = [...toolResponses]
  431. } else {
  432. modifiedApiConversationHistory = [...existingApiConversationHistory]
  433. modifiedOldUserContent = []
  434. }
  435. } else if (lastMessage.role === "user") {
  436. const previousAssistantMessage: Anthropic.Messages.MessageParam | undefined =
  437. existingApiConversationHistory[existingApiConversationHistory.length - 2]
  438. const existingUserContent: UserContent = Array.isArray(lastMessage.content)
  439. ? lastMessage.content
  440. : [{ type: "text", text: lastMessage.content }]
  441. if (previousAssistantMessage && previousAssistantMessage.role === "assistant") {
  442. const assistantContent = Array.isArray(previousAssistantMessage.content)
  443. ? previousAssistantMessage.content
  444. : [{ type: "text", text: previousAssistantMessage.content }]
  445. const toolUseBlocks = assistantContent.filter(
  446. (block) => block.type === "tool_use"
  447. ) as Anthropic.Messages.ToolUseBlock[]
  448. if (toolUseBlocks.length > 0) {
  449. const existingToolResults = existingUserContent.filter(
  450. (block) => block.type === "tool_result"
  451. ) as Anthropic.ToolResultBlockParam[]
  452. const missingToolResponses: Anthropic.ToolResultBlockParam[] = toolUseBlocks
  453. .filter(
  454. (toolUse) => !existingToolResults.some((result) => result.tool_use_id === toolUse.id)
  455. )
  456. .map((toolUse) => ({
  457. type: "tool_result",
  458. tool_use_id: toolUse.id,
  459. content: "Task was interrupted before this tool call could be completed.",
  460. }))
  461. modifiedApiConversationHistory = existingApiConversationHistory.slice(0, -1) // removes the last user message
  462. modifiedOldUserContent = [...existingUserContent, ...missingToolResponses]
  463. } else {
  464. modifiedApiConversationHistory = existingApiConversationHistory.slice(0, -1)
  465. modifiedOldUserContent = [...existingUserContent]
  466. }
  467. } else {
  468. modifiedApiConversationHistory = existingApiConversationHistory.slice(0, -1)
  469. modifiedOldUserContent = [...existingUserContent]
  470. }
  471. } else {
  472. throw new Error("Unexpected: Last message is not a user or assistant message")
  473. }
  474. } else {
  475. throw new Error("Unexpected: No existing API conversation history")
  476. }
  477. let newUserContent: UserContent = [...modifiedOldUserContent]
  478. const agoText = (() => {
  479. const timestamp = lastClaudeMessage?.ts ?? Date.now()
  480. const now = Date.now()
  481. const diff = now - timestamp
  482. const minutes = Math.floor(diff / 60000)
  483. const hours = Math.floor(minutes / 60)
  484. const days = Math.floor(hours / 24)
  485. if (days > 0) {
  486. return `${days} day${days > 1 ? "s" : ""} ago`
  487. }
  488. if (hours > 0) {
  489. return `${hours} hour${hours > 1 ? "s" : ""} ago`
  490. }
  491. if (minutes > 0) {
  492. return `${minutes} minute${minutes > 1 ? "s" : ""} ago`
  493. }
  494. return "just now"
  495. })()
  496. newUserContent.push({
  497. type: "text",
  498. text:
  499. `Task resumption: This autonomous coding task was interrupted ${agoText}. It may or may not be complete, so please reassess the task context. Be aware that the project state may have changed since then. The current working directory is now '${cwd.toPosix()}'. If the task has not been completed, retry the last step before interruption and proceed with completing the task.` +
  500. (responseText
  501. ? `\n\nNew instructions for task continuation:\n<user_message>\n${responseText}\n</user_message>`
  502. : ""),
  503. })
  504. if (responseImages && responseImages.length > 0) {
  505. newUserContent.push(...formatResponse.imageBlocks(responseImages))
  506. }
  507. await this.overwriteApiConversationHistory(modifiedApiConversationHistory)
  508. await this.initiateTaskLoop(newUserContent)
  509. }
  510. private async initiateTaskLoop(userContent: UserContent): Promise<void> {
  511. let nextUserContent = userContent
  512. let includeFileDetails = true
  513. while (!this.abort) {
  514. const didEndLoop = await this.recursivelyMakeClaudeRequests(nextUserContent, includeFileDetails)
  515. includeFileDetails = false // we only need file details the first time
  516. // 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.
  517. // 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.
  518. //const totalCost = this.calculateApiCost(totalInputTokens, totalOutputTokens)
  519. if (didEndLoop) {
  520. // For now a task never 'completes'. This will only happen if the user hits max requests and denies resetting the count.
  521. //this.say("task_completed", `Task completed. Total API usage cost: ${totalCost}`)
  522. break
  523. } else {
  524. // this.say(
  525. // "tool",
  526. // "Claude responded with only text blocks but has not called attempt_completion yet. Forcing him to continue with task..."
  527. // )
  528. nextUserContent = [
  529. {
  530. type: "text",
  531. text: formatResponse.noToolsUsed(),
  532. },
  533. ]
  534. this.consecutiveMistakeCount++
  535. }
  536. }
  537. }
  538. abortTask() {
  539. this.abort = true // will stop any autonomously running promises
  540. this.terminalManager.disposeAll()
  541. this.urlContentFetcher.closeBrowser()
  542. }
  543. // Tools
  544. async executeCommandTool(
  545. command: string,
  546. returnEmptyStringOnSuccess: boolean = false
  547. ): Promise<[boolean, ToolResponse]> {
  548. const terminalInfo = await this.terminalManager.getOrCreateTerminal(cwd)
  549. terminalInfo.terminal.show() // weird visual bug when creating new terminals (even manually) where there's an empty space at the top.
  550. const process = this.terminalManager.runCommand(terminalInfo, command)
  551. let userFeedback: { text?: string; images?: string[] } | undefined
  552. let didContinue = false
  553. const sendCommandOutput = async (line: string): Promise<void> => {
  554. try {
  555. const { response, text, images } = await this.ask("command_output", line)
  556. if (response === "yesButtonTapped") {
  557. // proceed while running
  558. } else {
  559. userFeedback = { text, images }
  560. }
  561. didContinue = true
  562. process.continue() // continue past the await
  563. } catch {
  564. // This can only happen if this ask promise was ignored, so ignore this error
  565. }
  566. }
  567. let result = ""
  568. process.on("line", (line) => {
  569. result += line + "\n"
  570. if (!didContinue) {
  571. sendCommandOutput(line)
  572. } else {
  573. this.say("command_output", line)
  574. }
  575. })
  576. let completed = false
  577. process.once("completed", () => {
  578. completed = true
  579. })
  580. process.once("no_shell_integration", async () => {
  581. await this.say("shell_integration_warning")
  582. })
  583. await process
  584. // Wait for a short delay to ensure all messages are sent to the webview
  585. // This delay allows time for non-awaited promises to be created and
  586. // for their associated messages to be sent to the webview, maintaining
  587. // the correct order of messages (although the webview is smart about
  588. // grouping command_output messages despite any gaps anyways)
  589. await delay(50)
  590. result = result.trim()
  591. if (userFeedback) {
  592. await this.say("user_feedback", userFeedback.text, userFeedback.images)
  593. return [
  594. true,
  595. formatResponse.toolResult(
  596. `Command is still running in the user's terminal.${
  597. result.length > 0 ? `\nHere's the output so far:\n${result}` : ""
  598. }\n\nThe user provided the following feedback:\n<feedback>\n${userFeedback.text}\n</feedback>`,
  599. userFeedback.images
  600. ),
  601. ]
  602. }
  603. // for attemptCompletion, we don't want to return the command output
  604. if (returnEmptyStringOnSuccess) {
  605. return [false, ""]
  606. }
  607. if (completed) {
  608. return [false, `Command executed.${result.length > 0 ? `\nOutput:\n${result}` : ""}`]
  609. } else {
  610. return [
  611. false,
  612. `Command is still running in the user's terminal.${
  613. result.length > 0 ? `\nHere's the output so far:\n${result}` : ""
  614. }\n\nYou will be updated on the terminal status and new output in the future.`,
  615. ]
  616. }
  617. }
  618. async attemptApiRequest(previousApiReqIndex: number): Promise<ApiStream> {
  619. try {
  620. let systemPrompt = await SYSTEM_PROMPT(cwd, this.api.getModel().info.supportsImages)
  621. if (this.customInstructions && this.customInstructions.trim()) {
  622. // altering the system prompt mid-task will break the prompt cache, but in the grand scheme this will not change often so it's better to not pollute user messages with it the way we have to with <potentially relevant details>
  623. systemPrompt += addCustomInstructions(this.customInstructions)
  624. }
  625. // 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
  626. if (previousApiReqIndex >= 0) {
  627. const previousRequest = this.claudeMessages[previousApiReqIndex]
  628. if (previousRequest && previousRequest.text) {
  629. const {
  630. tokensIn,
  631. tokensOut,
  632. cacheWrites,
  633. cacheReads,
  634. }: { tokensIn?: number; tokensOut?: number; cacheWrites?: number; cacheReads?: number } =
  635. JSON.parse(previousRequest.text)
  636. const totalTokens = (tokensIn || 0) + (tokensOut || 0) + (cacheWrites || 0) + (cacheReads || 0)
  637. const contextWindow = this.api.getModel().info.contextWindow
  638. const maxAllowedSize = Math.max(contextWindow - 40_000, contextWindow * 0.8)
  639. if (totalTokens >= maxAllowedSize) {
  640. const truncatedMessages = truncateHalfConversation(this.apiConversationHistory)
  641. await this.overwriteApiConversationHistory(truncatedMessages)
  642. }
  643. }
  644. }
  645. const stream = this.api.createMessage(systemPrompt, this.apiConversationHistory)
  646. return stream
  647. } catch (error) {
  648. const { response } = await this.ask(
  649. "api_req_failed",
  650. error.message ?? JSON.stringify(serializeError(error), null, 2)
  651. )
  652. if (response !== "yesButtonTapped") {
  653. // this will never happen since if noButtonTapped, we will clear current task, aborting this instance
  654. throw new Error("API request failed")
  655. }
  656. await this.say("api_req_retried")
  657. return this.attemptApiRequest(previousApiReqIndex)
  658. }
  659. }
  660. async presentAssistantMessage() {
  661. if (this.abort) {
  662. throw new Error("ClaudeDev instance aborted")
  663. }
  664. if (this.presentAssistantMessageLocked) {
  665. this.presentAssistantMessageHasPendingUpdates = true
  666. return
  667. }
  668. this.presentAssistantMessageLocked = true
  669. this.presentAssistantMessageHasPendingUpdates = false
  670. if (this.currentStreamingContentIndex >= this.assistantMessageContent.length) {
  671. // 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
  672. if (this.didCompleteReadingStream) {
  673. this.userMessageContentReady = true
  674. }
  675. // console.log("no more content blocks to stream! this shouldn't happen?")
  676. this.presentAssistantMessageLocked = false
  677. return
  678. //throw new Error("No more content blocks to stream! This shouldn't happen...") // remove and just return after testing
  679. }
  680. 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
  681. switch (block.type) {
  682. case "text":
  683. await this.say("text", block.content, undefined, block.partial)
  684. break
  685. case "tool_use":
  686. const toolDescription = () => {
  687. switch (block.name) {
  688. case "execute_command":
  689. return `[${block.name} for '${block.params.command}']`
  690. case "read_file":
  691. return `[${block.name} for '${block.params.path}']`
  692. case "write_to_file":
  693. return `[${block.name} for '${block.params.path}']`
  694. case "search_files":
  695. return `[${block.name} for '${block.params.regex}'${
  696. block.params.file_pattern ? ` in '${block.params.file_pattern}'` : ""
  697. }]`
  698. case "list_files":
  699. return `[${block.name} for '${block.params.path}']`
  700. case "list_code_definition_names":
  701. return `[${block.name} for '${block.params.path}']`
  702. case "inspect_site":
  703. return `[${block.name} for '${block.params.url}']`
  704. case "ask_followup_question":
  705. return `[${block.name} for '${block.params.question}']`
  706. case "attempt_completion":
  707. return `[${block.name}]`
  708. }
  709. }
  710. if (this.didRejectTool) {
  711. // ignore any tool content after user has rejected tool once
  712. // we'll fill it in with a rejection message when the message is complete
  713. if (!block.partial) {
  714. this.userMessageContent.push({
  715. type: "text",
  716. text: `Skipping tool ${toolDescription()} due to user rejecting a previous tool.`,
  717. })
  718. }
  719. break
  720. }
  721. const pushToolResult = (content: ToolResponse) => {
  722. this.userMessageContent.push({
  723. type: "text",
  724. text: `${toolDescription()} Result:`,
  725. })
  726. if (typeof content === "string") {
  727. this.userMessageContent.push({
  728. type: "text",
  729. text: content || "(tool did not return anything)",
  730. })
  731. } else {
  732. this.userMessageContent.push(...content)
  733. }
  734. }
  735. const askApproval = async (type: ClaudeAsk, partialMessage?: string) => {
  736. const { response, text, images } = await this.ask(type, partialMessage, false)
  737. if (response !== "yesButtonTapped") {
  738. if (response === "messageResponse") {
  739. await this.say("user_feedback", text, images)
  740. pushToolResult(
  741. formatResponse.toolResult(formatResponse.toolDeniedWithFeedback(text), images)
  742. )
  743. // this.userMessageContent.push({
  744. // type: "text",
  745. // text: `${toolDescription()}`,
  746. // })
  747. // this.toolResults.push({
  748. // type: "tool_result",
  749. // tool_use_id: toolUseId,
  750. // content: this.formatToolResponseWithImages(
  751. // await this.formatToolDeniedFeedback(text),
  752. // images
  753. // ),
  754. // })
  755. this.didRejectTool = true
  756. return false
  757. }
  758. pushToolResult(formatResponse.toolDenied())
  759. // this.toolResults.push({
  760. // type: "tool_result",
  761. // tool_use_id: toolUseId,
  762. // content: await this.formatToolDenied(),
  763. // })
  764. this.didRejectTool = true
  765. return false
  766. }
  767. return true
  768. }
  769. const handleError = async (action: string, error: Error) => {
  770. const errorString = `Error ${action}: ${JSON.stringify(serializeError(error))}`
  771. await this.say(
  772. "error",
  773. `Error ${action}:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`
  774. )
  775. // this.toolResults.push({
  776. // type: "tool_result",
  777. // tool_use_id: toolUseId,
  778. // content: await this.formatToolError(errorString),
  779. // })
  780. pushToolResult(formatResponse.toolError(errorString))
  781. }
  782. switch (block.name) {
  783. case "write_to_file": {
  784. const relPath: string | undefined = block.params.path
  785. let newContent: string | undefined = block.params.content
  786. if (!relPath || !newContent) {
  787. // checking for newContent ensure relPath is complete
  788. // wait so we can determine if it's a new file or editing an existing file
  789. break
  790. }
  791. // Check if file exists using cached map or fs.access
  792. let fileExists: boolean
  793. if (this.diffViewProvider.editType !== undefined) {
  794. fileExists = this.diffViewProvider.editType === "modify"
  795. } else {
  796. const absolutePath = path.resolve(cwd, relPath)
  797. fileExists = await fileExistsAtPath(absolutePath)
  798. this.diffViewProvider.editType = fileExists ? "modify" : "create"
  799. }
  800. const sharedMessageProps: ClaudeSayTool = {
  801. tool: fileExists ? "editedExistingFile" : "newFileCreated",
  802. path: getReadablePath(cwd, relPath),
  803. }
  804. try {
  805. if (block.partial) {
  806. // update gui message
  807. const partialMessage = JSON.stringify(sharedMessageProps)
  808. await this.ask("tool", partialMessage, block.partial).catch(() => {})
  809. // update editor
  810. if (!this.diffViewProvider.isEditing) {
  811. // open the editor and prepare to stream content in
  812. await this.diffViewProvider.open(relPath)
  813. }
  814. // editor is open, stream content in
  815. await this.diffViewProvider.update(newContent, false)
  816. break
  817. } else {
  818. if (!relPath) {
  819. this.consecutiveMistakeCount++
  820. pushToolResult(await this.sayAndCreateMissingParamError("write_to_file", "path"))
  821. await this.diffViewProvider.reset()
  822. break
  823. }
  824. if (!newContent) {
  825. this.consecutiveMistakeCount++
  826. pushToolResult(await this.sayAndCreateMissingParamError("write_to_file", "content"))
  827. await this.diffViewProvider.reset()
  828. break
  829. }
  830. this.consecutiveMistakeCount = 0
  831. // if isEditingFile false, that means we have the full contents of the file already.
  832. // it's important to note how this function works, you can't make the assumption that the block.partial conditional will always be called since it may immediately get complete, non-partial data. So this part of the logic will always be called.
  833. // in other words, you must always repeat the block.partial logic here
  834. if (!this.diffViewProvider.isEditing) {
  835. await this.diffViewProvider.open(relPath)
  836. }
  837. await this.diffViewProvider.update(newContent, true)
  838. await delay(300) // wait for diff view to update
  839. this.diffViewProvider.scrollToFirstDiff()
  840. const completeMessage = JSON.stringify({
  841. ...sharedMessageProps,
  842. content: fileExists ? undefined : newContent,
  843. diff: fileExists
  844. ? formatResponse.createPrettyPatch(
  845. relPath,
  846. this.diffViewProvider.originalContent,
  847. newContent
  848. )
  849. : undefined,
  850. } satisfies ClaudeSayTool)
  851. const didApprove = await askApproval("tool", completeMessage)
  852. if (!didApprove) {
  853. await this.diffViewProvider.revertChanges()
  854. break
  855. }
  856. const { newProblemsMessage, userEdits } = await this.diffViewProvider.saveChanges()
  857. this.didEditFile = true // used to determine if we should wait for busy terminal to update before sending api request
  858. if (userEdits) {
  859. await this.say(
  860. "user_feedback_diff",
  861. JSON.stringify({
  862. tool: fileExists ? "editedExistingFile" : "newFileCreated",
  863. path: getReadablePath(cwd, relPath),
  864. diff: userEdits,
  865. } satisfies ClaudeSayTool)
  866. )
  867. pushToolResult(
  868. `The user made the following updates to your content:\n\n${userEdits}\n\nThe updated content, which includes both your original modifications and the user's additional edits, has been successfully saved to ${relPath.toPosix()}. (Note this does not mean you need to re-write the file with the user's changes, as they have already been applied to the file.)${newProblemsMessage}`
  869. )
  870. } else {
  871. pushToolResult(
  872. `The content was successfully saved to ${relPath.toPosix()}.${newProblemsMessage}`
  873. )
  874. }
  875. await this.diffViewProvider.reset()
  876. break
  877. }
  878. } catch (error) {
  879. await handleError("writing file", error)
  880. await this.diffViewProvider.reset()
  881. break
  882. }
  883. }
  884. case "read_file": {
  885. const relPath: string | undefined = block.params.path
  886. const sharedMessageProps: ClaudeSayTool = {
  887. tool: "readFile",
  888. path: getReadablePath(cwd, relPath),
  889. }
  890. try {
  891. if (block.partial) {
  892. const partialMessage = JSON.stringify({
  893. ...sharedMessageProps,
  894. content: undefined,
  895. } satisfies ClaudeSayTool)
  896. if (this.alwaysAllowReadOnly) {
  897. await this.say("tool", partialMessage, undefined, block.partial)
  898. } else {
  899. await this.ask("tool", partialMessage, block.partial).catch(() => {})
  900. }
  901. break
  902. } else {
  903. if (!relPath) {
  904. this.consecutiveMistakeCount++
  905. pushToolResult(await this.sayAndCreateMissingParamError("read_file", "path"))
  906. break
  907. }
  908. this.consecutiveMistakeCount = 0
  909. const absolutePath = path.resolve(cwd, relPath)
  910. const completeMessage = JSON.stringify({
  911. ...sharedMessageProps,
  912. content: absolutePath,
  913. } satisfies ClaudeSayTool)
  914. if (this.alwaysAllowReadOnly) {
  915. await this.say("tool", completeMessage, undefined, false) // need to be sending partialValue bool, since undefined has its own purpose in that the message is treated neither as a partial or completion of a partial, but as a single complete message
  916. } else {
  917. const didApprove = await askApproval("tool", completeMessage)
  918. if (!didApprove) {
  919. break
  920. }
  921. }
  922. // now execute the tool like normal
  923. const content = await extractTextFromFile(absolutePath)
  924. pushToolResult(content)
  925. break
  926. }
  927. } catch (error) {
  928. await handleError("reading file", error)
  929. break
  930. }
  931. }
  932. case "list_files": {
  933. const relDirPath: string | undefined = block.params.path
  934. const recursiveRaw: string | undefined = block.params.recursive
  935. const recursive = recursiveRaw?.toLowerCase() === "true"
  936. const sharedMessageProps: ClaudeSayTool = {
  937. tool: !recursive ? "listFilesTopLevel" : "listFilesRecursive",
  938. path: getReadablePath(cwd, relDirPath),
  939. }
  940. try {
  941. if (block.partial) {
  942. const partialMessage = JSON.stringify({
  943. ...sharedMessageProps,
  944. content: "",
  945. } satisfies ClaudeSayTool)
  946. if (this.alwaysAllowReadOnly) {
  947. await this.say("tool", partialMessage, undefined, block.partial)
  948. } else {
  949. await this.ask("tool", partialMessage, block.partial).catch(() => {})
  950. }
  951. break
  952. } else {
  953. if (!relDirPath) {
  954. this.consecutiveMistakeCount++
  955. pushToolResult(await this.sayAndCreateMissingParamError("list_files", "path"))
  956. break
  957. }
  958. this.consecutiveMistakeCount = 0
  959. const absolutePath = path.resolve(cwd, relDirPath)
  960. const [files, didHitLimit] = await listFiles(absolutePath, recursive, 200)
  961. const result = formatResponse.formatFilesList(absolutePath, files, didHitLimit)
  962. const completeMessage = JSON.stringify({
  963. ...sharedMessageProps,
  964. content: result,
  965. } satisfies ClaudeSayTool)
  966. if (this.alwaysAllowReadOnly) {
  967. await this.say("tool", completeMessage, undefined, false)
  968. } else {
  969. const didApprove = await askApproval("tool", completeMessage)
  970. if (!didApprove) {
  971. break
  972. }
  973. }
  974. pushToolResult(result)
  975. break
  976. }
  977. } catch (error) {
  978. await handleError("listing files", error)
  979. break
  980. }
  981. }
  982. case "list_code_definition_names": {
  983. const relDirPath: string | undefined = block.params.path
  984. const sharedMessageProps: ClaudeSayTool = {
  985. tool: "listCodeDefinitionNames",
  986. path: getReadablePath(cwd, relDirPath),
  987. }
  988. try {
  989. if (block.partial) {
  990. const partialMessage = JSON.stringify({
  991. ...sharedMessageProps,
  992. content: "",
  993. } satisfies ClaudeSayTool)
  994. if (this.alwaysAllowReadOnly) {
  995. await this.say("tool", partialMessage, undefined, block.partial)
  996. } else {
  997. await this.ask("tool", partialMessage, block.partial).catch(() => {})
  998. }
  999. break
  1000. } else {
  1001. if (!relDirPath) {
  1002. this.consecutiveMistakeCount++
  1003. pushToolResult(
  1004. await this.sayAndCreateMissingParamError("list_code_definition_names", "path")
  1005. )
  1006. break
  1007. }
  1008. this.consecutiveMistakeCount = 0
  1009. const absolutePath = path.resolve(cwd, relDirPath)
  1010. const result = await parseSourceCodeForDefinitionsTopLevel(absolutePath)
  1011. const completeMessage = JSON.stringify({
  1012. ...sharedMessageProps,
  1013. content: result,
  1014. } satisfies ClaudeSayTool)
  1015. if (this.alwaysAllowReadOnly) {
  1016. await this.say("tool", completeMessage, undefined, false)
  1017. } else {
  1018. const didApprove = await askApproval("tool", completeMessage)
  1019. if (!didApprove) {
  1020. break
  1021. }
  1022. }
  1023. pushToolResult(result)
  1024. break
  1025. }
  1026. } catch (error) {
  1027. await handleError("parsing source code definitions", error)
  1028. break
  1029. }
  1030. }
  1031. case "search_files": {
  1032. const relDirPath: string | undefined = block.params.path
  1033. const regex: string | undefined = block.params.regex
  1034. const filePattern: string | undefined = block.params.file_pattern
  1035. const sharedMessageProps: ClaudeSayTool = {
  1036. tool: "searchFiles",
  1037. path: getReadablePath(cwd, relDirPath),
  1038. regex: regex || "",
  1039. filePattern: filePattern || "",
  1040. }
  1041. try {
  1042. if (block.partial) {
  1043. const partialMessage = JSON.stringify({
  1044. ...sharedMessageProps,
  1045. content: "",
  1046. } satisfies ClaudeSayTool)
  1047. if (this.alwaysAllowReadOnly) {
  1048. await this.say("tool", partialMessage, undefined, block.partial)
  1049. } else {
  1050. await this.ask("tool", partialMessage, block.partial).catch(() => {})
  1051. }
  1052. break
  1053. } else {
  1054. if (!relDirPath) {
  1055. this.consecutiveMistakeCount++
  1056. pushToolResult(await this.sayAndCreateMissingParamError("search_files", "path"))
  1057. break
  1058. }
  1059. if (!regex) {
  1060. this.consecutiveMistakeCount++
  1061. pushToolResult(await this.sayAndCreateMissingParamError("search_files", "regex"))
  1062. break
  1063. }
  1064. this.consecutiveMistakeCount = 0
  1065. const absolutePath = path.resolve(cwd, relDirPath)
  1066. const results = await regexSearchFiles(cwd, absolutePath, regex, filePattern)
  1067. const completeMessage = JSON.stringify({
  1068. ...sharedMessageProps,
  1069. content: results,
  1070. } satisfies ClaudeSayTool)
  1071. if (this.alwaysAllowReadOnly) {
  1072. await this.say("tool", completeMessage, undefined, false)
  1073. } else {
  1074. const didApprove = await askApproval("tool", completeMessage)
  1075. if (!didApprove) {
  1076. break
  1077. }
  1078. }
  1079. pushToolResult(results)
  1080. break
  1081. }
  1082. } catch (error) {
  1083. await handleError("searching files", error)
  1084. break
  1085. }
  1086. }
  1087. case "inspect_site": {
  1088. const url: string | undefined = block.params.url
  1089. const sharedMessageProps: ClaudeSayTool = {
  1090. tool: "inspectSite",
  1091. path: url || "",
  1092. }
  1093. try {
  1094. if (block.partial) {
  1095. const partialMessage = JSON.stringify(sharedMessageProps)
  1096. if (this.alwaysAllowReadOnly) {
  1097. await this.say("tool", partialMessage, undefined, block.partial)
  1098. } else {
  1099. await this.ask("tool", partialMessage, block.partial).catch(() => {})
  1100. }
  1101. break
  1102. } else {
  1103. if (!url) {
  1104. this.consecutiveMistakeCount++
  1105. pushToolResult(await this.sayAndCreateMissingParamError("inspect_site", "url"))
  1106. break
  1107. }
  1108. this.consecutiveMistakeCount = 0
  1109. const completeMessage = JSON.stringify(sharedMessageProps)
  1110. if (this.alwaysAllowReadOnly) {
  1111. await this.say("tool", completeMessage, undefined, false)
  1112. } else {
  1113. const didApprove = await askApproval("tool", completeMessage)
  1114. if (!didApprove) {
  1115. break
  1116. }
  1117. }
  1118. // execute tool
  1119. // NOTE: it's okay that we call this message since the partial inspect_site is finished streaming. The only scenario we have to avoid is sending messages WHILE a partial message exists at the end of the messages array. For example the api_req_finished message would interfere with the partial message, so we needed to remove that.
  1120. await this.say("inspect_site_result", "") // no result, starts the loading spinner waiting for result
  1121. await this.urlContentFetcher.launchBrowser()
  1122. let result: {
  1123. screenshot: string
  1124. logs: string
  1125. }
  1126. try {
  1127. result = await this.urlContentFetcher.urlToScreenshotAndLogs(url)
  1128. } finally {
  1129. await this.urlContentFetcher.closeBrowser()
  1130. }
  1131. const { screenshot, logs } = result
  1132. await this.say("inspect_site_result", logs, [screenshot])
  1133. pushToolResult(
  1134. formatResponse.toolResult(
  1135. `The site has been visited, with console logs captured and a screenshot taken for your analysis.\n\nConsole logs:\n${
  1136. logs || "(No logs)"
  1137. }`,
  1138. [screenshot]
  1139. )
  1140. )
  1141. break
  1142. }
  1143. } catch (error) {
  1144. await handleError("inspecting site", error)
  1145. break
  1146. }
  1147. }
  1148. case "execute_command": {
  1149. const command: string | undefined = block.params.command
  1150. try {
  1151. if (block.partial) {
  1152. await this.ask("command", command || "", block.partial).catch(() => {})
  1153. break
  1154. } else {
  1155. if (!command) {
  1156. this.consecutiveMistakeCount++
  1157. pushToolResult(
  1158. await this.sayAndCreateMissingParamError("execute_command", "command")
  1159. )
  1160. break
  1161. }
  1162. this.consecutiveMistakeCount = 0
  1163. const didApprove = await askApproval("command", command)
  1164. if (!didApprove) {
  1165. break
  1166. }
  1167. const [userRejected, result] = await this.executeCommandTool(command)
  1168. if (userRejected) {
  1169. this.didRejectTool = true // test whats going on here
  1170. }
  1171. pushToolResult(result)
  1172. break
  1173. }
  1174. } catch (error) {
  1175. await handleError("inspecting site", error)
  1176. break
  1177. }
  1178. }
  1179. case "ask_followup_question": {
  1180. const question: string | undefined = block.params.question
  1181. try {
  1182. if (block.partial) {
  1183. await this.ask("followup", question || "", block.partial).catch(() => {})
  1184. break
  1185. } else {
  1186. if (!question) {
  1187. this.consecutiveMistakeCount++
  1188. pushToolResult(
  1189. await this.sayAndCreateMissingParamError("ask_followup_question", "question")
  1190. )
  1191. break
  1192. }
  1193. this.consecutiveMistakeCount = 0
  1194. const { text, images } = await this.ask("followup", question, false)
  1195. await this.say("user_feedback", text ?? "", images)
  1196. pushToolResult(formatResponse.toolResult(`<answer>\n${text}\n</answer>`, images))
  1197. break
  1198. }
  1199. } catch (error) {
  1200. await handleError("asking question", error)
  1201. break
  1202. }
  1203. }
  1204. case "attempt_completion": {
  1205. /*
  1206. this.consecutiveMistakeCount = 0
  1207. let resultToSend = result
  1208. if (command) {
  1209. await this.say("completion_result", resultToSend)
  1210. // TODO: currently we don't handle if this command fails, it could be useful to let claude know and retry
  1211. const [didUserReject, commandResult] = await this.executeCommand(command, true)
  1212. // if we received non-empty string, the command was rejected or failed
  1213. if (commandResult) {
  1214. return [didUserReject, commandResult]
  1215. }
  1216. resultToSend = ""
  1217. }
  1218. const { response, text, images } = await this.ask("completion_result", resultToSend) // this prompts webview to show 'new task' button, and enable text input (which would be the 'text' here)
  1219. if (response === "yesButtonTapped") {
  1220. return [false, ""] // signals to recursive loop to stop (for now this never happens since yesButtonTapped will trigger a new task)
  1221. }
  1222. await this.say("user_feedback", text ?? "", images)
  1223. return [
  1224. */
  1225. const result: string | undefined = block.params.result
  1226. const command: string | undefined = block.params.command
  1227. try {
  1228. const lastMessage = this.claudeMessages.at(-1)
  1229. if (block.partial) {
  1230. if (command) {
  1231. // the attempt_completion text is done, now we're getting command
  1232. // remove the previous partial attempt_completion ask, replace with say, post state to webview, then stream command
  1233. // const secondLastMessage = this.claudeMessages.at(-2)
  1234. if (lastMessage && lastMessage.ask === "command") {
  1235. // update command
  1236. await this.ask("command", command || "", block.partial).catch(() => {})
  1237. } else {
  1238. // last message is completion_result
  1239. // we have command string, which means we have the result as well, so finish it (doesnt have to exist yet)
  1240. await this.say("completion_result", result, undefined, false)
  1241. await this.ask("command", command || "", block.partial).catch(() => {})
  1242. }
  1243. } else {
  1244. // no command, still outputting partial result
  1245. await this.say("completion_result", result || "", undefined, block.partial)
  1246. }
  1247. break
  1248. } else {
  1249. if (!result) {
  1250. this.consecutiveMistakeCount++
  1251. pushToolResult(
  1252. await this.sayAndCreateMissingParamError("attempt_completion", "result")
  1253. )
  1254. break
  1255. }
  1256. this.consecutiveMistakeCount = 0
  1257. if (command) {
  1258. if (lastMessage && lastMessage.ask !== "command") {
  1259. // havent sent a command message yet so first send completion_result then command
  1260. await this.say("completion_result", result, undefined, false)
  1261. }
  1262. // complete command message
  1263. const didApprove = await askApproval("command", command)
  1264. if (!didApprove) {
  1265. break
  1266. }
  1267. const [userRejected, commandResult] = await this.executeCommandTool(command!, true)
  1268. if (commandResult) {
  1269. if (userRejected) {
  1270. this.didRejectTool = true // test whats going on here
  1271. }
  1272. pushToolResult(commandResult)
  1273. break
  1274. }
  1275. } else {
  1276. await this.say("completion_result", result, undefined, false)
  1277. }
  1278. // we already sent completion_result says, an empty string asks relinquishes control over button and field
  1279. const { response, text, images } = await this.ask("completion_result", "", false)
  1280. if (response === "yesButtonTapped") {
  1281. pushToolResult("") // signals to recursive loop to stop (for now this never happens since yesButtonTapped will trigger a new task)
  1282. break
  1283. }
  1284. await this.say("user_feedback", text ?? "", images)
  1285. pushToolResult(
  1286. formatResponse.toolResult(
  1287. `The user has provided feedback on the results. Consider their input to continue the task, and then attempt completion again.\n<feedback>\n${text}\n</feedback>`,
  1288. images
  1289. )
  1290. )
  1291. break
  1292. }
  1293. } catch (error) {
  1294. await handleError("inspecting site", error)
  1295. break
  1296. }
  1297. }
  1298. }
  1299. break
  1300. }
  1301. /*
  1302. 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.
  1303. 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.
  1304. */
  1305. this.presentAssistantMessageLocked = false // this needs to be placed here, if not then calling this.presentAssistantMessage below would fail (sometimes) since it's locked
  1306. if (!block.partial) {
  1307. // block is finished streaming and executing
  1308. if (this.currentStreamingContentIndex === this.assistantMessageContent.length - 1) {
  1309. // 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.
  1310. // last block is complete and it is finished executing
  1311. this.userMessageContentReady = true // will allow pwaitfor to continue
  1312. }
  1313. // call next block if it exists (if not then read stream will call it when its ready)
  1314. this.currentStreamingContentIndex++ // need to increment regardless, so when read stream calls this function again it will be streaming the next block
  1315. if (this.currentStreamingContentIndex < this.assistantMessageContent.length) {
  1316. // there are already more content blocks to stream, so we'll call this function ourselves
  1317. // await this.presentAssistantContent()
  1318. this.presentAssistantMessage()
  1319. return
  1320. }
  1321. }
  1322. // block is partial, but the read stream may have finished
  1323. if (this.presentAssistantMessageHasPendingUpdates) {
  1324. this.presentAssistantMessage()
  1325. }
  1326. }
  1327. parseAssistantMessage(assistantMessage: string) {
  1328. // let text = ""
  1329. let textContent: TextContent = {
  1330. type: "text",
  1331. content: "",
  1332. partial: true,
  1333. }
  1334. let toolUses: ToolUse[] = []
  1335. let currentToolUse: ToolUse | undefined = undefined
  1336. let currentParamName: ToolParamName | undefined = undefined
  1337. let currentParamValueLines: string[] = []
  1338. let textContentLines: string[] = []
  1339. const rawLines = assistantMessage.split("\n")
  1340. if (rawLines.length === 1) {
  1341. const firstLine = rawLines[0].trim()
  1342. if (!firstLine.startsWith("<t") && firstLine.startsWith("<")) {
  1343. // (we ignore tags that start with <t since it's most like a <thinking> tag (and none of our tags start with t)
  1344. // content is just starting, if it starts with < we can assume it's a tool call, so we'll wait for the next line
  1345. return
  1346. }
  1347. }
  1348. if (
  1349. this.assistantMessageContent.length === 1 &&
  1350. this.assistantMessageContent[0].partial // first element is always TextContent
  1351. ) {
  1352. // we're updating text content, so if we have a partial xml tag on the last line we can ignore it until we get the full line.
  1353. const lastLine = rawLines.at(-1)?.trim()
  1354. if (lastLine && !lastLine.startsWith("<t") && lastLine.startsWith("<") && !lastLine.endsWith(">")) {
  1355. return
  1356. }
  1357. }
  1358. for (const line of rawLines) {
  1359. const trimmed = line.trim()
  1360. // if currenttoolcall or currentparamname look for closing tag, more efficient and safe
  1361. if (currentToolUse && currentParamName && trimmed === `</${currentParamName}>`) {
  1362. // End of a tool parameter
  1363. currentToolUse.params[currentParamName] = currentParamValueLines.join("\n")
  1364. currentParamName = undefined
  1365. currentParamValueLines = []
  1366. // currentParamValue = undefined
  1367. continue
  1368. } else if (currentToolUse && !currentParamName && trimmed === `</${currentToolUse.name}>`) {
  1369. // End of a tool call
  1370. currentToolUse.partial = false
  1371. toolUses.push(currentToolUse)
  1372. currentToolUse = undefined
  1373. continue
  1374. }
  1375. if (!currentParamName && trimmed.startsWith("<") && trimmed.endsWith(">")) {
  1376. const tag = trimmed.slice(1, -1)
  1377. if (toolUseNames.includes(tag as ToolUseName)) {
  1378. // Start of a new tool call
  1379. currentToolUse = {
  1380. type: "tool_use",
  1381. name: tag as ToolUseName,
  1382. params: {},
  1383. partial: true,
  1384. } satisfies ToolUse
  1385. // This also indicates the end of the text content
  1386. textContent.partial = false
  1387. continue
  1388. } else if (currentToolUse && toolParamNames.includes(tag as ToolParamName)) {
  1389. // Start of a parameter
  1390. currentParamName = tag as ToolParamName
  1391. // currentToolUse.params[currentParamName] = ""
  1392. continue
  1393. }
  1394. }
  1395. if (currentToolUse && !currentParamName) {
  1396. // current tool doesn't have a param match yet, it's likely partial so ignore
  1397. continue
  1398. }
  1399. if (currentToolUse && currentParamName) {
  1400. // add line to current param value
  1401. currentParamValueLines.push(line)
  1402. continue
  1403. }
  1404. // only add text content if we haven't started a tool yet
  1405. if (textContent.partial) {
  1406. textContentLines.push(line)
  1407. }
  1408. }
  1409. if (currentToolUse) {
  1410. // stream did not complete tool call, add it as partial
  1411. if (currentParamName) {
  1412. // tool call has a parameter that was not completed
  1413. currentToolUse.params[currentParamName] = currentParamValueLines.join("\n")
  1414. }
  1415. toolUses.push(currentToolUse)
  1416. }
  1417. textContent.content = textContentLines.join("\n")
  1418. const prevLength = this.assistantMessageContent.length
  1419. this.assistantMessageContent = [textContent, ...toolUses]
  1420. if (this.assistantMessageContent.length > prevLength) {
  1421. this.userMessageContentReady = false // new content we need to present, reset to false in case previous content set this to true
  1422. }
  1423. }
  1424. async recursivelyMakeClaudeRequests(
  1425. userContent: UserContent,
  1426. includeFileDetails: boolean = false
  1427. ): Promise<boolean> {
  1428. if (this.abort) {
  1429. throw new Error("ClaudeDev instance aborted")
  1430. }
  1431. if (this.consecutiveMistakeCount >= 3) {
  1432. const { response, text, images } = await this.ask(
  1433. "mistake_limit_reached",
  1434. this.api.getModel().id.includes("claude")
  1435. ? `This may indicate a failure in his thought process or inability to use a tool properly, which can be mitigated with some user guidance (e.g. "Try breaking down the task into smaller steps").`
  1436. : "Claude Dev uses complex prompts and iterative task execution that may be challenging for less capable models. For best results, it's recommended to use Claude 3.5 Sonnet for its advanced agentic coding capabilities."
  1437. )
  1438. if (response === "messageResponse") {
  1439. userContent.push(
  1440. ...[
  1441. {
  1442. type: "text",
  1443. text: formatResponse.tooManyMistakes(text),
  1444. } as Anthropic.Messages.TextBlockParam,
  1445. ...formatResponse.imageBlocks(images),
  1446. ]
  1447. )
  1448. }
  1449. this.consecutiveMistakeCount = 0
  1450. }
  1451. // get previous api req's index to check token usage and determine if we need to truncate conversation history
  1452. const previousApiReqIndex = findLastIndex(this.claudeMessages, (m) => m.say === "api_req_started")
  1453. // 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
  1454. // for the best UX we show a placeholder api_req_started message with a loading spinner as this happens
  1455. await this.say(
  1456. "api_req_started",
  1457. JSON.stringify({
  1458. request:
  1459. userContent
  1460. .map((block) => formatContentBlockToMarkdown(block, this.apiConversationHistory))
  1461. .join("\n\n") + "\n\nLoading...",
  1462. })
  1463. )
  1464. const [parsedUserContent, environmentDetails] = await this.loadContext(userContent, includeFileDetails)
  1465. userContent = parsedUserContent
  1466. // add environment details as its own text block, separate from tool results
  1467. userContent.push({ type: "text", text: environmentDetails })
  1468. await this.addToApiConversationHistory({ role: "user", content: userContent })
  1469. // 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
  1470. const lastApiReqIndex = findLastIndex(this.claudeMessages, (m) => m.say === "api_req_started")
  1471. this.claudeMessages[lastApiReqIndex].text = JSON.stringify({
  1472. request: userContent
  1473. .map((block) => formatContentBlockToMarkdown(block, this.apiConversationHistory))
  1474. .join("\n\n"),
  1475. })
  1476. await this.saveClaudeMessages()
  1477. await this.providerRef.deref()?.postStateToWebview()
  1478. try {
  1479. const stream = await this.attemptApiRequest(previousApiReqIndex)
  1480. let cacheWriteTokens = 0
  1481. let cacheReadTokens = 0
  1482. let inputTokens = 0
  1483. let outputTokens = 0
  1484. let totalCost: number | undefined
  1485. // reset streaming state
  1486. this.currentStreamingContentIndex = 0
  1487. this.assistantMessageContent = []
  1488. this.didCompleteReadingStream = false
  1489. this.userMessageContent = []
  1490. this.userMessageContentReady = false
  1491. this.didRejectTool = false
  1492. this.presentAssistantMessageLocked = false
  1493. this.presentAssistantMessageHasPendingUpdates = false
  1494. await this.diffViewProvider.reset()
  1495. let assistantMessage = ""
  1496. // TODO: handle error being thrown in stream
  1497. for await (const chunk of stream) {
  1498. switch (chunk.type) {
  1499. case "usage":
  1500. inputTokens += chunk.inputTokens
  1501. outputTokens += chunk.outputTokens
  1502. cacheWriteTokens += chunk.cacheWriteTokens ?? 0
  1503. cacheReadTokens += chunk.cacheReadTokens ?? 0
  1504. totalCost = chunk.totalCost
  1505. break
  1506. case "text":
  1507. assistantMessage += chunk.text
  1508. this.parseAssistantMessage(assistantMessage)
  1509. this.presentAssistantMessage()
  1510. break
  1511. }
  1512. }
  1513. this.didCompleteReadingStream = true
  1514. // in case no tool calls were made or tool call wasn't closed properly, set partial to false
  1515. // should not do this if text block is not the last block, since presentAssistantMessage presents the last block
  1516. if (this.assistantMessageContent.length === 1 && this.assistantMessageContent[0].partial) {
  1517. // this.assistantMessageContent.forEach((e) => (e.partial = false)) // cant just do this bc a tool could be in the middle of executing
  1518. // this was originally intended just to update text content in case no tools were called
  1519. this.assistantMessageContent[0].partial = false
  1520. this.presentAssistantMessage() // if there is content to update then it will complete and update this.userMessageContentReady to true, which we pwaitfor before making the next request
  1521. }
  1522. // let inputTokens = response.usage.input_tokens
  1523. // let outputTokens = response.usage.output_tokens
  1524. // let cacheCreationInputTokens =
  1525. // (response as Anthropic.Beta.PromptCaching.Messages.PromptCachingBetaMessage).usage
  1526. // .cache_creation_input_tokens || undefined
  1527. // let cacheReadInputTokens =
  1528. // (response as Anthropic.Beta.PromptCaching.Messages.PromptCachingBetaMessage).usage
  1529. // .cache_read_input_tokens || undefined
  1530. // @ts-ignore-next-line
  1531. // let totalCost = response.usage.total_cost
  1532. // update api_req_started. we can't use api_req_finished anymore since it's a unique case where it could come after a streaming message (ie in the middle of being updated or executed)
  1533. // 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
  1534. // (it's worth removing a few months from now)
  1535. this.claudeMessages[lastApiReqIndex].text = JSON.stringify({
  1536. ...JSON.parse(this.claudeMessages[lastApiReqIndex].text),
  1537. tokensIn: inputTokens,
  1538. tokensOut: outputTokens,
  1539. cacheWrites: cacheWriteTokens,
  1540. cacheReads: cacheReadTokens,
  1541. cost:
  1542. totalCost ??
  1543. calculateApiCost(
  1544. this.api.getModel().info,
  1545. inputTokens,
  1546. outputTokens,
  1547. cacheWriteTokens,
  1548. cacheReadTokens
  1549. ),
  1550. })
  1551. await this.saveClaudeMessages()
  1552. await this.providerRef.deref()?.postStateToWebview()
  1553. // now add to apiconversationhistory
  1554. // need to save assistant responses to file before proceeding to tool use since user can exit at any moment and we wouldn't be able to save the assistant's response
  1555. let didEndLoop = false
  1556. if (assistantMessage.length > 0) {
  1557. await this.addToApiConversationHistory({
  1558. role: "assistant",
  1559. content: [{ type: "text", text: assistantMessage }],
  1560. })
  1561. // in case the content blocks finished
  1562. // it may be the api stream finished after the last parsed content block was executed, so we are able to detect out of bounds and set userMessageContentReady to true (not you should not call presentAssistantMessage since if the last block is completed it will be presented again)
  1563. const completeBlocks = this.assistantMessageContent.filter((block) => !block.partial) // if there are any partial blocks after the stream ended we can consider them invalid
  1564. if (this.currentStreamingContentIndex >= completeBlocks.length) {
  1565. this.userMessageContentReady = true
  1566. }
  1567. await pWaitFor(() => this.userMessageContentReady)
  1568. // if the model did not tool use, then we need to tell it to either use a tool or attempt_completion
  1569. const didToolUse = this.assistantMessageContent.some((block) => block.type === "tool_use")
  1570. if (!didToolUse) {
  1571. this.userMessageContent.push({
  1572. type: "text",
  1573. text: formatResponse.noToolsUsed(),
  1574. })
  1575. }
  1576. const recDidEndLoop = await this.recursivelyMakeClaudeRequests(this.userMessageContent)
  1577. didEndLoop = recDidEndLoop
  1578. } else {
  1579. // 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
  1580. await this.say(
  1581. "error",
  1582. "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."
  1583. )
  1584. await this.addToApiConversationHistory({
  1585. role: "assistant",
  1586. content: [{ type: "text", text: "Failure: I did not provide a response." }],
  1587. })
  1588. }
  1589. return didEndLoop // will always be false for now
  1590. } catch (error) {
  1591. // this should never happen since the only thing that can throw an error is the attemptApiRequest, which is wrapped in a try catch that sends an ask where if noButtonTapped, will clear current task and destroy this instance. However to avoid unhandled promise rejection, we will end this loop which will end execution of this instance (see startTask)
  1592. return true
  1593. }
  1594. }
  1595. async loadContext(userContent: UserContent, includeFileDetails: boolean = false) {
  1596. return await Promise.all([
  1597. // Process userContent array, which contains various block types:
  1598. // TextBlockParam, ImageBlockParam, ToolUseBlockParam, and ToolResultBlockParam.
  1599. // We need to apply parseMentions() to:
  1600. // 1. All TextBlockParam's text (first user message with task)
  1601. // 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)
  1602. Promise.all(
  1603. userContent.map(async (block) => {
  1604. if (block.type === "text") {
  1605. return {
  1606. ...block,
  1607. text: await parseMentions(block.text, cwd, this.urlContentFetcher),
  1608. }
  1609. } else if (block.type === "tool_result") {
  1610. const isUserMessage = (text: string) => text.includes("<feedback>") || text.includes("<answer>")
  1611. if (typeof block.content === "string" && isUserMessage(block.content)) {
  1612. return {
  1613. ...block,
  1614. content: await parseMentions(block.content, cwd, this.urlContentFetcher),
  1615. }
  1616. } else if (Array.isArray(block.content)) {
  1617. const parsedContent = await Promise.all(
  1618. block.content.map(async (contentBlock) => {
  1619. if (contentBlock.type === "text" && isUserMessage(contentBlock.text)) {
  1620. return {
  1621. ...contentBlock,
  1622. text: await parseMentions(contentBlock.text, cwd, this.urlContentFetcher),
  1623. }
  1624. }
  1625. return contentBlock
  1626. })
  1627. )
  1628. return {
  1629. ...block,
  1630. content: parsedContent,
  1631. }
  1632. }
  1633. }
  1634. return block
  1635. })
  1636. ),
  1637. this.getEnvironmentDetails(includeFileDetails),
  1638. ])
  1639. }
  1640. async getEnvironmentDetails(includeFileDetails: boolean = false) {
  1641. let details = ""
  1642. // 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
  1643. details += "\n\n# VSCode Visible Files"
  1644. const visibleFiles = vscode.window.visibleTextEditors
  1645. ?.map((editor) => editor.document?.uri?.fsPath)
  1646. .filter(Boolean)
  1647. .map((absolutePath) => path.relative(cwd, absolutePath).toPosix())
  1648. .join("\n")
  1649. if (visibleFiles) {
  1650. details += `\n${visibleFiles}`
  1651. } else {
  1652. details += "\n(No visible files)"
  1653. }
  1654. details += "\n\n# VSCode Open Tabs"
  1655. const openTabs = vscode.window.tabGroups.all
  1656. .flatMap((group) => group.tabs)
  1657. .map((tab) => (tab.input as vscode.TabInputText)?.uri?.fsPath)
  1658. .filter(Boolean)
  1659. .map((absolutePath) => path.relative(cwd, absolutePath).toPosix())
  1660. .join("\n")
  1661. if (openTabs) {
  1662. details += `\n${openTabs}`
  1663. } else {
  1664. details += "\n(No open tabs)"
  1665. }
  1666. const busyTerminals = this.terminalManager.getTerminals(true)
  1667. const inactiveTerminals = this.terminalManager.getTerminals(false)
  1668. // const allTerminals = [...busyTerminals, ...inactiveTerminals]
  1669. if (busyTerminals.length > 0 && this.didEditFile) {
  1670. // || this.didEditFile
  1671. await delay(300) // delay after saving file to let terminals catch up
  1672. }
  1673. // let terminalWasBusy = false
  1674. if (busyTerminals.length > 0) {
  1675. // wait for terminals to cool down
  1676. // terminalWasBusy = allTerminals.some((t) => this.terminalManager.isProcessHot(t.id))
  1677. await pWaitFor(() => busyTerminals.every((t) => !this.terminalManager.isProcessHot(t.id)), {
  1678. interval: 100,
  1679. timeout: 15_000,
  1680. }).catch(() => {})
  1681. }
  1682. // 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
  1683. /*
  1684. let diagnosticsDetails = ""
  1685. 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
  1686. for (const [uri, fileDiagnostics] of diagnostics) {
  1687. const problems = fileDiagnostics.filter((d) => d.severity === vscode.DiagnosticSeverity.Error)
  1688. if (problems.length > 0) {
  1689. diagnosticsDetails += `\n## ${path.relative(cwd, uri.fsPath)}`
  1690. for (const diagnostic of problems) {
  1691. // let severity = diagnostic.severity === vscode.DiagnosticSeverity.Error ? "Error" : "Warning"
  1692. const line = diagnostic.range.start.line + 1 // VSCode lines are 0-indexed
  1693. const source = diagnostic.source ? `[${diagnostic.source}] ` : ""
  1694. diagnosticsDetails += `\n- ${source}Line ${line}: ${diagnostic.message}`
  1695. }
  1696. }
  1697. }
  1698. */
  1699. this.didEditFile = false // reset, this lets us know when to wait for saved files to update terminals
  1700. // waiting for updated diagnostics lets terminal output be the most up-to-date possible
  1701. let terminalDetails = ""
  1702. if (busyTerminals.length > 0) {
  1703. // terminals are cool, let's retrieve their output
  1704. terminalDetails += "\n\n# Active Terminals"
  1705. for (const busyTerminal of busyTerminals) {
  1706. terminalDetails += `\n## ${busyTerminal.lastCommand}`
  1707. const newOutput = this.terminalManager.getUnretrievedOutput(busyTerminal.id)
  1708. if (newOutput) {
  1709. terminalDetails += `\n### New Output\n${newOutput}`
  1710. } else {
  1711. // details += `\n(Still running, no new output)` // don't want to show this right after running the command
  1712. }
  1713. }
  1714. }
  1715. // only show inactive terminals if there's output to show
  1716. if (inactiveTerminals.length > 0) {
  1717. const inactiveTerminalOutputs = new Map<number, string>()
  1718. for (const inactiveTerminal of inactiveTerminals) {
  1719. const newOutput = this.terminalManager.getUnretrievedOutput(inactiveTerminal.id)
  1720. if (newOutput) {
  1721. inactiveTerminalOutputs.set(inactiveTerminal.id, newOutput)
  1722. }
  1723. }
  1724. if (inactiveTerminalOutputs.size > 0) {
  1725. terminalDetails += "\n\n# Inactive Terminals"
  1726. for (const [terminalId, newOutput] of inactiveTerminalOutputs) {
  1727. const inactiveTerminal = inactiveTerminals.find((t) => t.id === terminalId)
  1728. if (inactiveTerminal) {
  1729. terminalDetails += `\n## ${inactiveTerminal.lastCommand}`
  1730. terminalDetails += `\n### New Output\n${newOutput}`
  1731. }
  1732. }
  1733. }
  1734. }
  1735. // details += "\n\n# VSCode Workspace Errors"
  1736. // if (diagnosticsDetails) {
  1737. // details += diagnosticsDetails
  1738. // } else {
  1739. // details += "\n(No errors detected)"
  1740. // }
  1741. if (terminalDetails) {
  1742. details += terminalDetails
  1743. }
  1744. if (includeFileDetails) {
  1745. details += `\n\n# Current Working Directory (${cwd.toPosix()}) Files\n`
  1746. const isDesktop = arePathsEqual(cwd, path.join(os.homedir(), "Desktop"))
  1747. if (isDesktop) {
  1748. // don't want to immediately access desktop since it would show permission popup
  1749. details += "(Desktop files not shown automatically. Use list_files to explore if needed.)"
  1750. } else {
  1751. const [files, didHitLimit] = await listFiles(cwd, true, 200)
  1752. const result = formatResponse.formatFilesList(cwd, files, didHitLimit)
  1753. details += result
  1754. }
  1755. }
  1756. return `<environment_details>\n${details.trim()}\n</environment_details>`
  1757. }
  1758. }