Compare commits

...
22 Commits
Author SHA1 Message Date
Saoud Rizwan 8e7f15a655 Update version 2024-08-11 22:13:18 -04:00
Saoud Rizwan 240d002f32 New users default to Anthropic for now until OpenRouter issues are resolved 2024-08-11 22:10:47 -04:00
Saoud Rizwan e43f4cef98 New users default to OpenRouter 2024-08-11 17:52:14 -04:00
Saoud Rizwan 38085f156d Update Announcement 2024-08-11 17:27:17 -04:00
Saoud Rizwan 48d2411a11 Add option to set custom instructions 2024-08-11 17:14:05 -04:00
Saoud Rizwan f93e7946aa Remove unusable models; default to OpenRouter for new users; update Announcement 2024-08-11 15:37:09 -04:00
Saoud Rizwan ecc5eb30a5 Fix OpenRouter usage with better error handling 2024-08-11 04:47:29 -04:00
Saoud Rizwan 4c173254ed Update version and add new models feature to Announcement 2024-08-11 03:52:09 -04:00
Saoud Rizwan 6454163078 Remove unusable models; minor fixes 2024-08-11 03:24:10 -04:00
Saoud Rizwan 3c082e753b Add new models 2024-08-11 02:32:54 -04:00
Saoud Rizwan f54774b943 Add option to choose different models 2024-08-11 00:28:22 -04:00
Saoud Rizwan a863b26b7a Update prompt to be more explicit about not being able to access server that was exited 2024-08-10 03:53:43 -04:00
Saoud Rizwan 9f7d6d428b Gracefully terminate running subprocess if user cancels task in the middle of a command 2024-08-10 03:41:47 -04:00
Saoud Rizwan c1012dcd99 Clean up util functions 2024-08-10 03:28:52 -04:00
Saoud Rizwan 71b226b5c4 Update Claude Dev's description 2024-08-10 03:22:32 -04:00
Saoud Rizwan 7e17ad53ed Add version state; update README and Announcement with new image feature 2024-08-10 03:13:20 -04:00
Saoud Rizwan 60ca8baecb Allow messages with images and no text 2024-08-10 02:17:43 -04:00
Saoud Rizwan 7da81f651d Make prompt stricter around truncation and prevent getting stuck in a loop waiting for user input without using ask_followup_question 2024-08-10 02:03:16 -04:00
Saoud Rizwan 848f3fb7dd Update version 2024-08-08 09:09:31 -04:00
Saoud Rizwan 1c6136a934 Update system prompt to better handle commands that need to be run in specific working directories 2024-08-08 09:08:12 -04:00
Saoud Rizwan b2297846d9 Revert originalContent check for undefined 2024-08-08 08:50:58 -04:00
Saoud Rizwan a5435a662e Make empty tool use result more explicit about what was missing so retry request doesn't fail again 2024-08-08 08:40:57 -04:00
31 changed files with 867 additions and 322 deletions
+9
View File
@@ -4,6 +4,15 @@ All notable changes to the "claude-dev" extension will be documented in this fil
<!-- Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. -->
## [1.1.1]
- Adds option to choose other Claude models (+ GPT-4o, DeepSeek, and Mistral if you use OpenRouter)
- Adds option to add custom instructions to the end of the system prompt
## [1.1.0]
- Paste images in chat to use Claude's vision capabilities and turn mockups into fully functional applications or fix bugs with screenshots
## [1.0.9]
- Add support for OpenRouter and AWS Bedrock
+13 -9
View File
@@ -8,35 +8,34 @@
<a href="https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev"><strong>Download VSCode Extension</strong></a>
</p>
Thanks to [Claude 3.5 Sonnet's agentic coding capabilities](https://www-cdn.anthropic.com/fed9cc193a14b84131812372d8d5857f8f304c52/Model_Card_Claude_3_Addendum.pdf) Claude Dev can handle complex software development tasks step-by-step. With tools that let him create & edit files, analyze project source code, and execute terminal commands (after you grant permission), he can assist you in ways that go beyond simple code completion or tech support. From building software projects to running system operations, Claude Dev is only limited by your imagination.
While autonomous AI scripts traditionally run in sandboxed environments, Claude Dev provides a human-in-the-loop GUI to supervise every file changed and command executed, providing a safe and accessible way to explore the potential of agentic AI.
Thanks to [Claude 3.5 Sonnet's agentic coding capabilities](https://www-cdn.anthropic.com/fed9cc193a14b84131812372d8d5857f8f304c52/Model_Card_Claude_3_Addendum.pdf) Claude Dev can handle complex software development tasks step-by-step. With tools that let him create & edit files, explore complex projects, and execute terminal commands (after you grant permission), he can assist you in ways that go beyond simple code completion or tech support. While autonomous AI scripts traditionally run in sandboxed environments, Claude Dev provides a human-in-the-loop GUI to supervise every file changed and command executed, providing a safe and accessible way to explore the potential of agentic AI.
- Paste images in chat to use Claude's vision capabilities and turn mockups into fully functional applications or fix bugs with screenshots
- Inspect diffs of every change Claude makes right in the editor, and keep track with syntax highlighted previews in chat
- Runs CLI commands directly in chat, so you never have to open a terminal yourself (+ respond to interactive commands by sending a message)
- Presents permission buttons (i.e. 'Approve CLI command') before tool use or sending information to the API
- Runs terminal commands directly in chat, so you never have to open a terminal yourself (+ respond to interactive commands by sending a message)
- Presents permission buttons (i.e. 'Approve terminal command') before tool use or sending information to the API
- Keep track of total tokens and API usage cost for the entire task loop and individual requests
- Set a maximum # of API requests allowed for a task before being prompted for permission to proceed
- When a task is completed, Claude determines if he can present the result to you with a CLI command like `open -a "Google Chrome" index.html`, which you run with a click of a button
- When a task is completed, Claude determines if he can present the result to you with a terminal command like `open -a "Google Chrome" index.html`, which you run with a click of a button
_**Pro tip**: Use the `Cmd + Shift + P` shortcut to open the command palette and type `Claude Dev: Open In New Tab` to start a new task right in your editor._
## How it works
Claude Dev uses an agentic loop style implementation with chain-of-thought prompting and access to powerful tools that give him the ability to accomplish nearly any task. Start by providing a task and the agentic loop fires off, where it might use certain tools (with your permission) to accomplish each step in its thought process.
Claude Dev uses an autonomous task execution loop with chain-of-thought prompting and access to powerful tools that give him the ability to accomplish nearly any task. Start by providing a task and the loop fires off, where it might use certain tools (with your permission) to accomplish each step in its thought process.
### Tools
Claude Dev has access to the following capabilities:
1. **`execute_command`**: Execute CLI commands on the system (only with your permission, output is streamed into the chat and you can respond to stdin or exit long-running processes when you're ready)
1. **`execute_command`**: Execute terminal commands on the system (only with your permission, output is streamed into the chat and you can respond to stdin or exit long-running processes when you're ready)
2. **`list_files_top_level`**: List all paths for files at the top level of the specified directory (useful for generic file operations like retrieving a file from your Desktop)
3. **`list_files_recursive`**: List all paths for files in the specified directory and nested subdirectories (excludes files in .gitignore)
4. **`view_source_code_definitions_top_level`**: Parses all source code files at the top level of the specified directory to extract names of key elements like classes and functions (see more below)
5. **`read_file`**: Read the contents of a file at the specified path
6. **`write_to_file`**: Write content to a file at the specified path, automatically creating any necessary directories
7. **`ask_followup_question`**: Ask the user a question to gather additional information needed to complete a task (due to the autonomous nature of the program, this isn't a typical chatbotClaude Dev must explicitly interrupt his task loop to ask for more information)
8. **`attempt_completion`**: Present the result to the user after completing a task, potentially with a CLI command to kickoff a demonstration
8. **`attempt_completion`**: Present the result to the user after completing a task, potentially with a terminal command to kickoff a demonstration
### Working in Existing Projects
@@ -81,8 +80,13 @@ 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: NEW Coding Agent Can Generate Applications within VS Code!"](https://www.youtube.com/watch?v=UNsQHosbIoE) by [WorldofAI](https://www.youtube.com/@intheworldofai)
- ["Claude Dev: This Coding Agent can Generate Applications Within VS Code!"](https://www.youtube.com/watch?v=ufq6sHGe0zs) by [AICodeKing](https://www.youtube.com/@AICodeKing)
- ["ClaudeDev + NextJS + Supabase: Generate FULL-STACK Apps with Claude 3.5 Sonnet"](https://www.youtube.com/watch?v=GeZBfO1kxA4) 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/)
- ["Claude Dev Builds NextJS App! Aider, Continue Dev & GitHub Copilot Open-Source Alternative"](https://www.youtube.com/watch?v=Rv0wJZRpnCQ) by [Josh Pocock](https://www.youtube.com/@joshfpocock)
- ["Claude Dev Is Mindblowing. The Best Coding Assistant tool?"](https://www.youtube.com/watch?v=Vp1Z3VGZroA) by [Yaron Been](https://www.youtube.com/@ecomxfactor-YaronBeen)
- ["ClaudeDev: The Ultimate Coding Agent for VS Code"](https://www.youtube.com/watch?v=aq0yw_DtphQ) by [Blas](https://www.youtube.com/@blascerecer)
- ["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.
- [Claude Dev also hit top 10 posts of all time on r/ClaudeAI (thank you for all the lovely comments)](https://www.reddit.com/r/ClaudeAI/comments/1e3h0f1/my_submission_to_anthropics_build_with_claude/)
+3 -4
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Claude Dev",
"description": "Autonomous software engineer right in your IDE, capable of creating/editing files, executing commands, and more with your permission every step of the way.",
"version": "1.0.98",
"version": "1.1.12",
"icon": "icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -105,8 +105,7 @@
"install:all": "npm install && cd webview-ui && npm install",
"start:webview": "cd webview-ui && npm run start",
"build:webview": "cd webview-ui && npm run build",
"test:webview": "cd webview-ui && npm run test",
"upver": "tsx scripts/update-version.ts"
"test:webview": "cd webview-ui && npm run test"
},
"devDependencies": {
"@types/diff": "^5.2.1",
@@ -139,4 +138,4 @@
"tree-sitter-wasms": "^0.1.11",
"web-tree-sitter": "^0.22.6"
}
}
}
-29
View File
@@ -1,29 +0,0 @@
import * as fs from "fs"
import * as path from "path"
const newVersion = process.argv[2]
if (!newVersion) {
console.error("Please provide a version number")
process.exit(1)
}
// Update root package.json
const rootPackagePath = path.join(__dirname, "..", "package.json")
const rootPackage = JSON.parse(fs.readFileSync(rootPackagePath, "utf8"))
rootPackage.version = newVersion
fs.writeFileSync(rootPackagePath, JSON.stringify(rootPackage, null, 2))
// Update Announcement.tsx
const announcementPath = path.join(__dirname, "..", "webview-ui", "src", "components", "Announcement.tsx")
let announcementContent = fs.readFileSync(announcementPath, "utf8")
announcementContent = announcementContent.replace(/New in v[\d.]+<\/h3>/, `New in v${newVersion}</h3>`)
fs.writeFileSync(announcementPath, announcementContent)
// Update SettingsView.tsx
const settingsViewPath = path.join(__dirname, "..", "webview-ui", "src", "components", "SettingsView.tsx")
let settingsViewContent = fs.readFileSync(settingsViewPath, "utf8")
settingsViewContent = settingsViewContent.replace(/>v[\d.]+<\/p>/, `>v${newVersion}</p>`)
fs.writeFileSync(settingsViewPath, settingsViewContent)
console.log(`Version updated to ${newVersion}`)
+91 -50
View File
@@ -43,7 +43,7 @@ RULES
- Your current working directory is: ${cwd}
- You cannot \`cd\` into a different directory to complete a task. You are stuck operating from '${cwd}', so be sure to pass in the correct 'path' parameter when using tools that require a path.
- 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.
- 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. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '${cwd}', and if so prepend with \`cd\`'ing into that directory && then executing the command (as one command since you are stuck operating from '${cwd}'). For example, if you needed to run \`npm install\` in a project outside of '${cwd}', you would need to prepend with a \`cd\` i.e. pseudocode for this would be \`cd (path to project) && (command, in this case npm install)\`.
- 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.
@@ -175,7 +175,7 @@ const tools: Tool[] = [
{
name: "write_to_file",
description:
"Write content to a file at the specified path. If the file exists, only the necessary changes will be applied. If the file doesn't exist, it will be created. Always provide the full intended content of the file. This tool will automatically create any directories needed to write the file.",
"Write content to a file at the specified path. If the file exists, only the necessary changes will be applied. If the file doesn't exist, it will be created. Always provide the full intended content of the file, without any truncation. This tool will automatically create any directories needed to write the file.",
input_schema: {
type: "object",
properties: {
@@ -235,6 +235,7 @@ type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlo
export class ClaudeDev {
private api: ApiHandler
private maxRequestsPerTask: number
private customInstructions?: string
private requestCount = 0
apiConversationHistory: Anthropic.MessageParam[] = []
claudeMessages: ClaudeMessage[] = []
@@ -242,19 +243,22 @@ export class ClaudeDev {
private askResponseText?: string
private askResponseImages?: string[]
private lastMessageTs?: number
private executeCommandRunningProcess?: ResultPromise
private providerRef: WeakRef<ClaudeDevProvider>
abort: boolean = false
private abort: boolean = false
constructor(
provider: ClaudeDevProvider,
task: string,
apiConfiguration: ApiConfiguration,
maxRequestsPerTask?: number,
customInstructions?: string,
task?: string,
images?: string[]
) {
this.providerRef = new WeakRef(provider)
this.api = buildApiHandler(apiConfiguration)
this.maxRequestsPerTask = maxRequestsPerTask ?? DEFAULT_MAX_REQUESTS_PER_TASK
this.customInstructions = customInstructions
this.startTask(task, images)
}
@@ -267,6 +271,10 @@ export class ClaudeDev {
this.maxRequestsPerTask = maxRequestsPerTask ?? DEFAULT_MAX_REQUESTS_PER_TASK
}
updateCustomInstructions(customInstructions: string | undefined) {
this.customInstructions = customInstructions
}
async handleWebviewAskResponse(askResponse: ClaudeAskResponse, text?: string, images?: string[]) {
this.askResponse = askResponse
this.askResponseText = text
@@ -323,26 +331,24 @@ export class ClaudeDev {
: []
}
private formatIntoToolResponse(text?: string, images?: string[]): ToolResponse {
private formatIntoToolResponse(text: string, images?: string[]): ToolResponse {
if (images && images.length > 0) {
const textBlock: Anthropic.TextBlockParam = { type: "text", text: text ?? "" }
const textBlock: Anthropic.TextBlockParam = { type: "text", text }
const imageBlocks: Anthropic.ImageBlockParam[] = this.formatImagesIntoBlocks(images)
// Placing images after text leads to better results
return [textBlock, ...imageBlocks]
} else {
return text ?? ""
return text
}
}
private async startTask(task: string, images?: string[]): Promise<void> {
private async startTask(task?: string, images?: string[]): Promise<void> {
// conversationHistory (for API) and claudeMessages (for webview) need to be in sync
// if the extension process were killed, then on restart the claudeMessages might not be empty, so we need to set it to [] when we create a new ClaudeDev client (otherwise webview would show stale messages from previous session)
this.claudeMessages = []
this.apiConversationHistory = []
await this.providerRef.deref()?.postStateToWebview()
// This first message kicks off a task, it is not included in every subsequent message.
let textBlock: Anthropic.TextBlockParam = { type: "text", text: `Task: \"${task}\"` }
let imageBlocks: Anthropic.ImageBlockParam[] = this.formatImagesIntoBlocks(images)
@@ -354,7 +360,7 @@ export class ClaudeDev {
let totalInputTokens = 0
let totalOutputTokens = 0
while (this.requestCount < this.maxRequestsPerTask) {
while (!this.abort) {
const { didEndLoop, inputTokens, outputTokens } = await this.recursivelyMakeClaudeRequests([
textBlock,
...imageBlocks,
@@ -367,7 +373,7 @@ export class ClaudeDev {
//const totalCost = this.calculateApiCost(totalInputTokens, totalOutputTokens)
if (didEndLoop) {
// for now this never happens
// For now a task never 'completes'. This will only happen if the user hits max requests and denies resetting the count.
//this.say("task_completed", `Task completed. Total API usage cost: ${totalCost}`)
break
} else {
@@ -377,13 +383,21 @@ export class ClaudeDev {
// )
textBlock = {
type: "text",
text: "Ask yourself if you have completed the user's task. If you have, use the attempt_completion tool, otherwise proceed to the next step. (This is an automated message, so do not respond to it conversationally. Just proceed with the task.)",
text: "If you have completed the user's task, use the attempt_completion tool. If you require additional information from the user, use the ask_followup_question tool. Otherwise, if you have not completed the task and do not need additional information, then proceed with the next step of the task. (This is an automated message, so do not respond to it conversationally.)",
}
imageBlocks = []
}
}
}
abortTask() {
this.abort = true // will stop any autonomously running promises
const runningProcessId = this.executeCommandRunningProcess?.pid
if (runningProcessId) {
treeKill(runningProcessId, "SIGTERM")
}
}
async executeTool(toolName: ToolName, toolInput: any, isLastWriteToFile: boolean = false): Promise<ToolResponse> {
switch (toolName) {
case "write_to_file":
@@ -407,28 +421,29 @@ export class ClaudeDev {
}
}
// Calculates cost of a Claude 3.5 Sonnet API request
calculateApiCost(inputTokens: number, outputTokens: number): number {
const INPUT_COST_PER_MILLION = 3.0 // $3 per million input tokens
const OUTPUT_COST_PER_MILLION = 15.0 // $15 per million output tokens
const inputCost = (inputTokens / 1_000_000) * INPUT_COST_PER_MILLION
const outputCost = (outputTokens / 1_000_000) * OUTPUT_COST_PER_MILLION
const inputCost = (this.api.getModel().info.inputPrice / 1_000_000) * inputTokens
const outputCost = (this.api.getModel().info.outputPrice / 1_000_000) * outputTokens
const totalCost = inputCost + outputCost
return totalCost
}
async writeToFile(relPath?: string, newContent?: string, isLast: boolean = true): Promise<ToolResponse> {
if (relPath === undefined || newContent === undefined) {
if (newContent === undefined) {
// Special message for this case since this tends to happen the most
this.say(
"error",
`Claude tried to use write_to_file for '${relPath}' without any content. This tends to happen due to API token limits. Retrying...`
)
} else {
this.say("error", "Claude tried to use write_to_file with missing parameters. Retrying...")
}
return "Error: Missing required parameters. Please retry with complete parameters."
if (relPath === undefined) {
this.say(
"error",
"Claude tried to use write_to_file without value for required parameter 'path'. Retrying..."
)
return "Error: Missing value for required parameter 'path'. Please retry with complete response."
}
if (newContent === undefined) {
// Special message for this case since this tends to happen the most
this.say(
"error",
`Claude tried to use write_to_file for '${relPath}' without value for required parameter 'content'. This is likely due to output token limits. Retrying...`
)
return "Error: Missing value for required parameter 'content'. Please retry with complete response."
}
try {
@@ -438,12 +453,8 @@ export class ClaudeDev {
.then(() => true)
.catch(() => false)
let originalContent: string | undefined
if (fileExists) {
originalContent = await fs.readFile(absolutePath, "utf-8")
}
if (fileExists && originalContent && originalContent.length > 0) {
const originalContent = await fs.readFile(absolutePath, "utf-8")
// fix issue where claude always removes newline from the file
if (originalContent.endsWith("\n") && !newContent.endsWith("\n")) {
newContent += "\n"
@@ -566,8 +577,8 @@ export class ClaudeDev {
async readFile(relPath?: string): Promise<ToolResponse> {
if (relPath === undefined) {
this.say("error", "Claude tried to use read_file with missing parameters. Retrying...")
return "Error: Missing required parameters. Please retry with complete parameters."
this.say("error", "Claude tried to use read_file without value for required parameter 'path'. Retrying...")
return "Error: Missing value for required parameter 'path'. Please retry with complete response."
}
try {
const absolutePath = path.resolve(cwd, relPath)
@@ -596,8 +607,11 @@ export class ClaudeDev {
async listFilesTopLevel(relDirPath?: string): Promise<ToolResponse> {
if (relDirPath === undefined) {
this.say("error", "Claude tried to use list_files_top_level with missing parameters. Retrying...")
return "Error: Missing required parameters. Please retry with complete parameters."
this.say(
"error",
"Claude tried to use list_files_top_level without value for required parameter 'path'. Retrying..."
)
return "Error: Missing value for required parameter 'path'. Please retry with complete response."
}
try {
const absolutePath = path.resolve(cwd, relDirPath)
@@ -636,8 +650,11 @@ export class ClaudeDev {
async listFilesRecursive(relDirPath?: string): Promise<ToolResponse> {
if (relDirPath === undefined) {
this.say("error", "Claude tried to use list_files_recursive with missing parameters. Retrying...")
return "Error: Missing required parameters. Please retry with complete parameters."
this.say(
"error",
"Claude tried to use list_files_recursive without value for required parameter 'path'. Retrying..."
)
return "Error: Missing value for required parameter 'path'. Please retry with complete response."
}
try {
const absolutePath = path.resolve(cwd, relDirPath)
@@ -725,9 +742,9 @@ export class ClaudeDev {
if (relDirPath === undefined) {
this.say(
"error",
"Claude tried to use view_source_code_definitions_top_level with missing parameters. Retrying..."
"Claude tried to use view_source_code_definitions_top_level without value for required parameter 'path'. Retrying..."
)
return "Error: Missing required parameters. Please retry with complete parameters."
return "Error: Missing value for required parameter 'path'. Please retry with complete response."
}
try {
const absolutePath = path.resolve(cwd, relDirPath)
@@ -765,8 +782,11 @@ export class ClaudeDev {
async executeCommand(command?: string, returnEmptyStringOnSuccess: boolean = false): Promise<ToolResponse> {
if (command === undefined) {
this.say("error", "Claude tried to use execute_command with missing parameters. Retrying...")
return "Error: Missing required parameters. Please retry with complete parameters."
this.say(
"error",
"Claude tried to use execute_command without value for required parameter 'command'. Retrying..."
)
return "Error: Missing value for required parameter 'command'. Please retry with complete response."
}
const { response, text, images } = await this.ask("command", command)
if (response !== "yesButtonTapped") {
@@ -814,6 +834,7 @@ export class ClaudeDev {
// also worth noting that execa`input` and the execa(command) have nuanced differences like the template literal version handles escaping for you, while with the function call, you need to be more careful about how arguments are passed, especially when using shell: true.
// 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}`
this.executeCommandRunningProcess = subprocess
subprocess.stdout?.on("data", (data) => {
if (data) {
@@ -836,7 +857,7 @@ export class ClaudeDev {
} catch (e) {
if ((e as ExecaError).signal === "SIGINT") {
await this.say("command_output", `\nUser exited command...`)
result += `\n====\nUser terminated command process via SIGINT. This is not an error. Please continue with your task but keep in mind that the command is no longer running. In other words, if this command was used to start a server, the server is no longer running.`
result += `\n====\nUser terminated command process via SIGINT. This is not an error. Please continue with your task, but keep in mind that the command is no longer running. For example, if this command was used to start a server for a react app, the server is no longer running and you cannot open a browser to view it anymore.`
} else {
throw e // if the command was not terminated by user, let outer catch handle it as a real error
}
@@ -847,6 +868,7 @@ export class ClaudeDev {
// the correct order of messages (although the webview is smart about
// grouping command_output messages despite any gaps anyways)
await delay(100)
this.executeCommandRunningProcess = undefined
// for attemptCompletion, we don't want to return the command output
if (returnEmptyStringOnSuccess) {
return ""
@@ -857,14 +879,18 @@ export class ClaudeDev {
let errorMessage = error.message || JSON.stringify(serializeError(error), null, 2)
const errorString = `Error executing command:\n${errorMessage}`
this.say("error", `Error executing command:\n${errorMessage}`) // TODO: in webview show code block for command errors
this.executeCommandRunningProcess = undefined
return errorString
}
}
async askFollowupQuestion(question?: string): Promise<ToolResponse> {
if (question === undefined) {
this.say("error", "Claude tried to use ask_followup_question with missing parameters. Retrying...")
return "Error: Missing required parameters. Please retry with complete parameters."
this.say(
"error",
"Claude tried to use ask_followup_question without value for required parameter 'question'. Retrying..."
)
return "Error: Missing value for required parameter 'question'. Please retry with complete response."
}
const { text, images } = await this.ask("followup", question)
await this.say("user_feedback", text ?? "", images)
@@ -874,8 +900,11 @@ export class ClaudeDev {
async attemptCompletion(result?: string, command?: string): Promise<ToolResponse> {
// result is required, command is optional
if (result === undefined) {
this.say("error", "Claude tried to use attempt_completion with missing parameters. Retrying...")
return "Error: Missing required parameters. Please retry with complete parameters."
this.say(
"error",
"Claude tried to use attempt_completion without value for required parameter 'result'. Retrying..."
)
return "Error: Missing value for required parameter 'result'. Please retry with complete response."
}
let resultToSend = result
if (command) {
@@ -901,7 +930,19 @@ export class ClaudeDev {
async attemptApiRequest(): Promise<Anthropic.Messages.Message> {
try {
return await this.api.createMessage(SYSTEM_PROMPT(), this.apiConversationHistory, tools)
let systemPrompt = SYSTEM_PROMPT()
if (this.customInstructions && this.customInstructions.trim()) {
systemPrompt += `
====
USER'S CUSTOM INSTRUCTIONS
The following additional instructions are provided by the user. They should be followed and given precedence in case of conflicts with previous instructions.
${this.customInstructions.trim()}
`
}
return await this.api.createMessage(systemPrompt, this.apiConversationHistory, tools)
} catch (error) {
const { response } = await this.ask(
"api_req_failed",
+21 -9
View File
@@ -1,6 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ApiHandler, withoutImageData } from "."
import { ApiHandlerOptions } from "../shared/api"
import { anthropicDefaultModelId, AnthropicModelId, anthropicModels, ApiHandlerOptions, ModelInfo } from "../shared/api"
export class AnthropicHandler implements ApiHandler {
private options: ApiHandlerOptions
@@ -18,17 +18,20 @@ export class AnthropicHandler implements ApiHandler {
): Promise<Anthropic.Messages.Message> {
return await this.client.messages.create(
{
model: "claude-3-5-sonnet-20240620", // https://docs.anthropic.com/en/docs/about-claude/models
max_tokens: 8192, // beta max tokens
model: this.getModel().id,
max_tokens: this.getModel().info.maxTokens,
system: systemPrompt,
messages,
tools,
tool_choice: { type: "auto" },
},
{
// https://github.com/anthropics/anthropic-sdk-typescript?tab=readme-ov-file#default-headers
headers: { "anthropic-beta": "max-tokens-3-5-sonnet-2024-07-15" },
}
// https://x.com/alexalbert__/status/1812921642143900036
// https://github.com/anthropics/anthropic-sdk-typescript?tab=readme-ov-file#default-headers
this.getModel().id === "claude-3-5-sonnet-20240620"
? {
headers: { "anthropic-beta": "max-tokens-3-5-sonnet-2024-07-15" },
}
: undefined
)
}
@@ -41,12 +44,21 @@ export class AnthropicHandler implements ApiHandler {
>
): any {
return {
model: "claude-3-5-sonnet-20240620",
max_tokens: 8192,
model: this.getModel().id,
max_tokens: this.getModel().info.maxTokens,
system: "(see SYSTEM_PROMPT in src/ClaudeDev.ts)",
messages: [{ conversation_history: "..." }, { role: "user", content: withoutImageData(userContent) }],
tools: "(see tools in src/ClaudeDev.ts)",
tool_choice: { type: "auto" },
}
}
getModel(): { id: AnthropicModelId; info: ModelInfo } {
const modelId = this.options.apiModelId
if (modelId && modelId in anthropicModels) {
const id = modelId as AnthropicModelId
return { id, info: anthropicModels[id] }
}
return { id: anthropicDefaultModelId, info: anthropicModels[anthropicDefaultModelId] }
}
}
+14 -5
View File
@@ -1,7 +1,7 @@
import AnthropicBedrock from "@anthropic-ai/bedrock-sdk"
import { Anthropic } from "@anthropic-ai/sdk"
import { ApiHandlerOptions } from "../shared/api"
import { ApiHandler, withoutImageData } from "."
import { ApiHandlerOptions, bedrockDefaultModelId, BedrockModelId, bedrockModels, ModelInfo } from "../shared/api"
// https://docs.anthropic.com/en/api/claude-on-amazon-bedrock
export class AwsBedrockHandler implements ApiHandler {
@@ -28,8 +28,8 @@ export class AwsBedrockHandler implements ApiHandler {
tools: Anthropic.Messages.Tool[]
): Promise<Anthropic.Messages.Message> {
return await this.client.messages.create({
model: "anthropic.claude-3-5-sonnet-20240620-v1:0",
max_tokens: 4096,
model: this.getModel().id,
max_tokens: this.getModel().info.maxTokens,
system: systemPrompt,
messages,
tools,
@@ -46,12 +46,21 @@ export class AwsBedrockHandler implements ApiHandler {
>
): any {
return {
model: "anthropic.claude-3-5-sonnet-20240620-v1:0",
max_tokens: 4096,
model: this.getModel().id,
max_tokens: this.getModel().info.maxTokens,
system: "(see SYSTEM_PROMPT in src/ClaudeDev.ts)",
messages: [{ conversation_history: "..." }, { role: "user", content: withoutImageData(userContent) }],
tools: "(see tools in src/ClaudeDev.ts)",
tool_choice: { type: "auto" },
}
}
getModel(): { id: BedrockModelId; info: ModelInfo } {
const modelId = this.options.apiModelId
if (modelId && modelId in bedrockModels) {
const id = modelId as BedrockModelId
return { id, info: bedrockModels[id] }
}
return { id: bedrockDefaultModelId, info: bedrockModels[bedrockDefaultModelId] }
}
}
+3 -1
View File
@@ -1,5 +1,5 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ApiConfiguration } from "../shared/api"
import { ApiConfiguration, ApiModelId, ModelInfo } from "../shared/api"
import { AnthropicHandler } from "./anthropic"
import { AwsBedrockHandler } from "./bedrock"
import { OpenRouterHandler } from "./openrouter"
@@ -19,6 +19,8 @@ export interface ApiHandler {
| Anthropic.ToolResultBlockParam
>
): any
getModel(): { id: ApiModelId; info: ModelInfo }
}
export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
+27 -6
View File
@@ -1,7 +1,13 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiHandler, withoutImageData } from "."
import { ApiHandlerOptions } from "../shared/api"
import {
ApiHandlerOptions,
ModelInfo,
openRouterDefaultModelId,
OpenRouterModelId,
openRouterModels,
} from "../shared/api"
export class OpenRouterHandler implements ApiHandler {
private options: ApiHandlerOptions
@@ -41,13 +47,18 @@ export class OpenRouterHandler implements ApiHandler {
}))
const completion = await this.client.chat.completions.create({
model: "anthropic/claude-3.5-sonnet:beta",
max_tokens: 4096,
model: this.getModel().id,
max_tokens: this.getModel().info.maxTokens,
messages: openAiMessages,
tools: openAiTools,
tool_choice: "auto",
})
const errorMessage = (completion as any).error?.message // openrouter returns an error object instead of the openai sdk throwing an error
if (errorMessage) {
throw new Error(errorMessage)
}
// Convert OpenAI response to Anthropic format
const openAiMessage = completion.choices[0].message
const anthropicMessage: Anthropic.Messages.Message = {
@@ -240,7 +251,8 @@ export class OpenRouterHandler implements ApiHandler {
openAiMessages.push({
role: "assistant",
content,
tool_calls,
// Cannot be an empty array. API expects an array with minimum length 1, and will respond with an error if it's empty
tool_calls: tool_calls.length > 0 ? tool_calls : undefined,
})
}
}
@@ -258,11 +270,20 @@ export class OpenRouterHandler implements ApiHandler {
>
): any {
return {
model: "anthropic/claude-3.5-sonnet:beta",
max_tokens: 4096,
model: this.getModel().id,
max_tokens: this.getModel().info.maxTokens,
messages: [{ conversation_history: "..." }, { role: "user", content: withoutImageData(userContent) }],
tools: "(see tools in src/ClaudeDev.ts)",
tool_choice: "auto",
}
}
getModel(): { id: OpenRouterModelId; info: ModelInfo } {
const modelId = this.options.apiModelId
if (modelId && modelId in openRouterModels) {
const id = modelId as OpenRouterModelId
return { id, info: openRouterModels[id] }
}
return { id: openRouterDefaultModelId, info: openRouterModels[openRouterDefaultModelId] }
}
}
+67 -76
View File
@@ -1,12 +1,9 @@
import * as vscode from "vscode"
import { Uri, Webview } from "vscode"
import { ClaudeDev } from "../ClaudeDev"
import { ApiProvider } from "../shared/api"
import { ApiModelId, ApiProvider } from "../shared/api"
import { ExtensionMessage } from "../shared/ExtensionMessage"
import { WebviewMessage } from "../shared/WebviewMessage"
import { downloadTask } from "../utils/export-markdown"
import { selectImages } from "../utils/process-images"
import { downloadTask, getNonce, getUri, selectImages } from "../utils"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -14,7 +11,13 @@ https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/c
*/
type SecretKey = "apiKey" | "openRouterApiKey" | "awsAccessKey" | "awsSecretKey"
type GlobalStateKey = "apiProvider" | "awsRegion" | "maxRequestsPerTask" | "lastShownAnnouncementId"
type GlobalStateKey =
| "apiProvider"
| "apiModelId"
| "awsRegion"
| "maxRequestsPerTask"
| "lastShownAnnouncementId"
| "customInstructions"
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.
@@ -22,7 +25,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
private disposables: vscode.Disposable[] = []
private view?: vscode.WebviewView | vscode.WebviewPanel
private claudeDev?: ClaudeDev
private latestAnnouncementId = "jul-29-2024" // update to some unique identifier when we add a new announcement
private latestAnnouncementId = "aug-11-2024" // update to some unique identifier when we add a new announcement
constructor(
private readonly context: vscode.ExtensionContext,
@@ -133,17 +136,10 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
this.outputChannel.appendLine("Webview view resolved")
}
async initClaudeDevWithTask(task: string, images?: string[]) {
async initClaudeDevWithTask(task?: string, images?: string[]) {
await this.clearTask() // ensures that an exising task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
const { apiProvider, apiKey, openRouterApiKey, awsAccessKey, awsSecretKey, awsRegion, maxRequestsPerTask } =
await this.getState()
this.claudeDev = new ClaudeDev(
this,
task,
{ apiProvider, apiKey, openRouterApiKey, awsAccessKey, awsSecretKey, awsRegion },
maxRequestsPerTask,
images
)
const { maxRequestsPerTask, apiConfiguration, customInstructions } = await this.getState()
this.claudeDev = new ClaudeDev(this, apiConfiguration, maxRequestsPerTask, customInstructions, task, images)
}
// Send any JSON serializable data to the react app
@@ -254,13 +250,21 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
// Could also do this in extension .ts
//this.postMessageToWebview({ type: "text", text: `Extension: ${Date.now()}` })
// initializing new instance of ClaudeDev will make sure that any agentically running promises in old instance don't affect our new task. this essentially creates a fresh slate for the new task
await this.initClaudeDevWithTask(message.text!, message.images)
await this.initClaudeDevWithTask(message.text, message.images)
break
case "apiConfiguration":
if (message.apiConfiguration) {
const { apiProvider, apiKey, openRouterApiKey, awsAccessKey, awsSecretKey, awsRegion } =
message.apiConfiguration
const {
apiProvider,
apiModelId,
apiKey,
openRouterApiKey,
awsAccessKey,
awsSecretKey,
awsRegion,
} = message.apiConfiguration
await this.updateGlobalState("apiProvider", apiProvider)
await this.updateGlobalState("apiModelId", apiModelId)
await this.storeSecret("apiKey", apiKey)
await this.storeSecret("openRouterApiKey", openRouterApiKey)
await this.storeSecret("awsAccessKey", awsAccessKey)
@@ -282,6 +286,12 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
this.claudeDev?.updateMaxRequestsPerTask(result)
await this.postStateToWebview()
break
case "customInstructions":
// User may be clearing the field
await this.updateGlobalState("customInstructions", message.text || undefined)
this.claudeDev?.updateCustomInstructions(message.text || undefined)
await this.postStateToWebview()
break
case "askResponse":
this.claudeDev?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
break
@@ -311,21 +321,15 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
}
async postStateToWebview() {
const {
apiProvider,
apiKey,
openRouterApiKey,
awsAccessKey,
awsSecretKey,
awsRegion,
maxRequestsPerTask,
lastShownAnnouncementId,
} = await this.getState()
const { apiConfiguration, maxRequestsPerTask, lastShownAnnouncementId, customInstructions } =
await this.getState()
this.postMessageToWebview({
type: "state",
state: {
apiConfiguration: { apiProvider, apiKey, openRouterApiKey, awsAccessKey, awsSecretKey, awsRegion },
version: this.context.extension?.packageJSON?.version ?? "",
apiConfiguration,
maxRequestsPerTask,
customInstructions,
themeName: vscode.workspace.getConfiguration("workbench").get<string>("colorTheme"),
claudeMessages: this.claudeDev?.claudeMessages || [],
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
@@ -334,12 +338,8 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
}
async clearTask() {
if (this.claudeDev) {
this.claudeDev.abort = true // will stop any agentically running promises
this.claudeDev = undefined // removes reference to it, so once promises end it will be garbage collected
}
// this.setApiConversationHistory(undefined)
// this.setClaudeMessages(undefined)
this.claudeDev?.abortTask()
this.claudeDev = undefined // removes reference to it, so once promises end it will be garbage collected
}
// Caching mechanism to keep track of webview messages + API conversation history per provider instance
@@ -425,7 +425,8 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
async getState() {
const [
apiProvider,
storedApiProvider,
apiModelId,
apiKey,
openRouterApiKey,
awsAccessKey,
@@ -433,8 +434,10 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
awsRegion,
maxRequestsPerTask,
lastShownAnnouncementId,
customInstructions,
] = await Promise.all([
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
this.getGlobalState("apiModelId") as Promise<ApiModelId | undefined>,
this.getSecret("apiKey") as Promise<string | undefined>,
this.getSecret("openRouterApiKey") as Promise<string | undefined>,
this.getSecret("awsAccessKey") as Promise<string | undefined>,
@@ -442,16 +445,36 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
this.getGlobalState("awsRegion") as Promise<string | undefined>,
this.getGlobalState("maxRequestsPerTask") as Promise<number | undefined>,
this.getGlobalState("lastShownAnnouncementId") as Promise<string | undefined>,
this.getGlobalState("customInstructions") as Promise<string | undefined>,
])
let apiProvider: ApiProvider
if (storedApiProvider) {
apiProvider = storedApiProvider
} else {
// Either new user or legacy user that doesn't have the apiProvider stored in state
// (If they're using OpenRouter or Bedrock, then apiProvider state will exist)
if (apiKey) {
apiProvider = "anthropic"
} else {
// New users should default to anthropic (openrouter doesn't perform well with large files)
apiProvider = "anthropic"
}
}
return {
apiProvider: apiProvider || "anthropic", // for legacy users that were using Anthropic by default
apiKey,
openRouterApiKey,
awsAccessKey,
awsSecretKey,
awsRegion,
apiConfiguration: {
apiProvider,
apiModelId,
apiKey,
openRouterApiKey,
awsAccessKey,
awsSecretKey,
awsRegion,
},
maxRequestsPerTask,
lastShownAnnouncementId,
customInstructions,
}
}
@@ -499,35 +522,3 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
return await this.context.secrets.get(key)
}
}
/**
* A helper function that returns a unique alphanumeric identifier called a nonce.
*
* @remarks This function is primarily used to help enforce content security
* policies for resources/scripts being executed in a webview context.
*
* @returns A nonce
*/
export function getNonce() {
let text = ""
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length))
}
return text
}
/**
* A helper function which will get the webview URI of a given file or resource.
*
* @remarks This URI can be used within a webview's HTML as a link to the
* given file/resource.
*
* @param webview A reference to the extension webview
* @param extensionUri The URI of the directory containing the extension
* @param pathList An array of strings representing the path to a file/resource
* @returns A URI pointing to the file/resource
*/
export function getUri(webview: Webview, extensionUri: Uri, pathList: string[]) {
return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList))
}
+2
View File
@@ -12,8 +12,10 @@ export interface ExtensionMessage {
}
export interface ExtensionState {
version: string
apiConfiguration?: ApiConfiguration
maxRequestsPerTask?: number
customInstructions?: string
themeName?: string
claudeMessages: ClaudeMessage[]
shouldShowAnnouncement: boolean
+1
View File
@@ -4,6 +4,7 @@ export interface WebviewMessage {
type:
| "apiConfiguration"
| "maxRequestsPerTask"
| "customInstructions"
| "webviewDidLaunch"
| "newTask"
| "askResponse"
+195
View File
@@ -1,6 +1,7 @@
export type ApiProvider = "anthropic" | "openrouter" | "bedrock"
export interface ApiHandlerOptions {
apiModelId?: ApiModelId
apiKey?: string // anthropic
openRouterApiKey?: string
awsAccessKey?: string
@@ -11,3 +12,197 @@ export interface ApiHandlerOptions {
export type ApiConfiguration = ApiHandlerOptions & {
apiProvider?: ApiProvider
}
// Models
export interface ModelInfo {
maxTokens: number
supportsImages: boolean
inputPrice: number
outputPrice: number
}
export type ApiModelId = AnthropicModelId | OpenRouterModelId | BedrockModelId
// Anthropic
// https://docs.anthropic.com/en/docs/about-claude/models
export type AnthropicModelId = keyof typeof anthropicModels
export const anthropicDefaultModelId: AnthropicModelId = "claude-3-5-sonnet-20240620"
export const anthropicModels = {
"claude-3-5-sonnet-20240620": {
maxTokens: 8192,
supportsImages: true,
inputPrice: 3.0, // $3 per million input tokens
outputPrice: 15.0, // $15 per million output tokens
},
"claude-3-opus-20240229": {
maxTokens: 4096,
supportsImages: true,
inputPrice: 15.0,
outputPrice: 75.0,
},
"claude-3-sonnet-20240229": {
maxTokens: 4096,
supportsImages: true,
inputPrice: 3.0,
outputPrice: 15.0,
},
"claude-3-haiku-20240307": {
maxTokens: 4096,
supportsImages: true,
inputPrice: 0.25,
outputPrice: 1.25,
},
} as const satisfies Record<string, ModelInfo> // as const assertion makes the object deeply readonly
// AWS Bedrock
// https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html
export type BedrockModelId = keyof typeof bedrockModels
export const bedrockDefaultModelId: BedrockModelId = "anthropic.claude-3-5-sonnet-20240620-v1:0"
export const bedrockModels = {
"anthropic.claude-3-5-sonnet-20240620-v1:0": {
maxTokens: 4096,
supportsImages: true,
inputPrice: 3.0,
outputPrice: 15.0,
},
"anthropic.claude-3-opus-20240229-v1:0": {
maxTokens: 4096,
supportsImages: true,
inputPrice: 15.0,
outputPrice: 75.0,
},
"anthropic.claude-3-sonnet-20240229-v1:0": {
maxTokens: 4096,
supportsImages: true,
inputPrice: 3.0,
outputPrice: 15.0,
},
"anthropic.claude-3-haiku-20240307-v1:0": {
maxTokens: 4096,
supportsImages: true,
inputPrice: 0.25,
outputPrice: 1.25,
},
} as const satisfies Record<string, ModelInfo>
// OpenRouter
// https://openrouter.ai/models?order=newest&supported_parameters=tools
export type OpenRouterModelId = keyof typeof openRouterModels
export const openRouterDefaultModelId: OpenRouterModelId = "anthropic/claude-3.5-sonnet"
export const openRouterModels = {
"anthropic/claude-3.5-sonnet": {
maxTokens: 8192,
supportsImages: true,
inputPrice: 3.0,
outputPrice: 15.0,
},
"anthropic/claude-3-opus": {
maxTokens: 4096,
supportsImages: true,
inputPrice: 15,
outputPrice: 75,
},
"anthropic/claude-3-sonnet": {
maxTokens: 4096,
supportsImages: true,
inputPrice: 3,
outputPrice: 15,
},
"anthropic/claude-3-haiku": {
maxTokens: 4096,
supportsImages: true,
inputPrice: 0.25,
outputPrice: 1.25,
},
"openai/gpt-4o-2024-08-06": {
maxTokens: 16384,
supportsImages: true,
inputPrice: 2.5,
outputPrice: 10,
},
"openai/gpt-4o-mini-2024-07-18": {
maxTokens: 16384,
supportsImages: true,
inputPrice: 0.15,
outputPrice: 0.6,
},
"openai/gpt-4-turbo": {
maxTokens: 4096,
supportsImages: true,
inputPrice: 10,
outputPrice: 30,
},
// llama 3.1 models cannot use tools yet
// "meta-llama/llama-3.1-405b-instruct": {
// maxTokens: 2048,
// supportsImages: false,
// inputPrice: 2.7,
// outputPrice: 2.7,
// },
// "meta-llama/llama-3.1-70b-instruct": {
// maxTokens: 2048,
// supportsImages: false,
// inputPrice: 0.52,
// outputPrice: 0.75,
// },
// "meta-llama/llama-3.1-8b-instruct": {
// maxTokens: 2048,
// supportsImages: false,
// inputPrice: 0.06,
// outputPrice: 0.06,
// },
// OpenRouter needs to fix mapping gemini 1.5 responses for tool calls properly, they return content with line breaks formatted wrong (too many escapes), and throw errors for being in the wrong order when they're not. They also cannot handle feedback given to a request with multiple tools. Giving feedback to one tool use requests works fine. ("Please ensure that function response turn comes immediately after a function call turn. And the number of function response parts should be equal to number of function call parts of the function call turn.")
// "google/gemini-pro-1.5": {
// maxTokens: 8192,
// supportsImages: false, // "Function Calling is not supported with non-text input"
// inputPrice: 2.5,
// outputPrice: 7.5,
// },
// "google/gemini-flash-1.5": {
// maxTokens: 8192,
// supportsImages: false, // "Function Calling is not supported with non-text input"
// inputPrice: 0.25,
// outputPrice: 0.75,
// },
// "google/gemini-pro": {
// maxTokens: 8192,
// supportsImages: false, // "Function Calling is not supported with non-text input"
// inputPrice: 0.125,
// outputPrice: 0.375,
// },
// while deepseek coder can use tools, it may sometimes send tool invocation as a text block
"deepseek/deepseek-coder": {
maxTokens: 4096,
supportsImages: false,
inputPrice: 0.14,
outputPrice: 0.28,
},
// mistral models can use tools but aren't great at going step-by-step and proceeding to the next step
"mistralai/mistral-large": {
maxTokens: 8192,
supportsImages: false,
inputPrice: 3,
outputPrice: 9,
},
// This model is not capable of complex system/tool prompts
// "mistralai/mistral-7b-instruct-v0.1": {
// maxTokens: 4096,
// supportsImages: false,
// inputPrice: 0.06,
// outputPrice: 0.06,
// },
// cohere models are not capable of complex system/tool prompts
// "cohere/command-r-plus": {
// maxTokens: 4000,
// supportsImages: false,
// inputPrice: 3,
// outputPrice: 15,
// },
// "cohere/command-r": {
// maxTokens: 4000,
// supportsImages: false,
// inputPrice: 0.5,
// outputPrice: 1.5,
// },
} as const satisfies Record<string, ModelInfo>
+4
View File
@@ -0,0 +1,4 @@
export * from "./getNonce"
export * from "./getUri"
export * from "./process-images"
export * from "./export-markdown"
@@ -13,6 +13,87 @@ const rewire = require("rewire")
const defaults = rewire("react-scripts/scripts/build.js")
const config = defaults.__get__("config")
/* Modifying Webpack Configuration for 'shared' dir
This section uses Rewire to modify Create React App's webpack configuration without ejecting. Rewire allows us to inject and alter the internal build scripts of CRA at runtime. This allows us to maintain a flexible project structure that keeps shared code outside the webview-ui/src directory, while still adhering to CRA's security model that typically restricts imports to within src/.
1. Uses the ModuleScopePlugin to whitelist files from the shared directory, allowing them to be imported despite being outside src/. (see: https://stackoverflow.com/questions/44114436/the-create-react-app-imports-restriction-outside-of-src-directory/58321458#58321458)
2. Modifies the TypeScript rule to include the shared directory in compilation. This essentially transpiles and includes the ts files in shared dir in the output main.js file.
Before, we would just import types from shared dir and specifying include (and alias to have cleaner paths) in tsconfig.json was enough. But now that we are creating values (i.e. models in api.ts) to import into the react app, we must also include these files in the webpack resolution.
- Imports from the shared directory must use full paths relative to the src directory, without file extensions.
- Example: import { someFunction } from '../../src/shared/utils/helpers'
*/
const ModuleScopePlugin = require("react-dev-utils/ModuleScopePlugin")
const path = require("path")
const fs = require("fs")
// Get all files in the shared directory
const sharedDir = path.resolve(__dirname, "../../src/shared")
function getAllFiles(dir) {
let files = []
fs.readdirSync(dir).forEach((file) => {
const filePath = path.join(dir, file)
if (fs.statSync(filePath).isDirectory()) {
files = files.concat(getAllFiles(filePath))
} else {
const withoutExtension = filePath.split(".")[0]
files.push(withoutExtension)
}
})
return files
}
const sharedFiles = getAllFiles(sharedDir)
// config.resolve.plugins = config.resolve.plugins.filter((plugin) => !(plugin instanceof ModuleScopePlugin))
// Instead of excluding the whole ModuleScopePlugin, we just whitelist specific files that can be imported from outside src.
config.resolve.plugins.forEach((plugin) => {
if (plugin instanceof ModuleScopePlugin) {
console.log("Whitelisting shared files: ", sharedFiles)
sharedFiles.forEach((file) => plugin.allowedFiles.add(file))
}
})
/*
Webpack configuration
Webpack is a module bundler for JavaScript applications. It processes your project files, resolving dependencies and generating a deployable production build.
The webpack config is an object that tells webpack how to process and bundle your code. It defines entry points, output settings, and how to handle different file types.
This config.module section of the webpack config deals with how different file types (modules) should be treated.
config.module.rules:
Rules define how module files should be processed. Each rule can:
- Specify which files to process (test)
When webpack "processes" a file, it performs several operations:
1. Reads the file
2. Parses its content and analyzes dependencies
3. Applies transformations (e.g., converting TypeScript to JavaScript)
4. Potentially modifies the code (e.g., applying polyfills)
5. Includes the processed file in the final bundle
By specifying which files to process, we're telling webpack which files should go through this pipeline and be included in our application bundle. Files that aren't processed are ignored by webpack.
In our case, we're ensuring that TypeScript files in our shared directory are processed, allowing us to use them in our application.
- Define which folders to include or exclude
- Set which loaders to use for transformation
A loader transforms certain types of files into valid modules that webpack can process. For example, the TypeScript loader converts .ts files into JavaScript that webpack can understand.
By modifying these rules, we can change how webpack processes different files in our project, allowing us to include files from outside the standard src directory.
Why we need to modify the webpack config
Create React App (CRA) is designed to only process files within the src directory for security reasons. (CRA limits processing to the src directory to prevent accidental inclusion of sensitive files, reduce the attack surface, and ensure predictable builds, enhancing overall project security and consistency. Therefore it's essential that if you do include files outside src, you do so explicitly.)
To use files from the shared directory, we need to:
1. Modify ModuleScopePlugin to allow imports from the shared directory.
2. Update the TypeScript loader rule to process TypeScript files from the shared directory.
These changes tell webpack it's okay to import from the shared directory and ensure that TypeScript files in this directory are properly converted to JavaScript.
Modify webpack configuration to process TypeScript files from shared directory
This code modifies the webpack configuration to allow processing of TypeScript files from our shared directory, which is outside the standard src folder.
1. config.module.rules[1]: In Create React App's webpack config, the second rule (index 1) typically contains the rules for processing JavaScript and TypeScript files.
2. .oneOf: This array contains a list of loaders, and webpack will use the first matching loader for each file. We iterate through these to find the TypeScript loader.
3. We check each rule to see if it applies to TypeScript files by looking for 'ts|tsx' in the test regex.
4. When we find the TypeScript rule, we add our shared directory to its 'include' array. This tells webpack to also process TypeScript files from the shared directory.
Note: This code assumes a specific structure in the CRA webpack config. If CRA updates its config structure in future versions, this code might need to be adjusted.
*/
config.module.rules[1].oneOf.forEach((rule) => {
if (rule.test && rule.test.toString().includes("ts|tsx")) {
// rule.include is path to src by default, but we can update rule.include to be an array as it matches an expected schema by react-scripts
rule.include = [rule.include, sharedDir].filter(Boolean)
}
})
// Disable code splitting
config.optimization.splitChunks = {
cacheGroups: {
+18 -6
View File
@@ -1,27 +1,28 @@
import React, { useEffect, useState, useCallback } from "react"
import React, { useCallback, useEffect, useMemo, useState } from "react"
import { useEvent } from "react-use"
import { ApiConfiguration } from "../../src/shared/api"
import { ClaudeMessage, ExtensionMessage } from "../../src/shared/ExtensionMessage"
import "./App.css"
import { normalizeApiConfiguration } from "./components/ApiOptions"
import ChatView from "./components/ChatView"
import SettingsView from "./components/SettingsView"
import { ClaudeMessage, ExtensionMessage } from "@shared/ExtensionMessage"
import WelcomeView from "./components/WelcomeView"
import { vscode } from "./utils/vscode"
import { useEvent } from "react-use"
import { ApiConfiguration } from "@shared/api"
/*
The contents of webviews however are created when the webview becomes visible and destroyed when the webview is moved into the background. Any state inside the webview will be lost when the webview is moved to a background tab.
The best way to solve this is to make your webview stateless. Use message passing to save off the webview's state and then restore the state when the webview becomes visible again.
*/
const App: React.FC = () => {
const [didHydrateState, setDidHydrateState] = useState(false)
const [showSettings, setShowSettings] = useState(false)
const [showWelcome, setShowWelcome] = useState<boolean>(false)
const [version, setVersion] = useState<string>("")
const [apiConfiguration, setApiConfiguration] = useState<ApiConfiguration | undefined>(undefined)
const [maxRequestsPerTask, setMaxRequestsPerTask] = useState<string>("")
const [customInstructions, setCustomInstructions] = useState<string>("")
const [vscodeThemeName, setVscodeThemeName] = useState<string | undefined>(undefined)
const [claudeMessages, setClaudeMessages] = useState<ClaudeMessage[]>([])
const [showAnnouncement, setShowAnnouncement] = useState(false)
@@ -34,6 +35,7 @@ const App: React.FC = () => {
const message: ExtensionMessage = e.data
switch (message.type) {
case "state":
setVersion(message.state!.version)
const hasKey =
message.state!.apiConfiguration?.apiKey !== undefined ||
message.state!.apiConfiguration?.openRouterApiKey !== undefined ||
@@ -43,6 +45,7 @@ const App: React.FC = () => {
setMaxRequestsPerTask(
message.state!.maxRequestsPerTask !== undefined ? message.state!.maxRequestsPerTask.toString() : ""
)
setCustomInstructions(message.state!.customInstructions || "")
setVscodeThemeName(message.state!.themeName)
setClaudeMessages(message.state!.claudeMessages)
// don't update showAnnouncement to false if shouldShowAnnouncement is false
@@ -68,6 +71,10 @@ const App: React.FC = () => {
useEvent("message", handleMessage)
const { selectedModelInfo } = useMemo(() => {
return normalizeApiConfiguration(apiConfiguration)
}, [apiConfiguration])
if (!didHydrateState) {
return null
}
@@ -80,19 +87,24 @@ const App: React.FC = () => {
<>
{showSettings && (
<SettingsView
version={version}
apiConfiguration={apiConfiguration}
setApiConfiguration={setApiConfiguration}
maxRequestsPerTask={maxRequestsPerTask}
setMaxRequestsPerTask={setMaxRequestsPerTask}
customInstructions={customInstructions}
setCustomInstructions={setCustomInstructions}
onDone={() => setShowSettings(false)}
/>
)}
{/* Do not conditionally load ChatView, it's expensive and there's state we don't want to lose (user input, disableInput, askResponse promise, etc.) */}
<ChatView
version={version}
messages={claudeMessages}
isHidden={showSettings}
vscodeThemeName={vscodeThemeName}
showAnnouncement={showAnnouncement}
selectedModelSupportsImages={selectedModelInfo.supportsImages}
hideAnnouncement={() => setShowAnnouncement(false)}
/>
</>
+11 -23
View File
@@ -1,12 +1,13 @@
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
interface AnnouncementProps {
version: string
hideAnnouncement: () => void
}
/*
You must update the latestAnnouncementId in ClaudeDevProvider for new announcements to show to users. This new id will be compared with whats in state for the 'last announcement shown', and if it's different then the announcement will render. As soon as an announcement is shown, the id will be updated in state. This ensures that announcements are not shown more than once, even if the user doesn't close it themselves.
*/
const Announcement = ({ hideAnnouncement }: AnnouncementProps) => {
const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
return (
<div
style={{
@@ -22,35 +23,22 @@ const Announcement = ({ hideAnnouncement }: AnnouncementProps) => {
style={{ position: "absolute", top: "8px", right: "8px" }}>
<span className="codicon codicon-close"></span>
</VSCodeButton>
<h3 style={{ margin: "0 0 8px" }}>🎉{" "}New in v1.0.98</h3>
<h3 style={{ margin: "0 0 8px" }}>
🎉{" "}New in v{version}
</h3>
<ul style={{ margin: "0 0 8px", paddingLeft: "20px" }}>
<li>
Open in the editor (using{" "}
<span
className="codicon codicon-link-external"
style={{ display: "inline", fontSize: "12.5px", verticalAlign: "text-bottom" }}></span>{" "}
or <code>Claude Dev: Open In New Tab</code> in command palette) to see how Claude updates your
workspace more clearly
Paste images in chat and turn mockups into fully functional applications or fix bugs with
screenshots
</li>
<li>
New <code style={{ wordBreak: "break-all" }}>list_files_recursive</code> and{" "}
<code style={{ wordBreak: "break-all" }}>view_source_code_definitions_top_level</code> tools to help
Claude get a comprehensive overview of your project's file structure and source code definitions
<VSCodeLink
href="https://github.com/saoudrizwan/claude-dev?tab=readme-ov-file#working-in-existing-projects"
style={{ display: "inline" }}>
(more on this here)
</VSCodeLink>
Added option to choose other Claude models (+ GPT-4o, DeepSeek, and Mistral if you use OpenRouter)
</li>
<li>
Interact with CLI commands by sending messages to stdin and terminating long-running processes like
servers
You can now add custom instructions to the end of the system prompt (e.g. "Always use Python",
"Speak in Spanish")
</li>
<li>Provide feedback to tool use like editing files or running commands</li>
<li>Shows diff view of new or edited files right in the editor</li>
<li>Added ability to retry failed API requests (helpful for rate limits)</li>
<li>Export task to a markdown file (useful as context for future tasks)</li>
<li>Added OpenRouter and AWS Bedrock support</li>
<li>Improved support for running interactive terminal commands and servers</li>
</ul>
<p style={{ margin: "0" }}>
Follow me for more updates!{" "}
+147 -20
View File
@@ -1,34 +1,78 @@
import { ApiConfiguration } from "@shared/api"
import { VSCodeDropdown, VSCodeLink, VSCodeOption, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import React from "react"
import React, { useMemo } from "react"
import {
ApiConfiguration,
ApiModelId,
ModelInfo,
anthropicDefaultModelId,
anthropicModels,
bedrockDefaultModelId,
bedrockModels,
openRouterDefaultModelId,
openRouterModels,
} from "../../../src/shared/api"
interface ApiOptionsProps {
showModelOptions: boolean
apiConfiguration?: ApiConfiguration
setApiConfiguration: React.Dispatch<React.SetStateAction<ApiConfiguration | undefined>>
}
const ApiOptions: React.FC<ApiOptionsProps> = ({ apiConfiguration, setApiConfiguration }) => {
const ApiOptions: React.FC<ApiOptionsProps> = ({ showModelOptions, apiConfiguration, setApiConfiguration }) => {
const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
setApiConfiguration((prev) => ({ ...prev, [field]: event.target.value }))
}
const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo(() => {
return normalizeApiConfiguration(apiConfiguration)
}, [apiConfiguration])
/*
VSCodeDropdown has an open bug where dynamically rendered options don't auto select the provided value prop. You can see this for yourself by comparing it with normal select/option elements, which work as expected.
https://github.com/microsoft/vscode-webview-ui-toolkit/issues/433
In our case, when the user switches between providers, we recalculate the selectedModelId depending on the provider, the default model for that provider, and a modelId that the user may have selected. Unfortunately, the VSCodeDropdown component wouldn't select this calculated value, and would default to the first "Select a model..." option instead, which makes it seem like the model was cleared out when it wasn't.
As a workaround, we create separate instances of the dropdown for each provider, and then conditionally render the one that matches the current provider.
*/
const createDropdown = (models: Record<string, ModelInfo>) => {
return (
<VSCodeDropdown
id="model-id"
value={selectedModelId}
onChange={handleInputChange("apiModelId")}
style={{ width: "100%" }}>
<VSCodeOption value="">Select a model...</VSCodeOption>
{Object.keys(models).map((modelId) => (
<VSCodeOption
key={modelId}
value={modelId}
style={{
whiteSpace: "normal",
wordWrap: "break-word",
maxWidth: "100%",
}}>
{modelId}
</VSCodeOption>
))}
</VSCodeDropdown>
)
}
return (
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
<div className="dropdown-container">
<label htmlFor="api-provider">
<span style={{ fontWeight: 500 }}>API Provider</span>
</label>
<VSCodeDropdown
id="api-provider"
value={apiConfiguration?.apiProvider || "anthropic"}
onChange={handleInputChange("apiProvider")}>
<VSCodeDropdown id="api-provider" value={selectedProvider} onChange={handleInputChange("apiProvider")}>
<VSCodeOption value="anthropic">Anthropic</VSCodeOption>
<VSCodeOption value="openrouter">OpenRouter</VSCodeOption>
<VSCodeOption value="bedrock">AWS Bedrock</VSCodeOption>
</VSCodeDropdown>
</div>
{apiConfiguration?.apiProvider === "anthropic" && (
{selectedProvider === "anthropic" && (
<div>
<VSCodeTextField
value={apiConfiguration?.apiKey || ""}
@@ -51,7 +95,7 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({ apiConfiguration, setApiConfigu
</div>
)}
{apiConfiguration?.apiProvider === "openrouter" && (
{selectedProvider === "openrouter" && (
<div>
<VSCodeTextField
value={apiConfiguration?.openRouterApiKey || ""}
@@ -74,7 +118,7 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({ apiConfiguration, setApiConfigu
</div>
)}
{apiConfiguration?.apiProvider === "bedrock" && (
{selectedProvider === "bedrock" && (
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
<VSCodeTextField
value={apiConfiguration?.awsAccessKey || ""}
@@ -100,17 +144,17 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({ apiConfiguration, setApiConfigu
style={{ width: "100%" }}
onChange={handleInputChange("awsRegion")}>
<VSCodeOption value="">Select a region...</VSCodeOption>
{/* Currently Claude 3.5 Sonnet is only available in us-east-1 */}
{/* The user will have to choose a region that supports the model they use, but this shouldn't be a problem since they'd have to request access for it in that region in the first place. */}
<VSCodeOption value="us-east-1">US East (N. Virginia)</VSCodeOption>
{/* <VSCodeOption value="us-east-2">US East (Ohio)</VSCodeOption>
<VSCodeOption value="us-west-1">US West (N. California)</VSCodeOption>
{/* <VSCodeOption value="us-east-2">US East (Ohio)</VSCodeOption> */}
{/* <VSCodeOption value="us-west-1">US West (N. California)</VSCodeOption> */}
<VSCodeOption value="us-west-2">US West (Oregon)</VSCodeOption>
<VSCodeOption value="af-south-1">Africa (Cape Town)</VSCodeOption>
<VSCodeOption value="ap-east-1">Asia Pacific (Hong Kong)</VSCodeOption>
{/* <VSCodeOption value="af-south-1">Africa (Cape Town)</VSCodeOption> */}
{/* <VSCodeOption value="ap-east-1">Asia Pacific (Hong Kong)</VSCodeOption> */}
<VSCodeOption value="ap-south-1">Asia Pacific (Mumbai)</VSCodeOption>
<VSCodeOption value="ap-northeast-1">Asia Pacific (Tokyo)</VSCodeOption>
<VSCodeOption value="ap-northeast-2">Asia Pacific (Seoul)</VSCodeOption>
<VSCodeOption value="ap-northeast-3">Asia Pacific (Osaka)</VSCodeOption>
{/* <VSCodeOption value="ap-northeast-2">Asia Pacific (Seoul)</VSCodeOption> */}
{/* <VSCodeOption value="ap-northeast-3">Asia Pacific (Osaka)</VSCodeOption> */}
<VSCodeOption value="ap-southeast-1">Asia Pacific (Singapore)</VSCodeOption>
<VSCodeOption value="ap-southeast-2">Asia Pacific (Sydney)</VSCodeOption>
<VSCodeOption value="ca-central-1">Canada (Central)</VSCodeOption>
@@ -118,9 +162,9 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({ apiConfiguration, setApiConfigu
<VSCodeOption value="eu-west-1">Europe (Ireland)</VSCodeOption>
<VSCodeOption value="eu-west-2">Europe (London)</VSCodeOption>
<VSCodeOption value="eu-west-3">Europe (Paris)</VSCodeOption>
<VSCodeOption value="eu-north-1">Europe (Stockholm)</VSCodeOption>
<VSCodeOption value="me-south-1">Middle East (Bahrain)</VSCodeOption>
<VSCodeOption value="sa-east-1">South America (São Paulo)</VSCodeOption> */}
{/* <VSCodeOption value="eu-north-1">Europe (Stockholm)</VSCodeOption> */}
{/* <VSCodeOption value="me-south-1">Middle East (Bahrain)</VSCodeOption> */}
<VSCodeOption value="sa-east-1">South America (São Paulo)</VSCodeOption>
</VSCodeDropdown>
</div>
<p
@@ -138,8 +182,91 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({ apiConfiguration, setApiConfigu
</p>
</div>
)}
{showModelOptions && (
<>
<div className="dropdown-container">
<label htmlFor="model-id">
<span style={{ fontWeight: 500 }}>Model</span>
</label>
{selectedProvider === "anthropic" && createDropdown(anthropicModels)}
{selectedProvider === "openrouter" && createDropdown(openRouterModels)}
{selectedProvider === "bedrock" && createDropdown(bedrockModels)}
</div>
<ModelInfoView modelInfo={selectedModelInfo} />
</>
)}
</div>
)
}
const ModelInfoView = ({ modelInfo }: { modelInfo: ModelInfo }) => {
const formatPrice = (price: number) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(price)
}
return (
<p style={{ fontSize: "12px", marginTop: "2px", color: "var(--vscode-descriptionForeground)" }}>
<span
style={{
fontWeight: 500,
color: modelInfo.supportsImages
? "var(--vscode-testing-iconPassed)"
: "var(--vscode-errorForeground)",
}}>
<i
className={`codicon codicon-${modelInfo.supportsImages ? "check" : "x"}`}
style={{
marginRight: 4,
marginBottom: modelInfo.supportsImages ? 1 : -1,
fontSize: modelInfo.supportsImages ? 11 : 13,
fontWeight: 700,
display: "inline-block",
verticalAlign: "bottom",
}}></i>
{modelInfo.supportsImages ? "Supports images" : "Does not support images"}
</span>
<br />
<span style={{ fontWeight: 500 }}>Max output:</span> {modelInfo.maxTokens.toLocaleString()} tokens
<br />
<span style={{ fontWeight: 500 }}>Input price:</span> {formatPrice(modelInfo.inputPrice)} per million tokens
<br />
<span style={{ fontWeight: 500 }}>Output price:</span> {formatPrice(modelInfo.outputPrice)} per million
tokens
</p>
)
}
export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
const provider = apiConfiguration?.apiProvider || "anthropic"
const modelId = apiConfiguration?.apiModelId
const getProviderData = (models: Record<string, ModelInfo>, defaultId: ApiModelId) => {
let selectedModelId: ApiModelId
let selectedModelInfo: ModelInfo
if (modelId && modelId in models) {
selectedModelId = modelId
selectedModelInfo = models[modelId]
} else {
selectedModelId = defaultId
selectedModelInfo = models[defaultId]
}
return { selectedProvider: provider, selectedModelId, selectedModelInfo }
}
switch (provider) {
case "anthropic":
return getProviderData(anthropicModels, anthropicDefaultModelId)
case "openrouter":
return getProviderData(openRouterModels, openRouterDefaultModelId)
case "bedrock":
return getProviderData(bedrockModels, bedrockDefaultModelId)
}
}
export default ApiOptions
+3 -3
View File
@@ -1,11 +1,11 @@
import { ClaudeAsk, ClaudeMessage, ClaudeSay, ClaudeSayTool } from "@shared/ExtensionMessage"
import { VSCodeBadge, VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
import React from "react"
import Markdown from "react-markdown"
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
import { ClaudeAsk, ClaudeMessage, ClaudeSay, ClaudeSayTool } from "../../../src/shared/ExtensionMessage"
import { COMMAND_OUTPUT_STRING } from "../utils/combineCommandSequences"
import { SyntaxHighlighterStyle } from "../utils/getSyntaxHighlighterStyleFromTheme"
import CodeBlock from "./CodeBlock/CodeBlock"
import Markdown from "react-markdown"
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
import Thumbnails from "./Thumbnails"
interface ChatRowProps {
+37 -12
View File
@@ -1,31 +1,41 @@
import { ClaudeAsk, ClaudeMessage, ExtensionMessage } from "@shared/ExtensionMessage"
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"
import vsDarkPlus from "react-syntax-highlighter/dist/esm/styles/prism/vsc-dark-plus"
import DynamicTextArea from "react-textarea-autosize"
import { useEvent, useMount } from "react-use"
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
import { ClaudeAsk, ClaudeMessage, ExtensionMessage } from "../../../src/shared/ExtensionMessage"
import { combineApiRequests } from "../utils/combineApiRequests"
import { combineCommandSequences } from "../utils/combineCommandSequences"
import { getApiMetrics } from "../utils/getApiMetrics"
import { getSyntaxHighlighterStyleFromTheme } from "../utils/getSyntaxHighlighterStyleFromTheme"
import { vscode } from "../utils/vscode"
import Announcement from "./Announcement"
import ChatRow from "./ChatRow"
import TaskHeader from "./TaskHeader"
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
import Announcement from "./Announcement"
import Thumbnails from "./Thumbnails"
interface ChatViewProps {
version: string
messages: ClaudeMessage[]
isHidden: boolean
vscodeThemeName?: string
showAnnouncement: boolean
selectedModelSupportsImages: boolean
hideAnnouncement: () => void
}
const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideAnnouncement }: ChatViewProps) => {
const ChatView = ({
version,
messages,
isHidden,
vscodeThemeName,
showAnnouncement,
selectedModelSupportsImages,
hideAnnouncement,
}: ChatViewProps) => {
//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined
const task = messages.length > 0 ? messages[0] : undefined // leaving this less safe version here since if the first message is not a task, then the extension is in a bad state and needs to be debugged (see ClaudeDev.abort)
const modifiedMessages = useMemo(() => combineApiRequests(combineCommandSequences(messages.slice(1))), [messages])
@@ -177,7 +187,7 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
const handleSendMessage = () => {
const text = inputValue.trim()
if (text) {
if (text || selectedImages.length > 0) {
if (messages.length === 0) {
vscode.postMessage({ type: "newTask", text, images: selectedImages })
} else if (claudeAsk) {
@@ -270,6 +280,11 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
}
const handlePaste = async (e: React.ClipboardEvent) => {
if (shouldDisableImages) {
e.preventDefault()
return
}
const items = e.clipboardData.items
const acceptedTypes = ["png", "jpeg", "webp"] // supported by anthropic and openrouter (jpg is just a file extension but the image will be recognized as jpeg)
const imageItems = Array.from(items).filter((item) => {
@@ -380,6 +395,12 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
case "api_req_finished": // combineApiRequests removes this from modifiedMessages anyways
case "api_req_retried": // this message is used to update the latest api_req_started that the request was retried
return false
case "text":
// Sometimes Claude returns an empty text message, we don't want to render these. (We also use a say text for user messages, so in case they just sent images we still render that)
if ((message.text ?? "") === "" && (message.images?.length ?? 0) === 0) {
return false
}
break
}
return true
})
@@ -404,6 +425,12 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
return [text, false]
}, [task, messages])
const shouldDisableImages =
!selectedModelSupportsImages ||
textAreaDisabled ||
selectedImages.length >= MAX_IMAGES_PER_MESSAGE ||
isInputPipingToStdin
return (
<div
style={{
@@ -427,7 +454,7 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
/>
) : (
<>
{showAnnouncement && <Announcement hideAnnouncement={hideAnnouncement} />}
{showAnnouncement && <Announcement version={version} hideAnnouncement={hideAnnouncement} />}
<div style={{ padding: "0 20px" }}>
<h2>What can I do for you?</h2>
<p>
@@ -438,7 +465,7 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
Claude 3.5 Sonnet's agentic coding capabilities,
</VSCodeLink>{" "}
I can handle complex software development tasks step-by-step. With tools that let me create
& edit files, analyze project source code, and execute terminal commands (after you grant
& edit files, explore complex projects, and execute terminal commands (after you grant
permission), I can assist you in ways that go beyond simple code completion or tech support.
</p>
</div>
@@ -582,23 +609,21 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
height: "calc(100% - 20px)", // Full height minus top and bottom padding
}}>
<VSCodeButton
disabled={
textAreaDisabled || selectedImages.length >= MAX_IMAGES_PER_MESSAGE || isInputPipingToStdin
}
disabled={shouldDisableImages}
appearance="icon"
aria-label="Attach Images"
onClick={selectImages}
style={{ marginRight: "4px" }}>
<span
className="codicon codicon-device-camera"
style={{ fontSize: 18, marginLeft: -2, marginBottom: 1 }}></span>
style={{ fontSize: 18.5, marginLeft: -2, marginBottom: 1.5 }}></span>
</VSCodeButton>
<VSCodeButton
disabled={textAreaDisabled}
appearance="icon"
aria-label="Send Message"
onClick={handleSendMessage}>
<span className="codicon codicon-send" style={{ marginBottom: -1 }}></span>
<span className="codicon codicon-send"></span>
</VSCodeButton>
</div>
</div>
+94 -52
View File
@@ -1,23 +1,29 @@
import { ApiConfiguration } from "@shared/api"
import { VSCodeButton, VSCodeDivider, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { VSCodeButton, VSCodeLink, VSCodeTextArea, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import React, { useEffect, useState } from "react"
import { ApiConfiguration } from "../../../src/shared/api"
import { validateApiConfiguration, validateMaxRequestsPerTask } from "../utils/validate"
import { vscode } from "../utils/vscode"
import ApiOptions from "./ApiOptions"
type SettingsViewProps = {
version: string
apiConfiguration?: ApiConfiguration
setApiConfiguration: React.Dispatch<React.SetStateAction<ApiConfiguration | undefined>>
maxRequestsPerTask: string
setMaxRequestsPerTask: React.Dispatch<React.SetStateAction<string>>
customInstructions: string
setCustomInstructions: React.Dispatch<React.SetStateAction<string>>
onDone: () => void
}
const SettingsView = ({
version,
apiConfiguration,
setApiConfiguration,
maxRequestsPerTask,
setMaxRequestsPerTask,
customInstructions,
setCustomInstructions,
onDone,
}: SettingsViewProps) => {
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
@@ -33,6 +39,7 @@ const SettingsView = ({
if (!apiValidationResult && !maxRequestsValidationResult) {
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
vscode.postMessage({ type: "maxRequestsPerTask", text: maxRequestsPerTask })
vscode.postMessage({ type: "customInstructions", text: customInstructions })
onDone()
}
}
@@ -58,78 +65,113 @@ const SettingsView = ({
*/
return (
<div style={{ margin: "0 auto", paddingTop: "10px" }}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "17px",
}}>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>Settings</h3>
<VSCodeButton onClick={handleSubmit}>Done</VSCodeButton>
</div>
<div style={{ marginBottom: 5 }}>
<ApiOptions apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
{apiErrorMessage && (
<p
style={{
margin: "-5px 0 12px 0",
fontSize: "12px",
color: "var(--vscode-errorForeground)",
}}>
{apiErrorMessage}
</p>
)}
</div>
<div style={{ marginBottom: "20px" }}>
<VSCodeTextField
value={maxRequestsPerTask}
style={{ width: "100%" }}
placeholder="20"
onInput={(e: any) => setMaxRequestsPerTask(e.target?.value)}>
<span style={{ fontWeight: "500" }}>Maximum # Requests Per Task</span>
</VSCodeTextField>
<p
<div
style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
padding: "10px 18px 18px 20px",
display: "flex",
flexDirection: "column",
overflow: "hidden",
}}>
<div style={{ flexGrow: 1, overflow: "auto" }}>
<div
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "17px",
}}>
If Claude Dev reaches this limit, it will pause and ask for your permission before making additional
requests.
</p>
{maxRequestsErrorMessage && (
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>Settings</h3>
<VSCodeButton onClick={handleSubmit}>Done</VSCodeButton>
</div>
<div style={{ marginBottom: 5 }}>
<ApiOptions
apiConfiguration={apiConfiguration}
setApiConfiguration={setApiConfiguration}
showModelOptions={true}
/>
{apiErrorMessage && (
<p
style={{
margin: "-5px 0 12px 0",
fontSize: "12px",
color: "var(--vscode-errorForeground)",
}}>
{apiErrorMessage}
</p>
)}
</div>
<div style={{ marginBottom: 15 }}>
<VSCodeTextArea
value={customInstructions}
style={{ width: "100%" }}
rows={4}
placeholder={
'e.g. "Run unit tests at the end", "Use TypeScript with async/await", "Speak in Spanish"'
}
onInput={(e: any) => setCustomInstructions(e.target?.value || "")}>
<span style={{ fontWeight: "500" }}>Custom Instructions</span>
</VSCodeTextArea>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-errorForeground)",
color: "var(--vscode-descriptionForeground)",
}}>
{maxRequestsErrorMessage}
These instructions are added to the end of the system prompt sent with every request.
</p>
)}
</div>
</div>
<VSCodeDivider />
<div style={{ marginBottom: 20 }}>
<VSCodeTextField
value={maxRequestsPerTask}
style={{ width: "100%" }}
placeholder="20"
onInput={(e: any) => setMaxRequestsPerTask(e.target?.value)}>
<span style={{ fontWeight: "500" }}>Maximum # Requests Per Task</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
If Claude Dev reaches this limit, it will pause and ask for your permission before making
additional requests.
</p>
{maxRequestsErrorMessage && (
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-errorForeground)",
}}>
{maxRequestsErrorMessage}
</p>
)}
</div>
</div>
<div
style={{
marginTop: "20px",
textAlign: "center",
color: "var(--vscode-descriptionForeground)",
fontSize: "12px",
lineHeight: "1.2",
}}>
<p style={{ wordWrap: "break-word" }}>
<p style={{ wordWrap: "break-word", margin: 0, padding: 0 }}>
If you have any questions or feedback, feel free to open an issue at{" "}
<VSCodeLink href="https://github.com/saoudrizwan/claude-dev" style={{ display: "inline" }}>
https://github.com/saoudrizwan/claude-dev
</VSCodeLink>
</p>
<p style={{ fontStyle: "italic" }}>v1.0.98</p>
<p style={{ fontStyle: "italic", margin: "10px 0 0 0", padding: 0 }}>v{version}</p>
</div>
</div>
)
+1 -1
View File
@@ -2,8 +2,8 @@ import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import React, { useEffect, useRef, useState } from "react"
import TextTruncate from "react-text-truncate"
import { useWindowSize } from "react-use"
import { ClaudeMessage } from "../../../src/shared/ExtensionMessage"
import { vscode } from "../utils/vscode"
import { ClaudeMessage } from "@shared/ExtensionMessage"
import Thumbnails from "./Thumbnails"
interface TaskHeaderProps {
+7 -3
View File
@@ -1,6 +1,6 @@
import { ApiConfiguration } from "@shared/api"
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import React, { useEffect, useState } from "react"
import { ApiConfiguration } from "../../../src/shared/api"
import { validateApiConfiguration } from "../utils/validate"
import { vscode } from "../utils/vscode"
import ApiOptions from "./ApiOptions"
@@ -34,13 +34,17 @@ const WelcomeView: React.FC<WelcomeViewProps> = ({ apiConfiguration, setApiConfi
Claude 3.5 Sonnet's agentic coding capabilities.
</VSCodeLink>{" "}
I am prompted to think through tasks step-by-step and have access to tools that let me create & edit
files, analyze project source code, and execute terminal commands (with your permission, of course).
files, explore complex projects, and execute terminal commands (with your permission, of course).
</p>
<b>To get started, this extension needs an API key for Claude 3.5 Sonnet:</b>
<div style={{ marginTop: "15px" }}>
<ApiOptions apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
<ApiOptions
apiConfiguration={apiConfiguration}
setApiConfiguration={setApiConfiguration}
showModelOptions={false}
/>
<VSCodeButton onClick={handleSubmit} disabled={disableLetsGoButton} style={{ marginTop: "3px" }}>
Let's go!
</VSCodeButton>
+8
View File
@@ -129,3 +129,11 @@ https://github.com/microsoft/vscode-webview-ui-toolkit/tree/main/src/dropdown#wi
line-height: normal;
margin-bottom: 2px;
}
/* Fix scrollbar in dropdown */
vscode-dropdown::part(listbox) {
border-color: var(--vscode-scrollbarSlider-background);
transition: none;
scrollbar-color: var(--vscode-scrollbarSlider-background) transparent;
}
+4 -4
View File
@@ -1,15 +1,15 @@
import { ClaudeMessage } from "@shared/ExtensionMessage"
import { ClaudeMessage } from "../../../src/shared/ExtensionMessage"
/**
* Combines API request start and finish messages in an array of ClaudeMessages.
*
*
* This function looks for pairs of 'api_req_started' and 'api_req_finished' messages.
* When it finds a pair, it combines them into a single 'api_req_combined' message.
* The JSON data in the text fields of both messages are merged.
*
*
* @param messages - An array of ClaudeMessage objects to process.
* @returns A new array of ClaudeMessage objects with API requests combined.
*
*
* @example
* const messages = [
* { type: "say", say: "api_req_started", text: '{"request":"GET /api/data"}', ts: 1000 },
@@ -1,4 +1,4 @@
import { ClaudeMessage } from "@shared/ExtensionMessage"
import { ClaudeMessage } from "../../../src/shared/ExtensionMessage"
/**
* Combines sequences of command and command_output messages in an array of ClaudeMessages.
+1 -1
View File
@@ -1,4 +1,4 @@
import { ClaudeMessage } from "@shared/ExtensionMessage"
import { ClaudeMessage } from "../../../src/shared/ExtensionMessage"
interface ApiMetrics {
totalTokensIn: number
+1 -1
View File
@@ -1,4 +1,4 @@
import { ClaudeMessage } from "@shared/ExtensionMessage";
import { ClaudeMessage } from "../../../src/shared/ExtensionMessage"
export const mockMessages: ClaudeMessage[] = [
{
+1 -1
View File
@@ -1,4 +1,4 @@
import { ApiConfiguration } from "@shared/api"
import { ApiConfiguration } from "../../../src/shared/api"
export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): string | undefined {
if (apiConfiguration) {
+1 -1
View File
@@ -1,4 +1,4 @@
import { WebviewMessage } from "@shared/WebviewMessage"
import { WebviewMessage } from "../../../src/shared/WebviewMessage"
import type { WebviewApi } from "vscode-webview"
/**
+1 -4
View File
@@ -14,10 +14,7 @@
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"paths": {
"@shared/*": ["../src/shared/*"]
}
"jsx": "react-jsx"
},
"include": ["src", "../src/shared"]
}