Add API for other extensions to interact with Claude Dev

This commit is contained in:
Saoud Rizwan
2024-09-22 10:42:05 -04:00
parent 3bd09e4e1b
commit d4f15de199
7 changed files with 220 additions and 40 deletions

View File

@@ -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
src/extension-api/claude-dev.d.ts vendored Normal file
View File

@@ -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>
}

View File

@@ -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
}

View File

@@ -3,6 +3,7 @@
import * as vscode from "vscode" import * as vscode from "vscode"
import { ClaudeDevProvider } from "./providers/ClaudeDevProvider" import { ClaudeDevProvider } from "./providers/ClaudeDevProvider"
import delay from "delay" import delay from "delay"
import { createClaudeDevAPI } from "./extension-api"
/* /*
Built using https://github.com/microsoft/vscode-webview-ui-toolkit 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 })) context.subscriptions.push(vscode.window.registerUriHandler({ handleUri }))
return createClaudeDevAPI(outputChannel, sidebarProvider)
} }
// This method is called when your extension is deactivated // This method is called when your extension is deactivated

View File

@@ -373,10 +373,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
await this.postStateToWebview() await this.postStateToWebview()
break break
case "customInstructions": case "customInstructions":
// User may be clearing the field await this.updateCustomInstructions(message.text)
await this.updateGlobalState("customInstructions", message.text || undefined)
this.claudeDev?.updateCustomInstructions(message.text || undefined)
await this.postStateToWebview()
break break
case "alwaysAllowReadOnly": case "alwaysAllowReadOnly":
await this.updateGlobalState("alwaysAllowReadOnly", message.bool ?? undefined) 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 // Ollama
async getOllamaModels(baseUrl?: string) { async getOllamaModels(baseUrl?: string) {
@@ -782,11 +786,11 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
// global // global
private async updateGlobalState(key: GlobalStateKey, value: any) { async updateGlobalState(key: GlobalStateKey, value: any) {
await this.context.globalState.update(key, value) await this.context.globalState.update(key, value)
} }
private async getGlobalState(key: GlobalStateKey) { async getGlobalState(key: GlobalStateKey) {
return await this.context.globalState.get(key) return await this.context.globalState.get(key)
} }

View File

@@ -5,9 +5,10 @@ import { HistoryItem } from "./HistoryItem"
// webview will hold state // webview will hold state
export interface ExtensionMessage { export interface ExtensionMessage {
type: "action" | "state" | "selectedImages" | "ollamaModels" | "theme" | "workspaceUpdated" type: "action" | "state" | "selectedImages" | "ollamaModels" | "theme" | "workspaceUpdated" | "invoke"
text?: string text?: string
action?: "chatButtonTapped" | "settingsButtonTapped" | "historyButtonTapped" | "didBecomeVisible" action?: "chatButtonTapped" | "settingsButtonTapped" | "historyButtonTapped" | "didBecomeVisible"
invoke?: "sendMessage" | "primaryButtonClick" | "secondaryButtonClick"
state?: ExtensionState state?: ExtensionState
images?: string[] images?: string[]
models?: string[] models?: string[]

View File

@@ -181,11 +181,12 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
} }
}, [messages.length]) }, [messages.length])
const handleSendMessage = useCallback(() => { const handleSendMessage = useCallback(
const text = inputValue.trim() (text: string, images: string[]) => {
if (text || selectedImages.length > 0) { text = text.trim()
if (text || images.length > 0) {
if (messages.length === 0) { if (messages.length === 0) {
vscode.postMessage({ type: "newTask", text, images: selectedImages }) vscode.postMessage({ type: "newTask", text, images })
} else if (claudeAsk) { } else if (claudeAsk) {
switch (claudeAsk) { switch (claudeAsk) {
case "followup": case "followup":
@@ -200,7 +201,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
type: "askResponse", type: "askResponse",
askResponse: "messageResponse", askResponse: "messageResponse",
text, text,
images: selectedImages, images,
}) })
break break
// there is no other case that a textfield should be enabled // there is no other case that a textfield should be enabled
@@ -214,7 +215,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
// setPrimaryButtonText(undefined) // setPrimaryButtonText(undefined)
// setSecondaryButtonText(undefined) // setSecondaryButtonText(undefined)
} }
}, [inputValue, selectedImages, messages.length, claudeAsk]) },
[messages.length, claudeAsk]
)
const startNewTask = useCallback(() => { const startNewTask = useCallback(() => {
vscode.postMessage({ type: "clearTask" }) vscode.postMessage({ type: "clearTask" })
@@ -301,6 +304,18 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
) )
} }
break 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. // 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} placeholderText={placeholderText}
selectedImages={selectedImages} selectedImages={selectedImages}
setSelectedImages={setSelectedImages} setSelectedImages={setSelectedImages}
onSend={handleSendMessage} onSend={() => handleSendMessage(inputValue, selectedImages)}
onSelectImages={selectImages} onSelectImages={selectImages}
shouldDisableImages={shouldDisableImages} shouldDisableImages={shouldDisableImages}
onHeightChange={() => { onHeightChange={() => {