Add Maestro login button

This commit is contained in:
Saoud Rizwan
2024-08-22 11:02:25 -04:00
parent e8df2400bf
commit f6fd76823b
18 changed files with 375 additions and 11 deletions

50
package-lock.json generated
View File

@@ -1,17 +1,18 @@
{ {
"name": "claude-dev", "name": "claude-dev",
"version": "1.1.15", "version": "1.3.43",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "claude-dev", "name": "claude-dev",
"version": "1.1.15", "version": "1.3.43",
"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",
"@vscode/codicons": "^0.0.36", "@vscode/codicons": "^0.0.36",
"axios": "^1.7.4",
"default-shell": "^2.2.0", "default-shell": "^2.2.0",
"delay": "^6.0.0", "delay": "^6.0.0",
"diff": "^5.2.0", "diff": "^5.2.0",
@@ -23,7 +24,8 @@
"serialize-error": "^11.0.3", "serialize-error": "^11.0.3",
"tree-kill": "^1.2.2", "tree-kill": "^1.2.2",
"tree-sitter-wasms": "^0.1.11", "tree-sitter-wasms": "^0.1.11",
"web-tree-sitter": "^0.22.6" "web-tree-sitter": "^0.22.6",
"zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {
"@types/diff": "^5.2.1", "@types/diff": "^5.2.1",
@@ -5006,6 +5008,16 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -6243,6 +6255,25 @@
"dev": true, "dev": true,
"license": "ISC" "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": { "node_modules/for-each": {
"version": "0.3.3", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@@ -8523,6 +8554,11 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -10049,6 +10085,14 @@
"funding": { "funding": {
"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"
}
} }
} }
} }

View File

@@ -2,7 +2,7 @@
"name": "claude-dev", "name": "claude-dev",
"displayName": "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.", "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", "icon": "icon.png",
"engines": { "engines": {
"vscode": "^1.84.0" "vscode": "^1.84.0"
@@ -135,6 +135,7 @@
"@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",
"@vscode/codicons": "^0.0.36", "@vscode/codicons": "^0.0.36",
"axios": "^1.7.4",
"default-shell": "^2.2.0", "default-shell": "^2.2.0",
"delay": "^6.0.0", "delay": "^6.0.0",
"diff": "^5.2.0", "diff": "^5.2.0",
@@ -146,6 +147,7 @@
"serialize-error": "^11.0.3", "serialize-error": "^11.0.3",
"tree-kill": "^1.2.2", "tree-kill": "^1.2.2",
"tree-sitter-wasms": "^0.1.11", "tree-sitter-wasms": "^0.1.11",
"web-tree-sitter": "^0.22.6" "web-tree-sitter": "^0.22.6",
"zod": "^3.23.8"
} }
} }

View File

