Implement bidirectional extension webview messaging system; extension holds claude messages state to keep webview stateless

This commit is contained in:
Saoud Rizwan
2024-07-08 12:58:05 -04:00
parent 09559c314b
commit 4da785b822
9 changed files with 242 additions and 79 deletions

View File

@@ -1,8 +1,9 @@
import { Uri, Webview } from "vscode"
//import * as weather from "weather-js"
import * as vscode from "vscode"
import { ExtensionMessage } from "../shared/ExtensionMessage"
import { ClaudeMessage, ExtensionMessage } from "../shared/ExtensionMessage"
import { WebviewMessage } from "../shared/WebviewMessage"
import { ClaudeDev } from "../ClaudeDev"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -12,11 +13,13 @@ https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/c
type ExtensionSecretKey = "apiKey"
type ExtensionGlobalStateKey = "didOpenOnce" | "maxRequestsPerTask"
type ExtensionWorkspaceStateKey = "claudeMessages" | "apiConversationHistory"
export class SidebarProvider implements vscode.WebviewViewProvider {
public static readonly viewType = "claude-dev.SidebarProvider"
private _view?: vscode.WebviewView
private view?: vscode.WebviewView
private claudeDev?: ClaudeDev
constructor(private readonly context: vscode.ExtensionContext) {}
@@ -25,7 +28,7 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
context: vscode.WebviewViewResolveContext<unknown>,
token: vscode.CancellationToken
): void | Thenable<void> {
this._view = webviewView
this.view = webviewView
webviewView.webview.options = {
// Allow scripts in the webview
@@ -36,12 +39,22 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
// Sets up an event listener to listen for messages passed from the webview view context
// and executes code based on the message that is recieved
this._setWebviewMessageListener(webviewView.webview)
this.setWebviewMessageListener(webviewView.webview)
}
async tryToInitClaudeDevWithTask(task: string) {
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)
}
}
// Send any JSON serializable data to the react app
postMessageToWebview(message: ExtensionMessage) {
this._view?.webview.postMessage(message)
async postMessageToWebview(message: ExtensionMessage) {
await this.view?.webview.postMessage(message)
}
/**
@@ -131,24 +144,27 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
* @param webview A reference to the extension webview
* @param context A reference to the extension context
*/
private _setWebviewMessageListener(webview: vscode.Webview) {
private setWebviewMessageListener(webview: vscode.Webview) {
webview.onDidReceiveMessage(async (message: WebviewMessage) => {
switch (message.type) {
case "webviewDidLaunch":
await this.updateGlobalState("didOpenOnce", true)
await this.postStateToWebview()
break
case "text":
case "newTask":
// Code that should run in response to the hello message command
vscode.window.showInformationMessage(message.text!)
//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
this.postMessageToWebview({ type: "text", text: `Extension: ${Date.now()}` })
//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!)
break
case "apiKey":
await this.storeSecret("apiKey", message.text!)
this.claudeDev?.updateApiKey(message.text!)
await this.postStateToWebview()
break
case "maxRequestsPerTask":
@@ -160,25 +176,65 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
}
}
await this.updateGlobalState("maxRequestsPerTask", result)
this.claudeDev?.updateMaxRequestsPerTask(result)
await this.postStateToWebview()
break
case "askResponse":
this.claudeDev?.handleWebviewAskResponse(message.askResponse!, message.text)
// 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 postStateToWebview() {
const [didOpenOnce, apiKey, maxRequestsPerTask] = await Promise.all([
async postStateToWebview() {
const [didOpenOnce, apiKey, maxRequestsPerTask, claudeMessages] = 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.getClaudeMessages(),
])
this.postMessageToWebview({
type: "state",
state: { didOpenOnce: !!didOpenOnce, apiKey: apiKey, maxRequestsPerTask: maxRequestsPerTask },
state: { didOpenOnce: !!didOpenOnce, apiKey, maxRequestsPerTask, claudeMessages },
})
}
// client messages
async getClaudeMessages(): Promise<ClaudeMessage[]> {
const messages = (await this.getWorkspaceState("claudeMessages")) as ClaudeMessage[]
return messages || []
}
async setClaudeMessages(messages: ClaudeMessage[] | undefined) {
await this.updateWorkspaceState("claudeMessages", messages)
}
async addClaudeMessage(message: ClaudeMessage): Promise<ClaudeMessage[]> {
const messages = await this.getClaudeMessages()
messages.push(message)
await this.setClaudeMessages(messages)
return messages
}
// api conversation history
async getApiConversationHistory(): Promise<ClaudeMessage[]> {
const messages = (await this.getWorkspaceState("apiConversationHistory")) as ClaudeMessage[]
return messages || []
}
async setApiConversationHistory(messages: ClaudeMessage[] | undefined) {
await this.updateWorkspaceState("apiConversationHistory", messages)
}
async addMessageToApiConversationHistory(message: ClaudeMessage): Promise<ClaudeMessage[]> {
const messages = await this.getClaudeMessages()
messages.push(message)
await this.setClaudeMessages(messages)
return messages
}
/*
Storage
@@ -186,6 +242,8 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
https://www.eliostruyf.com/devhack-code-extension-storage-options/
*/
// global
private async updateGlobalState(key: ExtensionGlobalStateKey, value: any) {
await this.context.globalState.update(key, value)
}
@@ -194,6 +252,18 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
return await this.context.globalState.get(key)
}
// workspace
private async updateWorkspaceState(key: ExtensionWorkspaceStateKey, value: any) {
await this.context.workspaceState.update(key, value)
}
private async getWorkspaceState(key: ExtensionWorkspaceStateKey) {
return await this.context.workspaceState.get(key)
}
// secrets
private async storeSecret(key: ExtensionSecretKey, value: any) {
await this.context.secrets.store(key, value)
}