diff --git a/package-lock.json b/package-lock.json index 7d76abdf8f..fafef5e50a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "claude-dev", - "version": "1.3.43", + "version": "1.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-dev", - "version": "1.3.43", + "version": "1.4.0", "license": "MIT", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.26.0", + "@kodu-ai/cloud-api": "^1.0.1", "@vscode/codicons": "^0.0.36", "axios": "^1.7.4", "default-shell": "^2.2.0", @@ -2768,6 +2769,16 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kodu-ai/cloud-api": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@kodu-ai/cloud-api/-/cloud-api-1.0.1.tgz", + "integrity": "sha512-CnmZP4Gm72ReqSO8qZgRYgbuGbo6A7RBbG/kNjMSS9KsDmtXCQu8OIEj/ZBVdaW+vm32t633r7q4nLKv3SaUHQ==", + "dependencies": { + "@trpc/client": "^11.0.0-rc.485", + "@trpc/server": "^11.0.0-rc.485", + "superjson": "^2.2.1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -4480,6 +4491,25 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, + "node_modules/@trpc/client": { + "version": "11.0.0-rc.485", + "resolved": "https://registry.npmjs.org/@trpc/client/-/client-11.0.0-rc.485.tgz", + "integrity": "sha512-Ld1gQjdYyrku0rjP/4QMg/SdsKgujr0P5XNoWkCyPRjdw3PuJbZFebauQPRC17cbbqGcpJrR+T3vnkhjMb1sgw==", + "funding": [ + "https://trpc.io/sponsor" + ], + "peerDependencies": { + "@trpc/server": "11.0.0-rc.485+1c1d824cd" + } + }, + "node_modules/@trpc/server": { + "version": "11.0.0-rc.485", + "resolved": "https://registry.npmjs.org/@trpc/server/-/server-11.0.0-rc.485.tgz", + "integrity": "sha512-U9SK9jbqCjR8S9wGSe4UBu2e0fqxhQWriZiDb5BLzdxXzls4Jv+XhAkI65yBzlcTbt6VqXegZDAXB3IARPhUCg==", + "funding": [ + "https://trpc.io/sponsor" + ] + }, "node_modules/@types/diff": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.2.1.tgz", @@ -5399,6 +5429,20 @@ "dev": true, "license": "MIT" }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -7199,6 +7243,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -9318,6 +9373,17 @@ "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" }, + "node_modules/superjson": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.1.tgz", + "integrity": "sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA==", + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/supports-color": { "version": "9.4.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", diff --git a/package.json b/package.json index 87acaf959e..865c93306b 100644 --- a/package.json +++ b/package.json @@ -134,6 +134,7 @@ "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.26.0", + "@kodu-ai/cloud-api": "^1.0.1", "@vscode/codicons": "^0.0.36", "axios": "^1.7.4", "default-shell": "^2.2.0", diff --git a/src/api/index.ts b/src/api/index.ts index 0e8f943d8e..f50d43b271 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -3,7 +3,7 @@ import { ApiConfiguration, ApiModelId, ModelInfo } from "../shared/api" import { AnthropicHandler } from "./anthropic" import { AwsBedrockHandler } from "./bedrock" import { OpenRouterHandler } from "./openrouter" -import { MaestroHandler } from "./maestro" +import { KoduHandler } from "./kodu" export interface ApiHandler { createMessage( @@ -33,8 +33,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler { return new OpenRouterHandler(options) case "bedrock": return new AwsBedrockHandler(options) - case "maestro": - return new MaestroHandler(options) + case "kodu": + return new KoduHandler(options) default: return new AnthropicHandler(options) } diff --git a/src/api/kodu.ts b/src/api/kodu.ts new file mode 100644 index 0000000000..2eb49a7af7 --- /dev/null +++ b/src/api/kodu.ts @@ -0,0 +1,125 @@ +import { Anthropic } from "@anthropic-ai/sdk" +import { ApiHandler, withoutImageData } from "." +import { ApiHandlerOptions, koduDefaultModelId, KoduModelId, koduModels, ModelInfo } from "../shared/api" +import axios from "axios" +import * as vscode from "vscode" + +const KODU_BASE_URL = "https://claude-dev.com" + +export function didClickKoduSignIn() { + const loginUrl = `${KODU_BASE_URL}/auth/login?redirectTo=${vscode.env.uriScheme}://saoudrizwan.claude-dev&ext=1` + vscode.env.openExternal(vscode.Uri.parse(loginUrl)) +} + +export function didClickKoduAddCredits() { + const addCreditsUrl = `${KODU_BASE_URL}/user/addCredits?redirectTo=${vscode.env.uriScheme}://saoudrizwan.claude-dev&ext=1` + vscode.env.openExternal(vscode.Uri.parse(addCreditsUrl)) +} + +export async function fetchKoduCredits({ apiKey }: { apiKey: string }) { + const response = await axios.get(`${KODU_BASE_URL}/api/credits`, { + headers: { + "x-api-key": apiKey, + }, + }) + return (response.data.credits as number) || 0 +} + +export class KoduHandler implements ApiHandler { + private options: ApiHandlerOptions + + constructor(options: ApiHandlerOptions) { + this.options = options + } + + async createMessage( + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + tools: Anthropic.Messages.Tool[] + ): Promise { + const modelId = this.getModel().id + let requestBody: Anthropic.Beta.PromptCaching.Messages.MessageCreateParamsNonStreaming + switch (modelId) { + case "claude-3-5-sonnet-20240620": + case "claude-3-haiku-20240307": + const userMsgIndices = messages.reduce( + (acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc), + [] as number[] + ) + const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1 + const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1 + requestBody = { + model: modelId, + max_tokens: this.getModel().info.maxTokens, + system: [{ text: systemPrompt, type: "text", cache_control: { type: "ephemeral" } }], + messages: messages.map((message, index) => { + if (index === lastUserMsgIndex || index === secondLastMsgUserIndex) { + return { + ...message, + content: + typeof message.content === "string" + ? [ + { + type: "text", + text: message.content, + cache_control: { type: "ephemeral" }, + }, + ] + : message.content.map((content, contentIndex) => + contentIndex === message.content.length - 1 + ? { ...content, cache_control: { type: "ephemeral" } } + : content + ), + } + } + return message + }), + tools, + tool_choice: { type: "auto" }, + } + break + default: + requestBody = { + model: modelId, + max_tokens: this.getModel().info.maxTokens, + system: [{ text: systemPrompt, type: "text" }], + messages, + tools, + tool_choice: { type: "auto" }, + } + } + const response = await axios.post(`${KODU_BASE_URL}/api/inference`, requestBody, { + headers: { + "x-api-key": this.options.koduApiKey, + }, + }) + return response.data + } + + createUserReadableRequest( + userContent: Array< + | Anthropic.TextBlockParam + | Anthropic.ImageBlockParam + | Anthropic.ToolUseBlockParam + | Anthropic.ToolResultBlockParam + > + ): any { + return { + model: this.getModel().id, + max_tokens: this.getModel().info.maxTokens, + system: "(see SYSTEM_PROMPT in src/ClaudeDev.ts)", + messages: [{ conversation_history: "..." }, { role: "user", content: withoutImageData(userContent) }], + tools: "(see tools in src/ClaudeDev.ts)", + tool_choice: { type: "auto" }, + } + } + + getModel(): { id: KoduModelId; info: ModelInfo } { + const modelId = this.options.apiModelId + if (modelId && modelId in koduModels) { + const id = modelId as KoduModelId + return { id, info: koduModels[id] } + } + return { id: koduDefaultModelId, info: koduModels[koduDefaultModelId] } + } +} diff --git a/src/api/maestro.ts b/src/api/maestro.ts deleted file mode 100644 index cb0ca35aaa..0000000000 --- a/src/api/maestro.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { Anthropic } from "@anthropic-ai/sdk" -import { ApiHandler, withoutImageData } from "." -import { ApiHandlerOptions, maestroDefaultModelId, MaestroModelId, maestroModels, ModelInfo } from "../shared/api" - -export class MaestroHandler implements ApiHandler { - private options: ApiHandlerOptions - private client: Anthropic - - constructor(options: ApiHandlerOptions) { - this.options = options - this.client = new Anthropic({ apiKey: this.options.apiKey }) - } - - async createMessage( - systemPrompt: string, - messages: Anthropic.Messages.MessageParam[], - tools: Anthropic.Messages.Tool[] - ): Promise { - const modelId = this.getModel().id - switch (modelId) { - case "claude-3-5-sonnet-20240620": - case "claude-3-haiku-20240307": - const userMsgIndices = messages.reduce( - (acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc), - [] as number[] - ) - const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1 - const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1 - return await this.client.beta.promptCaching.messages.create( - { - model: modelId, - max_tokens: this.getModel().info.maxTokens, - system: [{ text: systemPrompt, type: "text", cache_control: { type: "ephemeral" } }], - messages: messages.map((message, index) => { - if (index === lastUserMsgIndex || index === secondLastMsgUserIndex) { - return { - ...message, - content: - typeof message.content === "string" - ? [ - { - type: "text", - text: message.content, - cache_control: { type: "ephemeral" }, - }, - ] - : message.content.map((content, contentIndex) => - contentIndex === message.content.length - 1 - ? { ...content, cache_control: { type: "ephemeral" } } - : content - ), - } - } - return message - }), - tools, - tool_choice: { type: "auto" }, - }, - (() => { - switch (modelId) { - case "claude-3-5-sonnet-20240620": - return { - headers: { - "anthropic-beta": "prompt-caching-2024-07-31", - }, - } - case "claude-3-haiku-20240307": - return { - headers: { "anthropic-beta": "prompt-caching-2024-07-31" }, - } - default: - return undefined - } - })() - ) - default: - return await this.client.messages.create({ - model: modelId, - max_tokens: this.getModel().info.maxTokens, - system: [{ text: systemPrompt, type: "text" }], - messages, - tools, - tool_choice: { type: "auto" }, - }) - } - } - - createUserReadableRequest( - userContent: Array< - | Anthropic.TextBlockParam - | Anthropic.ImageBlockParam - | Anthropic.ToolUseBlockParam - | Anthropic.ToolResultBlockParam - > - ): any { - return { - model: this.getModel().id, - max_tokens: this.getModel().info.maxTokens, - system: "(see SYSTEM_PROMPT in src/ClaudeDev.ts)", - messages: [{ conversation_history: "..." }, { role: "user", content: withoutImageData(userContent) }], - tools: "(see tools in src/ClaudeDev.ts)", - tool_choice: { type: "auto" }, - } - } - - getModel(): { id: MaestroModelId; info: ModelInfo } { - const modelId = this.options.apiModelId - if (modelId && modelId in maestroModels) { - const id = modelId as MaestroModelId - return { id, info: maestroModels[id] } - } - return { id: maestroDefaultModelId, info: maestroModels[maestroDefaultModelId] } - } -} diff --git a/src/extension.ts b/src/extension.ts index b1e2fb2369..ecb2273939 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -111,14 +111,11 @@ export function activate(context: vscode.ExtensionContext) { // URI Handler const handleUri = async (uri: vscode.Uri) => { - const query = new URLSearchParams(uri.query) + const query = new URLSearchParams(uri.query.replace(/\+/g, "%2B")) const token = query.get("token") - const fixedToken = token?.replaceAll("jwt?token=", "") - console.log(fixedToken) - console.log(uri) - - if (fixedToken) { - await sidebarProvider.saveMaestroToken(fixedToken) + const email = query.get("email") + if (token) { + await sidebarProvider.saveKoduApiKey(token, email || undefined) } } context.subscriptions.push(vscode.window.registerUriHandler({ handleUri })) diff --git a/src/maestro/auth.ts b/src/maestro/auth.ts deleted file mode 100644 index 23cfc62b1f..0000000000 --- a/src/maestro/auth.ts +++ /dev/null @@ -1,38 +0,0 @@ -import axios from "axios" -import * as vscode from "vscode" -import { MaestroUser, MaestroUserSchema } from "../shared/maestro" - -const MAESTRO_BASE_URL = "https://maestro.im-ada.ai" - -export function didClickMaestroSignIn() { - const loginUrl = `${MAESTRO_BASE_URL}/auth/login?ext=1&redirectTo=${vscode.env.uriScheme}://saoudrizwan.claude-dev?token=jwt` - vscode.env.openExternal(vscode.Uri.parse(loginUrl)) -} - -export async function validateMaestroToken({ - token, - showError = false, -}: { - token: string - showError?: boolean -}): Promise { - try { - const response = await axios.post(`${MAESTRO_BASE_URL}/api/extension/auth/callback`, { token }) - const user = MaestroUserSchema.parse(response.data.user) - console.log("retrieved user", user) - return user - } catch (error) { - if (showError) { - if (axios.isAxiosError(error)) { - vscode.window.showErrorMessage( - "Failed to validate token:", - error.response?.status, - error.response?.data - ) - } else { - vscode.window.showErrorMessage("An unexpected error occurred:", error) - } - } - throw error - } -} diff --git a/src/maestro/index.ts b/src/maestro/index.ts deleted file mode 100644 index 306751af96..0000000000 --- a/src/maestro/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./auth" diff --git a/src/providers/ClaudeDevProvider.ts b/src/providers/ClaudeDevProvider.ts index e0d53c78bd..7971287f60 100644 --- a/src/providers/ClaudeDevProvider.ts +++ b/src/providers/ClaudeDevProvider.ts @@ -8,8 +8,7 @@ import { downloadTask, getNonce, getUri, selectImages } from "../utils" import * as path from "path" import fs from "fs/promises" import { HistoryItem } from "../shared/HistoryItem" -import { didClickMaestroSignIn, validateMaestroToken } from "../maestro" -import { MaestroUser } from "../shared/maestro" +import { didClickKoduAddCredits, didClickKoduSignIn, fetchKoduCredits } from "../api/kodu" /* https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts @@ -17,11 +16,13 @@ https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/customSidebarViewProvider.ts */ -type SecretKey = "apiKey" | "openRouterApiKey" | "awsAccessKey" | "awsSecretKey" | "maestroToken" +type SecretKey = "apiKey" | "openRouterApiKey" | "awsAccessKey" | "awsSecretKey" | "koduApiKey" type GlobalStateKey = | "apiProvider" | "apiModelId" | "awsRegion" + | "koduEmail" + | "koduCredits" | "maxRequestsPerTask" | "lastShownAnnouncementId" | "customInstructions" @@ -34,11 +35,9 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider { private view?: vscode.WebviewView | vscode.WebviewPanel private claudeDev?: ClaudeDev private latestAnnouncementId = "aug-17-2024" // update to some unique identifier when we add a new announcement - private maestroUser?: MaestroUser constructor(readonly context: vscode.ExtensionContext, private readonly outputChannel: vscode.OutputChannel) { this.outputChannel.appendLine("ClaudeDevProvider instantiated") - this.fetchMaestroUser({}) } /* @@ -344,11 +343,23 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider { case "exportTaskWithId": this.exportTaskWithId(message.text!) break - case "didClickMaestroSignIn": - didClickMaestroSignIn() + case "didClickKoduSignIn": + didClickKoduSignIn() break - case "didClickMaestroSignOut": - await this.signOutMaestro() + case "didClickKoduSignOut": + await this.signOutKodu() + break + case "didClickKoduAddCredits": + didClickKoduAddCredits() + break + case "fetchKoduCredits": + const koduApiKey = await this.getSecret("koduApiKey") + if (koduApiKey) { + const credits = await fetchKoduCredits({ apiKey: koduApiKey }) + await this.updateGlobalState("koduCredits", credits) + await this.postStateToWebview() + await this.postMessageToWebview({ type: "action", action: "koduCreditsFetched" }) + } break // Add more switch case statements here as more webview message commands // are created within the webview context (i.e. inside media/main.js) @@ -359,33 +370,21 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider { ) } - // Maestro + // Kodu - async saveMaestroToken(token: string) { - await this.storeSecret("maestroToken", token) - await this.updateGlobalState("apiProvider", "maestro") - await this.fetchMaestroUser({ showError: true }) - this.claudeDev?.updateApi({ apiProvider: "maestro", maestroToken: token }) - } - - async fetchMaestroUser({ showError = false }: { showError?: boolean }): Promise { - if (this.maestroUser) { - return this.maestroUser - } - const token = await this.getSecret("maestroToken") - if (!token) { - return undefined - } - const user = await validateMaestroToken({ token, showError }) - this.maestroUser = user + async saveKoduApiKey(apiKey: string, email?: string) { + await this.storeSecret("koduApiKey", apiKey) + await this.updateGlobalState("koduEmail", email) + await this.updateGlobalState("apiProvider", "kodu") await this.postStateToWebview() - return user + this.claudeDev?.updateApi({ apiProvider: "kodu", koduApiKey: apiKey }) } - async signOutMaestro() { - await this.storeSecret("maestroToken", undefined) - this.claudeDev?.updateApi({ apiProvider: "maestro", maestroToken: undefined }) - this.maestroUser = undefined + async signOutKodu() { + await this.storeSecret("koduApiKey", undefined) + await this.updateGlobalState("koduEmail", undefined) + await this.updateGlobalState("koduCredits", undefined) + this.claudeDev?.updateApi({ apiProvider: "kodu", koduApiKey: undefined }) await this.postStateToWebview() } @@ -476,8 +475,14 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider { } async postStateToWebview() { - const { apiConfiguration, maxRequestsPerTask, lastShownAnnouncementId, customInstructions, taskHistory } = - await this.getState() + const { + apiConfiguration, + maxRequestsPerTask, + lastShownAnnouncementId, + customInstructions, + taskHistory, + koduCredits, + } = await this.getState() this.postMessageToWebview({ type: "state", state: { @@ -489,7 +494,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider { claudeMessages: this.claudeDev?.claudeMessages || [], taskHistory: (taskHistory || []).filter((item) => item.ts && item.task).sort((a, b) => b.ts - a.ts), shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId, - maestroUser: this.maestroUser, + koduCredits, }, }) } @@ -589,6 +594,9 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider { awsAccessKey, awsSecretKey, awsRegion, + koduApiKey, + koduEmail, + koduCredits, maxRequestsPerTask, lastShownAnnouncementId, customInstructions, @@ -601,6 +609,9 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider { this.getSecret("awsAccessKey") as Promise, this.getSecret("awsSecretKey") as Promise, this.getGlobalState("awsRegion") as Promise, + this.getSecret("koduApiKey") as Promise, + this.getGlobalState("koduEmail") as Promise, + this.getGlobalState("koduCredits") as Promise, this.getGlobalState("maxRequestsPerTask") as Promise, this.getGlobalState("lastShownAnnouncementId") as Promise, this.getGlobalState("customInstructions") as Promise, @@ -616,8 +627,8 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider { if (apiKey) { apiProvider = "anthropic" } else { - // New users should default to anthropic (openrouter has issues, bedrock is complicated) - apiProvider = "anthropic" + // New users should default to kodu + apiProvider = "kodu" } } @@ -630,11 +641,14 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider { awsAccessKey, awsSecretKey, awsRegion, + koduApiKey, + koduEmail, }, maxRequestsPerTask, lastShownAnnouncementId, customInstructions, taskHistory, + koduCredits, } } diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index b6744465d0..1f7e11ef88 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -2,13 +2,17 @@ import { ApiConfiguration } from "./api" import { HistoryItem } from "./HistoryItem" -import { MaestroUser } from "./maestro" // webview will hold state export interface ExtensionMessage { type: "action" | "state" | "selectedImages" text?: string - action?: "chatButtonTapped" | "settingsButtonTapped" | "historyButtonTapped" | "didBecomeVisible" + action?: + | "chatButtonTapped" + | "settingsButtonTapped" + | "historyButtonTapped" + | "didBecomeVisible" + | "koduCreditsFetched" state?: ExtensionState images?: string[] } @@ -22,7 +26,7 @@ export interface ExtensionState { claudeMessages: ClaudeMessage[] taskHistory: HistoryItem[] shouldShowAnnouncement: boolean - maestroUser?: MaestroUser + koduCredits?: number } export interface ClaudeMessage { diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 58dc8cbee7..e2a258edf8 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -15,8 +15,10 @@ export interface WebviewMessage { | "showTaskWithId" | "deleteTaskWithId" | "exportTaskWithId" - | "didClickMaestroSignIn" - | "didClickMaestroSignOut" + | "didClickKoduSignIn" + | "didClickKoduSignOut" + | "didClickKoduAddCredits" + | "fetchKoduCredits" text?: string askResponse?: ClaudeAskResponse apiConfiguration?: ApiConfiguration diff --git a/src/shared/api.ts b/src/shared/api.ts index 9e31534754..fa7d4bef79 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -1,4 +1,4 @@ -export type ApiProvider = "anthropic" | "openrouter" | "bedrock" | "maestro" +export type ApiProvider = "anthropic" | "openrouter" | "bedrock" | "kodu" export interface ApiHandlerOptions { apiModelId?: ApiModelId @@ -7,7 +7,8 @@ export interface ApiHandlerOptions { awsAccessKey?: string awsSecretKey?: string awsRegion?: string - maestroToken?: string + koduApiKey?: string + koduEmail?: string } export type ApiConfiguration = ApiHandlerOptions & { @@ -234,9 +235,9 @@ export const openRouterModels = { // }, } as const satisfies Record -// Maestro -export type MaestroModelId = keyof typeof maestroModels -export const maestroDefaultModelId: MaestroModelId = "claude-3-5-sonnet-20240620" -export const maestroModels = { +// Kodu +export type KoduModelId = keyof typeof koduModels +export const koduDefaultModelId: KoduModelId = "claude-3-5-sonnet-20240620" +export const koduModels = { ...anthropicModels, } as const satisfies Record diff --git a/src/shared/maestro.ts b/src/shared/maestro.ts deleted file mode 100644 index a0d5bcd901..0000000000 --- a/src/shared/maestro.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { z } from "zod" - -export const MaestroUserSchema = z.object({ - id: z.string(), - image: z.string().nullable(), - email: z.string().email(), - name: z.string().nullable(), - emailVerified: z.coerce.date().nullable(), -}) -export type MaestroUser = z.infer diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json index 38874523f7..e74837e3be 100644 --- a/webview-ui/package-lock.json +++ b/webview-ui/package-lock.json @@ -26,8 +26,7 @@ "react-virtuoso": "^4.7.13", "rewire": "^7.0.0", "typescript": "^4.9.5", - "web-vitals": "^2.1.4", - "zod": "^3.23.8" + "web-vitals": "^2.1.4" }, "devDependencies": { "@types/react-scroll": "^1.8.10", @@ -21450,14 +21449,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/zod": { - "version": "3.23.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", - "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/webview-ui/package.json b/webview-ui/package.json index 2c66e48730..65c0e10588 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -21,8 +21,7 @@ "react-virtuoso": "^4.7.13", "rewire": "^7.0.0", "typescript": "^4.9.5", - "web-vitals": "^2.1.4", - "zod": "^3.23.8" + "web-vitals": "^2.1.4" }, "scripts": { "start": "react-scripts start", diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index fa0fdd988c..c7f99fedce 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -10,7 +10,6 @@ import WelcomeView from "./components/WelcomeView" import { vscode } from "./utils/vscode" import HistoryView from "./components/HistoryView" import { HistoryItem } from "../../src/shared/HistoryItem" -import { MaestroUser } from "../../src/shared/maestro" /* The contents of webviews however are created when the webview becomes visible and destroyed when the webview is moved into the background. Any state inside the webview will be lost when the webview is moved to a background tab. @@ -31,7 +30,7 @@ const App: React.FC = () => { const [claudeMessages, setClaudeMessages] = useState([]) const [taskHistory, setTaskHistory] = useState([]) const [showAnnouncement, setShowAnnouncement] = useState(false) - const [maestroUser, setMaestroUser] = useState(undefined) + const [koduCredits, setKoduCredits] = useState(undefined) useEffect(() => { vscode.postMessage({ type: "webviewDidLaunch" }) @@ -45,7 +44,8 @@ const App: React.FC = () => { const hasKey = message.state!.apiConfiguration?.apiKey !== undefined || message.state!.apiConfiguration?.openRouterApiKey !== undefined || - message.state!.apiConfiguration?.awsAccessKey !== undefined + message.state!.apiConfiguration?.awsAccessKey !== undefined || + message.state!.apiConfiguration?.koduApiKey !== undefined setShowWelcome(!hasKey) setApiConfiguration(message.state!.apiConfiguration) setMaxRequestsPerTask( @@ -55,12 +55,12 @@ const App: React.FC = () => { setVscodeThemeName(message.state!.themeName) setClaudeMessages(message.state!.claudeMessages) setTaskHistory(message.state!.taskHistory) + setKoduCredits(message.state!.koduCredits) // don't update showAnnouncement to false if shouldShowAnnouncement is false if (message.state!.shouldShowAnnouncement) { setShowAnnouncement(true) vscode.postMessage({ type: "didShowAnnouncement" }) } - setMaestroUser(message.state!.maestroUser) setDidHydrateState(true) break case "action": @@ -103,8 +103,8 @@ const App: React.FC = () => { > - maestroUser?: MaestroUser + koduCredits?: number + apiErrorMessage?: string } const ApiOptions: React.FC = ({ showModelOptions, apiConfiguration, setApiConfiguration, - maestroUser, + koduCredits, + apiErrorMessage, }) => { + const [didFetchKoduCredits, setDidFetchKoduCredits] = useState(false) const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => { setApiConfiguration((prev) => ({ ...prev, [field]: event.target.value })) } @@ -75,6 +79,27 @@ const ApiOptions: React.FC = ({ ) } + useEffect(() => { + if (selectedProvider === "kodu" && apiConfiguration?.koduApiKey) { + setDidFetchKoduCredits(false) + vscode.postMessage({ type: "fetchKoduCredits" }) + } + }, [selectedProvider, apiConfiguration?.koduApiKey]) + + const handleMessage = useCallback((e: MessageEvent) => { + const message: ExtensionMessage = e.data + switch (message.type) { + case "action": + switch (message.action) { + case "koduCreditsFetched": + setDidFetchKoduCredits(true) + break + } + break + } + }, []) + useEvent("message", handleMessage) + return (
@@ -82,10 +107,10 @@ const ApiOptions: React.FC = ({ API Provider + Kodu Anthropic AWS Bedrock OpenRouter - Maestro
@@ -139,42 +164,54 @@ const ApiOptions: React.FC = ({
)} - {selectedProvider === "maestro" && ( + {selectedProvider === "kodu" && ( <> - {maestroUser ? ( + {apiConfiguration?.koduApiKey !== undefined ? (
- - - Signed in as {maestroUser.email} - -
- vscode.postMessage({ type: "didClickMaestroSignOut" })}> - Sign out - +
+ + Signed in as {apiConfiguration?.koduEmail || "Unknown"} + {" "} + vscode.postMessage({ type: "didClickKoduSignOut" })}> + (sign out?) +
+
+ Credits remaining:{" "} + + {formatPrice(koduCredits || 0)} + +
+ vscode.postMessage({ type: "didClickKoduAddCredits" })} + style={{ + width: "fit-content", + }}> + Add Credits + +

+ Kodu is recommended for its high rate limits and access to the latest features like + prompt caching. + + Learn more about Kodu here. + +

) : (
vscode.postMessage({ type: "didClickMaestroSignIn" })}> - Sign in to Maestro + onClick={() => vscode.postMessage({ type: "didClickKoduSignIn" })}> + Sign in to Kodu

= ({ marginTop: 5, color: "var(--vscode-descriptionForeground)", }}> - This will open your browser to sign in to Maestro. You will be redirected back to the + This will open your browser to sign in to Kodu. You will be redirected back to the extension after signing in.

@@ -256,6 +293,17 @@ const ApiOptions: React.FC = ({
)} + {apiErrorMessage && ( +

+ {apiErrorMessage} +

+ )} + {showModelOptions && ( <>
@@ -265,7 +313,7 @@ const ApiOptions: React.FC = ({ {selectedProvider === "anthropic" && createDropdown(anthropicModels)} {selectedProvider === "openrouter" && createDropdown(openRouterModels)} {selectedProvider === "bedrock" && createDropdown(bedrockModels)} - {selectedProvider === "maestro" && createDropdown(maestroModels)} + {selectedProvider === "kodu" && createDropdown(koduModels)}
@@ -275,16 +323,16 @@ const ApiOptions: React.FC = ({ ) } -const ModelInfoView = ({ modelInfo }: { modelInfo: ModelInfo }) => { - const formatPrice = (price: number) => { - return new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - minimumFractionDigits: 2, - maximumFractionDigits: 2, - }).format(price) - } +const formatPrice = (price: number) => { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(price) +} +const ModelInfoView = ({ modelInfo }: { modelInfo: ModelInfo }) => { return (

> + koduCredits?: number maxRequestsPerTask: string setMaxRequestsPerTask: React.Dispatch> customInstructions: string @@ -21,8 +20,8 @@ type SettingsViewProps = { const SettingsView = ({ version, apiConfiguration, - maestroUser, setApiConfiguration, + koduCredits, maxRequestsPerTask, setMaxRequestsPerTask, customInstructions, @@ -96,20 +95,11 @@ const SettingsView = ({

- {apiErrorMessage && ( -

- {apiErrorMessage} -

- )}
diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index e5d793f119..a7ca9ab38a 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -18,10 +18,10 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s return "You must provide a valid API key or choose a different provider." } break - case "maestro": - // if (!apiConfiguration.maestroApiKey) { - // return "You must provide a valid API key or choose a different provider." - // } + case "kodu": + if (!apiConfiguration.koduApiKey) { + return "You must sign in to Kodu to use it as an API provider." + } break } }