mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 20:31:37 -05:00
Add diagnostics context to environment details
This commit is contained in:
6
package-lock.json
generated
6
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "1.6.9",
|
||||
"version": "1.6.10",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "1.6.9",
|
||||
"version": "1.6.10",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/bedrock-sdk": "^0.10.2",
|
||||
@@ -21,6 +21,7 @@
|
||||
"default-shell": "^2.2.0",
|
||||
"delay": "^6.0.0",
|
||||
"diff": "^5.2.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"globby": "^14.0.2",
|
||||
"mammoth": "^1.8.0",
|
||||
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
||||
@@ -6143,7 +6144,6 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
|
||||
@@ -157,6 +157,7 @@
|
||||
"default-shell": "^2.2.0",
|
||||
"delay": "^6.0.0",
|
||||
"diff": "^5.2.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"globby": "^14.0.2",
|
||||
"mammoth": "^1.8.0",
|
||||
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
||||
|
||||
137
src/ClaudeDev.ts
137
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 DiagnosticsMonitor from "./integrations/DiagnosticsMonitor"
|
||||
|
||||
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.
|
||||
@@ -249,6 +250,8 @@ export class ClaudeDev {
|
||||
readonly taskId: string
|
||||
private api: ApiHandler
|
||||
private terminalManager: TerminalManager
|
||||
private diagnosticsMonitor: DiagnosticsMonitor
|
||||
private didEditFile: boolean = false
|
||||
private customInstructions?: string
|
||||
private alwaysAllowReadOnly: boolean
|
||||
apiConversationHistory: Anthropic.MessageParam[] = []
|
||||
@@ -273,6 +276,7 @@ export class ClaudeDev {
|
||||
this.providerRef = new WeakRef(provider)
|
||||
this.api = buildApiHandler(apiConfiguration)
|
||||
this.terminalManager = new TerminalManager()
|
||||
this.diagnosticsMonitor = new DiagnosticsMonitor()
|
||||
this.customInstructions = customInstructions
|
||||
this.alwaysAllowReadOnly = alwaysAllowReadOnly ?? false
|
||||
|
||||
@@ -673,6 +677,7 @@ export class ClaudeDev {
|
||||
abortTask() {
|
||||
this.abort = true // will stop any autonomously running promises
|
||||
this.terminalManager.disposeAll()
|
||||
this.diagnosticsMonitor.dispose()
|
||||
}
|
||||
|
||||
async executeTool(toolName: ToolName, toolInput: any): Promise<[boolean, ToolResponse]> {
|
||||
@@ -971,10 +976,12 @@ export class ClaudeDev {
|
||||
return [true, await this.formatToolDenied()]
|
||||
}
|
||||
|
||||
// Save the changes
|
||||
const editedContent = updatedDocument.getText()
|
||||
if (updatedDocument.isDirty) {
|
||||
await updatedDocument.save()
|
||||
}
|
||||
this.didEditFile = true
|
||||
|
||||
// Read the potentially edited content from the document
|
||||
|
||||
@@ -1620,8 +1627,9 @@ ${this.customInstructions.trim()}
|
||||
"api_req_started",
|
||||
JSON.stringify({
|
||||
request:
|
||||
userContent.map(formatContentBlockToMarkdown).join("\n\n") +
|
||||
"\n\n<environment_details>\nLoading...\n</environment_details>",
|
||||
userContent
|
||||
.map((block) => formatContentBlockToMarkdown(block, this.apiConversationHistory))
|
||||
.join("\n\n") + "\n\n<environment_details>\nLoading...\n</environment_details>",
|
||||
})
|
||||
)
|
||||
|
||||
@@ -1636,7 +1644,9 @@ ${this.customInstructions.trim()}
|
||||
// since we sent off a placeholder api_req_started message to update the webview while waiting to actually start the API request (to load potential details for example), we need to update the text of that message
|
||||
const lastApiReqIndex = findLastIndex(this.claudeMessages, (m) => m.say === "api_req_started")
|
||||
this.claudeMessages[lastApiReqIndex].text = JSON.stringify({
|
||||
request: userContent.map(formatContentBlockToMarkdown).join("\n\n"),
|
||||
request: userContent
|
||||
.map((block) => formatContentBlockToMarkdown(block, this.apiConversationHistory))
|
||||
.join("\n\n"),
|
||||
})
|
||||
await this.saveClaudeMessages()
|
||||
await this.providerRef.deref()?.postStateToWebview()
|
||||
@@ -1816,74 +1826,52 @@ ${this.customInstructions.trim()}
|
||||
}
|
||||
|
||||
async getEnvironmentDetails(includeFileDetails: boolean = false) {
|
||||
let details = `<environment_details>
|
||||
# VSCode Visible Files
|
||||
${
|
||||
vscode.window.visibleTextEditors
|
||||
?.map((editor) => editor.document?.uri?.fsPath)
|
||||
.filter(Boolean)
|
||||
.map((absolutePath) => path.relative(cwd, absolutePath))
|
||||
.join("\n") || "(No files open)"
|
||||
}
|
||||
let details = ""
|
||||
|
||||
# VSCode Open Tabs
|
||||
${
|
||||
vscode.window.tabGroups.all
|
||||
.flatMap((group) => group.tabs)
|
||||
.map((tab) => (tab.input as vscode.TabInputText)?.uri?.fsPath)
|
||||
.filter(Boolean)
|
||||
.map((absolutePath) => path.relative(cwd, absolutePath))
|
||||
.join("\n") || "(No tabs open)"
|
||||
}`
|
||||
const visibleFiles = vscode.window.visibleTextEditors
|
||||
?.map((editor) => editor.document?.uri?.fsPath)
|
||||
.filter(Boolean)
|
||||
.map((absolutePath) => path.relative(cwd, absolutePath))
|
||||
.join("\n")
|
||||
if (visibleFiles) {
|
||||
details += `\n\n# VSCode Visible Files\n${visibleFiles}`
|
||||
}
|
||||
|
||||
// Get diagnostics for all open files in the workspace
|
||||
// const diagnostics = vscode.languages.getDiagnostics()
|
||||
// const relevantDiagnostics = diagnostics.filter(([_, fileDiagnostics]) =>
|
||||
// fileDiagnostics.some(
|
||||
// (d) =>
|
||||
// d.severity === vscode.DiagnosticSeverity.Error || d.severity === vscode.DiagnosticSeverity.Warning
|
||||
// )
|
||||
// )
|
||||
|
||||
// if (relevantDiagnostics.length > 0) {
|
||||
// details += "\n\n# VSCode Workspace Diagnostics"
|
||||
// for (const [uri, fileDiagnostics] of relevantDiagnostics) {
|
||||
// const relativePath = path.relative(cwd, uri.fsPath)
|
||||
// details += `\n## ${relativePath}:`
|
||||
// for (const diagnostic of fileDiagnostics) {
|
||||
// if (
|
||||
// diagnostic.severity === vscode.DiagnosticSeverity.Error ||
|
||||
// diagnostic.severity === vscode.DiagnosticSeverity.Warning
|
||||
// ) {
|
||||
// let severity = diagnostic.severity === vscode.DiagnosticSeverity.Error ? "Error" : "Warning"
|
||||
// const line = diagnostic.range.start.line + 1 // VSCode lines are 0-indexed
|
||||
// details += `\n- [${severity}] Line ${line}: ${diagnostic.message}`
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
const openTabs = vscode.window.tabGroups.all
|
||||
.flatMap((group) => group.tabs)
|
||||
.map((tab) => (tab.input as vscode.TabInputText)?.uri?.fsPath)
|
||||
.filter(Boolean)
|
||||
.map((absolutePath) => path.relative(cwd, absolutePath))
|
||||
.join("\n")
|
||||
if (openTabs) {
|
||||
details += `\n\n# VSCode Open Tabs\n${openTabs}`
|
||||
}
|
||||
|
||||
const busyTerminals = this.terminalManager.getTerminals(true)
|
||||
|
||||
if (busyTerminals.length > 0 || this.didEditFile) {
|
||||
await delay(500) // delay after saving file to let terminals/diagnostics catch up
|
||||
}
|
||||
|
||||
let terminalDetails = "" // want to place these at the end, but want to wait for diagnostics to load last since dev servers (compilers like webpack) will first re-compile and then send diagnostics
|
||||
if (busyTerminals.length > 0) {
|
||||
// wait for terminals to cool down
|
||||
await delay(500) // delay after saving file
|
||||
await pWaitFor(() => busyTerminals.every((t) => !this.terminalManager.isProcessHot(t.id)), {
|
||||
interval: 100,
|
||||
timeout: 15_000,
|
||||
}).catch(() => {})
|
||||
// terminals are cool, let's retrieve their output
|
||||
details += "\n\n# Active Terminals"
|
||||
terminalDetails += "\n\n# Active Terminals"
|
||||
for (const busyTerminal of busyTerminals) {
|
||||
details += `\n## ${busyTerminal.lastCommand}`
|
||||
terminalDetails += `\n## ${busyTerminal.lastCommand}`
|
||||
const newOutput = this.terminalManager.getUnretrievedOutput(busyTerminal.id)
|
||||
if (newOutput) {
|
||||
details += `\n### New Output\n${newOutput}`
|
||||
terminalDetails += `\n### New Output\n${newOutput}`
|
||||
} else {
|
||||
// details += `\n(Still running, no new output)` // don't want to show this right after running the command
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// only show inactive terminals if there's output to show
|
||||
const inactiveTerminals = this.terminalManager.getTerminals(false)
|
||||
if (inactiveTerminals.length > 0) {
|
||||
@@ -1895,30 +1883,59 @@ ${
|
||||
}
|
||||
}
|
||||
if (inactiveTerminalOutputs.size > 0) {
|
||||
details += "\n\n# Inactive Terminals"
|
||||
terminalDetails += "\n\n# Inactive Terminals"
|
||||
for (const [terminalId, newOutput] of inactiveTerminalOutputs) {
|
||||
const inactiveTerminal = inactiveTerminals.find((t) => t.id === terminalId)
|
||||
if (inactiveTerminal) {
|
||||
details += `\n## ${inactiveTerminal.lastCommand}`
|
||||
details += `\n### New Output\n${newOutput}`
|
||||
terminalDetails += `\n## ${inactiveTerminal.lastCommand}`
|
||||
terminalDetails += `\n### New Output\n${newOutput}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// we want to get diagnostics AFTER terminal for a few reasons: terminal could be scaffolding a project, compiler could send issues to diagnostics, etc.
|
||||
let diagnosticsDetails = ""
|
||||
const diagnostics = await this.diagnosticsMonitor.getCurrentDiagnostics(this.didEditFile) // if claude edited the workspace then wait for updated diagnostics
|
||||
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 += `\n## ${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
|
||||
diagnosticsDetails += `\n- [${severity}] Line ${line}: ${diagnostic.message}`
|
||||
}
|
||||
}
|
||||
}
|
||||
this.didEditFile = false // reset, this lets us know when to wait for updated diagnostics
|
||||
|
||||
details += "\n\n# VSCode Workspace Diagnostics"
|
||||
if (diagnosticsDetails) {
|
||||
details += diagnosticsDetails
|
||||
} else {
|
||||
details += "\n(No problems detected)"
|
||||
}
|
||||
|
||||
if (terminalDetails) {
|
||||
details += terminalDetails
|
||||
}
|
||||
|
||||
if (includeFileDetails) {
|
||||
const isDesktop = cwd === path.join(os.homedir(), "Desktop")
|
||||
const files = await listFiles(cwd, !isDesktop)
|
||||
const result = this.formatFilesList(cwd, files)
|
||||
details += `\n\n# Current Working Directory ('${cwd}') Files${
|
||||
details += `\n\n# Current Working Directory (${cwd}) Files\n${result}${
|
||||
isDesktop
|
||||
? "\n(Desktop so only top-level contents shown for brevity, use list_files to explore further if necessary)"
|
||||
? "\n(Note: Only top-level contents shown for Desktop by default. Use list_files to explore further if necessary.)"
|
||||
: ""
|
||||
}\n${result}`
|
||||
}`
|
||||
}
|
||||
|
||||
details += "\n</environment_details>"
|
||||
return details
|
||||
return `<environment_details>\n${details.trim()}\n</environment_details>`
|
||||
}
|
||||
|
||||
async formatToolDeniedFeedback(feedback?: string) {
|
||||
|
||||
94
src/integrations/DiagnosticsMonitor.ts
Normal file
94
src/integrations/DiagnosticsMonitor.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import * as vscode from "vscode"
|
||||
import deepEqual from "fast-deep-equal"
|
||||
|
||||
type FileDiagnostics = [vscode.Uri, vscode.Diagnostic[]][]
|
||||
|
||||
/*
|
||||
About Diagnostics:
|
||||
The Problems tab shows diagnostics that have been reported for your project. These diagnostics are categorized into:
|
||||
Errors: Critical issues that usually prevent your code from compiling or running correctly.
|
||||
Warnings: Potential problems in the code that may not prevent it from running but could cause issues (e.g., bad practices, unused variables).
|
||||
Information: Non-critical suggestions or tips (e.g., formatting issues or notes from linters).
|
||||
The Problems tab displays diagnostics from various sources:
|
||||
1. Language Servers:
|
||||
- TypeScript: Type errors, missing imports, syntax issues
|
||||
- Python: Syntax errors, invalid type hints, undefined variables
|
||||
- JavaScript/Node.js: Parsing and execution errors
|
||||
2. Linters:
|
||||
- ESLint: Code style, best practices, potential bugs
|
||||
- Pylint: Unused imports, naming conventions
|
||||
- TSLint: Style and correctness issues in TypeScript
|
||||
3. Build Tools:
|
||||
- Webpack: Module resolution failures, build errors
|
||||
- Gulp: Build errors during task execution
|
||||
4. Custom Validators:
|
||||
- Extensions can generate custom diagnostics for specific languages or tools
|
||||
Each problem typically indicates its source (e.g., language server, linter, build tool).
|
||||
Diagnostics update in real-time as you edit code, helping identify issues quickly. For example, if you introduce a syntax error in a TypeScript file, the Problems tab will immediately display the new error.
|
||||
|
||||
Notes on diagnostics:
|
||||
- linter diagnostics are only captured for open editors
|
||||
- this works great for us since when claude edits/creates files its through vscode's textedit api's and we get those diagnostics for free
|
||||
- some tools might require you to save the file or manually refresh to clear the problem from the list.
|
||||
*/
|
||||
|
||||
class DiagnosticsMonitor {
|
||||
private diagnosticsChangeEmitter: vscode.EventEmitter<void> = new vscode.EventEmitter<void>()
|
||||
private disposables: vscode.Disposable[] = []
|
||||
private lastDiagnostics: FileDiagnostics = []
|
||||
|
||||
constructor() {
|
||||
this.disposables.push(
|
||||
vscode.languages.onDidChangeDiagnostics(() => {
|
||||
this.diagnosticsChangeEmitter.fire()
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
public async getCurrentDiagnostics(shouldWaitForChanges: boolean): Promise<FileDiagnostics> {
|
||||
const currentDiagnostics = vscode.languages.getDiagnostics() // get all diagnostics for files open in workspace (not just errors/warnings so our did update check is more likely to detect updated diagnostics)
|
||||
|
||||
if (!shouldWaitForChanges) {
|
||||
return currentDiagnostics
|
||||
}
|
||||
|
||||
// it doesn't matter if we don't even have all the diagnostics yet, since claude will get the rest in the next request. as long as somethings changed, he can react to that in this request.
|
||||
if (!deepEqual(this.lastDiagnostics, currentDiagnostics)) {
|
||||
this.lastDiagnostics = currentDiagnostics
|
||||
return currentDiagnostics
|
||||
}
|
||||
|
||||
return this.waitForUpdatedDiagnostics()
|
||||
}
|
||||
|
||||
private async waitForUpdatedDiagnostics(timeout: number = 1_000): Promise<FileDiagnostics> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
cleanup()
|
||||
const finalDiagnostics = vscode.languages.getDiagnostics()
|
||||
this.lastDiagnostics = finalDiagnostics
|
||||
resolve(finalDiagnostics)
|
||||
}, timeout)
|
||||
|
||||
const disposable = this.diagnosticsChangeEmitter.event(() => {
|
||||
cleanup()
|
||||
const updatedDiagnostics = vscode.languages.getDiagnostics()
|
||||
this.lastDiagnostics = updatedDiagnostics
|
||||
resolve(updatedDiagnostics)
|
||||
})
|
||||
|
||||
const cleanup = () => {
|
||||
clearTimeout(timer)
|
||||
disposable.dispose()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.disposables.forEach((d) => d.dispose())
|
||||
this.disposables = []
|
||||
this.diagnosticsChangeEmitter.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
export default DiagnosticsMonitor
|
||||
@@ -22,7 +22,7 @@ export async function downloadTask(dateTs: number, conversationHistory: Anthropi
|
||||
.map((message) => {
|
||||
const role = message.role === "user" ? "**User:**" : "**Assistant:**"
|
||||
const content = Array.isArray(message.content)
|
||||
? message.content.map(formatContentBlockToMarkdown).join("\n")
|
||||
? message.content.map((block) => formatContentBlockToMarkdown(block, conversationHistory)).join("\n")
|
||||
: message.content
|
||||
return `${role}\n\n${content}\n\n`
|
||||
})
|
||||
@@ -46,7 +46,8 @@ export function formatContentBlockToMarkdown(
|
||||
| Anthropic.TextBlockParam
|
||||
| Anthropic.ImageBlockParam
|
||||
| Anthropic.ToolUseBlockParam
|
||||
| Anthropic.ToolResultBlockParam
|
||||
| Anthropic.ToolResultBlockParam,
|
||||
messages: Anthropic.MessageParam[]
|
||||
): string {
|
||||
switch (block.type) {
|
||||
case "text":
|
||||
@@ -64,16 +65,30 @@ export function formatContentBlockToMarkdown(
|
||||
}
|
||||
return `[Tool Use: ${block.name}]\n${input}`
|
||||
case "tool_result":
|
||||
const toolName = findToolName(block.tool_use_id, messages)
|
||||
if (typeof block.content === "string") {
|
||||
return `[Tool Result${block.is_error ? " (Error)" : ""}]\n${block.content}`
|
||||
return `[${toolName}${block.is_error ? " (Error)" : ""}]\n${block.content}`
|
||||
} else if (Array.isArray(block.content)) {
|
||||
return `[Tool Result${block.is_error ? " (Error)" : ""}]\n${block.content
|
||||
.map(formatContentBlockToMarkdown)
|
||||
return `[${toolName}${block.is_error ? " (Error)" : ""}]\n${block.content
|
||||
.map((contentBlock) => formatContentBlockToMarkdown(contentBlock, messages))
|
||||
.join("\n")}`
|
||||
} else {
|
||||
return `[Tool Result${block.is_error ? " (Error)" : ""}]`
|
||||
return `[${toolName}${block.is_error ? " (Error)" : ""}]`
|
||||
}
|
||||
default:
|
||||
return "[Unexpected content type]"
|
||||
}
|
||||
}
|
||||
|
||||
function findToolName(toolCallId: string, messages: Anthropic.MessageParam[]): string {
|
||||
for (const message of messages) {
|
||||
if (Array.isArray(message.content)) {
|
||||
for (const block of message.content) {
|
||||
if (block.type === "tool_use" && block.id === toolCallId) {
|
||||
return block.name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "Unknown Tool"
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
const virtuosoRef = useRef<VirtuosoHandle>(null)
|
||||
const [expandedRows, setExpandedRows] = useState<Record<number, boolean>>({})
|
||||
const [isAtBottom, setIsAtBottom] = useState(false)
|
||||
const [didScrollFromApiReq, setDidScrollFromApiReq] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// if last message is an ask, show user ask UI
|
||||
@@ -461,15 +462,22 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
// dont scroll if we're just updating the api req started informational body
|
||||
const isLastApiReqStarted = visibleMessages.at(-1)?.say === "api_req_started"
|
||||
if (didScrollFromApiReq && isLastApiReqStarted) {
|
||||
return
|
||||
}
|
||||
|
||||
// We use a setTimeout to ensure new content is rendered before scrolling to the bottom. virtuoso's followOutput would scroll to the bottom before the new content could render.
|
||||
const timer = setTimeout(() => {
|
||||
// TODO: we can use virtuoso's isAtBottom to prevent scrolling if user is scrolled up, and show a 'scroll to bottom' button for better UX
|
||||
// NOTE: scroll to bottom may not work if you use margin, see virtuoso's troubleshooting
|
||||
virtuosoRef.current?.scrollTo({ top: Number.MAX_SAFE_INTEGER, behavior: "smooth" })
|
||||
setDidScrollFromApiReq(isLastApiReqStarted) // need to do this in timer since this effect can get called a few times simultaneously
|
||||
}, 50)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [visibleMessages])
|
||||
}, [visibleMessages, didScrollFromApiReq])
|
||||
|
||||
const placeholderText = useMemo(() => {
|
||||
const text = task ? "Type a message..." : "Type your task here..."
|
||||
|
||||
Reference in New Issue
Block a user