From d4f15de1998de2fab588d59c0acb2b24d53ea8e5 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Sun, 22 Sep 2024 10:42:05 -0400 Subject: [PATCH] Add API for other extensions to interact with Claude Dev --- src/extension-api/README.md | 55 +++++++++++++++++ src/extension-api/claude-dev.d.ts | 37 ++++++++++++ src/extension-api/index.ts | 65 +++++++++++++++++++++ src/extension.ts | 3 + src/providers/ClaudeDevProvider.ts | 16 +++-- src/shared/ExtensionMessage.ts | 3 +- webview-ui/src/components/ChatView.tsx | 81 +++++++++++++++----------- 7 files changed, 220 insertions(+), 40 deletions(-) create mode 100644 src/extension-api/README.md create mode 100644 src/extension-api/claude-dev.d.ts create mode 100644 src/extension-api/index.ts diff --git a/src/extension-api/README.md b/src/extension-api/README.md new file mode 100644 index 0000000..fba8d2e --- /dev/null +++ b/src/extension-api/README.md @@ -0,0 +1,55 @@ +# Claude Dev API + +The Claude Dev extension exposes an API that can be used by other extensions. To use this API in your extension: + +1. Copy `src/extension-api/claude-dev.d.ts` to your extension's source directory. +2. Include `claude-dev.d.ts` in your extension's compilation. +3. Get access to the API with the following code: + + ```ts + const claudeDevExtension = vscode.extensions.getExtension("saoudrizwan.claude-dev") + + if (!claudeDevExtension?.isActive) { + throw new Error("Claude Dev extension is not activated") + } + + const claudeDev = claudeDevExtension.exports + + if (claudeDev) { + // Now you can use the API + + // Set custom instructions + await claudeDev.setCustomInstructions("Talk like a pirate") + + // Get custom instructions + const instructions = await claudeDev.getCustomInstructions() + console.log("Current custom instructions:", instructions) + + // Start a new task with an initial message + await claudeDev.startNewTask("Hello, Claude! Let's make a new project...") + + // Start a new task with an initial message and images + await claudeDev.startNewTask("Use this design language", ["data:image/webp;base64,..."]) + + // Send a message to the current task + await claudeDev.sendMessage("Can you fix the @problems?") + + // Simulate pressing the primary button in the chat interface (e.g. 'Save' or 'Proceed While Running') + await claudeDev.pressPrimaryButton() + + // Simulate pressing the secondary button in the chat interface (e.g. 'Reject') + await claudeDev.pressSecondaryButton() + } else { + console.error("Claude Dev API is not available") + } + ``` + + **Note:** To ensure that the `saoudrizwan.claude-dev` extension is activated before your extension, add it to the `extensionDependencies` in your `package.json`: + + ```json + "extensionDependencies": [ + "saoudrizwan.claude-dev" + ] + ``` + +For detailed information on the available methods and their usage, refer to the `claude-dev.d.ts` file. diff --git a/src/extension-api/claude-dev.d.ts b/src/extension-api/claude-dev.d.ts new file mode 100644 index 0000000..679a323 --- /dev/null +++ b/src/extension-api/claude-dev.d.ts @@ -0,0 +1,37 @@ +export interface ClaudeDevAPI { + /** + * Sets the custom instructions in the global storage. + * @param value The custom instructions to be saved. + */ + setCustomInstructions(value: string): Promise + + /** + * Retrieves the custom instructions from the global storage. + * @returns The saved custom instructions, or undefined if not set. + */ + getCustomInstructions(): Promise + + /** + * Starts a new task with an optional initial message and images. + * @param task Optional initial task message. + * @param images Optional array of image data URIs (e.g., "data:image/webp;base64,..."). + */ + startNewTask(task?: string, images?: string[]): Promise + + /** + * Sends a message to the current task. + * @param message Optional message to send. + * @param images Optional array of image data URIs (e.g., "data:image/webp;base64,..."). + */ + sendMessage(message?: string, images?: string[]): Promise + + /** + * Simulates pressing the primary button in the chat interface. + */ + pressPrimaryButton(): Promise + + /** + * Simulates pressing the secondary button in the chat interface. + */ + pressSecondaryButton(): Promise +} diff --git a/src/extension-api/index.ts b/src/extension-api/index.ts new file mode 100644 index 0000000..225ebb4 --- /dev/null +++ b/src/extension-api/index.ts @@ -0,0 +1,65 @@ +import * as vscode from "vscode" +import { ClaudeDevProvider } from "../providers/ClaudeDevProvider" +import { ClaudeDevAPI } from "./claude-dev" + +export function createClaudeDevAPI( + outputChannel: vscode.OutputChannel, + sidebarProvider: ClaudeDevProvider +): ClaudeDevAPI { + const api: ClaudeDevAPI = { + setCustomInstructions: async (value: string) => { + await sidebarProvider.updateCustomInstructions(value) + outputChannel.appendLine("Custom instructions set") + }, + + getCustomInstructions: async () => { + return (await sidebarProvider.getGlobalState("customInstructions")) as string | undefined + }, + + startNewTask: async (task?: string, images?: string[]) => { + outputChannel.appendLine("Starting new task") + await sidebarProvider.clearTask() + await sidebarProvider.postStateToWebview() + await sidebarProvider.postMessageToWebview({ type: "action", action: "chatButtonTapped" }) + await sidebarProvider.postMessageToWebview({ + type: "invoke", + invoke: "sendMessage", + text: task, + images: images, + }) + outputChannel.appendLine( + `Task started with message: ${task ? `"${task}"` : "undefined"} and ${images?.length || 0} image(s)` + ) + }, + + sendMessage: async (message?: string, images?: string[]) => { + outputChannel.appendLine( + `Sending message: ${message ? `"${message}"` : "undefined"} with ${images?.length || 0} image(s)` + ) + await sidebarProvider.postMessageToWebview({ + type: "invoke", + invoke: "sendMessage", + text: message, + images: images, + }) + }, + + pressPrimaryButton: async () => { + outputChannel.appendLine("Pressing primary button") + await sidebarProvider.postMessageToWebview({ + type: "invoke", + invoke: "primaryButtonClick", + }) + }, + + pressSecondaryButton: async () => { + outputChannel.appendLine("Pressing secondary button") + await sidebarProvider.postMessageToWebview({ + type: "invoke", + invoke: "secondaryButtonClick", + }) + }, + } + + return api +} diff --git a/src/extension.ts b/src/extension.ts index a73b861..3a3a5ce 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,6 +3,7 @@ import * as vscode from "vscode" import { ClaudeDevProvider } from "./providers/ClaudeDevProvider" import delay from "delay" +import { createClaudeDevAPI } from "./extension-api" /* Built using https://github.com/microsoft/vscode-webview-ui-toolkit @@ -141,6 +142,8 @@ export function activate(context: vscode.ExtensionContext) { } } context.subscriptions.push(vscode.window.registerUriHandler({ handleUri })) + + return createClaudeDevAPI(outputChannel, sidebarProvider) } // This method is called when your extension is deactivated diff --git a/src/providers/ClaudeDevProvider.ts b/src/providers/ClaudeDevProvider.ts index 062b384..dbadf33 100644 --- a/src/providers/ClaudeDevProvider.ts +++ b/src/providers/ClaudeDevProvider.ts @@ -373,10 +373,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider { await this.postStateToWebview() break case "customInstructions": - // User may be clearing the field - await this.updateGlobalState("customInstructions", message.text || undefined) - this.claudeDev?.updateCustomInstructions(message.text || undefined) - await this.postStateToWebview() + await this.updateCustomInstructions(message.text) break case "alwaysAllowReadOnly": await this.updateGlobalState("alwaysAllowReadOnly", message.bool ?? undefined) @@ -439,6 +436,13 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider { ) } + async updateCustomInstructions(instructions?: string) { + // User may be clearing the field + await this.updateGlobalState("customInstructions", instructions || undefined) + this.claudeDev?.updateCustomInstructions(instructions || undefined) + await this.postStateToWebview() + } + // Ollama async getOllamaModels(baseUrl?: string) { @@ -782,11 +786,11 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider { // global - private async updateGlobalState(key: GlobalStateKey, value: any) { + async updateGlobalState(key: GlobalStateKey, value: any) { await this.context.globalState.update(key, value) } - private async getGlobalState(key: GlobalStateKey) { + async getGlobalState(key: GlobalStateKey) { return await this.context.globalState.get(key) } diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index c3bb0ce..4a92839 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -5,9 +5,10 @@ import { HistoryItem } from "./HistoryItem" // webview will hold state export interface ExtensionMessage { - type: "action" | "state" | "selectedImages" | "ollamaModels" | "theme" | "workspaceUpdated" + type: "action" | "state" | "selectedImages" | "ollamaModels" | "theme" | "workspaceUpdated" | "invoke" text?: string action?: "chatButtonTapped" | "settingsButtonTapped" | "historyButtonTapped" | "didBecomeVisible" + invoke?: "sendMessage" | "primaryButtonClick" | "secondaryButtonClick" state?: ExtensionState images?: string[] models?: string[] diff --git a/webview-ui/src/components/ChatView.tsx b/webview-ui/src/components/ChatView.tsx index b03fffe..4b4f97b 100644 --- a/webview-ui/src/components/ChatView.tsx +++ b/webview-ui/src/components/ChatView.tsx @@ -181,40 +181,43 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie } }, [messages.length]) - const handleSendMessage = useCallback(() => { - const text = inputValue.trim() - if (text || selectedImages.length > 0) { - if (messages.length === 0) { - vscode.postMessage({ type: "newTask", text, images: selectedImages }) - } else if (claudeAsk) { - switch (claudeAsk) { - case "followup": - case "tool": - case "command": // user can provide feedback to a tool or command use - case "command_output": // user can send input to command stdin - case "completion_result": // if this happens then the user has feedback for the completion result - case "resume_task": - case "resume_completed_task": - case "mistake_limit_reached": - vscode.postMessage({ - type: "askResponse", - askResponse: "messageResponse", - text, - images: selectedImages, - }) - break - // there is no other case that a textfield should be enabled + const handleSendMessage = useCallback( + (text: string, images: string[]) => { + text = text.trim() + if (text || images.length > 0) { + if (messages.length === 0) { + vscode.postMessage({ type: "newTask", text, images }) + } else if (claudeAsk) { + switch (claudeAsk) { + case "followup": + case "tool": + case "command": // user can provide feedback to a tool or command use + case "command_output": // user can send input to command stdin + case "completion_result": // if this happens then the user has feedback for the completion result + case "resume_task": + case "resume_completed_task": + case "mistake_limit_reached": + vscode.postMessage({ + type: "askResponse", + askResponse: "messageResponse", + text, + images, + }) + break + // there is no other case that a textfield should be enabled + } } + setInputValue("") + setTextAreaDisabled(true) + setSelectedImages([]) + setClaudeAsk(undefined) + setEnableButtons(false) + // setPrimaryButtonText(undefined) + // setSecondaryButtonText(undefined) } - setInputValue("") - setTextAreaDisabled(true) - setSelectedImages([]) - setClaudeAsk(undefined) - setEnableButtons(false) - // setPrimaryButtonText(undefined) - // setSecondaryButtonText(undefined) - } - }, [inputValue, selectedImages, messages.length, claudeAsk]) + }, + [messages.length, claudeAsk] + ) const startNewTask = useCallback(() => { vscode.postMessage({ type: "clearTask" }) @@ -301,6 +304,18 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie ) } break + case "invoke": + switch (message.invoke!) { + case "sendMessage": + handleSendMessage(message.text ?? "", message.images ?? []) + break + case "primaryButtonClick": + handlePrimaryButtonClick() + break + case "secondaryButtonClick": + handleSecondaryButtonClick() + break + } } // textAreaRef.current is not explicitly required here since react gaurantees that ref will be stable across re-renders, and we're not using its value but its reference. }, @@ -545,7 +560,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie placeholderText={placeholderText} selectedImages={selectedImages} setSelectedImages={setSelectedImages} - onSend={handleSendMessage} + onSend={() => handleSendMessage(inputValue, selectedImages)} onSelectImages={selectImages} shouldDisableImages={shouldDisableImages} onHeightChange={() => {