Jelajahi Sumber

Add Maestro login button

Saoud Rizwan 1 tahun lalu
induk
melakukan
f6fd76823b

+ 47 - 3
package-lock.json

@@ -1,17 +1,18 @@
 {
   "name": "claude-dev",
-  "version": "1.1.15",
+  "version": "1.3.43",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "claude-dev",
-      "version": "1.1.15",
+      "version": "1.3.43",
       "license": "MIT",
       "dependencies": {
         "@anthropic-ai/bedrock-sdk": "^0.10.2",
         "@anthropic-ai/sdk": "^0.26.0",
         "@vscode/codicons": "^0.0.36",
+        "axios": "^1.7.4",
         "default-shell": "^2.2.0",
         "delay": "^6.0.0",
         "diff": "^5.2.0",
@@ -23,7 +24,8 @@
         "serialize-error": "^11.0.3",
         "tree-kill": "^1.2.2",
         "tree-sitter-wasms": "^0.1.11",
-        "web-tree-sitter": "^0.22.6"
+        "web-tree-sitter": "^0.22.6",
+        "zod": "^3.23.8"
       },
       "devDependencies": {
         "@types/diff": "^5.2.1",
@@ -5006,6 +5008,16 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/axios": {
+      "version": "1.7.4",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
+      "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
+      "dependencies": {
+        "follow-redirects": "^1.15.6",
+        "form-data": "^4.0.0",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
     "node_modules/balanced-match": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -6243,6 +6255,25 @@
       "dev": true,
       "license": "ISC"
     },
+    "node_modules/follow-redirects": {
+      "version": "1.15.6",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
+      "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/for-each": {
       "version": "0.3.3",
       "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@@ -8523,6 +8554,11 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+    },
     "node_modules/punycode": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -10049,6 +10085,14 @@
       "funding": {
         "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"
+      }
     }
   }
 }

+ 4 - 2
package.json

@@ -2,7 +2,7 @@
   "name": "claude-dev",
   "displayName": "Claude Dev",
   "description": "Autonomous coding agent right in your IDE, capable of creating/editing files, executing commands, and more with your permission every step of the way.",
-  "version": "1.3.43",
+  "version": "1.4.0",
   "icon": "icon.png",
   "engines": {
     "vscode": "^1.84.0"
@@ -135,6 +135,7 @@
     "@anthropic-ai/bedrock-sdk": "^0.10.2",
     "@anthropic-ai/sdk": "^0.26.0",
     "@vscode/codicons": "^0.0.36",
+    "axios": "^1.7.4",
     "default-shell": "^2.2.0",
     "delay": "^6.0.0",
     "diff": "^5.2.0",
@@ -146,6 +147,7 @@
     "serialize-error": "^11.0.3",
     "tree-kill": "^1.2.2",
     "tree-sitter-wasms": "^0.1.11",
-    "web-tree-sitter": "^0.22.6"
+    "web-tree-sitter": "^0.22.6",
+    "zod": "^3.23.8"
   }
 }

+ 3 - 0
src/api/index.ts

@@ -3,6 +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"
 
 export interface ApiHandler {
 	createMessage(
@@ -32,6 +33,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
 			return new OpenRouterHandler(options)
 		case "bedrock":
 			return new AwsBedrockHandler(options)
+		case "maestro":
+			return new MaestroHandler(options)
 		default:
 			return new AnthropicHandler(options)
 	}

+ 114 - 0
src/api/maestro.ts

@@ -0,0 +1,114 @@
+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] }
+	}
+}

+ 14 - 0
src/extension.ts

@@ -108,6 +108,20 @@ export function activate(context: vscode.ExtensionContext) {
 	context.subscriptions.push(
 		vscode.workspace.registerTextDocumentContentProvider("claude-dev-diff", diffContentProvider)
 	)
+
+	// URI Handler
+	const handleUri = async (uri: vscode.Uri) => {
+		const query = new URLSearchParams(uri.query)
+		const token = query.get("token")
+		const fixedToken = token?.replaceAll("jwt?token=", "")
+		console.log(fixedToken)
+		console.log(uri)
+
+		if (fixedToken) {
+			await sidebarProvider.saveMaestroToken(fixedToken)
+		}
+	}
+	context.subscriptions.push(vscode.window.registerUriHandler({ handleUri }))
 }
 
 // This method is called when your extension is deactivated

+ 38 - 0
src/maestro/auth.ts

@@ -0,0 +1,38 @@
+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
+	}
+}

+ 1 - 0
src/maestro/index.ts

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

+ 42 - 1
src/providers/ClaudeDevProvider.ts