@@ -3,6 +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"
export interface ApiHandler { export interface ApiHandler {
createMessage( createMessage(
@@ -32,6 +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)
default: default:
return new AnthropicHandler(options) return new AnthropicHandler(options)
} }

114
src/api/maestro.ts Normal file
View File

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

View File

@@ -108,6 +108,20 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push( context.subscriptions.push(
vscode.workspace.registerTextDocumentContentProvider("claude-dev-diff", diffContentProvider) 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 // This method is called when your extension is deactivated

38
src/maestro/auth.ts Normal file
View File

@@ -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
src/maestro/index.ts Normal file
View File

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

View File

@@ -8,6 +8,8 @@ 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"
/* /*
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
@@ -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 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 = type GlobalStateKey =
| "apiProvider" | "apiProvider"
| "apiModelId" | "apiModelId"
@@ -32,9 +34,11 @@ 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({})
} }
/* /*
@@ -340,6 +344,12 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
case "exportTaskWithId": case "exportTaskWithId":
this.exportTaskWithId(message.text!) this.exportTaskWithId(message.text!)
break break
case "didClickMaestroSignIn":
didClickMaestroSignIn()
break
case "didClickMaestroSignOut":
await this.signOutMaestro()
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)
} }
@@ -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 // Task history
async getTaskWithId(id: string): Promise<{ async getTaskWithId(id: string): Promise<{
@@ -449,6 +489,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,
}, },
}) })
} }

View File

@@ -2,6 +2,7 @@
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 {
@@ -21,6 +22,7 @@ export interface ExtensionState {
claudeMessages: ClaudeMessage[] claudeMessages: ClaudeMessage[]
taskHistory: HistoryItem[] taskHistory: HistoryItem[]
shouldShowAnnouncement: boolean shouldShowAnnouncement: boolean
maestroUser?: MaestroUser
} }
export interface ClaudeMessage { export interface ClaudeMessage {

View File

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

View File

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

10
src/shared/maestro.ts Normal file
View File

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

View File

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

View File

@@ -21,7 +21,8 @@
"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" "web-vitals": "^2.1.4",
"zod": "^3.23.8"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",

View File

@@ -10,6 +10,7 @@ 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.
@@ -30,6 +31,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)
useEffect(() => { useEffect(() => {
vscode.postMessage({ type: "webviewDidLaunch" }) vscode.postMessage({ type: "webviewDidLaunch" })
@@ -58,6 +60,7 @@ const App: React.FC = () => {
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":
@@ -100,6 +103,7 @@ const App: React.FC = () => {
<SettingsView <SettingsView
version={version} version={version}
apiConfiguration={apiConfiguration} apiConfiguration={apiConfiguration}
maestroUser={maestroUser}
setApiConfiguration={setApiConfiguration} setApiConfiguration={setApiConfiguration}
maxRequestsPerTask={maxRequestsPerTask} maxRequestsPerTask={maxRequestsPerTask}
setMaxRequestsPerTask={setMaxRequestsPerTask} setMaxRequestsPerTask={setMaxRequestsPerTask}

View File

@@ -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 React, { useMemo } from "react"
import { import {
ApiConfiguration, ApiConfiguration,
@@ -8,17 +14,27 @@ import {
anthropicModels, anthropicModels,
bedrockDefaultModelId, bedrockDefaultModelId,
bedrockModels, bedrockModels,
maestroDefaultModelId,
maestroModels,
openRouterDefaultModelId, openRouterDefaultModelId,
openRouterModels, openRouterModels,
} from "../../../src/shared/api" } from "../../../src/shared/api"
import { vscode } from "../utils/vscode"
import { MaestroUser } from "../../../src/shared/maestro"
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
} }
const ApiOptions: React.FC<ApiOptionsProps> = ({ showModelOptions, apiConfiguration, setApiConfiguration }) => { const ApiOptions: React.FC<ApiOptionsProps> = ({
showModelOptions,
apiConfiguration,
setApiConfiguration,
maestroUser,
}) => {
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 }))
} }
@@ -69,6 +85,7 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({ showModelOptions, apiConfigurat
<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>
@@ -122,6 +139,48 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({ showModelOptions, apiConfigurat
</div> </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" && ( {selectedProvider === "bedrock" && (
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}> <div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
<VSCodeTextField <VSCodeTextField
@@ -196,6 +255,7 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({ showModelOptions, apiConfigurat
{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)}
</div> </div>
<ModelInfoView modelInfo={selectedModelInfo} /> <ModelInfoView modelInfo={selectedModelInfo} />
@@ -299,6 +359,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)
} }
} }

View File

@@ -4,10 +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>>
maxRequestsPerTask: string maxRequestsPerTask: string
setMaxRequestsPerTask: React.Dispatch<React.SetStateAction<string>> setMaxRequestsPerTask: React.Dispatch<React.SetStateAction<string>>
@@ -19,6 +21,7 @@ type SettingsViewProps = {
const SettingsView = ({ const SettingsView = ({
version, version,
apiConfiguration, apiConfiguration,
maestroUser,
setApiConfiguration, setApiConfiguration,
maxRequestsPerTask, maxRequestsPerTask,
setMaxRequestsPerTask, setMaxRequestsPerTask,
@@ -93,6 +96,7 @@ 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}
/> />

View File

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