Implement parseMentions

This commit is contained in:
Saoud Rizwan
2024-09-18 18:53:46 -04:00
parent ee8e64eece
commit b7dabb8d9e
4 changed files with 143 additions and 1 deletions

View File

@@ -26,6 +26,7 @@ import { findLast, findLastIndex, formatContentBlockToMarkdown } from "./utils"
import { truncateHalfConversation } from "./utils/context-management" import { truncateHalfConversation } from "./utils/context-management"
import { extractTextFromFile } from "./utils/extract-text" import { extractTextFromFile } from "./utils/extract-text"
import { regexSearchFiles } from "./utils/ripgrep" import { regexSearchFiles } from "./utils/ripgrep"
import { parseMentions } from "./utils/context-mentions"
const SYSTEM_PROMPT = 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. 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) { 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 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 } const result = { response: this.askResponse!, text: this.askResponseText, images: this.askResponseImages }
this.askResponse = undefined this.askResponse = undefined
this.askResponseText = undefined this.askResponseText = undefined

View File

@@ -54,7 +54,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
private view?: vscode.WebviewView | vscode.WebviewPanel private view?: vscode.WebviewView | vscode.WebviewPanel
private claudeDev?: ClaudeDev private claudeDev?: ClaudeDev
private workspaceTracker?: WorkspaceTracker private workspaceTracker?: WorkspaceTracker
private urlScraper?: UrlScraper urlScraper?: UrlScraper
private latestAnnouncementId = "sep-14-2024" // update to some unique identifier when we add a new announcement 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) { constructor(readonly context: vscode.ExtensionContext, private readonly outputChannel: vscode.OutputChannel) {

View File

@@ -1,6 +1,10 @@
import * as vscode from "vscode" import * as vscode from "vscode"
import * as path from "path" import * as path from "path"
import { openFile } from "./open-file" 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 { export function openMention(mention?: string): void {
if (!mention) { if (!mention) {
@@ -26,3 +30,126 @@ export function openMention(mention?: string): void {
vscode.env.openExternal(vscode.Uri.parse(mention)) vscode.env.openExternal(vscode.Uri.parse(mention))
} }
} }
export async function parseMentions(text: string, cwd: string, urlScraper?: UrlScraper): Promise<string> {
const mentions: Set<string> = 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<url_content url="${mention}">\n${markdown}\n</url_content>`
} catch (error) {
parsedText += `\n\n<url_content url="${mention}">\nError fetching content: ${error.message}\n</url_content>`
}
} 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<folder_content path="${mentionPath}">\n${content}\n</folder_content>`
} else {
parsedText += `\n\n<file_content path="${mentionPath}">\n${content}\n</file_content>`
}
} catch (error) {
if (mention.endsWith("/")) {
parsedText += `\n\n<folder_content path="${mentionPath}">\nError fetching content: ${error.message}\n</folder_content>`
} else {
parsedText += `\n\n<file_content path="${mentionPath}">\nError fetching content: ${error.message}\n</file_content>`
}
}
} else if (mention === "problems") {
try {
const diagnostics = await getWorkspaceDiagnostics(cwd)
parsedText += `\n\n<workspace_diagnostics>\n${diagnostics}\n</workspace_diagnostics>`
} catch (error) {
parsedText += `\n\n<workspace_diagnostics>\nError fetching diagnostics: ${error.message}\n</workspace_diagnostics>`
}
}
}
return parsedText
}
async function getFileOrFolderContent(mentionPath: string, cwd: string): Promise<string> {
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<string>[] = []
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) => `<file_content path="${filePath}">\n${content}\n</file_content>`)
.catch(
(error) =>
`<file_content path="${filePath}">\nError fetching content: ${error.message}\n</file_content>`
)
)
} 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<string> {
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
}

View File

@@ -18,6 +18,17 @@ export async function extractTextFromFile(filePath: string): Promise<string> {
return extractTextFromDOCX(filePath) return extractTextFromDOCX(filePath)
case ".ipynb": case ".ipynb":
return extractTextFromIPYNB(filePath) 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: default:
return await fs.readFile(filePath, "utf8") return await fs.readFile(filePath, "utf8")
} }