@@ -8,6 +8,8 @@ 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"
 
 /*
 https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -15,7 +17,7 @@ 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"
+type SecretKey = "apiKey" | "openRouterApiKey" | "awsAccessKey" | "awsSecretKey" | "maestroToken"
 type GlobalStateKey =
 	| "apiProvider"
 	| "apiModelId"
@@ -32,9 +34,11 @@ 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({})
 	}
 
 	/*
@@ -340,6 +344,12 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
 					case "exportTaskWithId":
 						this.exportTaskWithId(message.text!)
 						break
+					case "didClickMaestroSignIn":
+						didClickMaestroSignIn()
+						break
+					case "didClickMaestroSignOut":
+						await this.signOutMaestro()
+						break
 					// Add more switch case statements here as more webview message commands
 					// are created within the webview context (i.e. inside media/main.js)
 				}
@@ -349,6 +359,36 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
 		)
 	}
 
+	// Maestro
+
+	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
+		await this.postStateToWebview()
+		return user
+	}
+
+	async signOutMaestro() {
+		await this.storeSecret("maestroToken", undefined)
+		this.claudeDev?.updateApi({ apiProvider: "maestro", maestroToken: undefined })
+		this.maestroUser = undefined
+		await this.postStateToWebview()
+	}
+
 	// Task history
 
 	async getTaskWithId(id: string): Promise<{
@@ -449,6 +489,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,
 			},
 		})
 	}

+ 2 - 0
src/shared/ExtensionMessage.ts

@@ -2,6 +2,7 @@
 
 import { ApiConfiguration } from "./api"
 import { HistoryItem } from "./HistoryItem"
+import { MaestroUser } from "./maestro"
 
 // webview will hold state
 export interface ExtensionMessage {
@@ -21,6 +22,7 @@ export interface ExtensionState {
 	claudeMessages: ClaudeMessage[]
 	taskHistory: HistoryItem[]
 	shouldShowAnnouncement: boolean
+	maestroUser?: MaestroUser
 }
 
 export interface ClaudeMessage {

+ 2 - 0
src/shared/WebviewMessage.ts

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

+ 9 - 1
src/shared/api.ts

@@ -1,4 +1,4 @@
-export type ApiProvider = "anthropic" | "openrouter" | "bedrock"
+export type ApiProvider = "anthropic" | "openrouter" | "bedrock" | "maestro"
 
 export interface ApiHandlerOptions {
 	apiModelId?: ApiModelId
@@ -7,6 +7,7 @@ export interface ApiHandlerOptions {
 	awsAccessKey?: string
 	awsSecretKey?: string
 	awsRegion?: string
+	maestroToken?: string
 }
 
 export type ApiConfiguration = ApiHandlerOptions & {
@@ -232,3 +233,10 @@ export const openRouterModels = {
 	// 	outputPrice: 1.5,
 	// },
 } 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 = {
+	...anthropicModels,
+} as const satisfies Record<string, ModelInfo>

+ 10 - 0
src/shared/maestro.ts

@@ -0,0 +1,10 @@
+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>

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

@@ -26,7 +26,8 @@
         "react-virtuoso": "^4.7.13",
         "rewire": "^7.0.0",
         "typescript": "^4.9.5",
-        "web-vitals": "^2.1.4"
+        "web-vitals": "^2.1.4",
+        "zod": "^3.23.8"
       },
       "devDependencies": {
         "@types/react-scroll": "^1.8.10",
@@ -21449,6 +21450,14 @@
         "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",

+ 2 - 1
webview-ui/package.json

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

+ 4 - 0
webview-ui/src/App.tsx

@@ -10,6 +10,7 @@ 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.
@@ -30,6 +31,7 @@ const App: React.FC = () => {
 	const [claudeMessages, setClaudeMessages] = useState<ClaudeMessage[]>([])
 	const [taskHistory, setTaskHistory] = useState<HistoryItem[]>([])
 	const [showAnnouncement, setShowAnnouncement] = useState(false)
+	const [maestroUser, setMaestroUser] = useState<MaestroUser | undefined>(undefined)
 
 	useEffect(() => {
 		vscode.postMessage({ type: "webviewDidLaunch" })
@@ -58,6 +60,7 @@ const App: React.FC = () => {
 					setShowAnnouncement(true)
 					vscode.postMessage({ type: "didShowAnnouncement" })
 				}
+				setMaestroUser(message.state!.maestroUser)
 				setDidHydrateState(true)
 				break
 			case "action":
@@ -100,6 +103,7 @@ const App: React.FC = () => {
 						<SettingsView
 							version={version}
 							apiConfiguration={apiConfiguration}
+							maestroUser={maestroUser}
 							setApiConfiguration={setApiConfiguration}
 							maxRequestsPerTask={maxRequestsPerTask}
 							setMaxRequestsPerTask={setMaxRequestsPerTask}

+ 64 - 2
webview-ui/src/components/ApiOptions.tsx

@@ -1,4 +1,10 @@
-import { VSCodeDropdown, VSCodeLink, VSCodeOption, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
+import {
+	VSCodeButton,
+	VSCodeDropdown,
+	VSCodeLink,
+	VSCodeOption,
+	VSCodeTextField,
+} from "@vscode/webview-ui-toolkit/react"
 import React, { useMemo } from "react"
 import {
 	ApiConfiguration,
@@ -8,17 +14,27 @@ import {
 	anthropicModels,
 	bedrockDefaultModelId,
 	bedrockModels,
+	maestroDefaultModelId,
+	maestroModels,
 	openRouterDefaultModelId,
 	openRouterModels,
 } from "../../../src/shared/api"
+import { vscode } from "../utils/vscode"
+import { MaestroUser } from "../../../src/shared/maestro"
 
 interface ApiOptionsProps {
 	showModelOptions: boolean
 	apiConfiguration?: ApiConfiguration
 	setApiConfiguration: React.Dispatch<React.SetStateAction<ApiConfiguration | undefined>>
+	maestroUser?: MaestroUser
 }
 
-const ApiOptions: React.FC<ApiOptionsProps> = ({ showModelOptions, apiConfiguration, setApiConfiguration }) => {
+const ApiOptions: React.FC<ApiOptionsProps> = ({
+	showModelOptions,
+	apiConfiguration,
+	setApiConfiguration,
+	maestroUser,
+}) => {
 	const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
 		setApiConfiguration((prev) => ({ ...prev, [field]: event.target.value }))
 	}
@@ -69,6 +85,7 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({ showModelOptions, apiConfigurat
 					<VSCodeOption value="anthropic">Anthropic</VSCodeOption>
 					<VSCodeOption value="bedrock">AWS Bedrock</VSCodeOption>
 					<VSCodeOption value="openrouter">OpenRouter</VSCodeOption>
+					<VSCodeOption value="maestro">Maestro</VSCodeOption>
 				</VSCodeDropdown>
 			</div>
 
@@ -122,6 +139,48 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({ showModelOptions, apiConfigurat
 				</div>
 			)}
 
+			{selectedProvider === "maestro" && (
+				<>
+					{maestroUser ? (
+						<div>
+							<p
+								style={{
+									marginTop: 3,
+								}}>
+								<span style={{ fontWeight: 500 }}>Signed in as: </span>
+								<span style={{ color: "var(--vscode-testing-iconPassed)" }}>{maestroUser.email}</span>
+							</p>
+							<div style={{ margin: "4px 0px" }}>
+								<VSCodeButton
+									appearance="secondary"
+									onClick={() => vscode.postMessage({ type: "didClickMaestroSignOut" })}>
+									Sign out
+								</VSCodeButton>
+							</div>
+						</div>
+					) : (
+						<div>
+							<div style={{ margin: "4px 0px" }}>
+								<VSCodeButton
+									appearance="primary"
+									onClick={() => vscode.postMessage({ type: "didClickMaestroSignIn" })}>
+									Sign in to Maestro
+								</VSCodeButton>
+							</div>
+							<p
+								style={{
+									fontSize: 12,
+									marginTop: 5,
+									color: "var(--vscode-descriptionForeground)",
+								}}>
+								This will open your browser to sign in to Maestro. You will be redirected back to the
+								extension after signing in.
+							</p>
+						</div>
+					)}
+				</>
+			)}
+
 			{selectedProvider === "bedrock" && (
 				<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
 					<VSCodeTextField
@@ -196,6 +255,7 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({ showModelOptions, apiConfigurat
 						{selectedProvider === "anthropic" && createDropdown(anthropicModels)}
 						{selectedProvider === "openrouter" && createDropdown(openRouterModels)}
 						{selectedProvider === "bedrock" && createDropdown(bedrockModels)}
+						{selectedProvider === "maestro" && createDropdown(maestroModels)}
 					</div>
 
 					<ModelInfoView modelInfo={selectedModelInfo} />
@@ -299,6 +359,8 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
 			return getProviderData(openRouterModels, openRouterDefaultModelId)
 		case "bedrock":
 			return getProviderData(bedrockModels, bedrockDefaultModelId)
+		case "maestro":
+			return getProviderData(maestroModels, maestroDefaultModelId)
 	}
 }
 

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

@@ -4,10 +4,12 @@ import { ApiConfiguration } from "../../../src/shared/api"
 import { validateApiConfiguration, validateMaxRequestsPerTask } from "../utils/validate"
 import { vscode } from "../utils/vscode"
 import ApiOptions from "./ApiOptions"
+import { MaestroUser } from "../../../src/shared/maestro"
 
 type SettingsViewProps = {
 	version: string
 	apiConfiguration?: ApiConfiguration
+	maestroUser?: MaestroUser
 	setApiConfiguration: React.Dispatch<React.SetStateAction<ApiConfiguration | undefined>>
 	maxRequestsPerTask: string
 	setMaxRequestsPerTask: React.Dispatch<React.SetStateAction<string>>
@@ -19,6 +21,7 @@ type SettingsViewProps = {
 const SettingsView = ({
 	version,
 	apiConfiguration,
+	maestroUser,
 	setApiConfiguration,
 	maxRequestsPerTask,
 	setMaxRequestsPerTask,
@@ -93,6 +96,7 @@ const SettingsView = ({
 				<div style={{ marginBottom: 5 }}>
 					<ApiOptions
 						apiConfiguration={apiConfiguration}
+						maestroUser={maestroUser}
 						setApiConfiguration={setApiConfiguration}
 						showModelOptions={true}
 					/>

+ 5 - 0
webview-ui/src/utils/validate.ts

@@ -18,6 +18,11 @@ 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."
+				// }
+				break
 		}
 	}
 	return undefined