diff --git a/assets/icon.png b/assets/icon.png deleted file mode 100644 index a6cde3a..0000000 Binary files a/assets/icon.png and /dev/null differ diff --git a/package.json b/package.json index 66c4b8b..57d133f 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ { "id": "claude-dev-ActivityBar", "title": "Claude Dev", - "icon": "assets/icon.png" + "icon": "$(robot)" } ] }, diff --git a/src/extension.ts b/src/extension.ts index e1654da..3f079ae 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -29,7 +29,7 @@ export function activate(context: vscode.ExtensionContext) { // }) // context.subscriptions.push(disposable) - const provider = new SidebarProvider(context.extensionUri) + const provider = new SidebarProvider(context) context.subscriptions.push(vscode.window.registerWebviewViewProvider(SidebarProvider.viewType, provider)) diff --git a/src/providers/SidebarProvider.ts b/src/providers/SidebarProvider.ts index 2f1949c..71e7c66 100644 --- a/src/providers/SidebarProvider.ts +++ b/src/providers/SidebarProvider.ts @@ -11,13 +11,15 @@ 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 ExtensionSecretKey = "apiKey" +type ExtensionGlobalStateKey = "didOpenOnce" | "maxRequestsPerTask" export class SidebarProvider implements vscode.WebviewViewProvider { public static readonly viewType = "claude-dev.SidebarProvider" private _view?: vscode.WebviewView - constructor(private readonly _extensionUri: vscode.Uri) {} + constructor(private readonly context: vscode.ExtensionContext) {} resolveWebviewView( webviewView: vscode.WebviewView, @@ -29,7 +31,7 @@ export class SidebarProvider implements vscode.WebviewViewProvider { webviewView.webview.options = { // Allow scripts in the webview enableScripts: true, - localResourceRoots: [this._extensionUri], + localResourceRoots: [this.context.extensionUri], } webviewView.webview.html = this.getHtmlContent(webviewView.webview) @@ -59,15 +61,27 @@ export class SidebarProvider implements vscode.WebviewViewProvider { // then convert it to a uri we can use in the webview. // The CSS file from the React build output - const stylesUri = getUri(webview, this._extensionUri, ["webview-ui", "build", "static", "css", "main.css"]) + const stylesUri = getUri(webview, this.context.extensionUri, [ + "webview-ui", + "build", + "static", + "css", + "main.css", + ]) // The JS file from the React build output - const scriptUri = getUri(webview, this._extensionUri, ["webview-ui", "build", "static", "js", "main.js"]) + const scriptUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "static", "js", "main.js"]) // The codicon font from the React build output // https://github.com/microsoft/vscode-extension-samples/blob/main/webview-codicons-sample/src/extension.ts // we installed this package in the extension so that we can access it how its intended from the extension (the font file is likely bundled in vscode), and we just import the css fileinto our react app we don't have access to it // don't forget to add font-src ${webview.cspSource}; - const codiconsUri = getUri(webview, this._extensionUri, ["node_modules", "@vscode", "codicons", "dist", "codicon.css"]) + const codiconsUri = getUri(webview, this.context.extensionUri, [ + "node_modules", + "@vscode", + "codicons", + "dist", + "codicon.css", + ]) // const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "main.js")) @@ -78,7 +92,7 @@ export class SidebarProvider implements vscode.WebviewViewProvider { // const stylesheetUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "main.css")) // Use a nonce to only allow a specific script to be run. - /* + /* content security policy of your webview to only allow scripts that have a specific nonce 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. @@ -119,20 +133,73 @@ export class SidebarProvider implements vscode.WebviewViewProvider { * @param context A reference to the extension context */ private _setWebviewMessageListener(webview: vscode.Webview) { - webview.onDidReceiveMessage((message: WebviewMessage) => { + webview.onDidReceiveMessage(async (message: WebviewMessage) => { switch (message.type) { + case "webviewDidLaunch": + await this.updateGlobalState("didOpenOnce", true) + await this.postWebviewState() + break case "text": // Code that should run in response to the hello message command vscode.window.showInformationMessage(message.text!) // Send a message to our webview. - // You can send any JSON serializable data. - // Could also do this in extension .ts + // You can send any JSON serializable data. + // Could also do this in extension .ts this.postMessageToWebview({ type: "text", text: `Extension: ${Date.now()}` }) - return + break + case "apiKey": + await this.storeSecret("apiKey", message.text!) + await this.postWebviewState() + break + case "maxRequestsPerTask": + let result: number | undefined = undefined + if (message.text && message.text.trim()) { + const num = Number(message.text) + if (!isNaN(num)) { + result = num + } + } + await this.updateGlobalState("maxRequestsPerTask", result) + await this.postWebviewState() + break // Add more switch case statements here as more webview message commands // are created within the webview context (i.e. inside media/main.js) } }) } + + private async postWebviewState() { + const [didOpenOnce, apiKey, maxRequestsPerTask] = await Promise.all([ + this.getGlobalState("didOpenOnce") as Promise, + this.getSecret("apiKey") as Promise, + this.getGlobalState("maxRequestsPerTask") as Promise, + ]) + this.postMessageToWebview({ + type: "webviewState", + webviewState: { didOpenOnce: !!didOpenOnce, apiKey: apiKey, maxRequestsPerTask: maxRequestsPerTask }, + }) + } + + /* + Storage + https://dev.to/kompotkot/how-to-use-secretstorage-in-your-vscode-extensions-2hco + https://www.eliostruyf.com/devhack-code-extension-storage-options/ + */ + + private async updateGlobalState(key: ExtensionGlobalStateKey, value: any) { + await this.context.globalState.update(key, value) + } + + private async getGlobalState(key: ExtensionGlobalStateKey) { + return await this.context.globalState.get(key) + } + + private async storeSecret(key: ExtensionSecretKey, value: any) { + await this.context.secrets.store(key, value) + } + + private async getSecret(key: ExtensionSecretKey) { + return await this.context.secrets.get(key) + } } diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 9f39a6c..6a0ab59 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -2,7 +2,8 @@ // webview will hold state export interface ExtensionMessage { - type: "text" | "action" + type: "text" | "action" | "webviewState" text?: string action?: "plusButtonTapped" | "settingsButtonTapped" + webviewState?: { didOpenOnce: boolean, apiKey?: string, maxRequestsPerTask?: number } } \ No newline at end of file diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index f4bc3ab..4939c02 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -1,5 +1,5 @@ export interface WebviewMessage { - type: "text" | "action" + type: "text" | "action" | "apiKey" | "maxRequestsPerTask" | "webviewDidLaunch" text?: string action?: "newTaskButtonTapped" | "yesButtonTapped" | "noButtonTapped" | "executeButtonTapped" } \ No newline at end of file diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index d6eb288..acd50b7 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -4,27 +4,70 @@ import "./App.css" import ChatSidebar from "./components/ChatSidebar" import SettingsView from "./components/SettingsView" import { ExtensionMessage } from "@shared/ExtensionMessage" +import WelcomeView from "./components/WelcomeView" +import { vscode } from "./utilities/vscode" + +/* +The contents of webviews however are created when the webview becomes visible and destroyed when the webview is moved into the background. Any state inside the webview will be lost when the webview is moved to a background tab. + +The best way to solve this is to make your webview stateless. Use message passing to save off the webview's state and then restore the state when the webview becomes visible again. + + +*/ const App: React.FC = () => { const [showSettings, setShowSettings] = useState(false) + const [showWelcome, setShowWelcome] = useState(false) + const [apiKey, setApiKey] = useState("") + const [maxRequestsPerTask, setMaxRequestsPerTask] = useState("") useEffect(() => { + vscode.postMessage({ type: "webviewDidLaunch" }) window.addEventListener("message", (e: MessageEvent) => { const message: ExtensionMessage = e.data - if (message.type === "action") { - switch (message.action!) { - case "settingsButtonTapped": - setShowSettings(true) - break - case "plusButtonTapped": - setShowSettings(false) - break - } + // switch message.type + switch (message.type) { + case "webviewState": + const shouldShowWelcome = !message.webviewState!.didOpenOnce || !message.webviewState!.apiKey + setShowWelcome(shouldShowWelcome) + setApiKey(message.webviewState!.apiKey || "") + setMaxRequestsPerTask( + message.webviewState!.maxRequestsPerTask !== undefined + ? message.webviewState!.maxRequestsPerTask.toString() + : "" + ) + break + case "action": + switch (message.action!) { + case "settingsButtonTapped": + setShowSettings(true) + break + case "plusButtonTapped": + setShowSettings(false) + break + } + break } }) }, []) - return <>{showSettings ? : } + return ( + <> + {showWelcome ? ( + + ) : showSettings ? ( + setShowSettings(false)} + /> + ) : ( + + )} + + ) } export default App diff --git a/webview-ui/src/components/SettingsView.tsx b/webview-ui/src/components/SettingsView.tsx index f190980..3236b9b 100644 --- a/webview-ui/src/components/SettingsView.tsx +++ b/webview-ui/src/components/SettingsView.tsx @@ -1,12 +1,69 @@ -import React from "react" +import React, { useEffect, useState } from "react" import { VSCodeTextField, VSCodeDivider, VSCodeLink, VSCodeButton } from "@vscode/webview-ui-toolkit/react" +import { vscode } from "../utilities/vscode" -const SettingsView = () => { - const handleDoneClick = () => { - // Add your logic here for what should happen when the Done button is clicked - console.log("Done button clicked") +type SettingsViewProps = { + apiKey: string + setApiKey: React.Dispatch> + maxRequestsPerTask: string + setMaxRequestsPerTask: React.Dispatch> + onDone: () => void // Define the type of the onDone prop +} + +const SettingsView = ({ apiKey, setApiKey, maxRequestsPerTask, setMaxRequestsPerTask, onDone }: SettingsViewProps) => { + const [apiKeyErrorMessage, setApiKeyErrorMessage] = useState(undefined) + const [maxRequestsErrorMessage, setMaxRequestsErrorMessage] = useState(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 }) + + onDone() + } + + // validate as soon as the component is mounted + useEffect(() => { + validateApiKey(apiKey) + validateMaxRequests(maxRequestsPerTask) + }, []) + return (
{ display: "flex", justifyContent: "space-between", alignItems: "center", - marginBottom: "20px", + marginBottom: "17px", }}> -

