From ce71ed7cba85782b43083ee6c7c630a5ee9b9d5d Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Tue, 3 Sep 2024 04:49:44 -0400 Subject: [PATCH] Add oauth button to openrouter --- src/extension.ts | 32 +++++++++---- src/providers/ClaudeDevProvider.ts | 39 +++++++++++++++- webview-ui/src/components/Announcement.tsx | 53 ++++++++++++++++++---- webview-ui/src/components/ApiOptions.tsx | 49 ++++++++++++-------- 4 files changed, 134 insertions(+), 39 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 700a86e..432dabb 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -109,16 +109,28 @@ export function activate(context: vscode.ExtensionContext) { vscode.workspace.registerTextDocumentContentProvider("claude-dev-diff", diffContentProvider) ) - // // URI Handler - // const handleUri = async (uri: vscode.Uri) => { - // const query = new URLSearchParams(uri.query.replace(/\+/g, "%2B")) - // const token = query.get("token") - // const email = query.get("email") - // if (token) { - // await sidebarProvider.saveKoduApiKey(token, email || undefined) - // } - // } - // context.subscriptions.push(vscode.window.registerUriHandler({ handleUri })) + // URI Handler + const handleUri = async (uri: vscode.Uri) => { + console.log("handleUri", uri) + const path = uri.path + const query = new URLSearchParams(uri.query.replace(/\+/g, "%2B")) + const visibleProvider = ClaudeDevProvider.getVisibleInstance() + if (!visibleProvider) { + return + } + switch (path) { + case "/openrouter": { + const code = query.get("code") + if (code) { + await visibleProvider.handleOpenRouterCallback(code) + } + break + } + default: + break + } + } + context.subscriptions.push(vscode.window.registerUriHandler({ handleUri })) } // This method is called when your extension is deactivated diff --git a/src/providers/ClaudeDevProvider.ts b/src/providers/ClaudeDevProvider.ts index ca2c275..5b3c2a8 100644 --- a/src/providers/ClaudeDevProvider.ts +++ b/src/providers/ClaudeDevProvider.ts @@ -4,10 +4,11 @@ import { ClaudeDev } from "../ClaudeDev" import { ApiModelId, ApiProvider } from "../shared/api" import { ExtensionMessage } from "../shared/ExtensionMessage" import { WebviewMessage } from "../shared/WebviewMessage" -import { downloadTask, getNonce, getUri, selectImages } from "../utils" +import { downloadTask, findLast, getNonce, getUri, selectImages } from "../utils" import * as path from "path" import fs from "fs/promises" import { HistoryItem } from "../shared/HistoryItem" +import axios from "axios" /* https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts @@ -30,6 +31,7 @@ type GlobalStateKey = export class ClaudeDevProvider implements vscode.WebviewViewProvider { public static readonly sideBarId = "claude-dev.SidebarProvider" // used in package.json as the view's id. This value cannot be changed due to how vscode caches views based on their id, and updating the id would break existing instances of the extension. public static readonly tabPanelId = "claude-dev.TabPanelProvider" + private static activeInstances: Set = new Set() private disposables: vscode.Disposable[] = [] private view?: vscode.WebviewView | vscode.WebviewPanel private claudeDev?: ClaudeDev @@ -37,6 +39,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider { constructor(readonly context: vscode.ExtensionContext, private readonly outputChannel: vscode.OutputChannel) { this.outputChannel.appendLine("ClaudeDevProvider instantiated") + ClaudeDevProvider.activeInstances.add(this) this.revertKodu() } @@ -81,6 +84,11 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider { } } this.outputChannel.appendLine("Disposed all disposables") + ClaudeDevProvider.activeInstances.delete(this) + } + + public static getVisibleInstance(): ClaudeDevProvider | undefined { + return findLast(Array.from(this.activeInstances), (instance) => instance.view?.visible === true) } resolveWebviewView( @@ -373,6 +381,33 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider { ) } + // OpenRouter + + async handleOpenRouterCallback(code: string) { + console.log("handleOpenRouterCallback", code) + let apiKey: string + try { + const response = await axios.post("https://openrouter.ai/api/v1/auth/keys", { code }) + console.log("OpenRouter API response:", response.data) + + if (response.data && response.data.key) { + apiKey = response.data.key + } else { + throw new Error("Invalid response from OpenRouter API") + } + } catch (error) { + console.error("Error exchanging code for API key:", error) + throw error + } + + const openrouter: ApiProvider = "openrouter" + await this.updateGlobalState("apiProvider", openrouter) + await this.storeSecret("openRouterApiKey", apiKey) + await this.postStateToWebview() + this.claudeDev?.updateApi({ apiProvider: openrouter, openRouterApiKey: apiKey }) + await this.postMessageToWebview({ type: "action", action: "settingsButtonTapped" }) + } + // Task history async getTaskWithId(id: string): Promise<{ @@ -606,7 +641,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider { if (apiKey) { apiProvider = "anthropic" } else { - // New users should default to anthropic + // New users should default to anthropic for now, but will change to openrouter after fast edit mode apiProvider = "anthropic" } } diff --git a/webview-ui/src/components/Announcement.tsx b/webview-ui/src/components/Announcement.tsx index 3f5b6cc..2d47aa8 100644 --- a/webview-ui/src/components/Announcement.tsx +++ b/webview-ui/src/components/Announcement.tsx @@ -1,5 +1,8 @@ import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react" import { ApiConfiguration } from "../../../src/shared/api" +// import VSCodeButtonLink from "./VSCodeButtonLink" +// import { getOpenRouterAuthUrl } from "./ApiOptions" +// import { vscode } from "../utils/vscode" interface AnnouncementProps { version: string @@ -30,23 +33,55 @@ const Announcement = ({ version, hideAnnouncement, apiConfiguration, vscodeUriSc 🎉{" "}New in v{version}

