Просмотр исходного кода

Add API for other extensions to interact with Claude Dev

Saoud Rizwan 1 год назад
Родитель
Сommit
d4f15de199

+ 55 - 0
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<ClaudeDevAPI>("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.

+ 37 - 0
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<void>
+
+	/**
+	 * Retrieves the custom instructions from the global storage.
+	 * @returns The saved custom instructions, or undefined if not set.
+	 */
+	getCustomInstructions(): Promise<string | undefined>
+
+	/**
+	 * 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<void>
+
+	/**
+	 * 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<void>
+
+	/**
+	 * Simulates pressing the primary button in the chat interface.
+	 */
+	pressPrimaryButton(): Promise<void>
+
+	/**
+	 * Simulates pressing the secondary button in the chat interface.
+	 */
+	pressSecondaryButton(): Promise<void>
+}

+ 65 - 0
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
+}

+ 3 - 0
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

+ 10 - 6
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)
 	}
 

+ 2 - 1
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[]

+ 48 - 33
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={() => {