Settings

- Done +

Settings

+ + Done +
- + Anthropic API Key + {apiKeyErrorMessage && ( +

+ {apiKeyErrorMessage} +

+ )}

{

- + Maximum # Requests Per Task + {maxRequestsErrorMessage && ( +

+ {maxRequestsErrorMessage} +

+ )}

{ }}>

Made possible by the latest breakthroughs in Claude 3.5 Sonnet's agentic coding capabilities.

- This project was submitted to Anthropic's
"Build with Claude June 2024 contest" + This project was made for Anthropic's "Build with Claude June 2024 contest" https://github.com/saoudrizwan/claude-dev diff --git a/webview-ui/src/components/WelcomeView.tsx b/webview-ui/src/components/WelcomeView.tsx new file mode 100644 index 0000000..0c374e6 --- /dev/null +++ b/webview-ui/src/components/WelcomeView.tsx @@ -0,0 +1,91 @@ +import React, { useState, useEffect } from "react" +import { VSCodeButton, VSCodeTextField, VSCodeLink, VSCodeDivider } from "@vscode/webview-ui-toolkit/react" +import { vscode } from "../utilities/vscode" + +interface WelcomeViewProps { + apiKey: string + setApiKey: React.Dispatch> +} + +const WelcomeView: React.FC = ({ apiKey, setApiKey }) => { + const [apiKeyErrorMessage, setApiKeyErrorMessage] = useState(undefined) + + const disableLetsGoButton = apiKeyErrorMessage != 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 handleSubmit = () => { + vscode.postMessage({ type: "apiKey", text: apiKey }) + } + + useEffect(() => { + validateApiKey(apiKey) + }, []) + + return ( +