Follow me for more updates!{" "} diff --git a/webview-ui/src/components/ApiOptions.tsx b/webview-ui/src/components/ApiOptions.tsx index 3a019e7..14bf84b 100644 --- a/webview-ui/src/components/ApiOptions.tsx +++ b/webview-ui/src/components/ApiOptions.tsx @@ -14,6 +14,7 @@ import { vertexModels, } from "../../../src/shared/api" import { useExtensionState } from "../context/ExtensionStateContext" +import VSCodeButtonLink from "./VSCodeButtonLink" interface ApiOptionsProps { showModelOptions: boolean @@ -21,7 +22,7 @@ interface ApiOptionsProps { } const ApiOptions: React.FC = ({ showModelOptions, apiErrorMessage }) => { - const { apiConfiguration, setApiConfiguration } = useExtensionState() + const { apiConfiguration, setApiConfiguration, uriScheme } = useExtensionState() const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => { setApiConfiguration({ ...apiConfiguration, [field]: event.target.value }) } @@ -70,8 +71,8 @@ const ApiOptions: React.FC = ({ showModelOptions, apiErrorMessa Anthropic - AWS Bedrock OpenRouter + AWS Bedrock GCP Vertex AI @@ -93,9 +94,11 @@ const ApiOptions: React.FC = ({ showModelOptions, apiErrorMessa color: "var(--vscode-descriptionForeground)", }}> This key is stored locally and only used to make API requests from this extension. - - You can get an Anthropic API key by signing up here. - + {!apiConfiguration?.apiKey && ( + + You can get an Anthropic API key by signing up here. + + )}

)} @@ -110,20 +113,24 @@ const ApiOptions: React.FC = ({ showModelOptions, apiErrorMessa placeholder="Enter API Key..."> OpenRouter API Key + {!apiConfiguration?.openRouterApiKey && ( + + Get OpenRouter API Key + + )}

- This key is stored locally and only used to make API requests from this extension. - - You can get an OpenRouter API key by signing up here. - {" "} - - (Note: OpenRouter support is experimental and may - not work well with large files.) - + This key is stored locally and only used to make API requests from this extension.{" "} + {/* {!apiConfiguration?.openRouterApiKey && ( + + (Note: OpenRouter is recommended for high rate + limits, prompt caching, and wider selection of models.) + + )} */}

)} @@ -186,11 +193,13 @@ const ApiOptions: React.FC = ({ showModelOptions, apiErrorMessa color: "var(--vscode-descriptionForeground)", }}> These credentials are stored locally and only used to make API requests from this extension. - - You can find your AWS access key and secret key here. - + {!(apiConfiguration?.awsAccessKey && apiConfiguration?.awsSecretKey) && ( + + You can find your AWS access key and secret key here. + + )}

)} @@ -274,6 +283,10 @@ const ApiOptions: React.FC = ({ showModelOptions, apiErrorMessa ) } +export function getOpenRouterAuthUrl(uriScheme?: string) { + return `https://openrouter.ai/auth?callback_url=${uriScheme || "vscode"}://saoudrizwan.claude-dev/openrouter` +} + export const formatPrice = (price: number) => { return new Intl.NumberFormat("en-US", { style: "currency",