From b7dabb8d9e5bc0e1d7af9e2b717da2ba2b4e4edc Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Wed, 18 Sep 2024 18:53:46 -0400 Subject: [PATCH] Implement parseMentions --- src/ClaudeDev.ts | 4 + src/providers/ClaudeDevProvider.ts | 2 +- src/utils/context-mentions.ts | 127 +++++++++++++++++++++++++++++ src/utils/extract-text.ts | 11 +++ 4 files changed, 143 insertions(+), 1 deletion(-) diff --git a/src/ClaudeDev.ts b/src/ClaudeDev.ts index 60a3770..01af336 100644 --- a/src/ClaudeDev.ts +++ b/src/ClaudeDev.ts @@ -26,6 +26,7 @@ import { findLast, findLastIndex, formatContentBlockToMarkdown } from "./utils" import { truncateHalfConversation } from "./utils/context-management" import { extractTextFromFile } from "./utils/extract-text" import { regexSearchFiles } from "./utils/ripgrep" +import { parseMentions } from "./utils/context-mentions" const SYSTEM_PROMPT = async () => `You are Claude Dev, a highly skilled software developer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. @@ -420,6 +421,9 @@ export class ClaudeDev { if (this.lastMessageTs !== askTs) { 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 } + if (this.askResponse === "messageResponse" && this.askResponseText) { + this.askResponseText = await parseMentions(this.askResponseText, cwd, this.providerRef.deref()?.urlScraper) + } const result = { response: this.askResponse!, text: this.askResponseText, images: this.askResponseImages } this.askResponse = undefined this.askResponseText = undefined diff --git a/src/providers/ClaudeDevProvider.ts b/src/providers/ClaudeDevProvider.ts index 063bb3a..7cc4ab4 100644 --- a/src/providers/ClaudeDevProvider.ts +++ b/src/providers/ClaudeDevProvider.ts @@ -54,7 +54,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider { private view?: vscode.WebviewView | vscode.WebviewPanel private claudeDev?: ClaudeDev private workspaceTracker?: WorkspaceTracker - private urlScraper?: UrlScraper + urlScraper?: UrlScraper private latestAnnouncementId = "sep-14-2024" // update to some unique identifier when we add a new announcement constructor(readonly context: vscode.ExtensionContext, private readonly outputChannel: vscode.OutputChannel) { diff --git a/src/utils/context-mentions.ts b/src/utils/context-mentions.ts index 07e7c2d..2843295 100644 --- a/src/utils/context-mentions.ts +++ b/src/utils/context-mentions.ts @@ -1,6 +1,10 @@ import * as vscode from "vscode" import * as path from "path" import { openFile } from "./open-file" +import { UrlScraper } from "./UrlScraper" +import { mentionRegexGlobal } from "../shared/context-mentions" +import fs from "fs/promises" +import { extractTextFromFile } from "./extract-text" export function openMention(mention?: string): void { if (!mention) { @@ -26,3 +30,126 @@ export function openMention(mention?: string): void { vscode.env.openExternal(vscode.Uri.parse(mention)) } } + +export async function parseMentions(text: string, cwd: string, urlScraper?: UrlScraper): Promise { + const mentions: Set = new Set() + let parsedText = text.replace(mentionRegexGlobal, (match, mention) => { + mentions.add(mention) + if (mention.startsWith("http")) { + return `'${mention}' (see below for site content)` + } else if (mention.startsWith("/")) { + return mention.endsWith("/") + ? `'${mention}' (see below for folder contents)` + : `'${mention}' (see below for file contents)` + } else if (mention === "problems") { + return `Workspace Problems (see below for diagnostics)` + } + return match + }) + + for (const mention of mentions) { + if (mention.startsWith("http") && urlScraper) { + try { + const markdown = await urlScraper.urlToMarkdown(mention) + parsedText += `\n\n\n${markdown}\n` + } catch (error) { + parsedText += `\n\n\nError fetching content: ${error.message}\n` + } + } else if (mention.startsWith("/")) { + const mentionPath = mention.slice(1) // Remove the leading '/' + try { + const content = await getFileOrFolderContent(mentionPath, cwd) + if (mention.endsWith("/")) { + parsedText += `\n\n\n${content}\n` + } else { + parsedText += `\n\n\n${content}\n` + } + } catch (error) { + if (mention.endsWith("/")) { + parsedText += `\n\n\nError fetching content: ${error.message}\n` + } else { + parsedText += `\n\n\nError fetching content: ${error.message}\n` + } + } + } else if (mention === "problems") { + try { + const diagnostics = await getWorkspaceDiagnostics(cwd) + parsedText += `\n\n\n${diagnostics}\n` + } catch (error) { + parsedText += `\n\n\nError fetching diagnostics: ${error.message}\n` + } + } + } + + return parsedText +} + +async function getFileOrFolderContent(mentionPath: string, cwd: string): Promise { + const absPath = path.resolve(cwd, mentionPath) + + try { + const stats = await fs.stat(absPath) + + if (stats.isFile()) { + const content = await extractTextFromFile(absPath) + return content + } else if (stats.isDirectory()) { + const entries = await fs.readdir(absPath, { withFileTypes: true }) + let directoryContent = "" + const fileContentPromises: Promise[] = [] + entries.forEach((entry) => { + if (entry.isFile()) { + directoryContent += `- File: ${entry.name}\n` + const filePath = path.join(mentionPath, entry.name) + const absoluteFilePath = path.resolve(absPath, entry.name) + // const relativeFilePath = path.relative(cwd, absoluteFilePath); + fileContentPromises.push( + extractTextFromFile(absoluteFilePath) + .then((content) => `\n${content}\n`) + .catch( + (error) => + `\nError fetching content: ${error.message}\n` + ) + ) + } else if (entry.isDirectory()) { + directoryContent += `- Directory: ${entry.name}/\n` + // not recursively getting folder contents + } else { + directoryContent += `- Other: ${entry.name}\n` + } + }) + const fileContents = await Promise.all(fileContentPromises) + return `${directoryContent}\n${fileContents.join("\n")}` + } else { + return "Unsupported file type." + } + } catch (error) { + throw new Error(`Failed to access path "${mentionPath}": ${error.message}`) + } +} + +async function getWorkspaceDiagnostics(cwd: string): Promise { + const diagnostics = vscode.languages.getDiagnostics() + + let diagnosticsDetails = "" + for (const [uri, fileDiagnostics] of diagnostics) { + const problems = fileDiagnostics.filter( + (d) => d.severity === vscode.DiagnosticSeverity.Error || d.severity === vscode.DiagnosticSeverity.Warning + ) + if (problems.length > 0) { + diagnosticsDetails += `\nFile: ${path.relative(cwd, uri.fsPath)}` + for (const diagnostic of problems) { + let severity = diagnostic.severity === vscode.DiagnosticSeverity.Error ? "Error" : "Warning" + const line = diagnostic.range.start.line + 1 // VSCode lines are 0-indexed + const source = diagnostic.source ? `${diagnostic.source} ` : "" + diagnosticsDetails += `\n- [${source}${severity}] Line ${line}: ${diagnostic.message}` + } + } + } + + if (!diagnosticsDetails) { + return "No problems detected." + } + + return diagnosticsDetails +} diff --git a/src/utils/extract-text.ts b/src/utils/extract-text.ts index 65d5469..414f58f 100644 --- a/src/utils/extract-text.ts +++ b/src/utils/extract-text.ts @@ -18,6 +18,17 @@ export async function extractTextFromFile(filePath: string): Promise { return extractTextFromDOCX(filePath) case ".ipynb": return extractTextFromIPYNB(filePath) + case ".jpg": + case ".jpeg": + case ".png": + case ".gif": + case ".webp": + case ".mp4": + case ".mp3": + case ".wav": + case ".avi": + case ".mov": + return "Cannot read media file." default: return await fs.readFile(filePath, "utf8") }