Browse Source

Add Kodu provider

Saoud Rizwan 1 year ago
parent
commit
df4e8e7afc

+ 68 - 2
package-lock.json

@@ -1,16 +1,17 @@
 {
 {
   "name": "claude-dev",
   "name": "claude-dev",
-  "version": "1.3.43",
+  "version": "1.4.0",
   "lockfileVersion": 3,
   "lockfileVersion": 3,
   "requires": true,
   "requires": true,
   "packages": {
   "packages": {
     "": {
     "": {
       "name": "claude-dev",
       "name": "claude-dev",
-      "version": "1.3.43",
+      "version": "1.4.0",
       "license": "MIT",
       "license": "MIT",
       "dependencies": {
       "dependencies": {
         "@anthropic-ai/bedrock-sdk": "^0.10.2",
         "@anthropic-ai/bedrock-sdk": "^0.10.2",
         "@anthropic-ai/sdk": "^0.26.0",
         "@anthropic-ai/sdk": "^0.26.0",
+        "@kodu-ai/cloud-api": "^1.0.1",
         "@vscode/codicons": "^0.0.36",
         "@vscode/codicons": "^0.0.36",
         "axios": "^1.7.4",
         "axios": "^1.7.4",
         "default-shell": "^2.2.0",
         "default-shell": "^2.2.0",
@@ -2768,6 +2769,16 @@
         "@jridgewell/sourcemap-codec": "^1.4.14"
         "@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": {
     "node_modules/@nodelib/fs.scandir": {
       "version": "2.1.5",
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
       "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",
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
       "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
       "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": {
     "node_modules/@types/diff": {
       "version": "5.2.1",
       "version": "5.2.1",
       "resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.2.1.tgz",
       "resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.2.1.tgz",
@@ -5399,6 +5429,20 @@
       "dev": true,
       "dev": true,
       "license": "MIT"
       "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": {
     "node_modules/core-util-is": {
       "version": "1.0.3",
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
       "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"
         "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": {
     "node_modules/isarray": {
       "version": "1.0.0",
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
       "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",
       "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
       "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA=="
       "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": {
     "node_modules/supports-color": {
       "version": "9.4.0",
       "version": "9.4.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz",

+ 1 - 0
package.json

@@ -134,6 +134,7 @@
   "dependencies": {
   "dependencies": {
     "@anthropic-ai/bedrock-sdk": "^0.10.2",
     "@anthropic-ai/bedrock-sdk": "^0.10.2",
     "@anthropic-ai/sdk": "^0.26.0",
     "@anthropic-ai/sdk": "^0.26.0",
+    "@kodu-ai/cloud-api": "^1.0.1",
     "@vscode/codicons": "^0.0.36",
     "@vscode/codicons": "^0.0.36",
     "axios": "^1.7.4",
     "axios": "^1.7.4",
     "default-shell": "^2.2.0",
     "default-shell": "^2.2.0",

+ 3 - 3
src/api/index.ts

@@ -3,7 +3,7 @@ import { ApiConfiguration, ApiModelId, ModelInfo } from "../shared/api"
 import { AnthropicHandler } from "./anthropic"
 import { AnthropicHandler } from "./anthropic"
 import { AwsBedrockHandler } from "./bedrock"
 import { AwsBedrockHandler } from "./bedrock"
 import { OpenRouterHandler } from "./openrouter"
 import { OpenRouterHandler } from "./openrouter"
-import { MaestroHandler } from "./maestro"
+import { KoduHandler } from "./kodu"
 
 
 export interface ApiHandler {
 export interface ApiHandler {
 	createMessage(
 	createMessage(
@@ -33,8 +33,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
 			return new OpenRouterHandler(options)
 			return new OpenRouterHandler(options)
 		case "bedrock":
 		case "bedrock":
 			return new AwsBedrockHandler(options)
 			return new AwsBedrockHandler(options)
-		case "maestro":
-			return new MaestroHandler(options)
+		case "kodu":
+			return new KoduHandler(options)
 		default:
 		default:
 			return new AnthropicHandler(options)
 			return new AnthropicHandler(options)
 	}
 	}

+ 125 - 0
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<Anthropic.Messages.Message> {
+		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] }
+	}
+}

+ 0 - 114
src/api/maestro.ts

@@ -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<Anthropic.Messages.Message> {
-		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] }
-	}
-}

+ 4 - 7
src/extension.ts

@@ -111,14 +111,11 @@ export function activate(context: vscode.ExtensionContext) {
 
 
 	// URI Handler
 	// URI Handler
 	const handleUri = async (uri: vscode.Uri) => {
 	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 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 }))
 	context.subscriptions.push(vscode.window.registerUriHandler({ handleUri }))

+ 0 - 38
src/maestro/auth.ts

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

+ 0 - 1
src/maestro/index.ts

@@ -1 +0,0 @@
-export * from "./auth"

+ 51 - 37
src/providers/ClaudeDevProvider.ts

@@ -8,8 +8,7 @@ import { downloadTask, getNonce, getUri, selectImages } from "../utils"
 import * as path from "path"
 import * as path from "path"
 import fs from "fs/promises"
 import fs from "fs/promises"
 import { HistoryItem } from "../shared/HistoryItem"
 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
 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
 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 =
 type GlobalStateKey =
 	| "apiProvider"
 	| "apiProvider"
 	| "apiModelId"
 	| "apiModelId"
 	| "awsRegion"
 	| "awsRegion"
+	| "koduEmail"
+	| "koduCredits"
 	| "maxRequestsPerTask"
 	| "maxRequestsPerTask"
 	| "lastShownAnnouncementId"
 	| "lastShownAnnouncementId"
 	| "customInstructions"
 	| "customInstructions"
@@ -34,11 +35,9 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
 	private view?: vscode.WebviewView | vscode.WebviewPanel
 	private view?: vscode.WebviewView | vscode.WebviewPanel
 	private claudeDev?: ClaudeDev
 	private claudeDev?: ClaudeDev
 	private latestAnnouncementId = "aug-17-2024" // update to some unique identifier when we add a new announcement
 	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) {
 	constructor(readonly context: vscode.ExtensionContext, private readonly outputChannel: vscode.OutputChannel) {
 		this.outputChannel.appendLine("ClaudeDevProvider instantiated")
 		this.outputChannel.appendLine("ClaudeDevProvider instantiated")
-		this.fetchMaestroUser({})
 	}
 	}
 
 
 	/*
 	/*
@@ -344,11 +343,23 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
 					case "exportTaskWithId":
 					case "exportTaskWithId":
 						this.exportTaskWithId(message.text!)
 						this.exportTaskWithId(message.text!)
 						break
 						break
-					case "didClickMaestroSignIn":
-						didClickMaestroSignIn()
+					case "didClickKoduSignIn":
+						didClickKoduSignIn()
 						break
 						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
 						break
 					// Add more switch case statements here as more webview message commands
 					// Add more switch case statements here as more webview message commands
 					// are created within the webview context (i.e. inside media/main.js)
 					// 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<MaestroUser | undefined> {
-		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()
 		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()
 		await this.postStateToWebview()
 	}
 	}
 
 
@@ -476,8 +475,14 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
 	}
 	}
 
 
 	async postStateToWebview() {
 	async postStateToWebview() {
-		const { apiConfiguration, maxRequestsPerTask, lastShownAnnouncementId, customInstructions, taskHistory } =
-			await this.getState()
+		const {
+			apiConfiguration,
+			maxRequestsPerTask,
+			lastShownAnnouncementId,
+			customInstructions,
+			taskHistory,
+			koduCredits,
+		} = await this.getState()
 		this.postMessageToWebview({
 		this.postMessageToWebview({
 			type: "state",
 			type: "state",
 			state: {
 			state: {
@@ -489,7 +494,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
 				claudeMessages: this.claudeDev?.claudeMessages || [],
 				claudeMessages: this.claudeDev?.claudeMessages || [],
 				taskHistory: (taskHistory || []).filter((item) => item.ts && item.task).sort((a, b) => b.ts - a.ts),
 				taskHistory: (taskHistory || []).filter((item) => item.ts && item.task).sort((a, b) => b.ts - a.ts),
 				shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
 				shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
-				maestroUser: this.maestroUser,
+				koduCredits,
 			},
 			},
 		})
 		})
 	}
 	}
@@ -589,6 +594,9 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
 			awsAccessKey,
 			awsAccessKey,
 			awsSecretKey,
 			awsSecretKey,
 			awsRegion,
 			awsRegion,
+			koduApiKey,
+			koduEmail,
+			koduCredits,
 			maxRequestsPerTask,
 			maxRequestsPerTask,
 			lastShownAnnouncementId,
 			lastShownAnnouncementId,
 			customInstructions,
 			customInstructions,
@@ -601,6 +609,9 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
 			this.getSecret("awsAccessKey") as Promise<string | undefined>,
 			this.getSecret("awsAccessKey") as Promise<string | undefined>,
 			this.getSecret("awsSecretKey") as Promise<string | undefined>,
 			this.getSecret("awsSecretKey") as Promise<string | undefined>,
 			this.getGlobalState("awsRegion") as Promise<string | undefined>,
 			this.getGlobalState("awsRegion") as Promise<string | undefined>,
+			this.getSecret("koduApiKey") as Promise<string | undefined>,
+			this.getGlobalState("koduEmail") as Promise<string | undefined>,
+			this.getGlobalState("koduCredits") as Promise<number | undefined>,
 			this.getGlobalState("maxRequestsPerTask") as Promise<number | undefined>,
 			this.getGlobalState("maxRequestsPerTask") as Promise<number | undefined>,
 			this.getGlobalState("lastShownAnnouncementId") as Promise<string | undefined>,
 			this.getGlobalState("lastShownAnnouncementId") as Promise<string | undefined>,
 			this.getGlobalState("customInstructions") as Promise<string | undefined>,
 			this.getGlobalState("customInstructions") as Promise<string | undefined>,
@@ -616,8 +627,8 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
 			if (apiKey) {
 			if (apiKey) {
 				apiProvider = "anthropic"
 				apiProvider = "anthropic"
 			} else {
 			} 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,
 				awsAccessKey,
 				awsSecretKey,
 				awsSecretKey,
 				awsRegion,
 				awsRegion,
+				koduApiKey,
+				koduEmail,
 			},
 			},
 			maxRequestsPerTask,
 			maxRequestsPerTask,
 			lastShownAnnouncementId,
 			lastShownAnnouncementId,
 			customInstructions,
 			customInstructions,
 			taskHistory,
 			taskHistory,
+			koduCredits,
 		}
 		}
 	}
 	}
 
 

+ 7 - 3
src/shared/ExtensionMessage.ts

@@ -2,13 +2,17 @@
 
 
 import { ApiConfiguration } from "./api"
 import { ApiConfiguration } from "./api"
 import { HistoryItem } from "./HistoryItem"
 import { HistoryItem } from "./HistoryItem"
-import { MaestroUser } from "./maestro"
 
 
 // webview will hold state
 // webview will hold state
 export interface ExtensionMessage {
 export interface ExtensionMessage {
 	type: "action" | "state" | "selectedImages"
 	type: "action" | "state" | "selectedImages"
 	text?: string
 	text?: string
-	action?: "chatButtonTapped" | "settingsButtonTapped" | "historyButtonTapped" | "didBecomeVisible"
+	action?:
+		| "chatButtonTapped"
+		| "settingsButtonTapped"
+		| "historyButtonTapped"
+		| "didBecomeVisible"
+		| "koduCreditsFetched"
 	state?: ExtensionState
 	state?: ExtensionState
 	images?: string[]
 	images?: string[]
 }
 }
@@ -22,7 +26,7 @@ export interface ExtensionState {
 	claudeMessages: ClaudeMessage[]
 	claudeMessages: ClaudeMessage[]
 	taskHistory: HistoryItem[]
 	taskHistory: HistoryItem[]
 	shouldShowAnnouncement: boolean
 	shouldShowAnnouncement: boolean
-	maestroUser?: MaestroUser
+	koduCredits?: number
 }
 }
 
 
 export interface ClaudeMessage {
 export interface ClaudeMessage {

+ 4 - 2
src/shared/WebviewMessage.ts

@@ -15,8 +15,10 @@ export interface WebviewMessage {
 		| "showTaskWithId"
 		| "showTaskWithId"
 		| "deleteTaskWithId"
 		| "deleteTaskWithId"
 		| "exportTaskWithId"
 		| "exportTaskWithId"
-		| "didClickMaestroSignIn"
-		| "didClickMaestroSignOut"
+		| "didClickKoduSignIn"
+		| "didClickKoduSignOut"
+		| "didClickKoduAddCredits"
+		| "fetchKoduCredits"
 	text?: string
 	text?: string
 	askResponse?: ClaudeAskResponse
 	askResponse?: ClaudeAskResponse
 	apiConfiguration?: ApiConfiguration
 	apiConfiguration?: ApiConfiguration

+ 7 - 6
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 {
 export interface ApiHandlerOptions {
 	apiModelId?: ApiModelId
 	apiModelId?: ApiModelId
@@ -7,7 +7,8 @@ export interface ApiHandlerOptions {
 	awsAccessKey?: string
 	awsAccessKey?: string
 	awsSecretKey?: string
 	awsSecretKey?: string
 	awsRegion?: string
 	awsRegion?: string
-	maestroToken?: string
+	koduApiKey?: string
+	koduEmail?: string
 }
 }
 
 
 export type ApiConfiguration = ApiHandlerOptions & {
 export type ApiConfiguration = ApiHandlerOptions & {
@@ -234,9 +235,9 @@ export const openRouterModels = {
 	// },
 	// },
 } as const satisfies Record<string, ModelInfo>
 } as const satisfies Record<string, ModelInfo>
 
 
-// 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,
 	...anthropicModels,
 } as const satisfies Record<string, ModelInfo>
 } as const satisfies Record<string, ModelInfo>

+ 0 - 10
src/shared/maestro.ts

@@ -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<typeof MaestroUserSchema>

+ 1 - 10
webview-ui/package-lock.json

@@ -26,8 +26,7 @@
         "react-virtuoso": "^4.7.13",
         "react-virtuoso": "^4.7.13",
         "rewire": "^7.0.0",
         "rewire": "^7.0.0",
         "typescript": "^4.9.5",
         "typescript": "^4.9.5",
-        "web-vitals": "^2.1.4",
-        "zod": "^3.23.8"
+        "web-vitals": "^2.1.4"
       },
       },
       "devDependencies": {
       "devDependencies": {
         "@types/react-scroll": "^1.8.10",
         "@types/react-scroll": "^1.8.10",
@@ -21450,14 +21449,6 @@
         "url": "https://github.com/sponsors/sindresorhus"
         "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": {
     "node_modules/zwitch": {
       "version": "2.0.4",
       "version": "2.0.4",
       "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
       "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",

+ 1 - 2
webview-ui/package.json

@@ -21,8 +21,7 @@
     "react-virtuoso": "^4.7.13",
     "react-virtuoso": "^4.7.13",
     "rewire": "^7.0.0",
     "rewire": "^7.0.0",
     "typescript": "^4.9.5",
     "typescript": "^4.9.5",
-    "web-vitals": "^2.1.4",
-    "zod": "^3.23.8"
+    "web-vitals": "^2.1.4"
   },
   },
   "scripts": {
   "scripts": {
     "start": "react-scripts start",
     "start": "react-scripts start",

+ 5 - 5
webview-ui/src/App.tsx

@@ -10,7 +10,6 @@ import WelcomeView from "./components/WelcomeView"
 import { vscode } from "./utils/vscode"
 import { vscode } from "./utils/vscode"
 import HistoryView from "./components/HistoryView"
 import HistoryView from "./components/HistoryView"
 import { HistoryItem } from "../../src/shared/HistoryItem"
 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.
 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<ClaudeMessage[]>([])
 	const [claudeMessages, setClaudeMessages] = useState<ClaudeMessage[]>([])
 	const [taskHistory, setTaskHistory] = useState<HistoryItem[]>([])
 	const [taskHistory, setTaskHistory] = useState<HistoryItem[]>([])
 	const [showAnnouncement, setShowAnnouncement] = useState(false)
 	const [showAnnouncement, setShowAnnouncement] = useState(false)
-	const [maestroUser, setMaestroUser] = useState<MaestroUser | undefined>(undefined)
+	const [koduCredits, setKoduCredits] = useState<number | undefined>(undefined)
 
 
 	useEffect(() => {
 	useEffect(() => {
 		vscode.postMessage({ type: "webviewDidLaunch" })
 		vscode.postMessage({ type: "webviewDidLaunch" })
@@ -45,7 +44,8 @@ const App: React.FC = () => {
 				const hasKey =
 				const hasKey =
 					message.state!.apiConfiguration?.apiKey !== undefined ||
 					message.state!.apiConfiguration?.apiKey !== undefined ||
 					message.state!.apiConfiguration?.openRouterApiKey !== undefined ||
 					message.state!.apiConfiguration?.openRouterApiKey !== undefined ||
-					message.state!.apiConfiguration?.awsAccessKey !== undefined
+					message.state!.apiConfiguration?.awsAccessKey !== undefined ||
+					message.state!.apiConfiguration?.koduApiKey !== undefined
 				setShowWelcome(!hasKey)
 				setShowWelcome(!hasKey)
 				setApiConfiguration(message.state!.apiConfiguration)
 				setApiConfiguration(message.state!.apiConfiguration)
 				setMaxRequestsPerTask(
 				setMaxRequestsPerTask(
@@ -55,12 +55,12 @@ const App: React.FC = () => {
 				setVscodeThemeName(message.state!.themeName)
 				setVscodeThemeName(message.state!.themeName)
 				setClaudeMessages(message.state!.claudeMessages)
 				setClaudeMessages(message.state!.claudeMessages)
 				setTaskHistory(message.state!.taskHistory)
 				setTaskHistory(message.state!.taskHistory)
+				setKoduCredits(message.state!.koduCredits)
 				// don't update showAnnouncement to false if shouldShowAnnouncement is false
 				// don't update showAnnouncement to false if shouldShowAnnouncement is false
 				if (message.state!.shouldShowAnnouncement) {
 				if (message.state!.shouldShowAnnouncement) {
 					setShowAnnouncement(true)
 					setShowAnnouncement(true)
 					vscode.postMessage({ type: "didShowAnnouncement" })
 					vscode.postMessage({ type: "didShowAnnouncement" })
 				}
 				}
-				setMaestroUser(message.state!.maestroUser)
 				setDidHydrateState(true)
 				setDidHydrateState(true)
 				break
 				break
 			case "action":
 			case "action":
@@ -103,8 +103,8 @@ const App: React.FC = () => {
 						<SettingsView
 						<SettingsView
 							version={version}
 							version={version}
 							apiConfiguration={apiConfiguration}
 							apiConfiguration={apiConfiguration}
-							maestroUser={maestroUser}
 							setApiConfiguration={setApiConfiguration}
 							setApiConfiguration={setApiConfiguration}
+							koduCredits={koduCredits}
 							maxRequestsPerTask={maxRequestsPerTask}
 							maxRequestsPerTask={maxRequestsPerTask}
 							setMaxRequestsPerTask={setMaxRequestsPerTask}
 							setMaxRequestsPerTask={setMaxRequestsPerTask}
 							customInstructions={customInstructions}
 							customInstructions={customInstructions}

+ 94 - 46
webview-ui/src/components/ApiOptions.tsx

@@ -5,7 +5,7 @@ import {
 	VSCodeOption,
 	VSCodeOption,
 	VSCodeTextField,
 	VSCodeTextField,
 } from "@vscode/webview-ui-toolkit/react"
 } from "@vscode/webview-ui-toolkit/react"
-import React, { useMemo } from "react"
+import React, { useCallback, useEffect, useMemo, useState } from "react"
 import {
 import {
 	ApiConfiguration,
 	ApiConfiguration,
 	ApiModelId,
 	ApiModelId,
@@ -14,27 +14,31 @@ import {
 	anthropicModels,
 	anthropicModels,
 	bedrockDefaultModelId,
 	bedrockDefaultModelId,
 	bedrockModels,
 	bedrockModels,
-	maestroDefaultModelId,
-	maestroModels,
+	koduDefaultModelId,
+	koduModels,
 	openRouterDefaultModelId,
 	openRouterDefaultModelId,
 	openRouterModels,
 	openRouterModels,
 } from "../../../src/shared/api"
 } from "../../../src/shared/api"
 import { vscode } from "../utils/vscode"
 import { vscode } from "../utils/vscode"
-import { MaestroUser } from "../../../src/shared/maestro"
+import { useEvent } from "react-use"
+import { ExtensionMessage } from "../../../src/shared/ExtensionMessage"
 
 
 interface ApiOptionsProps {
 interface ApiOptionsProps {
 	showModelOptions: boolean
 	showModelOptions: boolean
 	apiConfiguration?: ApiConfiguration
 	apiConfiguration?: ApiConfiguration
 	setApiConfiguration: React.Dispatch<React.SetStateAction<ApiConfiguration | undefined>>
 	setApiConfiguration: React.Dispatch<React.SetStateAction<ApiConfiguration | undefined>>
-	maestroUser?: MaestroUser
+	koduCredits?: number
+	apiErrorMessage?: string
 }
 }
 
 
 const ApiOptions: React.FC<ApiOptionsProps> = ({
 const ApiOptions: React.FC<ApiOptionsProps> = ({
 	showModelOptions,
 	showModelOptions,
 	apiConfiguration,
 	apiConfiguration,
 	setApiConfiguration,
 	setApiConfiguration,
-	maestroUser,
+	koduCredits,
+	apiErrorMessage,
 }) => {
 }) => {
+	const [didFetchKoduCredits, setDidFetchKoduCredits] = useState(false)
 	const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
 	const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
 		setApiConfiguration((prev) => ({ ...prev, [field]: event.target.value }))
 		setApiConfiguration((prev) => ({ ...prev, [field]: event.target.value }))
 	}
 	}
@@ -75,6 +79,27 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({
 		)
 		)
 	}
 	}
 
 
+	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 (
 	return (
 		<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
 		<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
 			<div className="dropdown-container">
 			<div className="dropdown-container">
@@ -82,10 +107,10 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({
 					<span style={{ fontWeight: 500 }}>API Provider</span>
 					<span style={{ fontWeight: 500 }}>API Provider</span>
 				</label>
 				</label>
 				<VSCodeDropdown id="api-provider" value={selectedProvider} onChange={handleInputChange("apiProvider")}>
 				<VSCodeDropdown id="api-provider" value={selectedProvider} onChange={handleInputChange("apiProvider")}>
+					<VSCodeOption value="kodu">Kodu</VSCodeOption>
 					<VSCodeOption value="anthropic">Anthropic</VSCodeOption>
 					<VSCodeOption value="anthropic">Anthropic</VSCodeOption>
 					<VSCodeOption value="bedrock">AWS Bedrock</VSCodeOption>
 					<VSCodeOption value="bedrock">AWS Bedrock</VSCodeOption>
 					<VSCodeOption value="openrouter">OpenRouter</VSCodeOption>
 					<VSCodeOption value="openrouter">OpenRouter</VSCodeOption>
-					<VSCodeOption value="maestro">Maestro</VSCodeOption>
 				</VSCodeDropdown>
 				</VSCodeDropdown>
 			</div>
 			</div>
 
 
@@ -139,42 +164,54 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({
 				</div>
 				</div>
 			)}
 			)}
 
 
-			{selectedProvider === "maestro" && (
+			{selectedProvider === "kodu" && (
 				<>
 				<>
-					{maestroUser ? (
+					{apiConfiguration?.koduApiKey !== undefined ? (
 						<div>
 						<div>
-							<span
+							<div style={{ marginBottom: 5, marginTop: 3 }}>
+								<span style={{ color: "var(--vscode-descriptionForeground)" }}>
+									Signed in as {apiConfiguration?.koduEmail || "Unknown"}
+								</span>{" "}
+								<VSCodeLink
+									style={{ display: "inline" }}
+									onClick={() => vscode.postMessage({ type: "didClickKoduSignOut" })}>
+									(sign out?)
+								</VSCodeLink>
+							</div>
+							<div style={{ marginBottom: 7 }}>
+								Credits remaining:{" "}
+								<span style={{ fontWeight: 500, opacity: didFetchKoduCredits ? 1 : 0.6 }}>
+									{formatPrice(koduCredits || 0)}
+								</span>
+							</div>
+							<VSCodeButton
+								appearance="primary"
+								onClick={() => vscode.postMessage({ type: "didClickKoduAddCredits" })}
 								style={{
 								style={{
-									fontWeight: 500,
-									color: "var(--vscode-testing-iconPassed)",
+									width: "fit-content",
 								}}>
 								}}>
-								<i
-									className={`codicon codicon-check`}
-									style={{
-										marginRight: 4,
-										marginBottom: 1,
-										fontSize: 11,
-										fontWeight: 700,
-										display: "inline-block",
-										verticalAlign: "bottom",
-									}}></i>
-								Signed in as {maestroUser.email}
-							</span>
-							<div style={{ margin: "4px 0px 2px 0px" }}>
-								<VSCodeButton
-									appearance="secondary"
-									onClick={() => vscode.postMessage({ type: "didClickMaestroSignOut" })}>
-									Sign out
-								</VSCodeButton>
-							</div>
+								Add Credits
+							</VSCodeButton>
+							<p
+								style={{
+									fontSize: "12px",
+									marginTop: "7px",
+									color: "var(--vscode-descriptionForeground)",
+								}}>
+								Kodu is recommended for its high rate limits and access to the latest features like
+								prompt caching.
+								<VSCodeLink href="https://kodu.ai/" style={{ display: "inline", fontSize: "12px" }}>
+									Learn more about Kodu here.
+								</VSCodeLink>
+							</p>
 						</div>
 						</div>
 					) : (
 					) : (
 						<div>
 						<div>
 							<div style={{ margin: "4px 0px" }}>
 							<div style={{ margin: "4px 0px" }}>
 								<VSCodeButton
 								<VSCodeButton
 									appearance="primary"
 									appearance="primary"
-									onClick={() => vscode.postMessage({ type: "didClickMaestroSignIn" })}>
-									Sign in to Maestro
+									onClick={() => vscode.postMessage({ type: "didClickKoduSignIn" })}>
+									Sign in to Kodu
 								</VSCodeButton>
 								</VSCodeButton>
 							</div>
 							</div>
 							<p
 							<p
@@ -183,7 +220,7 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({
 									marginTop: 5,
 									marginTop: 5,
 									color: "var(--vscode-descriptionForeground)",
 									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.
 								extension after signing in.
 							</p>
 							</p>
 						</div>
 						</div>
@@ -256,6 +293,17 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({
 				</div>
 				</div>
 			)}
 			)}
 
 
+			{apiErrorMessage && (
+				<p
+					style={{
+						margin: "-10px 0 4px 0",
+						fontSize: 12,
+						color: "var(--vscode-errorForeground)",
+					}}>
+					{apiErrorMessage}
+				</p>
+			)}
+
 			{showModelOptions && (
 			{showModelOptions && (
 				<>
 				<>
 					<div className="dropdown-container">
 					<div className="dropdown-container">
@@ -265,7 +313,7 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({
 						{selectedProvider === "anthropic" && createDropdown(anthropicModels)}
 						{selectedProvider === "anthropic" && createDropdown(anthropicModels)}
 						{selectedProvider === "openrouter" && createDropdown(openRouterModels)}
 						{selectedProvider === "openrouter" && createDropdown(openRouterModels)}
 						{selectedProvider === "bedrock" && createDropdown(bedrockModels)}
 						{selectedProvider === "bedrock" && createDropdown(bedrockModels)}
-						{selectedProvider === "maestro" && createDropdown(maestroModels)}
+						{selectedProvider === "kodu" && createDropdown(koduModels)}
 					</div>
 					</div>
 
 
 					<ModelInfoView modelInfo={selectedModelInfo} />
 					<ModelInfoView modelInfo={selectedModelInfo} />
@@ -275,16 +323,16 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({
 	)
 	)
 }
 }
 
 
-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 (
 	return (
 		<p style={{ fontSize: "12px", marginTop: "2px", color: "var(--vscode-descriptionForeground)" }}>
 		<p style={{ fontSize: "12px", marginTop: "2px", color: "var(--vscode-descriptionForeground)" }}>
 			<ModelInfoSupportsItem
 			<ModelInfoSupportsItem
@@ -369,8 +417,8 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
 			return getProviderData(openRouterModels, openRouterDefaultModelId)
 			return getProviderData(openRouterModels, openRouterDefaultModelId)
 		case "bedrock":
 		case "bedrock":
 			return getProviderData(bedrockModels, bedrockDefaultModelId)
 			return getProviderData(bedrockModels, bedrockDefaultModelId)
-		case "maestro":
-			return getProviderData(maestroModels, maestroDefaultModelId)
+		case "kodu":
+			return getProviderData(koduModels, koduDefaultModelId)
 		default:
 		default:
 			return getProviderData(anthropicModels, anthropicDefaultModelId)
 			return getProviderData(anthropicModels, anthropicDefaultModelId)
 	}
 	}

+ 4 - 14
webview-ui/src/components/SettingsView.tsx

@@ -4,13 +4,12 @@ import { ApiConfiguration } from "../../../src/shared/api"
 import { validateApiConfiguration, validateMaxRequestsPerTask } from "../utils/validate"
 import { validateApiConfiguration, validateMaxRequestsPerTask } from "../utils/validate"
 import { vscode } from "../utils/vscode"
 import { vscode } from "../utils/vscode"
 import ApiOptions from "./ApiOptions"
 import ApiOptions from "./ApiOptions"
-import { MaestroUser } from "../../../src/shared/maestro"
 
 
 type SettingsViewProps = {
 type SettingsViewProps = {
 	version: string
 	version: string
 	apiConfiguration?: ApiConfiguration
 	apiConfiguration?: ApiConfiguration
-	maestroUser?: MaestroUser
 	setApiConfiguration: React.Dispatch<React.SetStateAction<ApiConfiguration | undefined>>
 	setApiConfiguration: React.Dispatch<React.SetStateAction<ApiConfiguration | undefined>>
+	koduCredits?: number
 	maxRequestsPerTask: string
 	maxRequestsPerTask: string
 	setMaxRequestsPerTask: React.Dispatch<React.SetStateAction<string>>
 	setMaxRequestsPerTask: React.Dispatch<React.SetStateAction<string>>
 	customInstructions: string
 	customInstructions: string
@@ -21,8 +20,8 @@ type SettingsViewProps = {
 const SettingsView = ({
 const SettingsView = ({
 	version,
 	version,
 	apiConfiguration,
 	apiConfiguration,
-	maestroUser,
 	setApiConfiguration,
 	setApiConfiguration,
+	koduCredits,
 	maxRequestsPerTask,
 	maxRequestsPerTask,
 	setMaxRequestsPerTask,
 	setMaxRequestsPerTask,
 	customInstructions,
 	customInstructions,
@@ -96,20 +95,11 @@ const SettingsView = ({
 				<div style={{ marginBottom: 5 }}>
 				<div style={{ marginBottom: 5 }}>
 					<ApiOptions
 					<ApiOptions
 						apiConfiguration={apiConfiguration}
 						apiConfiguration={apiConfiguration}
-						maestroUser={maestroUser}
 						setApiConfiguration={setApiConfiguration}
 						setApiConfiguration={setApiConfiguration}
 						showModelOptions={true}
 						showModelOptions={true}
+						koduCredits={koduCredits}
+						apiErrorMessage={apiErrorMessage}
 					/>
 					/>
-					{apiErrorMessage && (
-						<p
-							style={{
-								margin: "-5px 0 12px 0",
-								fontSize: "12px",
-								color: "var(--vscode-errorForeground)",
-							}}>
-							{apiErrorMessage}
-						</p>
-					)}
 				</div>
 				</div>
 
 
 				<div style={{ marginBottom: 5 }}>
 				<div style={{ marginBottom: 5 }}>

+ 4 - 4
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."
 					return "You must provide a valid API key or choose a different provider."
 				}
 				}
 				break
 				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
 				break
 		}
 		}
 	}
 	}