Browse Source

Refactor out file helpers into fs.ts

Saoud Rizwan 1 year ago
parent
commit
2b63b91bfb

+ 28 - 97
src/core/ClaudeDev.ts

@@ -42,6 +42,8 @@ import { formatResponse } from "./prompts/responses"
 import { SYSTEM_PROMPT } from "./prompts/system"
 import { truncateHalfConversation } from "./sliding-window"
 import { ClaudeDevProvider } from "./webview/ClaudeDevProvider"
+import { calculateApiCost } from "../utils/cost"
+import { createDirectoriesForFile, fileExistsAtPath } from "../utils/fs"
 
 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
@@ -128,10 +130,7 @@ export class ClaudeDev {
 
 	private async getSavedApiConversationHistory(): Promise<Anthropic.MessageParam[]> {
 		const filePath = path.join(await this.ensureTaskDirectoryExists(), "api_conversation_history.json")
-		const fileExists = await fs
-			.access(filePath)
-			.then(() => true)
-			.catch(() => false)
+		const fileExists = await fileExistsAtPath(filePath)
 		if (fileExists) {
 			return JSON.parse(await fs.readFile(filePath, "utf8"))
 		}
@@ -160,10 +159,7 @@ export class ClaudeDev {
 
 	private async getSavedClaudeMessages(): Promise<ClaudeMessage[]> {
 		const filePath = path.join(await this.ensureTaskDirectoryExists(), "claude_messages.json")
-		const fileExists = await fs
-			.access(filePath)
-			.then(() => true)
-			.catch(() => false)
+		const fileExists = await fileExistsAtPath(filePath)
 		if (fileExists) {
 			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> {
 		// 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)
@@ -591,28 +597,6 @@ export class ClaudeDev {
 		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]
 	async writeToFile(relPath?: string, newContent?: string): Promise<[boolean, ToolResponse]> {
 		if (relPath === undefined) {
@@ -636,10 +620,7 @@ export class ClaudeDev {
 		this.consecutiveMistakeCount = 0
 		try {
 			const absolutePath = path.resolve(cwd, relPath)
-			const fileExists = await fs
-				.access(absolutePath)
-				.then(() => true)
-				.catch(() => false)
+			const fileExists = await fileExistsAtPath(absolutePath)
 
 			// if the file is already open, ensure it's not dirty before getting its contents
 			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
 
 			// 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(", ")}`)
 			// make sure the file exists before we open it
 			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) {
 		const patch = diff.createPatch(filename.toPosix(), oldStr, newStr)
 		const lines = patch.split("\n")
@@ -1404,10 +1340,7 @@ ${this.customInstructions.trim()}
 							fileExists = this.isEditingExistingFile
 						} else {
 							const absolutePath = path.resolve(cwd, relPath)
-							fileExists = await fs
-								.access(absolutePath)
-								.then(() => true)
-								.catch(() => false)
+							fileExists = await fileExistsAtPath(absolutePath)
 
 							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
 
 									// Keep track of newly created directories
-									this.editFileCreatedDirs = await this.createDirectoriesForFile(absolutePath)
+									this.editFileCreatedDirs = await createDirectoriesForFile(absolutePath)
 									// console.log(`Created directories: ${createdDirs.join(", ")}`)
 									// make sure the file exists before we open it
 									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
 
 									// Keep track of newly created directories
-									this.editFileCreatedDirs = await this.createDirectoriesForFile(absolutePath)
+									this.editFileCreatedDirs = await createDirectoriesForFile(absolutePath)
 									// console.log(`Created directories: ${createdDirs.join(", ")}`)
 									// make sure the file exists before we open it
 									if (!fileExists) {
@@ -2438,7 +2371,15 @@ ${this.customInstructions.trim()}
 				tokensOut: outputTokens,
 				cacheWrites: cacheWriteTokens,
 				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.providerRef.deref()?.postStateToWebview()
@@ -2666,14 +2607,4 @@ ${this.customInstructions.trim()}
 
 		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))
-	}
 }

+ 4 - 12
src/core/webview/ClaudeDevProvider.ts

@@ -17,6 +17,7 @@ import { getTheme } from "../../integrations/theme/getTheme"
 import { openFile, openImage } from "../../integrations/misc/open-file"
 import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
 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
@@ -505,10 +506,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
 			const taskDirPath = path.join(this.context.globalStorageUri.fsPath, "tasks", id)
 			const apiConversationHistoryFilePath = path.join(taskDirPath, "api_conversation_history.json")
 			const claudeMessagesFilePath = path.join(taskDirPath, "claude_messages.json")
-			const fileExists = await fs
-				.access(apiConversationHistoryFilePath)
-				.then(() => true)
-				.catch(() => false)
+			const fileExists = await fileExistsAtPath(apiConversationHistoryFilePath)
 			if (fileExists) {
 				const apiConversationHistory = JSON.parse(await fs.readFile(apiConversationHistoryFilePath, "utf8"))
 				return {
@@ -547,17 +545,11 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
 		const { taskDirPath, apiConversationHistoryFilePath, claudeMessagesFilePath } = await this.getTaskWithId(id)
 
 		// Delete the task files
-		const apiConversationHistoryFileExists = await fs
-			.access(apiConversationHistoryFilePath)
-			.then(() => true)
-			.catch(() => false)
+		const apiConversationHistoryFileExists = await fileExistsAtPath(apiConversationHistoryFilePath)
 		if (apiConversationHistoryFileExists) {
 			await fs.unlink(apiConversationHistoryFilePath)
 		}
-		const claudeMessagesFileExists = await fs
-			.access(claudeMessagesFilePath)
-			.then(() => true)
-			.catch(() => false)
+		const claudeMessagesFileExists = await fileExistsAtPath(claudeMessagesFilePath)
 		if (claudeMessagesFileExists) {
 			await fs.unlink(claudeMessagesFilePath)
 		}

+ 2 - 4
src/services/browser/UrlContentFetcher.ts

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

+ 2 - 4
src/services/tree-sitter/index.ts

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

+ 24 - 0
src/utils/cost.ts

@@ -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 - 0
src/utils/fs.ts

@@ -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
+	}
+}