Fix streaming command stdout when a line is stuck waiting for stdin; normalize paths presented in webview; update README

This commit is contained in:
Saoud Rizwan
2024-08-06 21:04:42 -04:00
parent 1e5ffdb6b9
commit 4fc9708e16
3 changed files with 62 additions and 18 deletions

View File

@@ -60,7 +60,8 @@ Claude always asks for your permission first before any tools are executed or in
## Contribution
Feel free to contribute to this project by submitting issues and pull requests. Contributions are welcome and appreciated!
Paul Graham said it best, "if you build something now that barely works with AI, the next models will make it _really_ work." I've built this project with the assumption that scaling laws will continue to improve the quality (and cost) of AI models, and what might be difficult for Claude 3.5 Sonnet today will be effortless for future generations. That is the design philosophy I'd like to develop this project with, so it will always be updated with the best models, tools, and capabilities availablewithout wasting effort on implementing stopgaps like cheaper agents. With that said, I'm always open to suggestions and feedback, so please feel free to contribute to this project by submitting issues and pull requests. Contributions are welcome and appreciated!
To build Claude Dev locally, follow these steps:
1. Clone the repository:
@@ -80,6 +81,7 @@ To build Claude Dev locally, follow these steps:
## Reviews
- ["Claude Sonnet 3.5 Artifacts in VSCode With This Extension"](https://www.youtube.com/watch?v=5FbZ8ALfSTs) by [CoderOne](https://www.youtube.com/@CoderOne)
- ["ClaudeDev: This CODING Agent can Generate Applications within VS Code!"](https://www.youtube.com/watch?v=ufq6sHGe0zs) by [AICodeKing](https://www.youtube.com/@AICodeKing)
- ["Meet Claude Dev — An Open-Source AI Programmer In VS Code"](https://generativeai.pub/meet-claude-dev-an-open-source-autonomous-ai-programmer-in-vs-code-f457f9821b7b) and ["Build games with zero code using Claude Dev in VS Code](https://www.youtube.com/watch?v=VT-JYVi81ew) by [Jim Clyde Monge](https://jimclydemonge.medium.com/)
- ["AI Development with Claude Dev"](https://www.linkedin.com/pulse/ai-development-claude-dev-shannon-lal-3ql3e/) by Shannon Lal
- ["Code Smarter with Claude Dev: An AI Programmer for Your Projects"](https://www.linkedin.com/pulse/code-smarter-claude-dev-ai-programmer-your-projects-iana-detochka-jiqpe) by Iana D.

View File

@@ -45,6 +45,7 @@ RULES
- Do not use the ~ character or $HOME to refer to the home directory.
- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system.
- When editing files, always provide the complete file content in your response, regardless of the extent of changes. The system handles diff generation automatically.
- If you need to read or edit a file you have already read or edited, you can assume its contents have not changed since then (unless specified otherwise by the user) and skip using the read_file tool before proceeding.
- When creating a new project (such as an app, website, or any software project), organize all new files within a dedicated project directory unless the user specifies otherwise. Use appropriate file paths when writing files, as the write_to_file tool will automatically create any necessary directories. Structure the project logically, adhering to best practices for the specific type of project being created. Unless otherwise specified, new projects should be easily run without additional setup, for example most projects can be built in HTML, CSS, and JavaScript - which you can open in a browser.
- You must try to use multiple tools in one request when possible. For example if you were to create a website, you would use the write_to_file tool to create the necessary files with their appropriate contents all at once. Or if you wanted to analyze a project, you could use the read_file tool multiple times to look at several key files. This will help you accomplish the user's task more efficiently.
- Be sure to consider the type of project (e.g. Python, JavaScript, web application) when determining the appropriate structure and files to include. Also consider what files may be most relevant to accomplishing the task, for example looking at a project's manifest file would help you understand the project's dependencies, which you could incorporate into any code you write.
@@ -387,7 +388,7 @@ export class ClaudeDev {
newContent += "\n"
}
// condensed patch to return to claude
const diffResult = diff.createPatch(relPath, originalContent, newContent)
const diffResult = diff.createPatch(absolutePath, originalContent, newContent)
// full diff representation for webview
const diffRepresentation = diff
.diffLines(originalContent, newContent)
@@ -417,7 +418,7 @@ export class ClaudeDev {
"tool",
JSON.stringify({
tool: "editedExistingFile",
path: relPath,
path: this.getReadablePath(relPath),
diff: diffRepresentation,
} as ClaudeSayTool)
)
@@ -452,7 +453,11 @@ export class ClaudeDev {
)
const { response, text } = await this.ask(
"tool",
JSON.stringify({ tool: "newFileCreated", path: relPath, content: newContent } as ClaudeSayTool)
JSON.stringify({
tool: "newFileCreated",
path: this.getReadablePath(relPath),
content: newContent,
} as ClaudeSayTool)
)
if (response !== "yesButtonTapped") {
if (isLast) {
@@ -498,7 +503,7 @@ export class ClaudeDev {
const content = await fs.readFile(absolutePath, "utf-8")
const { response, text } = await this.ask(
"tool",
JSON.stringify({ tool: "readFile", path: relPath, content } as ClaudeSayTool)
JSON.stringify({ tool: "readFile", path: this.getReadablePath(relPath), content } as ClaudeSayTool)
)
if (response !== "yesButtonTapped") {
if (response === "textResponse" && text) {
@@ -524,7 +529,7 @@ export class ClaudeDev {
"tool",
JSON.stringify({
tool: "listFilesTopLevel",
path: this.getReadableDirPath(relDirPath),
path: this.getReadablePath(relDirPath),
content: result,
} as ClaudeSayTool)
)
@@ -557,7 +562,7 @@ export class ClaudeDev {
"tool",
JSON.stringify({
tool: "listFilesRecursive",
path: this.getReadableDirPath(relDirPath),
path: this.getReadablePath(relDirPath),
content: result,
} as ClaudeSayTool)
)
@@ -579,16 +584,24 @@ export class ClaudeDev {
}
}
getReadableDirPath(relDirPath: string): string {
const absolutePath = path.resolve(cwd, relDirPath)
getReadablePath(relPath: string): string {
// path.resolve is flexible in that it will resolve relative paths like '../../' to the cwd and even ignore the cwd if the relPath is actually an absolute path
const absolutePath = path.resolve(cwd, relPath)
if (cwd === path.join(os.homedir(), "Desktop")) {
// User opened vscode without a workspace, so cwd is the Desktop. Show the full absolute path to keep the user aware of where files are being created
return absolutePath
}
if (path.normalize(absolutePath) === path.normalize(cwd)) {
return path.basename(absolutePath) + "/"
return path.basename(absolutePath)
} else {
return relDirPath
// show the relative path to the cwd
const normalizedRelPath = path.relative(cwd, absolutePath)
if (absolutePath.includes(cwd)) {
return normalizedRelPath
} else {
// we are outside the cwd, so show the absolute path (useful for when claude passes in '../../' for example)
return absolutePath
}
}
}
@@ -613,6 +626,8 @@ export class ClaudeDev {
const truncatedList = sorted.slice(0, 1000).join("\n")
const remainingCount = sorted.length - 1000
return `${truncatedList}\n\n(${remainingCount} files not listed due to automatic truncation. Try listing files in subdirectories if you need to explore further.)`
} else if (sorted.length === 0 || (sorted.length === 1 && sorted[0] === "")) {
return "No files found or you do not have permission to view this directory."
} else {
return sorted.join("\n")
}
@@ -626,7 +641,7 @@ export class ClaudeDev {
"tool",
JSON.stringify({
tool: "viewSourceCodeDefinitionsTopLevel",
path: this.getReadableDirPath(relDirPath),
path: this.getReadablePath(relDirPath),
content: result,
} as ClaudeSayTool)
)
@@ -678,6 +693,7 @@ export class ClaudeDev {
} else {
// if the user sent some input, we send it to the command stdin
// add newline as cli programs expect a newline after each input
// (stdin needs to be set to `pipe` to send input to the command, execa does this by default when using template literals - other options are inherit (from parent process stdin) or null (no stdin))
subprocess.stdin?.write(text + "\n")
// Recurse with an empty string to continue listening for more input
sendCommandOutput(subprocess, "") // empty strings are effectively ignored by the webview, this is done solely to relinquish control over the exit command button
@@ -694,14 +710,24 @@ export class ClaudeDev {
// execa returns a promise-like object that is both a promise and a Subprocess that has properties like stdin
const subprocess = execa({ shell: true, cwd: cwd })`${command}`
try {
for await (const chunk of subprocess) {
const line = chunk.toString()
subprocess.stdout?.on("data", (data) => {
if (data) {
const output = data.toString()
// stream output to user in realtime
// do not await as we are not waiting for a response
sendCommandOutput(subprocess, line)
result += `${line}\n`
// do not await since it's sent as an ask and we are not waiting for a response
sendCommandOutput(subprocess, output)
result += output
}
})
try {
await subprocess
// NOTE: using for await to stream execa output does not return lines that expect user input, so we use listen to the stdout stream and handle data directly, allowing us to process output as soon as it's available even before a full line is complete.
// for await (const chunk of subprocess) {
// const line = chunk.toString()
// sendCommandOutput(subprocess, line)
// result += `${line}\n`
// }
} catch (e) {
if ((e as ExecaError).signal === "SIGINT") {
await this.say("command_output", `\nUser exited command...`)

View File

@@ -1,10 +1,20 @@
import * as fs from "fs/promises"
import { globby } from "globby"
import os from "os"
import * as path from "path"
import { LanguageParser, loadRequiredLanguageParsers } from "./languageParser"
// 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)
if (!dirExists) {
return "This directory does not exist or you do not have permission to access it."
}
// Get all files at top level (not gitignored)
const allFiles = await listFiles(dirPath, false)
@@ -45,11 +55,17 @@ export async function parseSourceCodeForDefinitionsTopLevel(dirPath: string): Pr
export async function listFiles(dirPath: string, recursive: boolean): Promise<string[]> {
const absolutePath = path.resolve(dirPath)
// Do not allow listing files in root or home directory, which Claude tends to want to do when the user's prompt is vague.
const root = process.platform === "win32" ? path.parse(absolutePath).root : "/"
const isRoot = absolutePath === root
if (isRoot) {
return [root]
}
const homeDir = os.homedir()
const isHomeDir = absolutePath === homeDir
if (isHomeDir) {
return [homeDir]
}
const dirsToIgnore = [
"node_modules",