mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
Add secret and global state management + messaging between web and extension; persist API key and max num requests; add Welcome screen
This commit is contained in:
BIN
assets/icon.png
BIN
assets/icon.png
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -17,7 +17,7 @@
|
||||
{
|
||||
"id": "claude-dev-ActivityBar",
|
||||
"title": "Claude Dev",
|
||||
"icon": "assets/icon.png"
|
||||
"icon": "$(robot)"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -119,8 +133,12 @@ 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!)
|
||||
@@ -129,10 +147,59 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
|
||||
// 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<boolean | undefined>,
|
||||
this.getSecret("apiKey") as Promise<string | undefined>,
|
||||
this.getGlobalState("maxRequestsPerTask") as Promise<number | undefined>,
|
||||
])
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
export interface WebviewMessage {
|
||||
type: "text" | "action"
|
||||
type: "text" | "action" | "apiKey" | "maxRequestsPerTask" | "webviewDidLaunch"
|
||||
text?: string
|
||||
action?: "newTaskButtonTapped" | "yesButtonTapped" | "noButtonTapped" | "executeButtonTapped"
|
||||
}
|
||||
@@ -4,14 +4,40 @@ 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<boolean>(false)
|
||||
const [apiKey, setApiKey] = useState<string>("")
|
||||
const [maxRequestsPerTask, setMaxRequestsPerTask] = useState<string>("")
|
||||
|
||||
useEffect(() => {
|
||||
vscode.postMessage({ type: "webviewDidLaunch" })
|
||||
window.addEventListener("message", (e: MessageEvent) => {
|
||||
const message: ExtensionMessage = e.data
|
||||
if (message.type === "action") {
|
||||
// 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)
|
||||
@@ -20,11 +46,28 @@ const App: React.FC = () => {
|
||||
setShowSettings(false)
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
return <>{showSettings ? <SettingsView /> : <ChatSidebar />}</>
|
||||
return (
|
||||
<>
|
||||
{showWelcome ? (
|
||||
<WelcomeView apiKey={apiKey} setApiKey={setApiKey} />
|
||||
) : showSettings ? (
|
||||
<SettingsView
|
||||
apiKey={apiKey}
|
||||
setApiKey={setApiKey}
|
||||
maxRequestsPerTask={maxRequestsPerTask}
|
||||
setMaxRequestsPerTask={setMaxRequestsPerTask}
|
||||
onDone={() => setShowSettings(false)}
|
||||
/>
|
||||
) : (
|
||||
<ChatSidebar />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
||||
@@ -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<React.SetStateAction<string>>
|
||||
maxRequestsPerTask: string
|
||||
setMaxRequestsPerTask: React.Dispatch<React.SetStateAction<string>>
|
||||
onDone: () => void // Define the type of the onDone prop
|
||||
}
|
||||
|
||||
const SettingsView = ({ apiKey, setApiKey, maxRequestsPerTask, setMaxRequestsPerTask, onDone }: SettingsViewProps) => {
|
||||
const [apiKeyErrorMessage, setApiKeyErrorMessage] = 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 })
|
||||
|
||||
onDone()
|
||||
}
|
||||
|
||||
// validate as soon as the component is mounted
|
||||
useEffect(() => {
|
||||
validateApiKey(apiKey)
|
||||
validateMaxRequests(maxRequestsPerTask)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div style={{ margin: "0 auto", paddingTop: "10px" }}>
|
||||
<div
|
||||
@@ -14,16 +71,32 @@ const SettingsView = () => {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: "20px",
|
||||
marginBottom: "17px",
|
||||
}}>
|
||||
<h2 style={{ color: "var(--vscode-foreground)", margin: 0 }}>Settings</h2>
|
||||
<VSCodeButton onClick={handleDoneClick}>Done</VSCodeButton>
|
||||
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>Settings</h3>
|
||||
<VSCodeButton onClick={handleSubmit} disabled={disableDoneButton}>
|
||||
Done
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: "20px" }}>
|
||||
<VSCodeTextField style={{ width: "100%" }} placeholder="Enter your Anthropic API Key">
|
||||
<VSCodeTextField
|
||||
value={apiKey}
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Enter your Anthropic API Key"
|
||||
onInput={handleApiKeyChange}>
|
||||
Anthropic API Key
|
||||
</VSCodeTextField>
|
||||
{apiKeyErrorMessage && (
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: "5px",
|
||||
color: "var(--vscode-errorForeground)",
|
||||
}}>
|
||||
{apiKeyErrorMessage}
|
||||
</p>
|
||||
)}
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
@@ -38,9 +111,23 @@ const SettingsView = () => {
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: "20px" }}>
|
||||
<VSCodeTextField style={{ width: "100%" }} placeholder="Enter maximum number of requests">
|
||||
<VSCodeTextField
|
||||
value={maxRequestsPerTask}
|
||||
style={{ width: "100%" }}
|
||||
placeholder="20"
|
||||
onInput={handleMaxRequestsChange}>
|
||||
Maximum # Requests Per Task
|
||||
</VSCodeTextField>
|
||||
{maxRequestsErrorMessage && (
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: "5px",
|
||||
color: "var(--vscode-errorForeground)",
|
||||
}}>
|
||||
{maxRequestsErrorMessage}
|
||||
</p>
|
||||
)}
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
@@ -65,7 +152,7 @@ const SettingsView = () => {
|
||||
}}>
|
||||
<p>Made possible by the latest breakthroughs in Claude 3.5 Sonnet's agentic coding capabilities.</p>
|
||||
<p>
|
||||
This project was submitted to Anthropic's<br/>"Build with Claude June 2024 contest"
|
||||
This project was made for Anthropic's "Build with Claude June 2024 contest"
|
||||
<VSCodeLink href="https://github.com/saoudrizwan/claude-dev">
|
||||
https://github.com/saoudrizwan/claude-dev
|
||||
</VSCodeLink>
|
||||
|
||||
91
webview-ui/src/components/WelcomeView.tsx
Normal file
91
webview-ui/src/components/WelcomeView.tsx
Normal file
@@ -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<React.SetStateAction<string>>
|
||||
}
|
||||
|
||||
const WelcomeView: React.FC<WelcomeViewProps> = ({ apiKey, setApiKey }) => {
|
||||
const [apiKeyErrorMessage, setApiKeyErrorMessage] = useState<string | undefined>(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 (
|
||||
<div style={{ maxWidth: "600px", margin: "0 auto", padding: "20px" }}>
|
||||
<h1 style={{ color: "var(--vscode-foreground)" }}>Hi, I'm Claude Dev</h1>
|
||||
<p>
|
||||
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).
|
||||
</p>
|
||||
|
||||
<h3>Here are some cool things I can do:</h3>
|
||||
<ul>
|
||||
<li>Create new projects from scratch based on your requirements</li>
|
||||
<li>Debug and fix code issues in your existing projects</li>
|
||||
<li>Refactor and optimize your codebase</li>
|
||||
<li>Analyze your system's performance and suggest improvements</li>
|
||||
<li>Generate documentation for your code</li>
|
||||
<li>Set up and configure development environments</li>
|
||||
<li>Perform code reviews and suggest best practices</li>
|
||||
</ul>
|
||||
|
||||
<h3>To get started, this extension needs an Anthropic API key:</h3>
|
||||
<ol>
|
||||
<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 for me (you can delete it any time)</li>
|
||||
</ol>
|
||||
|
||||
<VSCodeDivider />
|
||||
|
||||
<div style={{ marginTop: "20px", display: "flex", alignItems: "center" }}>
|
||||
<VSCodeTextField
|
||||
style={{ flexGrow: 1, marginRight: "10px" }}
|
||||
placeholder="Enter your Anthropic API Key"
|
||||
value={apiKey}
|
||||
onInput={handleApiKeyChange}
|
||||
/>
|
||||
<VSCodeButton onClick={handleSubmit} disabled={disableLetsGoButton}>
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
export default WelcomeView
|
||||
@@ -15,3 +15,7 @@ body {
|
||||
textarea:focus {
|
||||
outline: 1.5px solid var(--vscode-focusBorder, #007fd4);
|
||||
}
|
||||
|
||||
vscode-button::part(control):focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user