+

Hi, I'm Claude Dev

+

+ I can do all kinds of tasks thanks to the latest breakthroughs in Claude Sonnet 3.5's agentic coding + capabilities. I am prompted to think through tasks step-by-step and have access to tools that let me get + information about your project, read & write code, and execute terminal commands (with your permission, + of course). +

+ +

Here are some cool things I can do:

+
    +
  • Create new projects from scratch based on your requirements
  • +
  • Debug and fix code issues in your existing projects
  • +
  • Refactor and optimize your codebase
  • +
  • Analyze your system's performance and suggest improvements
  • +
  • Generate documentation for your code
  • +
  • Set up and configure development environments
  • +
  • Perform code reviews and suggest best practices
  • +
+ +

To get started, this extension needs an Anthropic API key:

+
    +
  1. + Go to{" "} + + https://console.anthropic.com/ + +
  2. +
  3. You may need to buy some credits (although Anthropic is offering $5 free credit for new users)
  4. +
  5. Click 'Get API Keys' and create a new key for me (you can delete it any time)
  6. +
+ + + +
+ + + Let's go! + +
+ +

+ Your API key is stored securely on your computer and used only for interacting with the Anthropic API. +

+
+ ) +} + +export default WelcomeView diff --git a/webview-ui/src/index.css b/webview-ui/src/index.css index 4140aac..bf8fc96 100644 --- a/webview-ui/src/index.css +++ b/webview-ui/src/index.css @@ -14,4 +14,8 @@ body { } textarea:focus { outline: 1.5px solid var(--vscode-focusBorder, #007fd4); -} \ No newline at end of file +} + +vscode-button::part(control):focus { + outline: none; +}