diff --git a/src/core/ClaudeDev.ts b/src/core/ClaudeDev.ts index 613824f..3526706 100644 --- a/src/core/ClaudeDev.ts +++ b/src/core/ClaudeDev.ts @@ -41,7 +41,7 @@ import { parseMentions } from "./mentions" import { formatResponse } from "./prompts/responses" import { SYSTEM_PROMPT } from "./prompts/system" import { truncateHalfConversation } from "./sliding-window" -import { ClaudeDevProvider } from "./webview/ClaudeDevProvider" +import { ClaudeDevProvider, GlobalFileNames } from "./webview/ClaudeDevProvider" import { calculateApiCost } from "../utils/cost" import { createDirectoriesForFile, fileExistsAtPath } from "../utils/fs" @@ -129,7 +129,7 @@ export class ClaudeDev { } private async getSavedApiConversationHistory(): Promise { - const filePath = path.join(await this.ensureTaskDirectoryExists(), "api_conversation_history.json") + const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.apiConversationHistory) const fileExists = await fileExistsAtPath(filePath) if (fileExists) { return JSON.parse(await fs.readFile(filePath, "utf8")) @@ -149,7 +149,7 @@ export class ClaudeDev { private async saveApiConversationHistory() { try { - const filePath = path.join(await this.ensureTaskDirectoryExists(), "api_conversation_history.json") + const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.apiConversationHistory) await fs.writeFile(filePath, JSON.stringify(this.apiConversationHistory)) } catch (error) { // in the off chance this fails, we don't want to stop the task @@ -158,7 +158,7 @@ export class ClaudeDev { } private async getSavedClaudeMessages(): Promise { - const filePath = path.join(await this.ensureTaskDirectoryExists(), "claude_messages.json") + const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.claudeMessages) const fileExists = await fileExistsAtPath(filePath) if (fileExists) { return JSON.parse(await fs.readFile(filePath, "utf8")) @@ -178,7 +178,7 @@ export class ClaudeDev { private async saveClaudeMessages() { try { - const filePath = path.join(await this.ensureTaskDirectoryExists(), "claude_messages.json") + const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.claudeMessages) await fs.writeFile(filePath, JSON.stringify(this.claudeMessages)) // combined as they are in ChatView const apiMetrics = getApiMetrics(combineApiRequests(combineCommandSequences(this.claudeMessages.slice(1)))) @@ -1019,45 +1019,6 @@ export class ClaudeDev { } } - formatFilesList(absolutePath: string, files: string[], didHitLimit: boolean): string { - const sorted = files - .map((file) => { - // convert absolute path to relative path - const relativePath = path.relative(absolutePath, file).toPosix() - return file.endsWith("/") ? relativePath + "/" : relativePath - }) - // Sort so files are listed under their respective directories to make it clear what files are children of what directories. Since we build file list top down, even if file list is truncated it will show directories that claude can then explore further. - .sort((a, b) => { - const aParts = a.split("/") // only works if we use toPosix first - const bParts = b.split("/") - for (let i = 0; i < Math.min(aParts.length, bParts.length); i++) { - if (aParts[i] !== bParts[i]) { - // If one is a directory and the other isn't at this level, sort the directory first - if (i + 1 === aParts.length && i + 1 < bParts.length) { - return -1 - } - if (i + 1 === bParts.length && i + 1 < aParts.length) { - return 1 - } - // Otherwise, sort alphabetically - return aParts[i].localeCompare(bParts[i], undefined, { numeric: true, sensitivity: "base" }) - } - } - // If all parts are the same up to the length of the shorter path, - // the shorter one comes first - return aParts.length - bParts.length - }) - if (didHitLimit) { - return `${sorted.join( - "\n" - )}\n\n(File list truncated. Use list_files on specific subdirectories if you need to explore further.)` - } else if (sorted.length === 0 || (sorted.length === 1 && sorted[0] === "")) { - return "No files found." - } else { - return sorted.join("\n") - } - } - async executeCommandTool( command: string, returnEmptyStringOnSuccess: boolean = false @@ -1739,7 +1700,7 @@ ${this.customInstructions.trim()} this.consecutiveMistakeCount = 0 const absolutePath = path.resolve(cwd, relDirPath) const [files, didHitLimit] = await listFiles(absolutePath, recursive, 200) - const result = this.formatFilesList(absolutePath, files, didHitLimit) + const result = formatResponse.formatFilesList(absolutePath, files, didHitLimit) const completeMessage = JSON.stringify({ ...sharedMessageProps, content: result, @@ -2600,7 +2561,7 @@ ${this.customInstructions.trim()} details += "(Desktop files not shown automatically. Use list_files to explore if needed.)" } else { const [files, didHitLimit] = await listFiles(cwd, true, 200) - const result = this.formatFilesList(cwd, files, didHitLimit) + const result = formatResponse.formatFilesList(cwd, files, didHitLimit) details += result } } diff --git a/src/core/prompts/responses.ts b/src/core/prompts/responses.ts index 9d2a613..47b87ab 100644 --- a/src/core/prompts/responses.ts +++ b/src/core/prompts/responses.ts @@ -1,4 +1,5 @@ import { Anthropic } from "@anthropic-ai/sdk" +import * as path from "path" export const formatResponse = { toolDenied: () => `The user denied this operation.`, @@ -34,6 +35,45 @@ export const formatResponse = { imageBlocks: (images?: string[]): Anthropic.ImageBlockParam[] => { return formatImagesIntoBlocks(images) }, + + formatFilesList: (absolutePath: string, files: string[], didHitLimit: boolean): string => { + const sorted = files + .map((file) => { + // convert absolute path to relative path + const relativePath = path.relative(absolutePath, file).toPosix() + return file.endsWith("/") ? relativePath + "/" : relativePath + }) + // Sort so files are listed under their respective directories to make it clear what files are children of what directories. Since we build file list top down, even if file list is truncated it will show directories that claude can then explore further. + .sort((a, b) => { + const aParts = a.split("/") // only works if we use toPosix first + const bParts = b.split("/") + for (let i = 0; i < Math.min(aParts.length, bParts.length); i++) { + if (aParts[i] !== bParts[i]) { + // If one is a directory and the other isn't at this level, sort the directory first + if (i + 1 === aParts.length && i + 1 < bParts.length) { + return -1 + } + if (i + 1 === bParts.length && i + 1 < aParts.length) { + return 1 + } + // Otherwise, sort alphabetically + return aParts[i].localeCompare(bParts[i], undefined, { numeric: true, sensitivity: "base" }) + } + } + // If all parts are the same up to the length of the shorter path, + // the shorter one comes first + return aParts.length - bParts.length + }) + if (didHitLimit) { + return `${sorted.join( + "\n" + )}\n\n(File list truncated. Use list_files on specific subdirectories if you need to explore further.)` + } else if (sorted.length === 0 || (sorted.length === 1 && sorted[0] === "")) { + return "No files found." + } else { + return sorted.join("\n") + } + }, } // to avoid circular dependency diff --git a/src/core/webview/ClaudeDevProvider.ts b/src/core/webview/ClaudeDevProvider.ts index 3bf4d42..065c2f7 100644 --- a/src/core/webview/ClaudeDevProvider.ts +++ b/src/core/webview/ClaudeDevProvider.ts @@ -51,6 +51,11 @@ type GlobalStateKey = | "anthropicBaseUrl" | "azureApiVersion" +export const GlobalFileNames = { + apiConversationHistory: "api_conversation_history.json", + claudeMessages: "claude_messages.json", +} + export class ClaudeDevProvider implements vscode.WebviewViewProvider { public static readonly sideBarId = "claude-dev.SidebarProvider" // used in package.json as the view's id. This value cannot be changed due to how vscode caches views based on their id, and updating the id would break existing instances of the extension. public static readonly tabPanelId = "claude-dev.TabPanelProvider" @@ -504,8 +509,8 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider { const historyItem = history.find((item) => item.id === id) if (historyItem) { 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 apiConversationHistoryFilePath = path.join(taskDirPath, GlobalFileNames.apiConversationHistory) + const claudeMessagesFilePath = path.join(taskDirPath, GlobalFileNames.claudeMessages) const fileExists = await fileExistsAtPath(apiConversationHistoryFilePath) if (fileExists) { const apiConversationHistory = JSON.parse(await fs.readFile(apiConversationHistoryFilePath, "utf8"))