Add ability to attach images to messages

This commit is contained in:
Saoud Rizwan
2024-08-08 02:44:51 -04:00
parent 9acae31fbb
commit 911dd159cd
16 changed files with 1129 additions and 179 deletions

View File

@@ -1,12 +1,14 @@
import { Uri, Webview } from "vscode"
import { Anthropic } from "@anthropic-ai/sdk"
import os from "os"
import * as path from "path"
import * as vscode from "vscode"
import { Uri, Webview } from "vscode"
import { ClaudeDev } from "../ClaudeDev"
import { ApiProvider } from "../shared/api"
import { ExtensionMessage } from "../shared/ExtensionMessage"
import { WebviewMessage } from "../shared/WebviewMessage"
import { processPastedImages, selectAndProcessImages } from "../utils/process-images"
import { downloadTask } from "../utils/export-markdown"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -134,7 +136,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
this.outputChannel.appendLine("Webview view resolved")
}
async initClaudeDevWithTask(task: string) {
async initClaudeDevWithTask(task: string, images?: string[]) {
await this.clearTask() // ensures that an exising task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
const { apiProvider, apiKey, openRouterApiKey, awsAccessKey, awsSecretKey, awsRegion, maxRequestsPerTask } =
await this.getState()
@@ -142,7 +144,8 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
this,
task,
{ apiProvider, apiKey, openRouterApiKey, awsAccessKey, awsSecretKey, awsRegion },
maxRequestsPerTask
maxRequestsPerTask,
images
)
}
@@ -203,7 +206,8 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
create a content security policy meta tag so that only loading scripts with a nonce is allowed
As your extension grows you will likely want to add custom styles, fonts, and/or images to your webview. If you do, you will need to update the content security policy meta tag to explicity allow for these resources. E.g.
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; font-src ${webview.cspSource}; img-src ${webview.cspSource} https:; script-src 'nonce-${nonce}';">
- 'unsafe-inline' is required for styles due to vscode-webview-toolkit's dynamic style injection
- since we pass base64 images to the webview, we need to specify img-src ${webview.cspSource} data:;
in meta tag we add nonce attribute: A cryptographic nonce (only used once) to allow scripts. The server must generate a unique nonce value each time it transmits a policy. It is critical to provide a nonce that cannot be guessed as bypassing a resource's policy is otherwise trivial.
*/
@@ -217,7 +221,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource}; style-src ${webview.cspSource}; script-src 'nonce-${nonce}';">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource}; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} data:; script-src 'nonce-${nonce}';">
<link rel="stylesheet" type="text/css" href="${stylesUri}">
<link href="${codiconsUri}" rel="stylesheet" />
<title>Claude Dev</title>
@@ -253,7 +257,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
// Could also do this in extension .ts
//this.postMessageToWebview({ type: "text", text: `Extension: ${Date.now()}` })
// initializing new instance of ClaudeDev will make sure that any agentically running promises in old instance don't affect our new task. this essentially creates a fresh slate for the new task
await this.initClaudeDevWithTask(message.text!)
await this.initClaudeDevWithTask(message.text!, message.images)
break
case "apiConfiguration":
if (message.apiConfiguration) {
@@ -282,7 +286,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
await this.postStateToWebview()
break
case "askResponse":
this.claudeDev?.handleWebviewAskResponse(message.askResponse!, message.text)
this.claudeDev?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
break
case "clearTask":
// newTask will start a new task with a given task text, while clear task resets the current session and allows for a new task to be started
@@ -294,7 +298,19 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
await this.postStateToWebview()
break
case "downloadTask":
this.downloadTask()
downloadTask(this.claudeDev?.apiConversationHistory ?? [])
break
case "selectImages":
const images = await selectAndProcessImages()
await this.postMessageToWebview({ type: "selectedImages", images })
break
case "processPastedImages":
const pastedImages = message.images ?? []
if (pastedImages.length > 0) {
const processedImages = await processPastedImages(pastedImages)
await this.postMessageToWebview({ type: "selectedImages", images: processedImages })
}
break
// Add more switch case statements here as more webview message commands
// are created within the webview context (i.e. inside media/main.js)
@@ -305,82 +321,6 @@ 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 {
apiProvider,