Compare commits

..
86 Commits
Author SHA1 Message Date
Saoud Rizwan 4e070a44c1 Update version 2024-08-05 01:01:03 -04:00
Saoud Rizwan c0ef27033b Ensure all command_outputs are sent to the webview before making next API request 2024-08-05 01:00:19 -04:00
Saoud Rizwan 84f89c569a Fix effect dep array issue by using latest messages value instead of previous claudeAsk value 2024-08-05 00:49:15 -04:00
Saoud Rizwan 91eedd7097 Update text 2024-08-05 00:37:05 -04:00
Saoud Rizwan 08737ac119 Update TaskHeader 2024-08-04 19:47:04 -04:00
Saoud Rizwan b299803ac5 Update version 2024-08-04 19:34:03 -04:00
Saoud Rizwan 38d461b784 Fix edge case issues when running commands with long outputs or as completion command 2024-08-04 19:32:45 -04:00
Saoud Rizwan c823cdd4f8 Update version 2024-08-03 21:24:21 -04:00
Saoud Rizwan 09050b8800 Fix issue where sending message to stdin during non-interactive long-running process would not relinquish control back over the exit command button 2024-08-03 21:23:33 -04:00
Saoud Rizwan bbdf66b45c Add video to README 2024-08-03 21:16:57 -04:00
Saoud Rizwan 0ac7f63332 Update text in permissions 2024-08-03 19:22:06 -04:00
Saoud Rizwan 582d178157 Update version and Announcement 2024-08-03 19:02:37 -04:00
Saoud Rizwan 1ea4c9beee Remove all AWS region options besides us-east-1 2024-08-03 18:56:50 -04:00
Saoud Rizwan d7ee0c0161 Fix OpenRouter calls by mapping openai sdk messages to and from anthropic sdk messages 2024-08-03 18:06:33 -04:00
Saoud Rizwan fa1068d92d Refactor utilities to utils for consistency 2024-08-03 15:47:41 -04:00
Saoud Rizwan b32f91b0ee Remove log 2024-08-03 15:46:04 -04:00
Saoud Rizwan 7f30a8bea7 Truncate files list for large projects 2024-08-03 15:39:29 -04:00
Saoud Rizwan 7656f8d653 Update version 2024-08-03 15:10:57 -04:00
Saoud Rizwan 2d37c290fa Add script to easily update version 2024-08-03 15:05:49 -04:00
Saoud Rizwan 7989410696 Remove secrets if we pass undefined to storeSecret 2024-08-03 14:38:23 -04:00
Saoud Rizwan c09a8462d7 Add support for OpenRouter and AWS Bedrock 2024-08-03 14:24:56 -04:00
Saoud Rizwan d441950b7f Update version 2024-08-02 18:04:53 -04:00
Saoud Rizwan 15f52696b9 Fix sending SIGINT to the command process's subprocesses 2024-08-02 18:01:44 -04:00
Saoud Rizwan c800f157da Update SYSTEM_PROMPT to break down complex steps into multiple iterations 2024-08-02 15:20:04 -04:00
Saoud Rizwan 67ed5edbe6 Update SYSTEM_PROMPT to better handle directory specific commands 2024-08-02 15:09:13 -04:00
Saoud Rizwan 89c242d966 Refactor apiConversationHistory and claudeMessages to be local to ClaudeDev instance to avoid polluting storage unnecessarily 2024-08-02 14:59:33 -04:00
Saoud Rizwan c994f64c6f Update SYSTEM_PROMPT to execute commands in specific directories when necessary 2024-08-02 13:39:01 -04:00
Saoud Rizwan 07f50e63a3 Persist API conversation history locally instead of in state as there seems to be an issue with some messages not being stringifyable 2024-08-01 15:18:29 -04:00
Saoud Rizwan d84e6c6fb1 Remove text area height calculation 2024-08-01 01:19:39 -04:00
Saoud Rizwan d3edb61dd3 Handle closing diff views more elegantly so that pane does not jump around 2024-08-01 00:16:24 -04:00
Saoud Rizwan 70634ddd33 Update version 2024-07-31 22:08:16 -04:00
Saoud Rizwan 683abb1ac3 Close diff view even if it loses focus 2024-07-31 22:07:32 -04:00
Saoud Rizwan 1d305b66c1 Fix issue where claude removes newline at end of files 2024-07-31 21:30:42 -04:00
Saoud Rizwan 5eb0c65419 Update Announcement 2024-07-31 21:08:17 -04:00
Saoud Rizwan 31d8472f59 Close current diff view when showing new edit right after; fix thinking tags rendering 2024-07-31 19:25:26 -04:00
Saoud Rizwan 624a4e4ed6 Update README 2024-07-31 18:26:15 -04:00
Saoud Rizwan 4c83833f32 Update CHANGELOG and Announcement 2024-07-31 18:20:51 -04:00
Saoud Rizwan 3fc5f2aac0 Open diff editor with virtual document for new files or edits 2024-07-31 18:17:40 -04:00
Saoud Rizwan 565acd921e Initialize Parser only once 2024-07-31 17:02:24 -04:00
Saoud Rizwan 5c38110933 Update demo.gif 2024-07-31 14:50:22 -04:00
Saoud Rizwan 6f0579a265 Update version 2024-07-31 14:25:54 -04:00
Saoud Rizwan c70e14c298 Add overflowWrap anywhere to force wrap long words (file paths) to avoid overflowing container 2024-07-31 14:24:04 -04:00
Saoud Rizwan dc97689836 Update extension description 2024-07-31 13:33:07 -04:00
Saoud Rizwan 2992baaaea Update version 2024-07-31 13:29:08 -04:00
Saoud Rizwan fd13081109 Sort top level files based on directory or not; fix task header styles 2024-07-31 13:28:00 -04:00
Saoud Rizwan 1ee733103a Update version 2024-07-31 12:50:27 -04:00
Saoud Rizwan 1dc3ba5f40 Update README 2024-07-31 12:46:32 -04:00
Saoud Rizwan b4b99bc331 Update CHANGELOG 2024-07-31 12:06:36 -04:00
Saoud Rizwan b29c512aa2 Update package lock 2024-07-31 12:03:26 -04:00
Saoud Rizwan 11dd56ae60 Turn SYSTEM_PROMPT into a function so it always has the latest values 2024-07-31 12:01:14 -04:00
Saoud Rizwan 1a4bae7f0d Revert announcement ID 2024-07-31 11:53:11 -04:00
Saoud Rizwan b672eeb307 Update README with info about the new view_source_code_definitions_top_level tool 2024-07-31 11:50:49 -04:00
Saoud Rizwan c69b515345 Fix code block wrapping behavior 2024-07-31 10:18:24 -04:00
Saoud Rizwan 9467583938 Fix code block wrapping behavior 2024-07-31 10:15:04 -04:00
Saoud Rizwan 838a3ae10f Rename extract_source_code_definitions_top_level to view_source_code_definitions_top_level and update Announcement 2024-07-31 10:10:04 -04:00
Saoud Rizwan 3daca3d68f Refactor analyze-project to parse-source-code 2024-07-31 09:52:36 -04:00
Saoud Rizwan 71a9ed1d1e Replace list_files/analyze_project with more explicit list_files_top_level, list_files_recursive, and extract_source_code_definitions_top_level 2024-07-31 09:50:07 -04:00
Saoud Rizwan 3fc300273d Limit web-tree-sitter to max # of files to parse 2024-07-31 08:17:10 -04:00
Saoud Rizwan d93a08af5d Revert "Remove web-tree-sitter"
This reverts commit 5c98a3d4af.

