mirror of
https://github.com/cline/cline.git
synced 2026-09-09 15:02:23 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c823cdd4f8 | ||
|
|
09050b8800 | ||
|
|
bbdf66b45c | ||
|
|
0ac7f63332 | ||
|
|
582d178157 | ||
|
|
1ea4c9beee | ||
|
|
d7ee0c0161 | ||
|
|
fa1068d92d | ||
|
|
b32f91b0ee | ||
|
|
7f30a8bea7 | ||
|
|
7656f8d653 | ||
|
|
2d37c290fa | ||
|
|
7989410696 | ||
|
|
c09a8462d7 | ||
|
|
d441950b7f | ||
|
|
15f52696b9 | ||
|
|
c800f157da | ||
|
|
67ed5edbe6 | ||
|
|
89c242d966 | ||
|
|
c994f64c6f |
@@ -4,6 +4,10 @@ All notable changes to the "claude-dev" extension will be documented in this fil
|
||||
|
||||
<!-- Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. -->
|
||||
|
||||
## [1.0.9]
|
||||
|
||||
- Add support for OpenRouter and AWS Bedrock
|
||||
|
||||
## [1.0.8]
|
||||
|
||||
- Shows diff view of new or edited files right in the editor
|
||||
|
||||
@@ -80,7 +80,7 @@ To build Claude Dev locally, follow these steps:
|
||||
## Reviews
|
||||
|
||||
- ["Claude Sonnet 3.5 Artifacts in VSCode With This Extension"](https://www.youtube.com/watch?v=5FbZ8ALfSTs) by [CoderOne](https://www.youtube.com/@CoderOne)
|
||||
- ["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/)
|
||||
|
||||
Generated
+3805
-2
File diff suppressed because it is too large
Load Diff
+7
-3
@@ -2,7 +2,7 @@
|
||||
"name": "claude-dev",
|
||||
"displayName": "Claude Dev",
|
||||
"description": "Autonomous software engineer right in your IDE, capable of creating/editing files, executing commands, and more with your permission every step of the way.",
|
||||
"version": "1.0.85",
|
||||
"version": "1.0.91",
|
||||
"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,19 @@
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}`)
|
||||
+89
-76
@@ -1,19 +1,22 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import defaultShell from "default-shell"
|
||||
import * as diff from "diff"
|
||||
import { execa, ExecaError } from "execa"
|
||||
import { execa, ExecaError, ResultPromise } from "execa"
|
||||
import fs from "fs/promises"
|
||||
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 treeKill from "tree-kill"
|
||||
import * as vscode from "vscode"
|
||||
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 { DEFAULT_MAX_REQUESTS_PER_TASK } from "./shared/Constants"
|
||||
import { ClaudeAsk, ClaudeSay, ClaudeSayTool } from "./shared/ExtensionMessage"
|
||||
import { ClaudeAsk, ClaudeMessage, ClaudeSay, ClaudeSayTool } from "./shared/ExtensionMessage"
|
||||
import { Tool, ToolName } from "./shared/Tool"
|
||||
import { ClaudeAskResponse } from "./shared/WebviewMessage"
|
||||
|
||||
@@ -42,7 +45,7 @@ RULES
|
||||
}
|
||||
- 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.
|
||||
- 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 to understand the user's environment and tailor your commands to ensure they are compatible with their system.
|
||||
- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory, 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.
|
||||
@@ -61,7 +64,7 @@ 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.
|
||||
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.
|
||||
@@ -224,25 +227,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) {
|
||||
@@ -263,7 +273,7 @@ export class ClaudeDev {
|
||||
this.askResponseText = undefined
|
||||
const askTs = Date.now()
|
||||
this.lastMessageTs = askTs
|
||||
await this.providerRef.deref()?.addClaudeMessage({ ts: askTs, type: "ask", ask: type, text: question })
|
||||
this.claudeMessages.push({ ts: askTs, type: "ask", ask: type, text: question })
|
||||
await this.providerRef.deref()?.postStateToWebview()
|
||||
await pWaitFor(() => this.askResponse !== undefined || this.lastMessageTs !== askTs, { interval: 100 })
|
||||
if (this.lastMessageTs !== askTs) {
|
||||
@@ -281,14 +291,15 @@ export class ClaudeDev {
|
||||
}
|
||||
const sayTs = Date.now()
|
||||
this.lastMessageTs = sayTs
|
||||
await this.providerRef.deref()?.addClaudeMessage({ ts: sayTs, type: "say", say: type, text: text })
|
||||
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.
|
||||
@@ -503,20 +514,7 @@ export class ClaudeDev {
|
||||
async listFilesTopLevel(dirPath: string): Promise<string> {
|
||||
try {
|
||||
const files = await listFiles(dirPath, false)
|
||||
const result = files
|
||||
.map((file) => {
|
||||
const relativePath = path.relative(dirPath, file)
|
||||
return file.endsWith("/") ? relativePath + "/" : relativePath
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const aIsDir = a.endsWith("/")
|
||||
const bIsDir = b.endsWith("/")
|
||||
if (aIsDir !== bIsDir) {
|
||||
return aIsDir ? -1 : 1
|
||||
}
|
||||
return a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" })
|
||||
})
|
||||
.join("\n")
|
||||
const result = this.formatFilesList(dirPath, files)
|
||||
const { response, text } = await this.ask(
|
||||
"tool",
|
||||
JSON.stringify({ tool: "listFilesTopLevel", path: dirPath, content: result } as ClaudeSayTool)
|
||||
@@ -544,7 +542,7 @@ export class ClaudeDev {
|
||||
async listFilesRecursive(dirPath: string): Promise<string> {
|
||||
try {
|
||||
const files = await listFiles(dirPath, true)
|
||||
const result = files.map((file) => path.relative(dirPath, file)).join("\n")
|
||||
const result = this.formatFilesList(dirPath, files)
|
||||
const { response, text } = await this.ask(
|
||||
"tool",
|
||||
JSON.stringify({ tool: "listFilesRecursive", path: dirPath, content: result } as ClaudeSayTool)
|
||||
@@ -567,6 +565,32 @@ export class ClaudeDev {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -607,6 +631,34 @@ 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 convert bash into javascript, so need to specify `shell: true` to use sh on unix or cmd.exe on windows
|
||||
@@ -619,21 +671,7 @@ export class ClaudeDev {
|
||||
const line = chunk.toString()
|
||||
// stream output to user in realtime
|
||||
// do not await as we are not waiting for a response
|
||||
this.ask("command_output", line)
|
||||
.then(({ response, text }) => {
|
||||
// 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)
|
||||
subprocess.kill("SIGINT") // will result in for loop throwing error
|
||||
} 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")
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// this can only happen if this ask promise was ignored, so ignore this error
|
||||
})
|
||||
sendCommandOutput(subprocess, line)
|
||||
result += `${line}\n`
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -687,22 +725,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",
|
||||
@@ -729,7 +752,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",
|
||||
@@ -739,7 +762,7 @@ export class ClaudeDev {
|
||||
if (response === "yesButtonTapped") {
|
||||
this.requestCount = 0
|
||||
} else {
|
||||
await this.providerRef.deref()?.addMessageToApiConversationHistory({
|
||||
this.apiConversationHistory.push({
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
@@ -756,14 +779,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 {
|
||||
@@ -808,6 +824,7 @@ export class ClaudeDev {
|
||||
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,
|
||||
@@ -823,13 +840,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." }],
|
||||
})
|
||||
@@ -859,10 +874,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: [
|
||||
{
|
||||
|
||||
@@ -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" },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,7 @@ export async function listFiles(dirPath: string, recursive: boolean): Promise<st
|
||||
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: recursive, // true by default, false means it will list directories on their own too
|
||||
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(recursive ? "**" : "*", options)
|
||||
|
||||
@@ -1,12 +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"
|
||||
import * as path from "path"
|
||||
import os from "os"
|
||||
|
||||
/*
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
@@ -14,12 +14,14 @@ 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 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-29-2024" // update to some unique identifier when we add a new announcement
|
||||
|
||||
@@ -132,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
|
||||
@@ -250,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":
|
||||
@@ -307,7 +319,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
||||
const fileName = `claude_dev_task_${month}-${day}-${year}_${hours}-${minutes}-${ampm}.md`
|
||||
|
||||
// Generate markdown
|
||||
const conversationHistory = await this.getApiConversationHistory()
|
||||
const conversationHistory = this.claudeDev?.apiConversationHistory || []
|
||||
const markdownContent = conversationHistory
|
||||
.map((message) => {
|
||||
const role = message.role === "user" ? "**User:**" : "**Assistant:**"
|
||||
@@ -370,19 +382,23 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
})
|
||||
@@ -393,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.
|
||||
@@ -411,32 +427,36 @@ 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
|
||||
|
||||
@@ -445,29 +465,28 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
||||
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.
|
||||
*/
|
||||
private apiConversationHistory: Anthropic.MessageParam[] = []
|
||||
|
||||
async getApiConversationHistory(): Promise<Anthropic.MessageParam[]> {
|
||||
// const history = (await this.getGlobalState(
|
||||
// this.getApiConversationHistoryStateKey()
|
||||
// )) as Anthropic.MessageParam[]
|
||||
// return history || []
|
||||
return this.apiConversationHistory
|
||||
}
|
||||
// getApiConversationHistory(): Anthropic.MessageParam[] {
|
||||
// // const history = (await this.getGlobalState(
|
||||
// // this.getApiConversationHistoryStateKey()
|
||||
// // )) as Anthropic.MessageParam[]
|
||||
// // return history || []
|
||||
// return this.apiConversationHistory
|
||||
// }
|
||||
|
||||
async setApiConversationHistory(history: Anthropic.MessageParam[] | undefined) {
|
||||
// await this.updateGlobalState(this.getApiConversationHistoryStateKey(), history)
|
||||
this.apiConversationHistory = history || []
|
||||
}
|
||||
// setApiConversationHistory(history: Anthropic.MessageParam[] | undefined) {
|
||||
// // await this.updateGlobalState(this.getApiConversationHistoryStateKey(), history)
|
||||
// this.apiConversationHistory = history || []
|
||||
// }
|
||||
|
||||
async addMessageToApiConversationHistory(message: Anthropic.MessageParam): Promise<Anthropic.MessageParam[]> {
|
||||
// const history = await this.getApiConversationHistory()
|
||||
// history.push(message)
|
||||
// await this.setApiConversationHistory(history)
|
||||
// return history
|
||||
this.apiConversationHistory.push(message)
|
||||
return this.apiConversationHistory
|
||||
}
|
||||
// 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
|
||||
@@ -475,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)
|
||||
}
|
||||
|
||||
@@ -507,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ApiConfiguration, ApiProvider } from "./api"
|
||||
|
||||
export interface WebviewMessage {
|
||||
type:
|
||||
| "apiKey"
|
||||
| "apiConfiguration"
|
||||
| "maxRequestsPerTask"
|
||||
| "webviewDidLaunch"
|
||||
| "newTask"
|
||||
@@ -10,6 +12,7 @@ export interface WebviewMessage {
|
||||
| "downloadTask"
|
||||
text?: string
|
||||
askResponse?: ClaudeAskResponse
|
||||
apiConfiguration?: ApiConfiguration
|
||||
}
|
||||
|
||||
export type ClaudeAskResponse = "yesButtonTapped" | "noButtonTapped" | "textResponse"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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)}
|
||||
|
||||
@@ -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.8</h3>
|
||||
<h3 style={{ margin: "0 0 8px" }}>🎉{" "}New in v1.0.91</h3>
|
||||
<ul style={{ margin: "0 0 8px", paddingLeft: "20px" }}>
|
||||
<li>
|
||||
Open in the editor (using{" "}
|
||||
@@ -50,6 +50,7 @@ const Announcement = ({ hideAnnouncement }: AnnouncementProps) => {
|
||||
<li>Shows diff view of new or edited files right in the editor</li>
|
||||
<li>Added ability to retry failed API requests (helpful for rate limits)</li>
|
||||
<li>Export task to a markdown file (useful as context for future tasks)</li>
|
||||
<li>Added OpenRouter and AWS Bedrock support</li>
|
||||
</ul>
|
||||
<p style={{ margin: "0" }}>
|
||||
Follow me for more updates!{" "}
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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.91</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -2,7 +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 "../utilities/vscode"
|
||||
import { vscode } from "../utils/vscode"
|
||||
|
||||
interface TaskHeaderProps {
|
||||
taskText: string
|
||||
|
||||
@@ -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" }}>
|
||||
@@ -42,35 +37,14 @@ const WelcomeView: React.FC<WelcomeViewProps> = ({ apiKey, setApiKey }) => {
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+5
-1
@@ -41,7 +41,11 @@ export function combineCommandSequences(messages: ClaudeMessage[]): ClaudeMessag
|
||||
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++
|
||||
}
|
||||
-1
@@ -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]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user