Refactor out file helpers into fs.ts

This commit is contained in:
Saoud Rizwan
2024-09-29 02:25:22 -04:00
parent ebead1b1fa
commit 2b63b91bfb
6 changed files with 107 additions and 117 deletions

View File

@@ -42,6 +42,8 @@ import { formatResponse } from "./prompts/responses"
import { SYSTEM_PROMPT } from "./prompts/system" import { SYSTEM_PROMPT } from "./prompts/system"
import { truncateHalfConversation } from "./sliding-window" import { truncateHalfConversation } from "./sliding-window"
import { ClaudeDevProvider } from "./webview/ClaudeDevProvider" import { ClaudeDevProvider } from "./webview/ClaudeDevProvider"
import { calculateApiCost } from "../utils/cost"
import { createDirectoriesForFile, fileExistsAtPath } from "../utils/fs"
const cwd = const cwd =
vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution 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
@@ -128,10 +130,7 @@ export class ClaudeDev {
private async getSavedApiConversationHistory(): Promise<Anthropic.MessageParam[]> { private async getSavedApiConversationHistory(): Promise<Anthropic.MessageParam[]> {
const filePath = path.join(await this.ensureTaskDirectoryExists(), "api_conversation_history.json") const filePath = path.join(await this.ensureTaskDirectoryExists(), "api_conversation_history.json")
const fileExists = await fs const fileExists = await fileExistsAtPath(filePath)
.access(filePath)
.then(() => true)
.catch(() => false)
if (fileExists) { if (fileExists) {
return JSON.parse(await fs.readFile(filePath, "utf8")) return JSON.parse(await fs.readFile(filePath, "utf8"))
} }
@@ -160,10 +159,7 @@ export class ClaudeDev {
private async getSavedClaudeMessages(): Promise<ClaudeMessage[]> { private async getSavedClaudeMessages(): Promise<ClaudeMessage[]> {
const filePath = path.join(await this.ensureTaskDirectoryExists(), "claude_messages.json") const filePath = path.join(await this.ensureTaskDirectoryExists(), "claude_messages.json")
const fileExists = await fs const fileExists = await fileExistsAtPath(filePath)
.access(filePath)
.then(() => true)
.catch(() => false)
if (fileExists) { if (fileExists) {
return JSON.parse(await fs.readFile(filePath, "utf8")) return JSON.parse(await fs.readFile(filePath, "utf8"))
} }
@@ -350,6 +346,16 @@ export class ClaudeDev {
} }
} }
async sayAndCreateMissingParamError(toolName: ToolName, paramName: string, relPath?: string) {
await this.say(
"error",
`Claude tried to use ${toolName}${
relPath ? ` for '${relPath.toPosix()}'` : ""
} without value for required parameter '${paramName}'. Retrying...`
)
return formatResponse.toolError(formatResponse.missingToolParameterError(paramName))
}
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 claudeMessages (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 ClaudeDev client (otherwise webview would show stale messages from previous session) // 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)
@@ -591,28 +597,6 @@ export class ClaudeDev {
this.urlContentFetcher.closeBrowser() this.urlContentFetcher.closeBrowser()
} }
calculateApiCost(
inputTokens: number,
outputTokens: number,
cacheCreationInputTokens?: number,
cacheReadInputTokens?: number
): number {
const modelCacheWritesPrice = this.api.getModel().info.cacheWritesPrice
let cacheWritesCost = 0
if (cacheCreationInputTokens && modelCacheWritesPrice) {
cacheWritesCost = (modelCacheWritesPrice / 1_000_000) * cacheCreationInputTokens
}
const modelCacheReadsPrice = this.api.getModel().info.cacheReadsPrice
let cacheReadsCost = 0
if (cacheReadInputTokens && modelCacheReadsPrice) {
cacheReadsCost = (modelCacheReadsPrice / 1_000_000) * cacheReadInputTokens
}
const baseInputCost = (this.api.getModel().info.inputPrice / 1_000_000) * inputTokens
const outputCost = (this.api.getModel().info.outputPrice / 1_000_000) * outputTokens
const totalCost = cacheWritesCost + cacheReadsCost + baseInputCost + outputCost
return totalCost
}
// return is [didUserRejectTool, ToolResponse] // return is [didUserRejectTool, ToolResponse]
async writeToFile(relPath?: string, newContent?: string): Promise<[boolean, ToolResponse]> { async writeToFile(relPath?: string, newContent?: string): Promise<[boolean, ToolResponse]> {
if (relPath === undefined) { if (relPath === undefined) {
@@ -636,10 +620,7 @@ export class ClaudeDev {
this.consecutiveMistakeCount = 0 this.consecutiveMistakeCount = 0
try { try {
const absolutePath = path.resolve(cwd, relPath) const absolutePath = path.resolve(cwd, relPath)
const fileExists = await fs const fileExists = await fileExistsAtPath(absolutePath)
.access(absolutePath)
.then(() => true)
.catch(() => false)
// if the file is already open, ensure it's not dirty before getting its contents // if the file is already open, ensure it's not dirty before getting its contents
if (fileExists) { if (fileExists) {
@@ -671,7 +652,7 @@ export class ClaudeDev {
// for new files, create any necessary directories and keep track of new directories to delete if the user denies the operation // for new files, create any necessary directories and keep track of new directories to delete if the user denies the operation
// Keep track of newly created directories // Keep track of newly created directories
const createdDirs: string[] = await this.createDirectoriesForFile(absolutePath) const createdDirs: string[] = await createDirectoriesForFile(absolutePath)
// console.log(`Created directories: ${createdDirs.join(", ")}`) // console.log(`Created directories: ${createdDirs.join(", ")}`)
// make sure the file exists before we open it // make sure the file exists before we open it
if (!fileExists) { if (!fileExists) {
@@ -992,51 +973,6 @@ export class ClaudeDev {
} }
} }
/**
* Asynchronously creates all non-existing subdirectories for a given file path
* and collects them in an array for later deletion.
*
* @param filePath - The full path to a file.
* @returns A promise that resolves to an array of newly created directories.
*/
async createDirectoriesForFile(filePath: string): Promise<string[]> {
const newDirectories: string[] = []
const normalizedFilePath = path.normalize(filePath) // Normalize path for cross-platform compatibility
const directoryPath = path.dirname(normalizedFilePath)
let currentPath = directoryPath
const dirsToCreate: string[] = []
// Traverse up the directory tree and collect missing directories
while (!(await this.exists(currentPath))) {
dirsToCreate.push(currentPath)
currentPath = path.dirname(currentPath)
}
// Create directories from the topmost missing one down to the target directory
for (let i = dirsToCreate.length - 1; i >= 0; i--) {
await fs.mkdir(dirsToCreate[i])
newDirectories.push(dirsToCreate[i])
}
return newDirectories
}
/**
* Helper function to check if a path exists.
*
* @param path - The path to check.
* @returns A promise that resolves to true if the path exists, false otherwise.
*/
async exists(filePath: string): Promise<boolean> {
try {
await fs.access(filePath)
return true
} catch {
return false
}
}
createPrettyPatch(filename = "file", oldStr: string, newStr: string) { createPrettyPatch(filename = "file", oldStr: string, newStr: string) {
const patch = diff.createPatch(filename.toPosix(), oldStr, newStr) const patch = diff.createPatch(filename.toPosix(), oldStr, newStr)
const lines = patch.split("\n") const lines = patch.split("\n")
@@ -1404,10 +1340,7 @@ ${this.customInstructions.trim()}
fileExists = this.isEditingExistingFile fileExists = this.isEditingExistingFile
} else { } else {
const absolutePath = path.resolve(cwd, relPath) const absolutePath = path.resolve(cwd, relPath)
fileExists = await fs fileExists = await fileExistsAtPath(absolutePath)
.access(absolutePath)
.then(() => true)
.catch(() => false)
this.isEditingExistingFile = fileExists this.isEditingExistingFile = fileExists
} }
@@ -1440,7 +1373,7 @@ ${this.customInstructions.trim()}
// for new files, create any necessary directories and keep track of new directories to delete if the user denies the operation // for new files, create any necessary directories and keep track of new directories to delete if the user denies the operation
// Keep track of newly created directories // Keep track of newly created directories
this.editFileCreatedDirs = await this.createDirectoriesForFile(absolutePath) this.editFileCreatedDirs = await createDirectoriesForFile(absolutePath)
// console.log(`Created directories: ${createdDirs.join(", ")}`) // console.log(`Created directories: ${createdDirs.join(", ")}`)
// make sure the file exists before we open it // make sure the file exists before we open it
if (!fileExists) { if (!fileExists) {
@@ -1528,7 +1461,7 @@ ${this.customInstructions.trim()}
// for new files, create any necessary directories and keep track of new directories to delete if the user denies the operation // for new files, create any necessary directories and keep track of new directories to delete if the user denies the operation
// Keep track of newly created directories // Keep track of newly created directories
this.editFileCreatedDirs = await this.createDirectoriesForFile(absolutePath) this.editFileCreatedDirs = await createDirectoriesForFile(absolutePath)
// console.log(`Created directories: ${createdDirs.join(", ")}`) // console.log(`Created directories: ${createdDirs.join(", ")}`)
// make sure the file exists before we open it // make sure the file exists before we open it
if (!fileExists) { if (!fileExists) {
@@ -2438,7 +2371,15 @@ ${this.customInstructions.trim()}
tokensOut: outputTokens, tokensOut: outputTokens,
cacheWrites: cacheWriteTokens, cacheWrites: cacheWriteTokens,
cacheReads: cacheReadTokens, cacheReads: cacheReadTokens,
cost: totalCost ?? this.calculateApiCost(inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens), cost:
totalCost ??
calculateApiCost(
this.api.getModel().info,
inputTokens,
outputTokens,
cacheWriteTokens,
cacheReadTokens
),
}) })
await this.saveClaudeMessages() await this.saveClaudeMessages()
await this.providerRef.deref()?.postStateToWebview() await this.providerRef.deref()?.postStateToWebview()
@@ -2666,14 +2607,4 @@ ${this.customInstructions.trim()}
return `<environment_details>\n${details.trim()}\n</environment_details>` return `<environment_details>\n${details.trim()}\n</environment_details>`
} }
async sayAndCreateMissingParamError(toolName: ToolName, paramName: string, relPath?: string) {
await this.say(
"error",
`Claude tried to use ${toolName}${
relPath ? ` for '${relPath.toPosix()}'` : ""
} without value for required parameter '${paramName}'. Retrying...`
)
return formatResponse.toolError(formatResponse.missingToolParameterError(paramName))
}
} }

View File

@@ -17,6 +17,7 @@ import { getTheme } from "../../integrations/theme/getTheme"
import { openFile, openImage } from "../../integrations/misc/open-file" import { openFile, openImage } from "../../integrations/misc/open-file"
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker" import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
import { openMention } from "../mentions" import { openMention } from "../mentions"
import { fileExistsAtPath } from "../../utils/fs"
/* /*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -505,10 +506,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
const taskDirPath = path.join(this.context.globalStorageUri.fsPath, "tasks", id) const taskDirPath = path.join(this.context.globalStorageUri.fsPath, "tasks", id)
const apiConversationHistoryFilePath = path.join(taskDirPath, "api_conversation_history.json") const apiConversationHistoryFilePath = path.join(taskDirPath, "api_conversation_history.json")
const claudeMessagesFilePath = path.join(taskDirPath, "claude_messages.json") const claudeMessagesFilePath = path.join(taskDirPath, "claude_messages.json")
const fileExists = await fs const fileExists = await fileExistsAtPath(apiConversationHistoryFilePath)
.access(apiConversationHistoryFilePath)
.then(() => true)
.catch(() => false)
if (fileExists) { if (fileExists) {
const apiConversationHistory = JSON.parse(await fs.readFile(apiConversationHistoryFilePath, "utf8")) const apiConversationHistory = JSON.parse(await fs.readFile(apiConversationHistoryFilePath, "utf8"))
return { return {
@@ -547,17 +545,11 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
const { taskDirPath, apiConversationHistoryFilePath, claudeMessagesFilePath } = await this.getTaskWithId(id) const { taskDirPath, apiConversationHistoryFilePath, claudeMessagesFilePath } = await this.getTaskWithId(id)
// Delete the task files // Delete the task files
const apiConversationHistoryFileExists = await fs const apiConversationHistoryFileExists = await fileExistsAtPath(apiConversationHistoryFilePath)
.access(apiConversationHistoryFilePath)
.then(() => true)
.catch(() => false)
if (apiConversationHistoryFileExists) { if (apiConversationHistoryFileExists) {
await fs.unlink(apiConversationHistoryFilePath) await fs.unlink(apiConversationHistoryFilePath)
} }
const claudeMessagesFileExists = await fs const claudeMessagesFileExists = await fileExistsAtPath(claudeMessagesFilePath)
.access(claudeMessagesFilePath)
.then(() => true)
.catch(() => false)
if (claudeMessagesFileExists) { if (claudeMessagesFileExists) {
await fs.unlink(claudeMessagesFilePath) await fs.unlink(claudeMessagesFilePath)
} }

View File

@@ -8,6 +8,7 @@ import TurndownService from "turndown"
import PCR from "puppeteer-chromium-resolver" import PCR from "puppeteer-chromium-resolver"
import pWaitFor from "p-wait-for" import pWaitFor from "p-wait-for"
import delay from "delay" import delay from "delay"
import { fileExistsAtPath } from "../../utils/fs"
interface PCRStats { interface PCRStats {
puppeteer: { launch: typeof launch } puppeteer: { launch: typeof launch }
@@ -30,10 +31,7 @@ export class UrlContentFetcher {
} }
const puppeteerDir = path.join(globalStoragePath, "puppeteer") const puppeteerDir = path.join(globalStoragePath, "puppeteer")
const dirExists = await fs const dirExists = await fileExistsAtPath(puppeteerDir)
.access(puppeteerDir)
.then(() => true)
.catch(() => false)
if (!dirExists) { if (!dirExists) {
await fs.mkdir(puppeteerDir, { recursive: true }) await fs.mkdir(puppeteerDir, { recursive: true })
} }

View File

@@ -2,14 +2,12 @@ import * as fs from "fs/promises"
import * as path from "path" import * as path from "path"
import { listFiles } from "../glob/list-files" import { listFiles } from "../glob/list-files"
import { LanguageParser, loadRequiredLanguageParsers } from "./languageParser" import { LanguageParser, loadRequiredLanguageParsers } from "./languageParser"
import { fileExistsAtPath } from "../../utils/fs"
// TODO: implement caching behavior to avoid having to keep analyzing project for new tasks. // TODO: implement caching behavior to avoid having to keep analyzing project for new tasks.
export async function parseSourceCodeForDefinitionsTopLevel(dirPath: string): Promise<string> { export async function parseSourceCodeForDefinitionsTopLevel(dirPath: string): Promise<string> {
// check if the path exists // check if the path exists
const dirExists = await fs const dirExists = await fileExistsAtPath(path.resolve(dirPath))
.access(path.resolve(dirPath))
.then(() => true)
.catch(() => false)
if (!dirExists) { if (!dirExists) {
return "This directory does not exist or you do not have permission to access it." return "This directory does not exist or you do not have permission to access it."
} }

24
src/utils/cost.ts Normal file
View File

@@ -0,0 +1,24 @@
import { ModelInfo } from "../shared/api"
export function calculateApiCost(
modelInfo: ModelInfo,
inputTokens: number,
outputTokens: number,
cacheCreationInputTokens?: number,
cacheReadInputTokens?: number
): number {
const modelCacheWritesPrice = modelInfo.cacheWritesPrice
let cacheWritesCost = 0
if (cacheCreationInputTokens && modelCacheWritesPrice) {
cacheWritesCost = (modelCacheWritesPrice / 1_000_000) * cacheCreationInputTokens
}
const modelCacheReadsPrice = modelInfo.cacheReadsPrice
let cacheReadsCost = 0
if (cacheReadInputTokens && modelCacheReadsPrice) {
cacheReadsCost = (modelCacheReadsPrice / 1_000_000) * cacheReadInputTokens
}
const baseInputCost = (modelInfo.inputPrice / 1_000_000) * inputTokens
const outputCost = (modelInfo.outputPrice / 1_000_000) * outputTokens
const totalCost = cacheWritesCost + cacheReadsCost + baseInputCost + outputCost
return totalCost
}

47
src/utils/fs.ts Normal file
View File

@@ -0,0 +1,47 @@
import fs from "fs/promises"
import * as path from "path"
/**
* Asynchronously creates all non-existing subdirectories for a given file path
* and collects them in an array for later deletion.
*
* @param filePath - The full path to a file.
* @returns A promise that resolves to an array of newly created directories.
*/
export async function createDirectoriesForFile(filePath: string): Promise<string[]> {
const newDirectories: string[] = []
const normalizedFilePath = path.normalize(filePath) // Normalize path for cross-platform compatibility
const directoryPath = path.dirname(normalizedFilePath)
let currentPath = directoryPath
const dirsToCreate: string[] = []
// Traverse up the directory tree and collect missing directories
while (!(await fileExistsAtPath(currentPath))) {
dirsToCreate.push(currentPath)
currentPath = path.dirname(currentPath)
}
// Create directories from the topmost missing one down to the target directory
for (let i = dirsToCreate.length - 1; i >= 0; i--) {
await fs.mkdir(dirsToCreate[i])
newDirectories.push(dirsToCreate[i])
}
return newDirectories
}
/**
* Helper function to check if a path exists.
*
* @param path - The path to check.
* @returns A promise that resolves to true if the path exists, false otherwise.
*/
export async function fileExistsAtPath(filePath: string): Promise<boolean> {
try {
await fs.access(filePath)
return true
} catch {
return false
}
}