# Conflicts:
#	package.json
2024-07-31 07:56:42 -04:00
Saoud Rizwan 45ee2c3e1c Update version 2024-07-30 12:54:20 -04:00
Saoud Rizwan 5c98a3d4af Remove web-tree-sitter 2024-07-30 12:51:18 -04:00
Saoud Rizwan 24666fed30 Update queries 2024-07-30 11:01:23 -04:00
Saoud Rizwan 7afae36ab7 Update version 2024-07-30 04:25:17 -04:00
Saoud Rizwan e5d86ffb8d Revert "Rollback virtualized list changes"
This reverts commit e6f6d754b2.
2024-07-30 04:22:47 -04:00
Saoud Rizwan d7d3f44a5d Update version 2024-07-30 03:05:08 -04:00
Saoud Rizwan e6f6d754b2 Rollback virtualized list changes 2024-07-30 03:04:46 -04:00
Saoud Rizwan 92eaa53db3 Update README 2024-07-29 21:51:33 -04:00
Saoud Rizwan ace755c1d0 Update Announcement 2024-07-29 21:39:00 -04:00
Saoud Rizwan 43d23adc34 Open markdown export after saving 2024-07-29 20:56:37 -04:00
Saoud Rizwan b23ecca086 Add ability to export tasks as markdown 2024-07-29 20:48:26 -04:00
Saoud Rizwan fd0b3a18f7 Show progress indicator while command is executing 2024-07-29 18:46:23 -04:00
Saoud Rizwan d324afdf14 Gracefully handle when user exits command early 2024-07-29 17:44:49 -04:00
Saoud Rizwan cf2ab16538 Add support for interactive command line execution where user input is sent to stdin 2024-07-29 13:22:06 -04:00
Saoud Rizwan 0e4000e1f2 Update Announcement 2024-07-29 10:08:37 -04:00
Saoud Rizwan c1137277dc Update README and Announcement 2024-07-29 10:01:51 -04:00
Saoud Rizwan 31a6da944a Refactor queries 2024-07-28 18:33:43 -04:00
Saoud Rizwan 998aa8b7eb Update demo.gif 2024-07-28 13:36:59 -04:00
Saoud Rizwan 7842b07d97 Update demo.gif 2024-07-28 13:29:36 -04:00
Saoud Rizwan fc82d693ac Update CHANGELOG 2024-07-28 12:53:57 -04:00
Saoud Rizwan 2b9a1e2c7e Add context about user's vscode visible editor and opened tabs 2024-07-28 12:45:41 -04:00
Saoud Rizwan 890d340d7f Update view IDs and add comment about vscode caching behavior 2024-07-28 11:50:31 -04:00
Saoud Rizwan e356294754 Update .vscodeignore 2024-07-28 11:24:49 -04:00
Saoud Rizwan 181a5f299d Update version 2024-07-28 11:19:27 -04:00
Saoud Rizwan 3a63dfc31f Update README with information about analyze_project 2024-07-28 11:17:53 -04:00
Saoud Rizwan ecaaaaf4af Update demo.gif 2024-07-28 00:02:22 -04:00
Saoud Rizwan 86e743cd99 Update demo.gif 2024-07-27 23:52:36 -04:00
59 changed files with 5556 additions and 678 deletions
+1
View File
@@ -18,6 +18,7 @@ vsc-extension-quickstart.md
demo.gif
.nvmrc
.gitattributes
.prettierignore
# Ignore all webview-ui files except the build directory (https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/frameworks/hello-world-react-cra/.vscodeignore)
webview-ui/src/**
+27 -3
View File
@@ -4,11 +4,35 @@ 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. -->
## [Unreleased]
## [1.0.9]
## [0.0.7]
- Add support for OpenRouter and AWS Bedrock
-
## [1.0.8]
- Shows diff view of new or edited files right in the editor
## [1.0.7]
- Replace `list_files` and `analyze_project` with more explicit `list_files_top_level`, `list_files_recursive`, and `view_source_code_definitions_top_level` to get source code definitions only for files relevant to the task
## [1.0.6]
- Interact with CLI commands by sending messages to stdin and terminating long-running processes like servers
- Export tasks to markdown files (useful as context for future tasks)
## [1.0.5]
- Claude now has context about vscode's visible editors and opened tabs
## [1.0.4]
- Open in the editor (using menu bar or `Claude Dev: Open In New Tab` in command palette) to see how Claude updates your workspace more clearly
- New `analyze_project` tool to help Claude get a comprehensive overview of your project's source code definitions and file structure
- Provide feedback to tool use like terminal commands and file edits
- Updated max output tokens to 8192 so less lazy coding (`// rest of code here...`)
- Added ability to retry failed API requests (helpful for rate limits)
- Quality of life improvements like markdown rendering, memory optimizations, better theme support
## [0.0.6]
+32 -18
View File
@@ -6,22 +6,20 @@
<p align="center">
<a href="https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev"><strong>Download VSCode Extension</strong></a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.youtube.com/@saoudrizwan"><strong>Tutorials (coming soon)</strong></a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="#contribution"><strong>Make Contribution</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 read & write 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.
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 offers a human-in-the-loop GUI to supervise every file change and command executed, providing a safe and accessible way to explore the potential of agentic AI.
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.
- View syntax highlighted file previews and diffs for every change Claude makes
- Streams command execution output into the chat, so you never have to open a terminal yourself
- 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
- 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 Dev 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 CLI 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
@@ -31,13 +29,28 @@ Claude Dev uses an agentic loop style implementation with chain-of-thought promp
Claude Dev has access to the following capabilities:
1. **`execute_command`**: Execute CLI commands on the system
2. **`analyze_project`**: Analyze the project's source code and file structure
3. **`list_files`**: List all file paths at the top level of the specified directory
4. **`read_file`**: Read the contents of a file at the specified path
5. **`write_to_file`**: Write content to a file at the specified path
6. **`ask_followup_question`**: Ask the user a question to gather additional information needed to complete a task
7. **`attempt_completion`**: Present the result to the user after completing a task, potentially with a CLI command to kickoff a demonstration
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)
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
### Working in Existing Projects
Developers tend to name directories, files, classes, functions, and other components in ways that reflect their purpose and role within the larger system. These names often encapsulate high-level concepts and relationships that are crucial for understanding a project's overall architecture.
Claude Dev leverages the fact that large language models are fundamentally built on natural language processing, and by focusing on these named elements we provide the LLM with a structural understanding of the codebase that aligns closely with how developers conceptualize and organize their code. By effectively extracting the "language" of the codebase, we enable the LLM to grasp structure and intent without wasting context on implementation details.
1. **File Structure**: Claude first uses the `list_files_recursive` tool to get a complete picture of the project's file structure. It turns out Claude 3.5 Sonnet is _really_ good at inferring what it needs to process further just from these file names alone.
2. **High-Level Code Overview**: Claude may then use the `view_source_code_definitions_top_level` tool on specific directories of interest. This tool uses [tree-sitter](https://github.com/tree-sitter/tree-sitter) to parse source code with custom tag queries that extract names of classes, functions, methods, and other definitions. `view_source_code_definitions_top_level` works by first identifying source code files that tree-sitter can parse (currently supports `python`, `javascript`, `typescript`, `ruby`, `go`, `java`, `php`, `rust`, `c`, `c++`, `c#`, `swift`), then parsing each file into an abstract syntax tree, and finally applying a language-specific query to extract definition names (you can see the exact query used for each language in `src/parse-source-code/queries`). The results are formatted into a concise & readable output that Claude can easily interpret to quickly understand the code's structure and purpose.
3. **Read Relevant Files**: With the insights gained from the project file structure and high-level code overview, Claude can then use the `read_file` tool to examine specific files that are most relevant to the task at hand.
By carefully managing what information is added to context, Claude can provide valuable assistance even for complex, large-scale projects without overwhelming its context window (200k tokens, translating to roughly 150k words or about 500 pages of a typical book).
### Only With Your Permission
@@ -67,7 +80,8 @@ 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)
- ["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) by [Jim Clyde Monge](https://jimclydemonge.medium.com/)
- ["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.
- [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/)
@@ -77,7 +91,7 @@ This project is licensed under the MIT License. See the [LICENSE](./LICENSE) fil
## Questions?
Contact me on twitter <a href="https://x.com/sdrzn" target="_blank">@sdrzn</a>. Please create an <a href="https://github.com/saoudrizwan/claude-dev/issues">issue</a> if you come across a bug or would like a feature to be added.
Contact me on X <a href="https://x.com/sdrzn" target="_blank">@sdrzn</a>. Please create an <a href="https://github.com/saoudrizwan/claude-dev/issues">issue</a> if you come across a bug or would like a feature to be added.
## Acknowledgments
+2 -2
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d234220ea054c34483c74b662c34115f3ed7ce538ba135a59c599d6eab68105b
size 83423820
oid sha256:3368935be83186a5c7b603646fa8cedb87f5982014be307b80f659cc4eee2fba
size 21811087
+3817 -2
View File
File diff suppressed because it is too large Load Diff
+9 -4
View File
@@ -1,8 +1,8 @@
{
"name": "claude-dev",
"displayName": "Claude Dev",
"description": "Autonomous software engineer right in your IDE, capable of reading/writing files, executing commands, and more with your permission every step of the way.",
"version": "1.0.4",
"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.94",
"icon": "icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -105,7 +105,8 @@
"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"
"test:webview": "cd webview-ui && npm run test",
"upver": "tsx scripts/update-version.ts"
},
"devDependencies": {
"@types/diff": "^5.2.1",
@@ -122,16 +123,20 @@
"typescript": "^5.4.5"
},
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.24.3",
"@vscode/codicons": "^0.0.36",
"default-shell": "^2.2.0",
"delay": "^6.0.0",
"diff": "^5.2.0",
"execa": "^9.3.0",
"globby": "^14.0.2",
"openai": "^4.54.0",
"os-name": "^6.0.0",
"p-wait-for": "^5.0.2",
"serialize-error": "^11.0.3",
"tree-kill": "^1.2.2",
"tree-sitter-wasms": "^0.1.11",
"web-tree-sitter": "^0.22.6"
}
}
}
+29
View File
@@ -0,0 +1,29 @@
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}`)
+327 -156
View File
@@ -1,24 +1,28 @@
import { Anthropic } from "@anthropic-ai/sdk"
import defaultShell from "default-shell"
import * as diff from "diff"
import { execa } from "execa"
import { execa, ExecaError, ResultPromise } from "execa"
import fs from "fs/promises"
import { globby } from "globby"
import os from "os"
import osName from "os-name"
import pWaitFor from "p-wait-for"
import * as path from "path"
import { serializeError } from "serialize-error"
import { DEFAULT_MAX_REQUESTS_PER_TASK } from "./shared/Constants"
import { Tool, ToolName } from "./shared/Tool"
import { ClaudeAsk, ClaudeSay, ClaudeSayTool } from "./shared/ExtensionMessage"
import treeKill from "tree-kill"
import * as vscode from "vscode"
import pWaitFor from "p-wait-for"
import { ClaudeAskResponse } from "./shared/WebviewMessage"
import { ApiHandler, buildApiHandler } from "./api"
import { listFiles, parseSourceCodeForDefinitionsTopLevel } from "./parse-source-code"
import { ClaudeDevProvider } from "./providers/ClaudeDevProvider"
import { ApiConfiguration } from "./shared/api"
import { ClaudeRequestResult } from "./shared/ClaudeRequestResult"
import os from "os"
import { analyzeProject } from "./analyze-project"
import { DEFAULT_MAX_REQUESTS_PER_TASK } from "./shared/Constants"
import { ClaudeAsk, ClaudeMessage, ClaudeSay, ClaudeSayTool } from "./shared/ExtensionMessage"
import { Tool, ToolName } from "./shared/Tool"
import { ClaudeAskResponse } from "./shared/WebviewMessage"
import delay from "delay"
const SYSTEM_PROMPT = `You are Claude Dev, a highly skilled software developer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.
const SYSTEM_PROMPT =
() => `You are Claude Dev, a highly skilled software developer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.
====
@@ -26,24 +30,23 @@ CAPABILITIES
- You can read and analyze code in various programming languages, and can write clean, efficient, and well-documented code.
- You can debug complex issues and providing detailed explanations, offering architectural insights and design patterns.
- You have access to tools that let you analyze software projects, execute CLI commands on the user's computer, list files in a directory, read and write files, and ask follow-up questions. These tools help you effectively accomplish a wide range of tasks, such as writing code, making edits or improvements to existing files, understanding the current state of a project, performing system operations, and much more.
- For example, when asked to make edits or improvements you might use the analyze_project and read_file tools to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the write_to_file tool to implement changes.
- You can use the analyze_project tool to get a comprehensive view of a software project's file structure and important syntactic nodes such as functions, classes, and methods. This can be particularly useful when you need to understand the broader context and relationships between different parts of the code, as well as the overall organization of files and directories.
- The execute_command tool lets you run commands on the user's computer and should be used whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run.
- You have access to tools that let you execute CLI commands on the user's computer, list files in a directory (top level or recursively), extract source code definitions, read and write files, and ask follow-up questions. These tools help you effectively accomplish a wide range of tasks, such as writing code, making edits or improvements to existing files, understanding the current state of a project, performing system operations, and much more.
- You can use the list_files_recursive tool to get an overview of the project's file structure, which can provide key insights into the project from directory/file names (how developers conceptualize and organize their code) or file extensions (the language used). The list_files_top_level tool is better suited for generic directories you don't necessarily need the nested structure of, like the Desktop.
- You can use the view_source_code_definitions_top_level tool to get an overview of source code definitions for all files at the top level of a specified directory. This can be particularly useful when you need to understand the broader context and relationships between certain parts of the code. You may need to call this tool multiple times to understand various parts of the codebase related to the task.
- For example, when asked to make edits or improvements you might use list_files_recursive to get an overview of the project's file structure, then view_source_code_definitions_top_level to get an overview of source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the write_to_file tool to implement changes.
- The execute_command tool lets you run commands on the user's computer and should be used whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Interactive and long-running commands are allowed, since the user has the ability to send input to stdin and terminate the command on their own if needed.
====
RULES
- Unless otherwise specified by the user, you MUST accomplish your task within the following directory: ${
vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop")
}
vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ??
path.join(os.homedir(), "Desktop")
}
- Your current working directory is '${process.cwd()}', and you cannot \`cd\` into a different directory to complete a task. You are stuck operating from '${process.cwd()}', so be sure to pass in the appropriate 'path' parameter when using tools that require a path.
- If you do not know the contents of an existing file you need to edit, use the read_file tool to help you make informed changes. However if you have seen this file before, you should be able to remember its contents.
- When editing files, always provide the complete file content in your response, regardless of the extent of changes. The system handles diff generation automatically.
- Before using the execute_command tool, you must first think about the System Information context provided by the user to understand their environment and tailor your commands to ensure they are compatible with the user's system.
- When using the execute_command tool, avoid running servers or executing commands that don't terminate on their own (e.g. Flask web servers, continuous scripts). If a task requires such a process or server, explain in your task completion result why you can't execute it directly and provide clear instructions on how the user can run it themselves.
- Try not to use the analyze_project tool more than once since you can refer back to it along with any changes you made to get an adequate understanding of the project. But don't be hesitant to use it in the first place when you know you will be doing a coding task on an existing project. Prefer to use analyze_project over list_files, unless you think list_files is more appropriate for the job i.e. when viewing files on the Desktop.
- 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, and if so prepend with \`cd\`'ing into that directory && then executing the command (as one command since you are stuck operating from '${process.cwd()}'). For example, if you needed to run \`npm install\` in a project, 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 creating a new project (such as an app, website, or any software project), unless the user specifies otherwise, organize all new files within a dedicated project directory. 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.
@@ -62,9 +65,9 @@ OBJECTIVE
You accomplish a given task iteratively, breaking it down into clear steps and working through them methodically.
1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order.
2. Work through these goals sequentially, utilizing available tools as necessary. Each goal should correspond to a distinct step in your problem-solving process.
2. Work through these goals sequentially, utilizing available tools as necessary. Each goal should correspond to a distinct step in your problem-solving process. It is okay for certain steps to take multiple iterations, i.e. if you need to create many files but are limited by your max output limitations, it's okay to create a few files at a time as each subsequent iteration will keep you informed on the work completed and what's remaining.
3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within <thinking></thinking> tags. First, think about which of the provided tools is the relevant tool to answer the user's request. Second, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool call. BUT, if one of the values for a required parameter is missing, DO NOT invoke the function (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided.
4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. \`open index.html\` to show the website you've built. Avoid commands that run indefinitely (like servers). Instead, if such a command is needed, include instructions for the user to run it in the 'result' parameter.
4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. \`open index.html\` to show the website you've built.
5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.
====
@@ -73,45 +76,42 @@ SYSTEM INFORMATION
Operating System: ${osName()}
Default Shell: ${defaultShell}
VSCode Visible Files: ${
vscode.window.visibleTextEditors
?.map((editor) => editor.document?.uri?.fsPath)
.filter(Boolean)
.join(", ") || "(No files open)"
}
VSCode Opened Tabs: ${
vscode.window.tabGroups.all
.flatMap((group) => group.tabs)
.map((tab) => (tab.input as vscode.TabInputText)?.uri?.fsPath)
.filter(Boolean)
.join(", ") || "(No tabs open)"
}
`
const tools: Tool[] = [
{
name: "execute_command",
description:
"Execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Do not run servers or commands that don't terminate on their own. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run.",
"Execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run.",
input_schema: {
type: "object",
properties: {
command: {
type: "string",
description:
"The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. Avoid commands that run indefinitely (like servers) that don't terminate on their own.",
"The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions.",
},
},
required: ["command"],
},
},
{
name: "analyze_project",
name: "list_files_top_level",
description:
"Analyze the project structure by listing file paths and parsing supported source code to extract their key elements. This tool provides insights into the codebase structure, focusing on important code constructs like functions, classes, and methods. This also helps to understand the contents and structure of a directory by examining file names and extensions. All this information can guide decision-making on which files to process or explore further.",
input_schema: {
type: "object",
properties: {
path: {
type: "string",
description:
"The path of the directory to analyze. The tool will recursively scan this directory, list all file paths, and parse supported source code files.",
},
},
required: ["path"],
},
},
{
name: "list_files",
description:
"List all files and directories at the top level of the specified directory. This should only be used for generic directories you don't necessarily need the nested structure of, like the Desktop. If you think you need the nested structure of a directory, use the analyze_project tool instead.",
"List all files and directories at the top level of the specified directory. This should only be used for generic directories you don't necessarily need the nested structure of, like the Desktop.",
input_schema: {
type: "object",
properties: {
@@ -123,6 +123,37 @@ const tools: Tool[] = [
required: ["path"],
},
},
{
name: "list_files_recursive",
description:
"Recursively list all files and directories within the specified directory. This provides a comprehensive view of the project structure, and can guide decision-making on which files to process or explore further.",
input_schema: {
type: "object",
properties: {
path: {
type: "string",
description: "The path of the directory to recursively list contents for.",
},
},
required: ["path"],
},
},
{
name: "view_source_code_definitions_top_level",
description:
"Parse all source code files at the top level of the specified directory to extract names of key elements like classes and functions. This tool provides insights into the codebase structure and important constructs, encapsulating high-level concepts and relationships that are crucial for understanding the overall architecture.",
input_schema: {
type: "object",
properties: {
path: {
type: "string",
description:
"The path of the directory to parse top level source code files for to view their definitions.",
},
},
required: ["path"],
},
},
{
name: "read_file",
description:
@@ -197,24 +228,32 @@ const tools: Tool[] = [
]
export class ClaudeDev {
private client: Anthropic
private api: ApiHandler
private maxRequestsPerTask: number
private requestCount = 0
apiConversationHistory: Anthropic.MessageParam[] = []
claudeMessages: ClaudeMessage[] = []
private askResponse?: ClaudeAskResponse
private askResponseText?: string
private lastMessageTs?: number
private providerRef: WeakRef<ClaudeDevProvider>
abort: boolean = false
constructor(provider: ClaudeDevProvider, task: string, apiKey: string, maxRequestsPerTask?: number) {
constructor(
provider: ClaudeDevProvider,
task: string,
apiConfiguration: ApiConfiguration,
maxRequestsPerTask?: number
) {
this.providerRef = new WeakRef(provider)
this.client = new Anthropic({ apiKey })
this.api = buildApiHandler(apiConfiguration)
this.maxRequestsPerTask = maxRequestsPerTask ?? DEFAULT_MAX_REQUESTS_PER_TASK
this.startTask(task)
}
updateApiKey(apiKey: string) {
this.client = new Anthropic({ apiKey })
updateApi(apiConfiguration: ApiConfiguration) {
this.api = buildApiHandler(apiConfiguration)
}
updateMaxRequestsPerTask(maxRequestsPerTask: number | undefined) {
@@ -233,9 +272,14 @@ export class ClaudeDev {
}
this.askResponse = undefined
this.askResponseText = undefined
await this.providerRef.deref()?.addClaudeMessage({ ts: Date.now(), type: "ask", ask: type, text: question })
const askTs = Date.now()
this.lastMessageTs = askTs
this.claudeMessages.push({ ts: askTs, type: "ask", ask: type, text: question })
await this.providerRef.deref()?.postStateToWebview()
await pWaitFor(() => this.askResponse !== undefined, { interval: 100 })
await pWaitFor(() => this.askResponse !== undefined || this.lastMessageTs !== askTs, { interval: 100 })
if (this.lastMessageTs !== askTs) {
throw new Error("Current ask promise was ignored") // could happen if we send multiple asks in a row i.e. with command_output. It's important that when we know an ask could fail, it is handled gracefully
}
const result = { response: this.askResponse!, text: this.askResponseText }
this.askResponse = undefined
this.askResponseText = undefined
@@ -246,14 +290,17 @@ export class ClaudeDev {
if (this.abort) {
throw new Error("ClaudeDev instance aborted")
}
await this.providerRef.deref()?.addClaudeMessage({ ts: Date.now(), type: "say", say: type, text: text })
const sayTs = Date.now()
this.lastMessageTs = sayTs
this.claudeMessages.push({ ts: sayTs, type: "say", say: type, text: text })
await this.providerRef.deref()?.postStateToWebview()
}
private async startTask(task: 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)
await this.providerRef.deref()?.setClaudeMessages(undefined)
this.claudeMessages = []
this.apiConversationHistory = []
await this.providerRef.deref()?.postStateToWebview()
// This first message kicks off a task, it is not included in every subsequent message.
@@ -292,16 +339,18 @@ export class ClaudeDev {
}
}
async executeTool(toolName: ToolName, toolInput: any): Promise<string> {
async executeTool(toolName: ToolName, toolInput: any, isLastWriteToFile: boolean = false): Promise<string> {
switch (toolName) {
case "write_to_file":
return this.writeToFile(toolInput.path, toolInput.content)
return this.writeToFile(toolInput.path, toolInput.content, isLastWriteToFile)
case "read_file":
return this.readFile(toolInput.path)
case "analyze_project":
return this.analyzeProject(toolInput.path)
case "list_files":
return this.listFiles(toolInput.path)
case "list_files_top_level":
return this.listFilesTopLevel(toolInput.path)
case "list_files_recursive":
return this.listFilesRecursive(toolInput.path)
case "view_source_code_definitions_top_level":
return this.viewSourceCodeDefinitionsTopLevel(toolInput.path)
case "execute_command":
return this.executeCommand(toolInput.command)
case "ask_followup_question":
@@ -323,7 +372,7 @@ export class ClaudeDev {
return totalCost
}
async writeToFile(filePath: string, newContent: string): Promise<string> {
async writeToFile(filePath: string, newContent: string, isLast: boolean): Promise<string> {
try {
const fileExists = await fs
.access(filePath)
@@ -331,6 +380,10 @@ export class ClaudeDev {
.catch(() => false)
if (fileExists) {
const originalContent = await fs.readFile(filePath, "utf-8")
// fix issue where claude always removes newline from the file
if (originalContent.endsWith("\n") && !newContent.endsWith("\n")) {
newContent += "\n"
}
// condensed patch to return to claude
const diffResult = diff.createPatch(filePath, originalContent, newContent)
// full diff representation for webview
@@ -345,6 +398,19 @@ export class ClaudeDev {
})
.join("")
// Create virtual document with new file, then open diff editor
const fileName = path.basename(filePath)
vscode.commands.executeCommand(
"vscode.diff",
vscode.Uri.file(filePath),
// to create a virtual doc we use a uri scheme registered in extension.ts, which then converts this base64 content into a text document
// (providing file name with extension in the uri lets vscode know the language of the file and apply syntax highlighting)
vscode.Uri.parse(`claude-dev-diff:${fileName}`).with({
query: Buffer.from(newContent).toString("base64"),
}),
`${fileName}: Original ↔ Suggested Changes`
)
const { response, text } = await this.ask(
"tool",
JSON.stringify({
@@ -354,21 +420,42 @@ export class ClaudeDev {
} as ClaudeSayTool)
)
if (response !== "yesButtonTapped") {
if (isLast) {
await this.closeDiffViews()
}
if (response === "textResponse" && text) {
await this.say("user_feedback", text)
return `The user denied this operation and provided the following feedback:\n\"${text}\"`
}
return "The user denied this operation."
}
await fs.writeFile(filePath, newContent)
// Finish by opening the edited file in the editor
await vscode.window.showTextDocument(vscode.Uri.file(filePath), { preview: false })
if (isLast) {
await this.closeDiffViews()
}
return `Changes applied to ${filePath}:\n${diffResult}`
} else {
const fileName = path.basename(filePath)
vscode.commands.executeCommand(
"vscode.diff",
vscode.Uri.parse(`claude-dev-diff:${fileName}`).with({
query: Buffer.from("").toString("base64"),
}),
vscode.Uri.parse(`claude-dev-diff:${fileName}`).with({
query: Buffer.from(newContent).toString("base64"),
}),
`${fileName}: New File`
)
const { response, text } = await this.ask(
"tool",
JSON.stringify({ tool: "newFileCreated", path: filePath, content: newContent } as ClaudeSayTool)
)
if (response !== "yesButtonTapped") {
if (isLast) {
await this.closeDiffViews()
}
if (response === "textResponse" && text) {
await this.say("user_feedback", text)
return `The user denied this operation and provided the following feedback:\n\"${text}\"`
@@ -377,6 +464,10 @@ export class ClaudeDev {
}
await fs.mkdir(path.dirname(filePath), { recursive: true })
await fs.writeFile(filePath, newContent)
await vscode.window.showTextDocument(vscode.Uri.file(filePath), { preview: false })
if (isLast) {
await this.closeDiffViews()
}
return `New file created and content written to ${filePath}`
}
} catch (error) {
@@ -386,6 +477,19 @@ export class ClaudeDev {
}
}
async closeDiffViews() {
const tabs = vscode.window.tabGroups.all
.map((tg) => tg.tabs)
.flat()
.filter(
(tab) =>
tab.input instanceof vscode.TabInputTextDiff && tab.input?.modified?.scheme === "claude-dev-diff"
)
for (const tab of tabs) {
await vscode.window.tabGroups.close(tab)
}
}
async readFile(filePath: string): Promise<string> {
try {
const content = await fs.readFile(filePath, "utf-8")
@@ -408,64 +512,13 @@ export class ClaudeDev {
}
}
async analyzeProject(dirPath: string): Promise<string> {
async listFilesTopLevel(dirPath: string): Promise<string> {
try {
const analysis = await analyzeProject(dirPath)
const files = await listFiles(dirPath, false)
const result = this.formatFilesList(dirPath, files)
const { response, text } = await this.ask(
"tool",
JSON.stringify({ tool: "analyzeProject", path: dirPath, content: analysis } as ClaudeSayTool)
)
if (response !== "yesButtonTapped") {
if (response === "textResponse" && text) {
await this.say("user_feedback", text)
return `The user denied this operation and provided the following feedback:\n\"${text}\"`
}
return "The user denied this operation."
}
return analysis
} catch (error) {
const errorString = `Error analyzing project: ${JSON.stringify(serializeError(error))}`
this.say(
"error",
`Error analyzing project:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`
)
return errorString
}
}
async listFiles(dirPath: string): Promise<string> {
const absolutePath = path.resolve(dirPath)
const root = process.platform === "win32" ? path.parse(absolutePath).root : "/"
const isRoot = absolutePath === root
if (isRoot) {
const { response, text } = await this.ask(
"tool",
JSON.stringify({ tool: "listFiles", path: dirPath, content: root } as ClaudeSayTool)
)
if (response !== "yesButtonTapped") {
if (response === "textResponse" && text) {
await this.say("user_feedback", text)
return `The user denied this operation and provided the following feedback:\n\"${text}\"`
}
return "The user denied this operation."
}
return root
}
try {
const options = {
cwd: dirPath,
dot: true, // Allow patterns to match files/directories that start with '.', even if the pattern does not start with '.'
absolute: false,
markDirectories: true, // Append a / on any directories matched
onlyFiles: false,
}
// * globs all files in one dir, ** globs files in nested directories
const entries = await globby("*", options)
const result = entries.join("\n")
const { response, text } = await this.ask(
"tool",
JSON.stringify({ tool: "listFiles", path: dirPath, content: result } as ClaudeSayTool)
JSON.stringify({ tool: "listFilesTopLevel", path: dirPath, content: result } as ClaudeSayTool)
)
if (response !== "yesButtonTapped") {
if (response === "textResponse" && text) {
@@ -487,6 +540,89 @@ export class ClaudeDev {
}
}
async listFilesRecursive(dirPath: string): Promise<string> {
try {
const files = await listFiles(dirPath, true)
const result = this.formatFilesList(dirPath, files)
const { response, text } = await this.ask(
"tool",
JSON.stringify({ tool: "listFilesRecursive", path: dirPath, content: result } as ClaudeSayTool)
)
if (response !== "yesButtonTapped") {
if (response === "textResponse" && text) {
await this.say("user_feedback", text)
return `The user denied this operation and provided the following feedback:\n\"${text}\"`
}
return "The user denied this operation."
}
return result
} catch (error) {
const errorString = `Error listing files recursively: ${JSON.stringify(serializeError(error))}`
this.say(
"error",
`Error listing files recursively:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`
)
return errorString
}
}
formatFilesList(dirPath: string, files: string[]): string {
const sorted = files
.map((file) => {
// convert absolute path to relative path
const relativePath = path.relative(dirPath, file)
return file.endsWith("/") ? relativePath + "/" : relativePath
})
.sort((a, b) => {
// sort directories before files
const aIsDir = a.endsWith("/")
const bIsDir = b.endsWith("/")
if (aIsDir !== bIsDir) {
return aIsDir ? -1 : 1
}
return a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" })
})
if (sorted.length > 1000) {
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 {
return sorted.join("\n")
}
}
async viewSourceCodeDefinitionsTopLevel(dirPath: string): Promise<string> {
try {
const result = await parseSourceCodeForDefinitionsTopLevel(dirPath)
const { response, text } = await this.ask(
"tool",
JSON.stringify({
tool: "viewSourceCodeDefinitionsTopLevel",
path: dirPath,
content: result,
} as ClaudeSayTool)
)
if (response !== "yesButtonTapped") {
if (response === "textResponse" && text) {
await this.say("user_feedback", text)
return `The user denied this operation and provided the following feedback:\n\"${text}\"`
}
return "The user denied this operation."
}
return result
} catch (error) {
const errorString = `Error parsing source code definitions: ${JSON.stringify(serializeError(error))}`
this.say(
"error",
`Error parsing source code definitions:\n${
error.message ?? JSON.stringify(serializeError(error), null, 2)
}`
)
return errorString
}
}
async executeCommand(command: string, returnEmptyStringOnSuccess: boolean = false): Promise<string> {
const { response, text } = await this.ask("command", command)
if (response !== "yesButtonTapped") {
@@ -496,20 +632,69 @@ export class ClaudeDev {
}
return "The user denied this operation."
}
const sendCommandOutput = async (subprocess: ResultPromise, line: string): Promise<void> => {
try {
const { response, text } = await this.ask("command_output", line)
// if this ask promise is not ignored, that means the user responded to it somehow either by clicking primary button or by typing text
if (response === "yesButtonTapped") {
// SIGINT is typically what's sent when a user interrupts a process (like pressing Ctrl+C)
/*
.kill sends SIGINT by default. However by not passing any options into .kill(), execa internally sends a SIGKILL after a grace period if the SIGINT failed.
however it turns out that even this isn't enough for certain processes like npm starting servers. therefore we use the tree-kill package to kill all processes in the process tree, including the root process.
- Sends signal to all children processes of the process with pid pid, including pid. Signal defaults to SIGTERM.
*/
if (subprocess.pid) {
//subprocess.kill("SIGINT") // will result in for loop throwing error
treeKill(subprocess.pid, "SIGINT")
}
} 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
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
}
} catch {
// This can only happen if this ask promise was ignored, so ignore this error
}
}
try {
let result = ""
// execa by default tries to convery bash into javascript
// by using shell: true we use sh on unix or cmd.exe on windows
// also worth noting that execa`input` runs commands and the execa() creates a new instance
for await (const line of execa({ shell: true })`${command}`) {
this.say("command_output", line) // stream output to user in realtime
result += `${line}\n`
// execa by default tries to convert bash into javascript, so need to specify `shell: true` to use sh on unix or cmd.exe on windows
// 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 })`${command}`
try {
for await (const chunk of subprocess) {
const line = chunk.toString()
// stream output to user in realtime
// do not await as we are not waiting for a response
sendCommandOutput(subprocess, line)
result += `${line}\n`
}
} catch (e) {
if ((e as ExecaError).signal === "SIGINT") {
const line = `\nUser terminated process via SIGINT...`
await this.say("command_output", line)
result += line
} else {
throw e // if the command was not terminated by user, let outer catch handle it as a real error
}
}
// Wait for a short delay to ensure all messages are sent to the webview
// This delay allows time for non-awaited promises to be created and
// for their associated messages to be sent to the webview, maintaining
// the correct order of messages (although the webview is smart about
// grouping command_output messages despite any gaps anyways)
await delay(100)
// for attemptCompletion, we don't want to return the command output
if (returnEmptyStringOnSuccess) {
return ""
}
return `Command executed successfully. Output:\n${result}`
return `Command Output:\n${result}`
} catch (e) {
const error = e as any
let errorMessage = error.message || JSON.stringify(serializeError(error), null, 2)
@@ -547,22 +732,7 @@ export class ClaudeDev {
async attemptApiRequest(): Promise<Anthropic.Messages.Message> {
try {
const response = await this.client.messages.create(
{
model: "claude-3-5-sonnet-20240620", // https://docs.anthropic.com/en/docs/about-claude/models
// beta max tokens
max_tokens: 8192,
system: SYSTEM_PROMPT,
messages: (await this.providerRef.deref()?.getApiConversationHistory()) || [],
tools: 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" },
}
)
return response
return await this.api.createMessage(SYSTEM_PROMPT(), this.apiConversationHistory, tools)
} catch (error) {
const { response } = await this.ask(
"api_req_failed",
@@ -589,7 +759,7 @@ export class ClaudeDev {
throw new Error("ClaudeDev instance aborted")
}
await this.providerRef.deref()?.addMessageToApiConversationHistory({ role: "user", content: userContent })
this.apiConversationHistory.push({ role: "user", content: userContent })
if (this.requestCount >= this.maxRequestsPerTask) {
const { response } = await this.ask(
"request_limit_reached",
@@ -599,7 +769,7 @@ export class ClaudeDev {
if (response === "yesButtonTapped") {
this.requestCount = 0
} else {
await this.providerRef.deref()?.addMessageToApiConversationHistory({
this.apiConversationHistory.push({
role: "assistant",
content: [
{
@@ -616,14 +786,7 @@ export class ClaudeDev {
await this.say(
"api_req_started",
JSON.stringify({
request: {
model: "claude-3-5-sonnet-20240620",
max_tokens: 8192,
system: "(see SYSTEM_PROMPT in https://github.com/saoudrizwan/claude-dev/blob/main/src/ClaudeDev.ts)",
messages: [{ conversation_history: "..." }, { role: "user", content: userContent }],
tools: "(see tools in https://github.com/saoudrizwan/claude-dev/blob/main/src/ClaudeDev.ts)",
tool_choice: { type: "auto" },
},
request: this.api.createUserReadableRequest(userContent),
})
)
try {
@@ -652,6 +815,10 @@ export class ClaudeDev {
let toolResults: Anthropic.ToolResultBlockParam[] = []
let attemptCompletionBlock: Anthropic.Messages.ToolUseBlock | undefined
const writeToFileCount = response.content.filter(
(block) => block.type === "tool_use" && (block.name as ToolName) === "write_to_file"
).length
let currentWriteToFile = 0
for (const contentBlock of response.content) {
if (contentBlock.type === "tool_use") {
assistantResponses.push(contentBlock)
@@ -661,7 +828,15 @@ export class ClaudeDev {
if (toolName === "attempt_completion") {
attemptCompletionBlock = contentBlock
} else {
const result = await this.executeTool(toolName, toolInput)
if (toolName === "write_to_file") {
currentWriteToFile++
}
// NOTE: while anthropic sdk accepts string or array of string/image, openai sdk (openrouter) only accepts a string
const result = await this.executeTool(
toolName,
toolInput,
currentWriteToFile === writeToFileCount
)
// this.say(
// "tool",
// `\nTool Used: ${toolName}\nTool Input: ${JSON.stringify(toolInput)}\nTool Result: ${result}`
@@ -672,13 +847,11 @@ export class ClaudeDev {
}
if (assistantResponses.length > 0) {
await this.providerRef
.deref()
?.addMessageToApiConversationHistory({ role: "assistant", content: assistantResponses })
this.apiConversationHistory.push({ role: "assistant", content: assistantResponses })
} else {
// this should never happen! it there's no assistant_responses, that means we got no text or tool_use content blocks from API which we should assume is an error
this.say("error", "Unexpected Error: No assistant messages were found in the API response")
await this.providerRef.deref()?.addMessageToApiConversationHistory({
this.apiConversationHistory.push({
role: "assistant",
content: [{ type: "text", text: "Failure: I did not have a response to provide." }],
})
@@ -708,10 +881,8 @@ export class ClaudeDev {
if (toolResults.length > 0) {
if (didEndLoop) {
await this.providerRef
.deref()
?.addMessageToApiConversationHistory({ role: "user", content: toolResults })
await this.providerRef.deref()?.addMessageToApiConversationHistory({
this.apiConversationHistory.push({ role: "user", content: toolResults })
this.apiConversationHistory.push({
role: "assistant",
content: [
{
@@ -1,40 +0,0 @@
/*
- struct definitions
- enum definitions
- union definitions
- type aliases
- method definitions
- function definitions
- trait definitions
- module definitions
- macro definitions
*/
export default `
(struct_item
name: (type_identifier) @name.definition.class) @definition.class
(enum_item
name: (type_identifier) @name.definition.class) @definition.class
(union_item
name: (type_identifier) @name.definition.class) @definition.class
(type_item
name: (type_identifier) @name.definition.class) @definition.class
(declaration_list
(function_item
name: (identifier) @name.definition.method)) @definition.method
(function_item
name: (identifier) @name.definition.function) @definition.function
(trait_item
name: (type_identifier) @name.definition.interface) @definition.interface
(mod_item
name: (identifier) @name.definition.module) @definition.module
(macro_definition
name: (identifier) @name.definition.macro) @definition.macro
`
+52
View File
@@ -0,0 +1,52 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ApiHandler } from "."
import { ApiHandlerOptions } from "../shared/api"
export class AnthropicHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: Anthropic
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new Anthropic({ apiKey: this.options.apiKey })
}
async createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
tools: Anthropic.Messages.Tool[]
): 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
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" },
}
)
}
createUserReadableRequest(
userContent: Array<
| Anthropic.TextBlockParam
| Anthropic.ImageBlockParam
| Anthropic.ToolUseBlockParam
| Anthropic.ToolResultBlockParam
>
): any {
return {
model: "claude-3-5-sonnet-20240620",
max_tokens: 8192,
system: "(see SYSTEM_PROMPT in src/ClaudeDev.ts)",
messages: [{ conversation_history: "..." }, { role: "user", content: userContent }],
tools: "(see tools in src/ClaudeDev.ts)",
tool_choice: { type: "auto" },
}
}
}
+57
View File
@@ -0,0 +1,57 @@
import AnthropicBedrock from "@anthropic-ai/bedrock-sdk"
import { Anthropic } from "@anthropic-ai/sdk"
import { ApiHandlerOptions } from "../shared/api"
import { ApiHandler } from "."
// https://docs.anthropic.com/en/api/claude-on-amazon-bedrock
export class AwsBedrockHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: AnthropicBedrock
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new AnthropicBedrock({
// Authenticate by either providing the keys below or use the default AWS credential providers, such as
// using ~/.aws/credentials or the "AWS_SECRET_ACCESS_KEY" and "AWS_ACCESS_KEY_ID" environment variables.
awsAccessKey: this.options.awsAccessKey,
awsSecretKey: this.options.awsSecretKey,
// awsRegion changes the aws region to which the request is made. By default, we read AWS_REGION,
// and if that's not present, we default to us-east-1. Note that we do not read ~/.aws/config for the region.
awsRegion: this.options.awsRegion,
})
}
async createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
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,
system: systemPrompt,
messages,
tools,
tool_choice: { type: "auto" },
})
}
createUserReadableRequest(
userContent: Array<
| Anthropic.TextBlockParam
| Anthropic.ImageBlockParam
| Anthropic.ToolUseBlockParam
| Anthropic.ToolResultBlockParam
>
): any {
return {
model: "anthropic.claude-3-5-sonnet-20240620-v1:0",
max_tokens: 4096,
system: "(see SYSTEM_PROMPT in src/ClaudeDev.ts)",
messages: [{ conversation_history: "..." }, { role: "user", content: userContent }],
tools: "(see tools in src/ClaudeDev.ts)",
tool_choice: { type: "auto" },
}
}
}
+36
View File
@@ -0,0 +1,36 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ApiConfiguration } from "../shared/api"
import { AnthropicHandler } from "./anthropic"
import { AwsBedrockHandler } from "./bedrock"
import { OpenRouterHandler } from "./openrouter"
export interface ApiHandler {
createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
tools: Anthropic.Messages.Tool[]
): Promise<Anthropic.Messages.Message>
createUserReadableRequest(
userContent: Array<
| Anthropic.TextBlockParam
| Anthropic.ImageBlockParam
| Anthropic.ToolUseBlockParam
| Anthropic.ToolResultBlockParam
>
): any
}
export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
const { apiProvider, ...options } = configuration
switch (apiProvider) {
case "anthropic":
return new AnthropicHandler(options)
case "openrouter":
return new OpenRouterHandler(options)
case "bedrock":
return new AwsBedrockHandler(options)
default:
return new AnthropicHandler(options)
}
}
+247
View File
@@ -0,0 +1,247 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiHandler } from "."
import { ApiHandlerOptions } from "../shared/api"
export class OpenRouterHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://openrouter.ai/api/v1",
apiKey: this.options.openRouterApiKey,
defaultHeaders: {
"HTTP-Referer": "https://github.com/saoudrizwan/claude-dev", // Optional, for including your app on openrouter.ai rankings.
"X-Title": "claude-dev", // Optional. Shows in rankings on openrouter.ai.
},
})
}
async createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
tools: Anthropic.Messages.Tool[]
): Promise<Anthropic.Messages.Message> {
// Convert Anthropic messages to OpenAI format
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...this.convertToOpenAiMessages(messages),
]
// Convert Anthropic tools to OpenAI tools
const openAiTools: OpenAI.Chat.ChatCompletionTool[] = tools.map((tool) => ({
type: "function",
function: {
name: tool.name,
description: tool.description,
parameters: tool.input_schema, // matches anthropic tool input schema (see https://platform.openai.com/docs/guides/function-calling)
},
}))
const completion = await this.client.chat.completions.create({
model: "anthropic/claude-3.5-sonnet:beta",
max_tokens: 4096,
messages: openAiMessages,
tools: openAiTools,
tool_choice: "auto",
})
// Convert OpenAI response to Anthropic format
const openAiMessage = completion.choices[0].message
const anthropicMessage: Anthropic.Messages.Message = {
id: completion.id,
type: "message",
role: openAiMessage.role, // always "assistant"
content: [
{
type: "text",
text: openAiMessage.content || "",
},
],
model: completion.model,
stop_reason: this.mapFinishReason(completion.choices[0].finish_reason),
stop_sequence: null, // which custom stop_sequence was generated, if any (not applicable if you don't use stop_sequence)
usage: {
input_tokens: completion.usage?.prompt_tokens || 0,
output_tokens: completion.usage?.completion_tokens || 0,
},
}
if (openAiMessage.tool_calls && openAiMessage.tool_calls.length > 0) {
anthropicMessage.content.push(
...openAiMessage.tool_calls.map((toolCall): Anthropic.ToolUseBlock => {
let parsedInput = {}
try {
parsedInput = JSON.parse(toolCall.function.arguments || "{}")
} catch (error) {
console.error("Failed to parse tool arguments:", error)
}
return {
type: "tool_use",
id: toolCall.id,
name: toolCall.function.name,
input: parsedInput,
}
})
)
}
return anthropicMessage
}
private mapFinishReason(
finishReason: OpenAI.Chat.ChatCompletion.Choice["finish_reason"]
): Anthropic.Messages.Message["stop_reason"] {
switch (finishReason) {
case "stop":
return "end_turn"
case "length":
return "max_tokens"
case "tool_calls":
return "tool_use"
case "content_filter":
return null // Anthropic doesn't have an exact equivalent
default:
return null
}
}
convertToOpenAiMessages(
anthropicMessages: Anthropic.Messages.MessageParam[]
): OpenAI.Chat.ChatCompletionMessageParam[] {
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = []
for (const anthropicMessage of anthropicMessages) {
if (typeof anthropicMessage.content === "string") {
openAiMessages.push({ role: anthropicMessage.role, content: anthropicMessage.content })
} else {
// image_url.url is base64 encoded image data
/*
{ role: "user", content: "" | { type: "text", text: string } | { type: "image_url", image_url: { url: string } } },
// content required unless tool_calls is present
{ role: "assistant", content?: "" | null, tool_calls?: [{ id: "", function: { name: "", arguments: "" }, type: "function" }] },
{ role: "tool", tool_call_id: "", content: ""}
*/
if (anthropicMessage.role === "user") {
const { nonToolMessages, toolMessages } = anthropicMessage.content.reduce<{
nonToolMessages: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[]
toolMessages: Anthropic.ToolResultBlockParam[]
}>(
(acc, part) => {
if (part.type === "tool_result") {
acc.toolMessages.push(part)
} else if (part.type === "text" || part.type === "image") {
acc.nonToolMessages.push(part)
} // user cannot send tool_use messages
return acc
},
{ nonToolMessages: [], toolMessages: [] }
)
// Process non-tool messages
if (nonToolMessages.length > 0) {
openAiMessages.push({
role: "user",
content: nonToolMessages.map((part) => {
if (part.type === "image") {
return { type: "image_url", image_url: { url: part.source.data } }
}
return { type: "text", text: part.text }
}),
})
}
// Process tool result messages
toolMessages.forEach((toolMessage) => {
// The Anthropic SDK allows tool results to be a string or an array of text and image blocks, enabling rich and structured content. In contrast, the OpenAI SDK only supports tool results as a single string, so we map the Anthropic tool result parts into one concatenated string to maintain compatibility.
let content: string
if (typeof toolMessage.content === "string") {
content = toolMessage.content
} else {
content =
toolMessage.content
?.map((part) => {
if (part.type === "image") {
return `{ type: "image_url", image_url: { url: ${part.source.data} } }`
}
return part.text
})
.join("\n") ?? ""
}
openAiMessages.push({
role: "tool",
tool_call_id: toolMessage.tool_use_id,
content: content,
})
})
} else if (anthropicMessage.role === "assistant") {
const { nonToolMessages, toolMessages } = anthropicMessage.content.reduce<{
nonToolMessages: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[]
toolMessages: Anthropic.ToolUseBlockParam[]
}>(
(acc, part) => {
if (part.type === "tool_use") {
acc.toolMessages.push(part)
} else if (part.type === "text" || part.type === "image") {
acc.nonToolMessages.push(part)
} // assistant cannot send tool_result messages
return acc
},
{ nonToolMessages: [], toolMessages: [] }
)
// Process non-tool messages
let content: string | undefined
if (nonToolMessages.length > 0) {
content = nonToolMessages
.map((part) => {
if (part.type === "image") {
return `{ type: "image_url", image_url: { url: ${part.source.data} } }`
}
return part.text
})
.join("\n")
}
// Process tool use messages
let tool_calls: OpenAI.Chat.ChatCompletionMessageToolCall[] = toolMessages.map((toolMessage) => ({
id: toolMessage.id,
type: "function",
function: {
name: toolMessage.name,
// json string
arguments: JSON.stringify(toolMessage.input),
},
}))
openAiMessages.push({
role: "assistant",
content,
tool_calls,
})
}
}
}
return openAiMessages
}
createUserReadableRequest(
userContent: Array<
| Anthropic.TextBlockParam
| Anthropic.ImageBlockParam
| Anthropic.ToolUseBlockParam
| Anthropic.ToolResultBlockParam
>
): any {
return {
model: "anthropic/claude-3.5-sonnet:beta",
max_tokens: 4096,
messages: [{ conversation_history: "..." }, { role: "user", content: userContent }],
tools: "(see tools in src/ClaudeDev.ts)",
tool_choice: "auto",
}
}
}
+18 -2
View File
@@ -39,7 +39,7 @@ export function activate(context: vscode.ExtensionContext) {
const sidebarProvider = new ClaudeDevProvider(context, outputChannel)
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(ClaudeDevProvider.viewType, sidebarProvider, {
vscode.window.registerWebviewViewProvider(ClaudeDevProvider.sideBarId, sidebarProvider, {
webviewOptions: { retainContextWhenHidden: true },
})
)
@@ -61,7 +61,7 @@ export function activate(context: vscode.ExtensionContext) {
//const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined
const lastCol = Math.max(...vscode.window.visibleTextEditors.map((editor) => editor.viewColumn || 0))
const targetCol = Math.max(lastCol + 1, 1)
const panel = vscode.window.createWebviewPanel(ClaudeDevProvider.viewType, "Claude Dev", targetCol, {
const panel = vscode.window.createWebviewPanel(ClaudeDevProvider.tabPanelId, "Claude Dev", targetCol, {
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [context.extensionUri],
@@ -86,6 +86,22 @@ export function activate(context: vscode.ExtensionContext) {
sidebarProvider.postMessageToWebview({ type: "action", action: "settingsButtonTapped" })
})
)
/*
We use the text document content provider API to show a diff view for new files/edits by creating a virtual document for the new content.
- This API allows you to create readonly documents in VSCode from arbitrary sources, and works by claiming an uri-scheme for which your provider then returns text contents. The scheme must be provided when registering a provider and cannot change afterwards.
- Note how the provider doesn't create uris for virtual documents - its role is to provide contents given such an uri. In return, content providers are wired into the open document logic so that providers are always considered.
https://code.visualstudio.com/api/extension-guides/virtual-documents
*/
const diffContentProvider = new (class implements vscode.TextDocumentContentProvider {
provideTextDocumentContent(uri: vscode.Uri): string {
return Buffer.from(uri.query, "base64").toString("utf-8")
}
})()
context.subscriptions.push(
vscode.workspace.registerTextDocumentContentProvider("claude-dev-diff", diffContentProvider)
)
}
// This method is called when your extension is deactivated
@@ -3,45 +3,54 @@ import { globby } from "globby"
import * as path from "path"
import { LanguageParser, loadRequiredLanguageParsers } from "./languageParser"
async function analyzeProject(dirPath: string): Promise<string> {
let result = ""
// TODO: implement caching behavior to avoid having to keep analyzing project for new tasks.
export async function parseSourceCodeForDefinitionsTopLevel(dirPath: string): Promise<string> {
// Get all files at top level (not gitignored)
const allFiles = await listFiles(dirPath, false)
// Get all files (not gitignored)
const allFiles = await getAllProjectFiles(dirPath)
let result = ""
// Separate files to parse and remaining files
const { filesToParse, remainingFiles } = separateFiles(allFiles)
// Load only the necessary language parsers
const languageParsers = await loadRequiredLanguageParsers(filesToParse)
// Parse specific files we have language parsers for
const filesWithoutDefinitions: string[] = []
// const filesWithoutDefinitions: string[] = []
for (const file of filesToParse) {
const definitions = await parseFile(file, languageParsers)
if (definitions) {
if (!result) {
result += "# Source code definitions:\n\n"
}
result += `${path.relative(dirPath, file)}\n${definitions}\n`
} else {
filesWithoutDefinitions.push(file)
}
// else {
// filesWithoutDefinitions.push(file)
// }
}
// List remaining files' paths
result += "# Unparsed files:\n\n"
filesWithoutDefinitions
.concat(remainingFiles)
.sort()
.forEach((file) => {
result += `${path.relative(dirPath, file)}\n`
})
// let didFindUnparsedFiles = false
// filesWithoutDefinitions
// .concat(remainingFiles)
// .sort()
// .forEach((file) => {
// if (!didFindUnparsedFiles) {
// result += "# Unparsed Files\n\n"
// didFindUnparsedFiles = true
// }
// result += `${path.relative(dirPath, file)}\n`
// })
return result
return result ? result : "No source code definitions found."
}
async function getAllProjectFiles(dirPath: string): Promise<string[]> {
export async function listFiles(dirPath: string, recursive: boolean): Promise<string[]> {
const absolutePath = path.resolve(dirPath)
const root = process.platform === "win32" ? path.parse(absolutePath).root : "/"
const isRoot = absolutePath === root
if (isRoot) {
return [root]
}
const dirsToIgnore = [
"node_modules",
"__pycache__",
@@ -65,12 +74,13 @@ async function getAllProjectFiles(dirPath: string): Promise<string[]> {
cwd: dirPath,
dot: true, // do not ignore hidden files/directories
absolute: true,
markDirectories: true,
gitignore: true, // globby ignores any files that are gitignored
ignore: dirsToIgnore, // just in case there is no gitignore, we ignore sensible defaults
markDirectories: true, // Append a / on any directories matched
gitignore: recursive, // globby ignores any files that are gitignored
ignore: recursive ? dirsToIgnore : undefined, // just in case there is no gitignore, we ignore sensible defaults
onlyFiles: false, // true by default, false means it will list directories on their own too
}
// * globs all files in one dir, ** globs files in nested directories
const files = await globby("**", options)
const files = await globby(recursive ? "**" : "*", options)
return files
}
@@ -98,8 +108,8 @@ function separateFiles(allFiles: string[]): { filesToParse: string[]; remainingF
"php",
"swift",
].map((e) => `.${e}`)
const filesToParse = allFiles.filter((file) => extensions.includes(path.extname(file)))
const remainingFiles = allFiles.filter((file) => !extensions.includes(path.extname(file)))
const filesToParse = allFiles.filter((file) => extensions.includes(path.extname(file))).slice(0, 50) // 50 files max
const remainingFiles = allFiles.filter((file) => !filesToParse.includes(file))
return { filesToParse, remainingFiles }
}
@@ -182,5 +192,3 @@ async function parseFile(filePath: string, languageParsers: LanguageParser): Pro
}
return undefined
}
export { analyzeProject }
@@ -13,7 +13,7 @@ import {
javaQuery,
phpQuery,
swiftQuery,
} from "./tree-sitter-queries/tags"
} from "./queries"
export interface LanguageParser {
[key: string]: {
@@ -26,6 +26,15 @@ async function loadLanguage(langName: string) {
return await Parser.Language.load(path.join(__dirname, `tree-sitter-${langName}.wasm`))
}
let isParserInitialized = false
async function initializeParser() {
if (!isParserInitialized) {
await Parser.init()
isParserInitialized = true
}
}
/*
Using node bindings for tree-sitter is problematic in vscode extensions
because of incompatibility with electron. Going the .wasm route has the
@@ -49,7 +58,7 @@ Sources:
- https://github.com/tree-sitter/tree-sitter/blob/master/lib/binding_web/test/query-test.js
*/
export async function loadRequiredLanguageParsers(filesToParse: string[]): Promise<LanguageParser> {
await Parser.init()
await initializeParser()
const extensionsToLoad = new Set(filesToParse.map((file) => path.extname(file).toLowerCase().slice(1)))
const parsers: LanguageParser = {}
for (const ext of extensionsToLoad) {
@@ -3,7 +3,6 @@
- union declarations
- function declarations
- typedef declarations
- enum declarations
*/
export default `
(struct_specifier name: (type_identifier) @name.definition.class body:(_)) @definition.class
@@ -13,6 +12,4 @@ export default `
(function_declarator declarator: (identifier) @name.definition.function) @definition.function
(type_definition declarator: (type_identifier) @name.definition.type) @definition.type
(enum_specifier name: (type_identifier) @name.definition.type) @definition.type
`
@@ -4,7 +4,6 @@
- function declarations
- method declarations (with namespace scope)
- typedef declarations
- enum declarations
- class declarations
*/
export default `
@@ -20,7 +19,5 @@ export default `
(type_definition declarator: (type_identifier) @name.definition.type) @definition.type
(enum_specifier name: (type_identifier) @name.definition.type) @definition.type
(class_specifier name: (type_identifier) @name.definition.class) @definition.class
`
@@ -2,7 +2,6 @@
- function declarations (with associated comments)
- method declarations (with associated comments)
- type specifications
- type references
*/
export default `
(
@@ -25,6 +24,4 @@ export default `
(type_spec
name: (type_identifier) @name.definition.type) @definition.type
(type_identifier) @name.reference.type @reference.type
`
@@ -2,7 +2,6 @@
- class declarations
- method declarations
- interface declarations
- superclass references
*/
export default `
(class_declaration
@@ -13,6 +12,4 @@ export default `
(interface_declaration
name: (identifier) @name.definition.interface) @definition.interface
(superclass (type_identifier) @name.reference.class) @reference.class
`
@@ -3,7 +3,6 @@
- method definitions
- named function declarations
- arrow functions and function expressions assigned to variables
- exported constants
*/
export default `
(
@@ -63,15 +62,4 @@ export default `
(#strip! @doc "^[\\s\\*/]+|^[\\s\\*/]$")
(#select-adjacent! @doc @definition.function)
)
(export_statement value: (assignment_expression left: (identifier) @name right: ([
(number)
(string)
(identifier)
(undefined)
(null)
(new_expression)
(binary_expression)
(call_expression)
]))) @definition.constant
`
+16
View File
@@ -0,0 +1,16 @@
/*
- struct definitions
- method definitions
- function definitions
*/
export default `
(struct_item
name: (type_identifier) @name.definition.class) @definition.class
(declaration_list
(function_item
name: (identifier) @name.definition.method)) @definition.method
(function_item
name: (identifier) @name.definition.function) @definition.function
`
@@ -1,6 +1,5 @@
/*
- class declarations
- protocol declarations
- method declarations (including initializers and deinitializers)
- property declarations
- function declarations
@@ -27,20 +26,6 @@ export default `
)
) @definition.method
(protocol_declaration
(protocol_body
[
(protocol_function_declaration
name: (simple_identifier) @name
)
(subscript_declaration
(parameter (simple_identifier) @name)
)
(init_declaration "init" @name)
]
)
) @definition.method
(class_declaration
(class_body
[
@@ -4,9 +4,6 @@
- abstract method signatures
- class declarations (including abstract classes)
- module declarations
- interface declarations
- type alias declarations
- enum declarations
*/
export default `
(function_signature
@@ -24,9 +21,6 @@ export default `
(module
name: (identifier) @name.definition.module) @definition.module
(interface_declaration
name: (type_identifier) @name.definition.interface) @definition.interface
(function_declaration
name: (identifier) @name.definition.function) @definition.function
@@ -35,10 +29,4 @@ export default `
(class_declaration
name: (type_identifier) @name.definition.class) @definition.class
(type_alias_declaration
name: (type_identifier) @name.definition.type) @definition.type
(enum_declaration
name: (identifier) @name.definition.enum) @definition.enum
`
+217 -69
View File
@@ -1,10 +1,12 @@
import { Uri, Webview } from "vscode"
//import * as weather from "weather-js"
import { Anthropic } from "@anthropic-ai/sdk"
import os from "os"
import * as path from "path"
import * as vscode from "vscode"
import { ClaudeDev } from "../ClaudeDev"
import { ClaudeMessage, ExtensionMessage } from "../shared/ExtensionMessage"
import { ApiProvider } from "../shared/api"
import { ExtensionMessage } from "../shared/ExtensionMessage"
import { WebviewMessage } from "../shared/WebviewMessage"
import { Anthropic } from "@anthropic-ai/sdk"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -12,13 +14,16 @@ https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default
https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/customSidebarViewProvider.ts
*/
type SecretKey = "apiKey" | "openRouterApiKey" | "awsAccessKey" | "awsSecretKey"
type GlobalStateKey = "apiProvider" | "awsRegion" | "maxRequestsPerTask" | "lastShownAnnouncementId"
export class ClaudeDevProvider implements vscode.WebviewViewProvider {
public static readonly viewType = "claude-dev.SidebarProvider"
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.
public static readonly tabPanelId = "claude-dev.TabPanelProvider"
private disposables: vscode.Disposable[] = []
private view?: vscode.WebviewView | vscode.WebviewPanel
private providerInstanceIdentifier = Date.now()
private claudeDev?: ClaudeDev
private latestAnnouncementId = "jul-25-2024" // update to some unique identifier when we add a new announcement
private latestAnnouncementId = "jul-29-2024" // update to some unique identifier when we add a new announcement
constructor(
private readonly context: vscode.ExtensionContext,
@@ -129,15 +134,16 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
this.outputChannel.appendLine("Webview view resolved")
}
async tryToInitClaudeDevWithTask(task: string) {
async initClaudeDevWithTask(task: 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 [apiKey, maxRequestsPerTask] = await Promise.all([
this.getSecret("apiKey") as Promise<string | undefined>,
this.getGlobalState("maxRequestsPerTask") as Promise<number | undefined>,
])
if (this.view && apiKey) {
this.claudeDev = new ClaudeDev(this, task, apiKey, maxRequestsPerTask)
}
const { apiProvider, apiKey, openRouterApiKey, awsAccessKey, awsSecretKey, awsRegion, maxRequestsPerTask } =
await this.getState()
this.claudeDev = new ClaudeDev(
this,
task,
{ apiProvider, apiKey, openRouterApiKey, awsAccessKey, awsSecretKey, awsRegion },
maxRequestsPerTask
)
}
// Send any JSON serializable data to the react app
@@ -247,11 +253,20 @@ 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.tryToInitClaudeDevWithTask(message.text!)
await this.initClaudeDevWithTask(message.text!)
break
case "apiKey":
await this.storeSecret("apiKey", message.text!)
this.claudeDev?.updateApiKey(message.text!)
case "apiConfiguration":
if (message.apiConfiguration) {
const { apiProvider, apiKey, openRouterApiKey, awsAccessKey, awsSecretKey, awsRegion } =
message.apiConfiguration
await this.updateGlobalState("apiProvider", apiProvider)
await this.storeSecret("apiKey", apiKey)
await this.storeSecret("openRouterApiKey", openRouterApiKey)
await this.storeSecret("awsAccessKey", awsAccessKey)
await this.storeSecret("awsSecretKey", awsSecretKey)
await this.updateGlobalState("awsRegion", awsRegion)
this.claudeDev?.updateApi(message.apiConfiguration)
}
await this.postStateToWebview()
break
case "maxRequestsPerTask":
@@ -278,6 +293,9 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("lastShownAnnouncementId", this.latestAnnouncementId)
await this.postStateToWebview()
break
case "downloadTask":
this.downloadTask()
break
// Add more switch case statements here as more webview message commands
// are created within the webview context (i.e. inside media/main.js)
}
@@ -287,20 +305,100 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
)
}
async downloadTask() {
// File name
const date = new Date()
const month = date.toLocaleString("en-US", { month: "short" }).toLowerCase()
const day = date.getDate()
const year = date.getFullYear()
let hours = date.getHours()
const minutes = date.getMinutes().toString().padStart(2, "0")
const ampm = hours >= 12 ? "pm" : "am"
hours = hours % 12
hours = hours ? hours : 12 // the hour '0' should be '12'
const fileName = `claude_dev_task_${month}-${day}-${year}_${hours}-${minutes}-${ampm}.md`
// Generate markdown
const conversationHistory = this.claudeDev?.apiConversationHistory || []
const markdownContent = conversationHistory
.map((message) => {
const role = message.role === "user" ? "**User:**" : "**Assistant:**"
const content = Array.isArray(message.content)
? message.content.map(this.formatContentBlockToMarkdown).join("\n")
: message.content
return `${role}\n\n${content}\n\n`
})
.join("---\n\n")
// Prompt user for save location
const saveUri = await vscode.window.showSaveDialog({
filters: { Markdown: ["md"] },
defaultUri: vscode.Uri.file(path.join(os.homedir(), "Downloads", fileName)),
})
if (saveUri) {
// Write content to the selected location
await vscode.workspace.fs.writeFile(saveUri, Buffer.from(markdownContent))
vscode.window.showTextDocument(saveUri, { preview: true })
}
}
private formatContentBlockToMarkdown(
block:
| Anthropic.TextBlockParam
| Anthropic.ImageBlockParam
| Anthropic.ToolUseBlockParam
| Anthropic.ToolResultBlockParam
): string {
switch (block.type) {
case "text":
return block.text
case "image":
return `[Image: ${block.source.media_type}]`
case "tool_use":
let input: string
if (typeof block.input === "object" && block.input !== null) {
input = Object.entries(block.input)
.map(([key, value]) => `${key.charAt(0).toUpperCase() + key.slice(1)}: ${value}`)
.join("\n")
} else {
input = String(block.input)
}
return `[Tool Use: ${block.name}]\n${input}`
case "tool_result":
if (typeof block.content === "string") {
return `[Tool Result${block.is_error ? " (Error)" : ""}]\n${block.content}`
} else if (Array.isArray(block.content)) {
return `[Tool Result${block.is_error ? " (Error)" : ""}]\n${block.content
.map(this.formatContentBlockToMarkdown)
.join("\n")}`
} else {
return `[Tool Result${block.is_error ? " (Error)" : ""}]`
}
default:
return "[Unexpected content type]"
}
}
async postStateToWebview() {
const [apiKey, maxRequestsPerTask, claudeMessages, lastShownAnnouncementId] = await Promise.all([
this.getSecret("apiKey") as Promise<string | undefined>,
this.getGlobalState("maxRequestsPerTask") as Promise<number | undefined>,
this.getClaudeMessages(),
this.getGlobalState("lastShownAnnouncementId") as Promise<string | undefined>,
])
const {
apiProvider,
apiKey,
openRouterApiKey,
awsAccessKey,
awsSecretKey,
awsRegion,
maxRequestsPerTask,
lastShownAnnouncementId,
} = await this.getState()
this.postMessageToWebview({
type: "state",
state: {
apiKey,
apiConfiguration: { apiProvider, apiKey, openRouterApiKey, awsAccessKey, awsSecretKey, awsRegion },
maxRequestsPerTask,
themeName: vscode.workspace.getConfiguration("workbench").get<string>("colorTheme"),
claudeMessages,
claudeMessages: this.claudeDev?.claudeMessages || [],
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
},
})
@@ -311,14 +409,14 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
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
}
await this.setApiConversationHistory(undefined)
await this.setClaudeMessages(undefined)
// this.setApiConversationHistory(undefined)
// this.setClaudeMessages(undefined)
}
// Caching mechanism to keep track of webview messages + API conversation history per provider instance
/*
Now that we use retainContextWhenHidden, we don't have to store a cache of claude messages in the user's state, but we do to reduce memory footprint in long conversations.
Now that we use retainContextWhenHidden, we don't have to store a cache of claude messages in the user's state, but we could to reduce memory footprint in long conversations.
- We have to be careful of what state is shared between ClaudeDevProvider instances since there could be multiple instances of the extension running at once. For example when we cached claude messages using the same key, two instances of the extension could end up using the same key and overwriting each other's messages.
- Some state does need to be shared between the instances, i.e. the API key--however there doesn't seem to be a good way to notfy the other instances that the API key has changed.
@@ -329,52 +427,66 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
However in the future when we implement task history, we'll need to use a unique identifier for each task. As well as manage a data structure that keeps track of task history with their associated identifiers and the task message itself, to present in a 'Task History' view.
Task history is a significant undertaking as it would require refactoring how we wait for ask responses--it would need to be a hidden claudeMessage, so that user's can resume tasks that ended with an ask.
*/
// private providerInstanceIdentifier = Date.now()
// getClaudeMessagesStateKey() {
// return `claudeMessages-${this.providerInstanceIdentifier}`
// }
getClaudeMessagesStateKey() {
return `claudeMessages-${this.providerInstanceIdentifier}`
}
getApiConversationHistoryStateKey() {
return `apiConversationHistory-${this.providerInstanceIdentifier}`
}
// getApiConversationHistoryStateKey() {
// return `apiConversationHistory-${this.providerInstanceIdentifier}`
// }
// claude messages to present in the webview
async getClaudeMessages(): Promise<ClaudeMessage[]> {
const messages = (await this.getGlobalState(this.getClaudeMessagesStateKey())) as ClaudeMessage[]
return messages || []
}
// getClaudeMessages(): ClaudeMessage[] {
// // const messages = (await this.getGlobalState(this.getClaudeMessagesStateKey())) as ClaudeMessage[]
// // return messages || []
// return this.claudeMessages
// }
async setClaudeMessages(messages: ClaudeMessage[] | undefined) {
await this.updateGlobalState(this.getClaudeMessagesStateKey(), messages)
}
// setClaudeMessages(messages: ClaudeMessage[] | undefined) {
// // await this.updateGlobalState(this.getClaudeMessagesStateKey(), messages)
// this.claudeMessages = messages || []
// }
async addClaudeMessage(message: ClaudeMessage): Promise<ClaudeMessage[]> {
const messages = await this.getClaudeMessages()
messages.push(message)
await this.setClaudeMessages(messages)
return messages
}
// addClaudeMessage(message: ClaudeMessage): ClaudeMessage[] {
// // const messages = await this.getClaudeMessages()
// // messages.push(message)
// // await this.setClaudeMessages(messages)
// // return messages
// this.claudeMessages.push(message)
// return this.claudeMessages
// }
// conversation history to send in API requests
async getApiConversationHistory(): Promise<Anthropic.MessageParam[]> {
const history = (await this.getGlobalState(
this.getApiConversationHistoryStateKey()
)) as Anthropic.MessageParam[]
return history || []
}
/*
It seems that some API messages do not comply with vscode state requirements. Either the Anthropic library is manipulating these values somehow in the backend in a way thats creating cyclic references, or the API returns a function or a Symbol as part of the message content.
VSCode docs about state: "The value must be JSON-stringifyable ... value — A value. MUST not contain cyclic references."
For now we'll store the conversation history in memory, and if we need to store in state directly we'd need to do a manual conversion to ensure proper json stringification.
*/
async setApiConversationHistory(history: Anthropic.MessageParam[] | undefined) {
await this.updateGlobalState(this.getApiConversationHistoryStateKey(), history)
}
// getApiConversationHistory(): Anthropic.MessageParam[] {
// // const history = (await this.getGlobalState(
// // this.getApiConversationHistoryStateKey()
// // )) as Anthropic.MessageParam[]
// // return history || []
// return this.apiConversationHistory
// }
async addMessageToApiConversationHistory(message: Anthropic.MessageParam): Promise<Anthropic.MessageParam[]> {
const history = await this.getApiConversationHistory()
history.push(message)
await this.setApiConversationHistory(history)
return history
}
// setApiConversationHistory(history: Anthropic.MessageParam[] | undefined) {
// // await this.updateGlobalState(this.getApiConversationHistoryStateKey(), history)
// this.apiConversationHistory = history || []
// }
// addMessageToApiConversationHistory(message: Anthropic.MessageParam): Anthropic.MessageParam[] {
// // const history = await this.getApiConversationHistory()
// // history.push(message)
// // await this.setApiConversationHistory(history)
// // return history
// this.apiConversationHistory.push(message)
// return this.apiConversationHistory
// }
/*
Storage
@@ -382,13 +494,45 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
https://www.eliostruyf.com/devhack-code-extension-storage-options/
*/
async getState() {
const [
apiProvider,
apiKey,
openRouterApiKey,
awsAccessKey,
awsSecretKey,
awsRegion,
maxRequestsPerTask,
lastShownAnnouncementId,
] = await Promise.all([
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
this.getSecret("apiKey") as Promise<string | undefined>,
this.getSecret("openRouterApiKey") as Promise<string | undefined>,
this.getSecret("awsAccessKey") as Promise<string | undefined>,
this.getSecret("awsSecretKey") as Promise<string | undefined>,
this.getGlobalState("awsRegion") as Promise<string | undefined>,
this.getGlobalState("maxRequestsPerTask") as Promise<number | undefined>,
this.getGlobalState("lastShownAnnouncementId") as Promise<string | undefined>,
])
return {
apiProvider: apiProvider || "anthropic", // for legacy users that were using Anthropic by default
apiKey,
openRouterApiKey,
awsAccessKey,
awsSecretKey,
awsRegion,
maxRequestsPerTask,
lastShownAnnouncementId,
}
}
// global
private async updateGlobalState(key: string, value: any) {
private async updateGlobalState(key: GlobalStateKey, value: any) {
await this.context.globalState.update(key, value)
}
private async getGlobalState(key: string) {
private async getGlobalState(key: GlobalStateKey) {
return await this.context.globalState.get(key)
}
@@ -414,11 +558,15 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
// secrets
private async storeSecret(key: string, value: any) {
await this.context.secrets.store(key, value)
private async storeSecret(key: SecretKey, value?: string) {
if (value) {
await this.context.secrets.store(key, value)
} else {
await this.context.secrets.delete(key)
}
}
private async getSecret(key: string) {
private async getSecret(key: SecretKey) {
return await this.context.secrets.get(key)
}
}
+12 -3
View File
@@ -1,5 +1,7 @@
// type that represents json data that is sent from extension to webview, called ExtensionMessage and has 'type' enum which can be 'plusButtonTapped' or 'settingsButtonTapped' or 'hello'
import { ApiConfiguration } from "./api"
// webview will hold state
export interface ExtensionMessage {
type: "action" | "state"
@@ -9,7 +11,7 @@ export interface ExtensionMessage {
}
export interface ExtensionState {
apiKey?: string
apiConfiguration?: ApiConfiguration
maxRequestsPerTask?: number
themeName?: string
claudeMessages: ClaudeMessage[]
@@ -28,6 +30,7 @@ export type ClaudeAsk =
| "request_limit_reached"
| "followup"
| "command"
| "command_output"
| "completion_result"
| "tool"
| "api_req_failed"
@@ -38,13 +41,19 @@ export type ClaudeSay =
| "api_req_started"
| "api_req_finished"
| "text"
| "command_output"
| "completion_result"
| "user_feedback"
| "api_req_retried"
| "command_output"
export interface ClaudeSayTool {
tool: "editedExistingFile" | "newFileCreated" | "readFile" | "listFiles" | "analyzeProject"
tool:
| "editedExistingFile"
| "newFileCreated"
| "readFile"
| "listFilesTopLevel"
| "listFilesRecursive"
| "viewSourceCodeDefinitionsTopLevel"
path?: string
diff?: string
content?: string
+3 -2
View File
@@ -3,8 +3,9 @@ import { Anthropic } from "@anthropic-ai/sdk"
export type ToolName =
| "write_to_file"
| "read_file"
| "analyze_project"
| "list_files"
| "list_files_top_level"
| "list_files_recursive"
| "view_source_code_definitions_top_level"
| "execute_command"
| "ask_followup_question"
| "attempt_completion"
+5 -1
View File
@@ -1,14 +1,18 @@
import { ApiConfiguration, ApiProvider } from "./api"
export interface WebviewMessage {
type:
| "apiKey"
| "apiConfiguration"
| "maxRequestsPerTask"
| "webviewDidLaunch"
| "newTask"
| "askResponse"
| "clearTask"
| "didShowAnnouncement"
| "downloadTask"
text?: string
askResponse?: ClaudeAskResponse
apiConfiguration?: ApiConfiguration
}
export type ClaudeAskResponse = "yesButtonTapped" | "noButtonTapped" | "textResponse"
+13
View File
@@ -0,0 +1,13 @@
export type ApiProvider = "anthropic" | "openrouter" | "bedrock"
export interface ApiHandlerOptions {
apiKey?: string // anthropic
openRouterApiKey?: string
awsAccessKey?: string
awsSecretKey?: string
awsRegion?: string
}
export type ApiConfiguration = ApiHandlerOptions & {
apiProvider?: ApiProvider
}
+16
View File
@@ -0,0 +1,16 @@
/**
* 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
}
+15
View File
@@ -0,0 +1,15 @@
import { Uri, Webview } from "vscode"
/**
* 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))
}
+3 -2
View File
@@ -12,7 +12,7 @@
"noImplicitReturns": true,
"noUnusedLocals": false,
"resolveJsonModule": true,
"rootDir": "src",
"rootDir": ".",
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
@@ -20,5 +20,6 @@
"useDefineForClassFields": true,
"useUnknownInCatchVariables": false
},
"include": ["src/**/*", "scripts/**/*"],
"exclude": ["node_modules", ".vscode-test", "webview-ui"]
}
}
+12 -7
View File
@@ -4,8 +4,9 @@ import ChatView from "./components/ChatView"
import SettingsView from "./components/SettingsView"
import { ClaudeMessage, ExtensionMessage } from "@shared/ExtensionMessage"
import WelcomeView from "./components/WelcomeView"
import { vscode } from "./utilities/vscode"
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.
@@ -19,7 +20,7 @@ const App: React.FC = () => {
const [didHydrateState, setDidHydrateState] = useState(false)
const [showSettings, setShowSettings] = useState(false)
const [showWelcome, setShowWelcome] = useState<boolean>(false)
const [apiKey, setApiKey] = useState<string>("")
const [apiConfiguration, setApiConfiguration] = useState<ApiConfiguration | undefined>(undefined)
const [maxRequestsPerTask, setMaxRequestsPerTask] = useState<string>("")
const [vscodeThemeName, setVscodeThemeName] = useState<string | undefined>(undefined)
const [claudeMessages, setClaudeMessages] = useState<ClaudeMessage[]>([])
@@ -33,8 +34,12 @@ const App: React.FC = () => {
const message: ExtensionMessage = e.data
switch (message.type) {
case "state":
setShowWelcome(!message.state!.apiKey)
setApiKey(message.state!.apiKey || "")
const hasKey =
message.state!.apiConfiguration?.apiKey !== undefined ||
message.state!.apiConfiguration?.openRouterApiKey !== undefined ||
message.state!.apiConfiguration?.awsAccessKey !== undefined
setShowWelcome(!hasKey)
setApiConfiguration(message.state!.apiConfiguration)
setMaxRequestsPerTask(
message.state!.maxRequestsPerTask !== undefined ? message.state!.maxRequestsPerTask.toString() : ""
)
@@ -70,13 +75,13 @@ const App: React.FC = () => {
return (
<>
{showWelcome ? (
<WelcomeView apiKey={apiKey} setApiKey={setApiKey} />
<WelcomeView apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
) : (
<>
{showSettings && (
<SettingsView
apiKey={apiKey}
setApiKey={setApiKey}
apiConfiguration={apiConfiguration}
setApiConfiguration={setApiConfiguration}
maxRequestsPerTask={maxRequestsPerTask}
setMaxRequestsPerTask={setMaxRequestsPerTask}
onDone={() => setShowSettings(false)}
+18 -11
View File
@@ -22,7 +22,7 @@ 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.0</h3>
<h3 style={{ margin: "0 0 8px" }}>🎉{" "}New in v1.0.94</h3>
<ul style={{ margin: "0 0 8px", paddingLeft: "20px" }}>
<li>
Open in the editor (using{" "}
@@ -33,22 +33,29 @@ const Announcement = ({ hideAnnouncement }: AnnouncementProps) => {
workspace more clearly
</li>
<li>
New <code>analyze_project</code> tool to help Claude get a comprehensive overview of your project's
source code definitions and file structure
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>
</li>
<li>Provide feedback to tool use like terminal commands and file edits</li>
<li>
Updated max output tokens to 8192 so less lazy coding (<code>{"// rest of code here..."}</code>)
Interact with CLI commands by sending messages to stdin and terminating long-running processes like
servers
</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>
Quality of life improvements like markdown rendering, memory optimizations, better theme support
</li>
<li>Export task to a markdown file (useful as context for future tasks)</li>
<li>Added OpenRouter and AWS Bedrock support</li>
</ul>
<p style={{ margin: "0" }}>
Subscribe to my new YouTube to see how to get the most out of Claude Dev!{" "}
<VSCodeLink href="https://youtube.com/@saoudrizwan" style={{ display: "inline" }}>
https://youtube.com/@saoudrizwan
Follow me for more updates!{" "}
<VSCodeLink href="https://x.com/sdrzn" style={{ display: "inline" }}>
@sdrzn
</VSCodeLink>
</p>
</div>
+145
View File
@@ -0,0 +1,145 @@
import { ApiConfiguration } from "@shared/api"
import { VSCodeDropdown, VSCodeLink, VSCodeOption, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import React from "react"
interface ApiOptionsProps {
apiConfiguration?: ApiConfiguration
setApiConfiguration: React.Dispatch<React.SetStateAction<ApiConfiguration | undefined>>
}
const ApiOptions: React.FC<ApiOptionsProps> = ({ apiConfiguration, setApiConfiguration }) => {
const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
setApiConfiguration((prev) => ({ ...prev, [field]: event.target.value }))
}
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")}>
<VSCodeOption value="anthropic">Anthropic</VSCodeOption>
<VSCodeOption value="openrouter">OpenRouter</VSCodeOption>
<VSCodeOption value="bedrock">AWS Bedrock</VSCodeOption>
</VSCodeDropdown>
</div>
{apiConfiguration?.apiProvider === "anthropic" && (
<div>
<VSCodeTextField
value={apiConfiguration?.apiKey || ""}
style={{ width: "100%" }}
onInput={handleInputChange("apiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>Anthropic API Key</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
This key is stored locally and only used to make API requests from this extension.
<VSCodeLink href="https://console.anthropic.com/" style={{ display: "inline" }}>
You can get an Anthropic API key by signing up here.
</VSCodeLink>
</p>
</div>
)}
{apiConfiguration?.apiProvider === "openrouter" && (
<div>
<VSCodeTextField
value={apiConfiguration?.openRouterApiKey || ""}
style={{ width: "100%" }}
onInput={handleInputChange("openRouterApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>OpenRouter API Key</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
This key is stored locally and only used to make API requests from this extension.
<VSCodeLink href="https://openrouter.ai/" style={{ display: "inline" }}>
You can get an OpenRouter API key by signing up here.
</VSCodeLink>
</p>
</div>
)}
{apiConfiguration?.apiProvider === "bedrock" && (
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
<VSCodeTextField
value={apiConfiguration?.awsAccessKey || ""}
style={{ width: "100%" }}
onInput={handleInputChange("awsAccessKey")}
placeholder="Enter Access Key...">
<span style={{ fontWeight: 500 }}>AWS Access Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.awsSecretKey || ""}
style={{ width: "100%" }}
onInput={handleInputChange("awsSecretKey")}
placeholder="Enter Secret Key...">
<span style={{ fontWeight: 500 }}>AWS Secret Key</span>
</VSCodeTextField>
<div className="dropdown-container">
<label htmlFor="aws-region-dropdown">
<span style={{ fontWeight: 500 }}>AWS Region</span>
</label>
<VSCodeDropdown
id="aws-region-dropdown"
value={apiConfiguration?.awsRegion || ""}
style={{ width: "100%" }}
onChange={handleInputChange("awsRegion")}>
<VSCodeOption value="">Select a region...</VSCodeOption>
{/* Currently Claude 3.5 Sonnet is only available in us-east-1 */}
<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-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="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-southeast-1">Asia Pacific (Singapore)</VSCodeOption>
<VSCodeOption value="ap-southeast-2">Asia Pacific (Sydney)</VSCodeOption>
<VSCodeOption value="ca-central-1">Canada (Central)</VSCodeOption>
<VSCodeOption value="eu-central-1">Europe (Frankfurt)</VSCodeOption>
<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> */}
</VSCodeDropdown>
</div>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
These credentials are stored locally and only used to make API requests from this extension.
<VSCodeLink
href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html"
style={{ display: "inline" }}>
You can find your AWS access key and secret key here.
</VSCodeLink>
</p>
</div>
)}
</div>
)
}
export default ApiOptions
+172 -95
View File
@@ -1,8 +1,8 @@
import { ClaudeAsk, ClaudeMessage, ClaudeSay, ClaudeSayTool } from "@shared/ExtensionMessage"
import { VSCodeBadge, VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
import React from "react"
import { COMMAND_OUTPUT_STRING } from "../utilities/combineCommandSequences"
import { SyntaxHighlighterStyle } from "../utilities/getSyntaxHighlighterStyleFromTheme"
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"
@@ -12,7 +12,8 @@ interface ChatRowProps {
syntaxHighlighterStyle: SyntaxHighlighterStyle
isExpanded: boolean
onToggleExpand: () => void
apiRequestFailedMessage?: string
lastModifiedMessage?: ClaudeMessage
isLast: boolean
}
const ChatRow: React.FC<ChatRowProps> = ({
@@ -20,15 +21,37 @@ const ChatRow: React.FC<ChatRowProps> = ({
syntaxHighlighterStyle,
isExpanded,
onToggleExpand,
apiRequestFailedMessage,
lastModifiedMessage,
isLast,
}) => {
const cost = message.text != null && message.say === "api_req_started" ? JSON.parse(message.text).cost : undefined
const apiRequestFailedMessage =
isLast && lastModifiedMessage?.ask === "api_req_failed" // if request is retried then the latest message is a api_req_retried
? lastModifiedMessage?.text
: undefined
const isCommandExecuting =
isLast && lastModifiedMessage?.ask === "command" && lastModifiedMessage?.text?.includes(COMMAND_OUTPUT_STRING)
const getIconAndTitle = (type: ClaudeAsk | ClaudeSay | undefined): [JSX.Element | null, JSX.Element | null] => {
const normalColor = "var(--vscode-foreground)"
const errorColor = "var(--vscode-errorForeground)"
const successColor = "var(--vscode-testing-iconPassed)"
const ProgressIndicator = (
<div
style={{
width: "16px",
height: "16px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}>
<div style={{ transform: "scale(0.55)", transformOrigin: "center" }}>
<VSCodeProgressRing />
</div>
</div>
)
switch (type) {
case "request_limit_reached":
return [
@@ -46,9 +69,13 @@ const ChatRow: React.FC<ChatRowProps> = ({
]
case "command":
return [
<span
className="codicon codicon-terminal"
style={{ color: normalColor, marginBottom: "-1.5px" }}></span>,
isCommandExecuting ? (
ProgressIndicator
) : (
<span
className="codicon codicon-terminal"
style={{ color: normalColor, marginBottom: "-1.5px" }}></span>
),
<span style={{ color: normalColor, fontWeight: "bold" }}>
Claude wants to execute this command:
</span>,
@@ -71,18 +98,7 @@ const ChatRow: React.FC<ChatRowProps> = ({
className="codicon codicon-error"
style={{ color: errorColor, marginBottom: "-1.5px" }}></span>
) : (
<div
style={{
width: "16px",
height: "16px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}>
<div style={{ transform: "scale(0.55)", transformOrigin: "center" }}>
<VSCodeProgressRing />
</div>
</div>
ProgressIndicator
),
cost ? (
<span style={{ color: normalColor, fontWeight: "bold" }}>API Request Complete</span>
@@ -92,6 +108,13 @@ const ChatRow: React.FC<ChatRowProps> = ({
<span style={{ color: normalColor, fontWeight: "bold" }}>Making API Request...</span>
),
]
case "followup":
return [
<span
className="codicon codicon-question"
style={{ color: normalColor, marginBottom: "-1.5px" }}></span>,
<span style={{ color: normalColor, fontWeight: "bold" }}>Claude has a question:</span>,
]
default:
return [null, null]
}
@@ -99,78 +122,111 @@ const ChatRow: React.FC<ChatRowProps> = ({
const renderMarkdown = (markdown: string = "") => {
// react-markdown lets us customize elements, so here we're using their example of replacing code blocks with SyntaxHighlighter. However when there are no language matches (` or ``` without a language specifier) then we default to a normal code element for inline code. Code blocks without a language specifier shouldn't be a common occurrence as we prompt Claude to always use a language specifier.
// when claude wraps text in thinking tags, he doesnt use line breaks so we need to insert those ourselves to render markdown correctly
const parsed = markdown.replace(/<thinking>([\s\S]*?)<\/thinking>/g, (match, content) => {
return `_<thinking>_\n\n${content}\n\n_</thinking>_`
})
return (
<Markdown
children={markdown}
components={{
p(props) {
const { style, ...rest } = props
return (
<p
style={{
...style,
margin: 0,
marginTop: 0,
marginBottom: 0,
whiteSpace: "pre-wrap",
wordBreak: "break-word",
}}
{...rest}
/>
)
},
ol(props) {
const { style, ...rest } = props
return <ol style={{ ...style, padding: "0 0 0 20px", margin: "10px 0" }} {...rest} />
},
ul(props) {
const { style, ...rest } = props
return <ul style={{ ...style, padding: "0 0 0 20px", margin: "10px 0" }} {...rest} />
},
// https://github.com/remarkjs/react-markdown?tab=readme-ov-file#use-custom-components-syntax-highlight
code(props) {
const { children, className, node, ...rest } = props
const match = /language-(\w+)/.exec(className || "")
return match ? (
<SyntaxHighlighter
{...(rest as any)} // will be passed down to pre
PreTag="div"
children={String(children).replace(/\n$/, "")}
language={match[1]}
style={{
...syntaxHighlighterStyle,
'code[class*="language-"]': {
background: "var(--vscode-editor-background)",
},
'pre[class*="language-"]': {
background: "var(--vscode-editor-background)",
},
}}
customStyle={{
overflowX: "auto",
overflowY: "hidden",
maxWidth: "100%",
margin: 0,
padding: "10px",
// important to note that min-width: max-content is not required here how it is in CodeBlock.tsx
borderRadius: 3,
border: "1px solid var(--vscode-sideBar-border)",
fontSize: "var(--vscode-editor-font-size)",
lineHeight: "var(--vscode-editor-line-height)",
fontFamily: "var(--vscode-editor-font-family)",
}}
/>
) : (
<code
{...rest}
className={className}
style={{ whiteSpace: "pre-line", wordBreak: "break-word" }}>
{children}
</code>
)
},
}}
/>
<div style={{ wordBreak: "break-word", overflowWrap: "anywhere" }}>
<Markdown
children={parsed}
components={{
p(props) {
const { style, ...rest } = props
return (
<p
style={{
...style,
margin: 0,
marginTop: 0,
marginBottom: 0,
whiteSpace: "pre-wrap",
wordBreak: "break-word",
overflowWrap: "anywhere",
}}
{...rest}
/>
)
},
ol(props) {
const { style, ...rest } = props
return (
<ol
style={{
...style,
padding: "0 0 0 20px",
margin: "10px 0",
wordBreak: "break-word",
overflowWrap: "anywhere",
}}
{...rest}
/>
)
},
ul(props) {
const { style, ...rest } = props
return (
<ul
style={{
...style,
padding: "0 0 0 20px",
margin: "10px 0",
wordBreak: "break-word",
overflowWrap: "anywhere",
}}
{...rest}
/>
)
},
// https://github.com/remarkjs/react-markdown?tab=readme-ov-file#use-custom-components-syntax-highlight
code(props) {
const { children, className, node, ...rest } = props
const match = /language-(\w+)/.exec(className || "")
return match ? (
<SyntaxHighlighter
{...(rest as any)} // will be passed down to pre
PreTag="div"
children={String(children).replace(/\n$/, "")}
language={match[1]}
style={{
...syntaxHighlighterStyle,
'code[class*="language-"]': {
background: "var(--vscode-editor-background)",
},
'pre[class*="language-"]': {
background: "var(--vscode-editor-background)",
},
}}
customStyle={{
overflowX: "auto",
overflowY: "hidden",
maxWidth: "100%",
margin: 0,
padding: "10px",
// important to note that min-width: max-content is not required here how it is in CodeBlock.tsx
borderRadius: 3,
border: "1px solid var(--vscode-sideBar-border)",
fontSize: "var(--vscode-editor-font-size)",
lineHeight: "var(--vscode-editor-line-height)",
fontFamily: "var(--vscode-editor-font-family)",
}}
/>
) : (
<code
{...rest}
className={className}
style={{
whiteSpace: "pre-line",
wordBreak: "break-word",
overflowWrap: "anywhere",
}}>
{children}
</code>
)
},
}}
/>
</div>
)
}
@@ -186,8 +242,9 @@ const ChatRow: React.FC<ChatRowProps> = ({
const pStyle: React.CSSProperties = {
margin: 0,
whiteSpace: "pre-line",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
overflowWrap: "anywhere",
}
switch (message.type) {
@@ -338,13 +395,13 @@ const ChatRow: React.FC<ChatRowProps> = ({
/>
</>
)
case "listFiles":
case "listFilesTopLevel":
return (
<>
<div style={headerStyle}>
{toolIcon("folder-opened")}
<span style={{ fontWeight: "bold" }}>
Claude wants to view this directory:
Claude wants to view the top level files in this directory:
</span>
</div>
<CodeBlock
@@ -357,13 +414,33 @@ const ChatRow: React.FC<ChatRowProps> = ({
/>
</>
)
case "analyzeProject":
case "listFilesRecursive":
return (
<>
<div style={headerStyle}>
{toolIcon("folder-opened")}
<span style={{ fontWeight: "bold" }}>
Claude wants to recursively view all files in this directory:
</span>
</div>
<CodeBlock
code={tool.content!}
path={tool.path!}
language="shell-session"
syntaxHighlighterStyle={syntaxHighlighterStyle}
isExpanded={isExpanded}
onToggleExpand={onToggleExpand}
/>
</>
)
case "viewSourceCodeDefinitionsTopLevel":
return (
<>
<div style={headerStyle}>
{toolIcon("file-code")}
<span style={{ fontWeight: "bold" }}>
Claude wants to analyze this project:
Claude wants to view source code definitions in files at the top level
of this directory:
</span>
</div>
<CodeBlock
+35 -21
View File
@@ -4,11 +4,11 @@ import { KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from
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 { combineApiRequests } from "../utilities/combineApiRequests"
import { combineCommandSequences } from "../utilities/combineCommandSequences"
import { getApiMetrics } from "../utilities/getApiMetrics"
import { getSyntaxHighlighterStyleFromTheme } from "../utilities/getSyntaxHighlighterStyleFromTheme"
import { vscode } from "../utilities/vscode"
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 ChatRow from "./ChatRow"
import TaskHeader from "./TaskHeader"
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
@@ -31,7 +31,6 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
const [inputValue, setInputValue] = useState("")
const textAreaRef = useRef<HTMLTextAreaElement>(null)
const [textAreaHeight, setTextAreaHeight] = useState<number | undefined>(undefined)
const [textAreaDisabled, setTextAreaDisabled] = useState(false)
// we need to hold on to the ask because useEffect > lastMessage will always let us know when an ask comes in and handle it, but by the time handleMessage is called, the last message might not be the ask anymore (it could be a say that followed)
@@ -108,6 +107,13 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
setPrimaryButtonText("Run Command")
setSecondaryButtonText("Reject")
break
case "command_output":
setTextAreaDisabled(false)
setClaudeAsk("command_output")
setEnableButtons(true)
setPrimaryButtonText("Exit Command")
setSecondaryButtonText(undefined)
break
case "completion_result":
// extension waiting for feedback. but we can just present a new task button
setTextAreaDisabled(false)
@@ -126,6 +132,13 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
case "error":
break
case "api_req_started":
if (messages.at(-2)?.ask === "command_output") {
// if the last ask is a command_output, and we receive an api_req_started, then that means the command has finished and we don't need input from the user anymore (in every other case, the user has to interact with input field or buttons to continue, which does the following automatically)
setInputValue("")
setTextAreaDisabled(true)
setClaudeAsk(undefined)
setEnableButtons(false)
}
break
case "api_req_finished":
break
@@ -168,6 +181,7 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
case "followup":
case "tool":
case "command": // user can provide feedback to a tool or command use
case "command_output": // user can send input to command stdin
case "completion_result": // if this happens then the user has feedback for the completion result
vscode.postMessage({ type: "askResponse", askResponse: "textResponse", text })
break
@@ -191,6 +205,7 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
case "request_limit_reached":
case "api_req_failed":
case "command":
case "command_output":
case "tool":
vscode.postMessage({ type: "askResponse", askResponse: "yesButtonTapped" })
break
@@ -266,12 +281,6 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
textAreaRef.current?.focus()
})
useEffect(() => {
if (textAreaRef.current && !textAreaHeight) {
setTextAreaHeight(textAreaRef.current.offsetHeight)
}
}, [textAreaHeight])
useEffect(() => {
const timer = setTimeout(() => {
if (!isHidden && !textAreaDisabled && !enableButtons) {
@@ -315,6 +324,13 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
return () => clearTimeout(timer)
}, [visibleMessages])
const placeholderText = useMemo(() => {
if (messages.at(-1)?.ask === "command_output") {
return "Type input to command stdin..."
}
return task ? "Type a message..." : "Type your task here..."
}, [task, messages])
return (
<div
style={{
@@ -348,8 +364,8 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
style={{ display: "inline" }}>
Claude 3.5 Sonnet's agentic coding capabilities,
</VSCodeLink>{" "}
I can handle complex software development tasks step-by-step. With tools that let me read &
write files, analyze project source code, and execute terminal commands (after you grant
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
permission), I can assist you in ways that go beyond simple code completion or tech support.
</p>
</div>
@@ -378,11 +394,8 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
syntaxHighlighterStyle={syntaxHighlighterStyle}
isExpanded={expandedRows[message.ts] || false}
onToggleExpand={() => toggleRowExpansion(message.ts)}
apiRequestFailedMessage={
index === visibleMessages.length - 1 && modifiedMessages.at(-1)?.ask === "api_req_failed" // if request is retried then the latest message is a api_req_retried
? modifiedMessages.at(-1)?.text
: undefined
}
lastModifiedMessage={modifiedMessages.at(-1)}
isLast={index === visibleMessages.length - 1}
/>
)}
/>
@@ -425,7 +438,7 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
//virtuosoRef.current?.scrollToIndex({ index: "LAST", align: "end", behavior: "auto" })
virtuosoRef.current?.scrollTo({ top: Number.MAX_SAFE_INTEGER, behavior: "auto" })
}
placeholder={task ? "Type a message..." : "Type your task here..."}
placeholder={placeholderText}
maxRows={10}
autoFocus={true}
style={{
@@ -450,7 +463,8 @@ const ChatView = ({ messages, isHidden, vscodeThemeName, showAnnouncement, hideA
right: 20,
display: "flex",
alignItems: "center",
...(!!textAreaHeight ? { height: textAreaHeight, bottom: 12 } : { top: 0, bottom: 1.5 }),
top: 0,
bottom: 1.5,
}}>
<VSCodeButton
disabled={textAreaDisabled}
@@ -1,7 +1,7 @@
import { useMemo } from "react"
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
import { getLanguageFromPath } from "../../utilities/getLanguageFromPath"
import { SyntaxHighlighterStyle } from "../../utilities/getSyntaxHighlighterStyleFromTheme"
import { getLanguageFromPath } from "../../utils/getLanguageFromPath"
import { SyntaxHighlighterStyle } from "../../utils/getSyntaxHighlighterStyleFromTheme"
/*
const vscodeSyntaxStyle: React.CSSProperties = {
+54 -94
View File
@@ -1,64 +1,50 @@
import { ApiConfiguration } from "@shared/api"
import { VSCodeButton, VSCodeDivider, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import React, { useState } from "react"
import { useEffectOnce } from "react-use"
import { vscode } from "../utilities/vscode"
import React, { useEffect, useState } from "react"
import { validateApiConfiguration, validateMaxRequestsPerTask } from "../utils/validate"
import { vscode } from "../utils/vscode"
import ApiOptions from "./ApiOptions"
type SettingsViewProps = {
apiKey: string
setApiKey: React.Dispatch<React.SetStateAction<string>>
apiConfiguration?: ApiConfiguration
setApiConfiguration: React.Dispatch<React.SetStateAction<ApiConfiguration | undefined>>
maxRequestsPerTask: string
setMaxRequestsPerTask: React.Dispatch<React.SetStateAction<string>>
onDone: () => void // Define the type of the onDone prop
onDone: () => void
}
const SettingsView = ({ apiKey, setApiKey, maxRequestsPerTask, setMaxRequestsPerTask, onDone }: SettingsViewProps) => {
const [apiKeyErrorMessage, setApiKeyErrorMessage] = useState<string | undefined>(undefined)
const SettingsView = ({
apiConfiguration,
setApiConfiguration,
maxRequestsPerTask,
setMaxRequestsPerTask,
onDone,
}: SettingsViewProps) => {
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const [maxRequestsErrorMessage, setMaxRequestsErrorMessage] = useState<string | undefined>(undefined)
const disableDoneButton = apiKeyErrorMessage != null || maxRequestsErrorMessage != null
const handleApiKeyChange = (event: any) => {
const input = event.target.value
setApiKey(input)
validateApiKey(input)
}
const validateApiKey = (value: string) => {
if (value.trim() === "") {
setApiKeyErrorMessage("API Key cannot be empty")
} else {
setApiKeyErrorMessage(undefined)
}
}
const handleMaxRequestsChange = (event: any) => {
const input = event.target.value
setMaxRequestsPerTask(input)
validateMaxRequests(input)
}
const validateMaxRequests = (value: string | undefined) => {
if (value?.trim()) {
const num = Number(value)
if (isNaN(num)) {
setMaxRequestsErrorMessage("Maximum requests must be a number")
} else if (num < 3 || num > 100) {
setMaxRequestsErrorMessage("Maximum requests must be between 3 and 100")
} else {
setMaxRequestsErrorMessage(undefined)
}
} else {
setMaxRequestsErrorMessage(undefined)
}
}
const handleSubmit = () => {
vscode.postMessage({ type: "apiKey", text: apiKey })
vscode.postMessage({ type: "maxRequestsPerTask", text: maxRequestsPerTask })
const apiValidationResult = validateApiConfiguration(apiConfiguration)
const maxRequestsValidationResult = validateMaxRequestsPerTask(maxRequestsPerTask)
onDone()
setApiErrorMessage(apiValidationResult)
setMaxRequestsErrorMessage(maxRequestsValidationResult)
if (!apiValidationResult && !maxRequestsValidationResult) {
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
vscode.postMessage({ type: "maxRequestsPerTask", text: maxRequestsPerTask })
onDone()
}
}
useEffect(() => {
setApiErrorMessage(undefined)
}, [apiConfiguration])
useEffect(() => {
setMaxRequestsErrorMessage(undefined)
}, [maxRequestsPerTask])
// validate as soon as the component is mounted
/*
useEffect will use stale values of variables if they are not included in the dependency array. so trying to use useEffect with a dependency array of only one value for example will use any other variables' old values. In most cases you don't want this, and should opt to use react-use hooks.
@@ -70,14 +56,6 @@ const SettingsView = ({ apiKey, setApiKey, maxRequestsPerTask, setMaxRequestsPer
If we only want to run code once on mount we can use react-use's useEffectOnce or useMount
*/
useEffectOnce(() => {
const timeoutId = setTimeout(() => {
validateApiKey(apiKey)
validateMaxRequests(maxRequestsPerTask)
}, 1000)
return () => clearTimeout(timeoutId)
})
return (
<div style={{ margin: "0 auto", paddingTop: "10px" }}>
@@ -89,40 +67,21 @@ const SettingsView = ({ apiKey, setApiKey, maxRequestsPerTask, setMaxRequestsPer
marginBottom: "17px",
}}>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>Settings</h3>
<VSCodeButton onClick={handleSubmit} disabled={disableDoneButton}>
Done
</VSCodeButton>
<VSCodeButton onClick={handleSubmit}>Done</VSCodeButton>
</div>
<div style={{ marginBottom: "20px" }}>
<VSCodeTextField
value={apiKey}
style={{ width: "100%" }}
placeholder="Enter your Anthropic API Key"
onInput={handleApiKeyChange}>
<span style={{ fontWeight: "500" }}>Anthropic API Key</span>
</VSCodeTextField>
{apiKeyErrorMessage && (
<div style={{ marginBottom: 5 }}>
<ApiOptions apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
{apiErrorMessage && (
<p
style={{
margin: "-5px 0 12px 0",
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-errorForeground)",
}}>
{apiKeyErrorMessage}
{apiErrorMessage}
</p>
)}
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
This key is not shared with anyone and only used to make API requests from the extension.
<VSCodeLink href="https://console.anthropic.com/" style={{ display: "inline" }}>
You can get an API key by signing up here.
</VSCodeLink>
</p>
</div>
<div style={{ marginBottom: "20px" }}>
@@ -130,9 +89,18 @@ const SettingsView = ({ apiKey, setApiKey, maxRequestsPerTask, setMaxRequestsPer
value={maxRequestsPerTask}
style={{ width: "100%" }}
placeholder="20"
onInput={handleMaxRequestsChange}>
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={{
@@ -143,15 +111,6 @@ const SettingsView = ({ apiKey, setApiKey, maxRequestsPerTask, setMaxRequestsPer
{maxRequestsErrorMessage}
</p>
)}
<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>
</div>
<VSCodeDivider />
@@ -163,13 +122,14 @@ const SettingsView = ({ apiKey, setApiKey, maxRequestsPerTask, setMaxRequestsPer
color: "var(--vscode-descriptionForeground)",
fontSize: "12px",
lineHeight: "1.2",
fontStyle: "italic",
}}>
<p>
<VSCodeLink href="https://github.com/saoudrizwan/claude-dev">
<p style={{ wordWrap: "break-word" }}>
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.94</p>
</div>
</div>
)
+34 -11
View File
@@ -2,6 +2,7 @@ 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 { vscode } from "../utils/vscode"
interface TaskHeaderProps {
taskText: string
@@ -71,6 +72,10 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({ taskText, tokensIn, tokensOut,
const toggleExpand = () => setIsExpanded(!isExpanded)
const handleDownload = () => {
vscode.postMessage({ type: "downloadTask" })
}
return (
<div style={{ padding: "15px 15px 10px 15px" }}>
<div
@@ -82,6 +87,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({ taskText, tokensIn, tokensOut,
display: "flex",
flexDirection: "column",
gap: "8px",
position: "relative",
}}>
<div
style={{
@@ -103,6 +109,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({ taskText, tokensIn, tokensOut,
fontSize: "var(--vscode-font-size)",
overflowY: isExpanded ? "auto" : "hidden",
wordBreak: "break-word",
overflowWrap: "anywhere",
}}>
<TextTruncate
key={textTruncateKey}
@@ -135,15 +142,8 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({ taskText, tokensIn, tokensOut,
)}
</div>
<div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<span style={{ fontWeight: "bold" }}>Tokens:</span>
<span style={{ display: "flex", alignItems: "center", gap: "3px" }}>
<i
className="codicon codicon-arrow-down"
style={{ fontSize: "12px", fontWeight: "bold", marginBottom: "-2px" }}
/>
{tokensOut.toLocaleString()}
</span>
<span style={{ display: "flex", alignItems: "center", gap: "3px" }}>
<i
className="codicon codicon-arrow-up"
@@ -151,10 +151,33 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({ taskText, tokensIn, tokensOut,
/>
{tokensIn.toLocaleString()}
</span>
<span style={{ display: "flex", alignItems: "center", gap: "3px" }}>
<i
className="codicon codicon-arrow-down"
style={{ fontSize: "12px", fontWeight: "bold", marginBottom: "-2px" }}
/>
{tokensOut.toLocaleString()}
</span>
</div>
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<span style={{ fontWeight: "bold" }}>API Cost:</span>
<span>${totalCost.toFixed(4)}</span>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}>
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<span style={{ fontWeight: "bold" }}>API Cost:</span>
<span>${totalCost.toFixed(4)}</span>
</div>
<VSCodeButton
appearance="icon"
onClick={handleDownload}
style={{
marginBottom: "-2px",
marginRight: "-2.5px",
}}>
<div style={{ fontSize: "10.5px", fontWeight: "bold", opacity: 0.6 }}>EXPORT .MD</div>
</VSCodeButton>
</div>
</div>
</div>
+20 -46
View File
@@ -1,32 +1,27 @@
import React, { useState, useEffect } from "react"
import { VSCodeButton, VSCodeTextField, VSCodeLink, VSCodeDivider } from "@vscode/webview-ui-toolkit/react"
import { vscode } from "../utilities/vscode"
import { ApiConfiguration } from "@shared/api"
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import React, { useEffect, useState } from "react"
import { validateApiConfiguration } from "../utils/validate"
import { vscode } from "../utils/vscode"
import ApiOptions from "./ApiOptions"
interface WelcomeViewProps {
apiKey: string
setApiKey: React.Dispatch<React.SetStateAction<string>>
apiConfiguration?: ApiConfiguration
setApiConfiguration: React.Dispatch<React.SetStateAction<ApiConfiguration | undefined>>
}
const WelcomeView: React.FC<WelcomeViewProps> = ({ apiKey, setApiKey }) => {
const [apiKeyErrorMessage, setApiKeyErrorMessage] = useState<string | undefined>(undefined)
const WelcomeView: React.FC<WelcomeViewProps> = ({ apiConfiguration, setApiConfiguration }) => {
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const disableLetsGoButton = apiKeyErrorMessage != null
const validateApiKey = (value: string) => {
if (value.trim() === "") {
setApiKeyErrorMessage("API Key cannot be empty")
} else {
setApiKeyErrorMessage(undefined)
}
}
const disableLetsGoButton = apiErrorMessage != null
const handleSubmit = () => {
vscode.postMessage({ type: "apiKey", text: apiKey })
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
}
useEffect(() => {
validateApiKey(apiKey)
}, [apiKey])
setApiErrorMessage(validateApiConfiguration(apiConfiguration))
}, [apiConfiguration])
return (
<div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, padding: "0 20px" }}>
@@ -38,39 +33,18 @@ const WelcomeView: React.FC<WelcomeViewProps> = ({ apiKey, setApiKey }) => {
style={{ display: "inline" }}>
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 read & write
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).
</p>
<b>To get started, this extension needs an Anthropic API key:</b>
<ol style={{ paddingLeft: "15px" }}>
<li>
Go to{" "}
<VSCodeLink href="https://console.anthropic.com" style={{ display: "inline" }}>
https://console.anthropic.com
</VSCodeLink>
</li>
<li>You may need to buy some credits (although Anthropic is offering $5 free credit for new users)</li>
<li>Click 'Get API Keys' and create a new key (you can delete it any time)</li>
</ol>
<b>To get started, this extension needs an API key for Claude 3.5 Sonnet:</b>
<VSCodeDivider />
<div style={{ marginTop: "20px", display: "flex", alignItems: "center" }}>
<VSCodeTextField
style={{ flexGrow: 1, marginRight: "10px" }}
placeholder="Enter API Key..."
value={apiKey}
onInput={(e: any) => setApiKey(e.target.value)}
/>
<VSCodeButton onClick={handleSubmit} disabled={disableLetsGoButton}>
Submit
<div style={{ marginTop: "15px" }}>
<ApiOptions apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
<VSCodeButton onClick={handleSubmit} disabled={disableLetsGoButton} style={{ marginTop: "3px" }}>
Let's go!
</VSCodeButton>
</div>
<p style={{ fontSize: "12px", marginTop: "10px", color: "var(--vscode-descriptionForeground)" }}>
Your API key is stored securely on your computer and used only for interacting with the Anthropic API.
</p>
</div>
)
}
+20
View File
@@ -109,3 +109,23 @@ The above scrollbar styling uses some transparent background color magic to acco
.code-block-scrollable::-webkit-scrollbar-corner {
background-color: transparent;
}
/*
Dropdown label
https://github.com/microsoft/vscode-webview-ui-toolkit/tree/main/src/dropdown#with-label
*/
.dropdown-container {
box-sizing: border-box;
display: flex;
flex-flow: column nowrap;
align-items: flex-start;
justify-content: flex-start;
}
.dropdown-container label {
display: block;
color: var(--vscode-foreground);
cursor: pointer;
font-size: var(--vscode-font-size);
line-height: normal;
margin-bottom: 2px;
}
@@ -14,8 +14,8 @@ import { ClaudeMessage } from "@shared/ExtensionMessage"
* @example
* const messages: ClaudeMessage[] = [
* { type: 'ask', ask: 'command', text: 'ls', ts: 1625097600000 },
* { type: 'say', say: 'command_output', text: 'file1.txt', ts: 1625097601000 },
* { type: 'say', say: 'command_output', text: 'file2.txt', ts: 1625097602000 }
* { type: 'ask', ask: 'command_output', text: 'file1.txt', ts: 1625097601000 },
* { type: 'ask', ask: 'command_output', text: 'file2.txt', ts: 1625097602000 }
* ];
* const result = simpleCombineCommandSequences(messages);
* // Result: [{ type: 'ask', ask: 'command', text: 'ls\nfile1.txt\nfile2.txt', ts: 1625097600000 }]
@@ -35,13 +35,17 @@ export function combineCommandSequences(messages: ClaudeMessage[]): ClaudeMessag
// Stop if we encounter the next command
break
}
if (messages[j].type === "say" && messages[j].say === "command_output") {
if (messages[j].ask === "command_output" || messages[j].say === "command_output") {
if (!didAddOutput) {
// Add a newline before the first output
combinedText += `\n${COMMAND_OUTPUT_STRING}`
didAddOutput = true
}
combinedText += "\n" + (messages[j].text || "")
// handle cases where we receive empty command_output (ie when extension is relinquishing control over exit command button)
const output = messages[j].text || ""
if (output.length > 0) {
combinedText += "\n" + output
}
}
j++
}
@@ -57,7 +61,7 @@ export function combineCommandSequences(messages: ClaudeMessage[]): ClaudeMessag
// Second pass: remove command_outputs and replace original commands with combined ones
return messages
.filter((msg) => !(msg.type === "say" && msg.say === "command_output"))
.filter((msg) => !(msg.ask === "command_output" || msg.say === "command_output"))
.map((msg) => {
if (msg.type === "ask" && msg.ask === "command") {
const combinedCommand = combinedCommands.find((cmd) => cmd.ts === msg.ts)
@@ -85,6 +85,5 @@ const extensionToLanguage: { [key: string]: string } = {
export function getLanguageFromPath(path: string): string | undefined {
const extension = path.split(".").pop()?.toLowerCase() || ""
console.log(`converted path to language: ${path} -> ${extensionToLanguage[extension]}`)
return extensionToLanguage[extension]
}
+34
View File
@@ -0,0 +1,34 @@
import { ApiConfiguration } from "@shared/api"
export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): string | undefined {
if (apiConfiguration) {
switch (apiConfiguration.apiProvider) {
case "anthropic":
if (!apiConfiguration.apiKey) {
return "You must provide a valid API key or choose a different provider."
}
break
case "bedrock":
if (!apiConfiguration.awsAccessKey || !apiConfiguration.awsSecretKey || !apiConfiguration.awsRegion) {
return "You must provide a valid AWS access key, secret key, and region."
}
break
case "openrouter":
if (!apiConfiguration.openRouterApiKey) {
return "You must provide a valid API key or choose a different provider."
}
break
}
}
return undefined
}
export function validateMaxRequestsPerTask(maxRequestsPerTask?: string): string | undefined {
if (maxRequestsPerTask && maxRequestsPerTask.trim()) {
const num = Number(maxRequestsPerTask)
if (isNaN(num) || num < 3 || num > 100) {
return "Maximum requests must be between 3 and 100"
}
}
return undefined
}