Add Kodu provider

This commit is contained in:
Saoud Rizwan
2024-08-24 09:18:27 -04:00
parent cc9637e0fe
commit df4e8e7afc
19 changed files with 380 additions and 305 deletions

View File

@@ -26,8 +26,7 @@
"react-virtuoso": "^4.7.13",
"rewire": "^7.0.0",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4",
"zod": "^3.23.8"
"web-vitals": "^2.1.4"
},
"devDependencies": {
"@types/react-scroll": "^1.8.10",
@@ -21450,14 +21449,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zod": {
"version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",

View File

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

View File

@@ -10,7 +10,6 @@ import WelcomeView from "./components/WelcomeView"
import { vscode } from "./utils/vscode"
import HistoryView from "./components/HistoryView"
import { HistoryItem } from "../../src/shared/HistoryItem"
import { MaestroUser } from "../../src/shared/maestro"
/*
The contents of webviews however are created when the webview becomes visible and destroyed when the webview is moved into the background. Any state inside the webview will be lost when the webview is moved to a background tab.
@@ -31,7 +30,7 @@ const App: React.FC = () => {
const [claudeMessages, setClaudeMessages] = useState<ClaudeMessage[]>([])
const [taskHistory, setTaskHistory] = useState<HistoryItem[]>([])
const [showAnnouncement, setShowAnnouncement] = useState(false)
const [maestroUser, setMaestroUser] = useState<MaestroUser | undefined>(undefined)
const [koduCredits, setKoduCredits] = useState<number | undefined>(undefined)
useEffect(() => {
vscode.postMessage({ type: "webviewDidLaunch" })
@@ -45,7 +44,8 @@ const App: React.FC = () => {
const hasKey =
message.state!.apiConfiguration?.apiKey !== undefined ||
message.state!.apiConfiguration?.openRouterApiKey !== undefined ||
message.state!.apiConfiguration?.awsAccessKey !== undefined
message.state!.apiConfiguration?.awsAccessKey !== undefined ||
message.state!.apiConfiguration?.koduApiKey !== undefined
setShowWelcome(!hasKey)
setApiConfiguration(message.state!.apiConfiguration)
setMaxRequestsPerTask(
@@ -55,12 +55,12 @@ const App: React.FC = () => {
setVscodeThemeName(message.state!.themeName)
setClaudeMessages(message.state!.claudeMessages)
setTaskHistory(message.state!.taskHistory)
setKoduCredits(message.state!.koduCredits)
// don't update showAnnouncement to false if shouldShowAnnouncement is false
if (message.state!.shouldShowAnnouncement) {
setShowAnnouncement(true)
vscode.postMessage({ type: "didShowAnnouncement" })
}
setMaestroUser(message.state!.maestroUser)
setDidHydrateState(true)
break
case "action":
@@ -103,8 +103,8 @@ const App: React.FC = () => {
<SettingsView
version={version}
apiConfiguration={apiConfiguration}
maestroUser={maestroUser}
setApiConfiguration={setApiConfiguration}
koduCredits={koduCredits}
maxRequestsPerTask={maxRequestsPerTask}
setMaxRequestsPerTask={setMaxRequestsPerTask}
customInstructions={customInstructions}

View File

@@ -5,7 +5,7 @@ import {
VSCodeOption,
VSCodeTextField,
} from "@vscode/webview-ui-toolkit/react"
import React, { useMemo } from "react"
import React, { useCallback, useEffect, useMemo, useState } from "react"
import {
ApiConfiguration,
ApiModelId,
@@ -14,27 +14,31 @@ import {
anthropicModels,
bedrockDefaultModelId,
bedrockModels,
maestroDefaultModelId,
maestroModels,
koduDefaultModelId,
koduModels,
openRouterDefaultModelId,
openRouterModels,
} from "../../../src/shared/api"
import { vscode } from "../utils/vscode"
import { MaestroUser } from "../../../src/shared/maestro"
import { useEvent } from "react-use"
import { ExtensionMessage } from "../../../src/shared/ExtensionMessage"
interface ApiOptionsProps {
showModelOptions: boolean
apiConfiguration?: ApiConfiguration
setApiConfiguration: React.Dispatch<React.SetStateAction<ApiConfiguration | undefined>>
maestroUser?: MaestroUser
koduCredits?: number
apiErrorMessage?: string
}
const ApiOptions: React.FC<ApiOptionsProps> = ({
showModelOptions,
apiConfiguration,
setApiConfiguration,
maestroUser,
koduCredits,
apiErrorMessage,
}) => {
const [didFetchKoduCredits, setDidFetchKoduCredits] = useState(false)
const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
setApiConfiguration((prev) => ({ ...prev, [field]: event.target.value }))
}
@@ -75,6 +79,27 @@ const ApiOptions: React.FC<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 (
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
<div className="dropdown-container">
@@ -82,10 +107,10 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({
<span style={{ fontWeight: 500 }}>API Provider</span>
</label>
<VSCodeDropdown id="api-provider" value={selectedProvider} onChange={handleInputChange("apiProvider")}>
<VSCodeOption value="kodu">Kodu</VSCodeOption>
<VSCodeOption value="anthropic">Anthropic</VSCodeOption>
<VSCodeOption value="bedrock">AWS Bedrock</VSCodeOption>
<VSCodeOption value="openrouter">OpenRouter</VSCodeOption>
<VSCodeOption value="maestro">Maestro</VSCodeOption>
</VSCodeDropdown>
</div>
@@ -139,42 +164,54 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({
</div>
)}
{selectedProvider === "maestro" && (
{selectedProvider === "kodu" && (
<>
{maestroUser ? (
{apiConfiguration?.koduApiKey !== undefined ? (
<div>
<span
style={{
fontWeight: 500,
color: "var(--vscode-testing-iconPassed)",
}}>
<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 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={{
width: "fit-content",
}}>
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 style={{ margin: "4px 0px" }}>
<VSCodeButton
appearance="primary"
onClick={() => vscode.postMessage({ type: "didClickMaestroSignIn" })}>
Sign in to Maestro
onClick={() => vscode.postMessage({ type: "didClickKoduSignIn" })}>
Sign in to Kodu
</VSCodeButton>
</div>
<p
@@ -183,7 +220,7 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({
marginTop: 5,
color: "var(--vscode-descriptionForeground)",
}}>
This will open your browser to sign in to Maestro. You will be redirected back to the
This will open your browser to sign in to Kodu. You will be redirected back to the
extension after signing in.
</p>
</div>
@@ -256,6 +293,17 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({
</div>
)}
{apiErrorMessage && (
<p
style={{
margin: "-10px 0 4px 0",
fontSize: 12,
color: "var(--vscode-errorForeground)",
}}>
{apiErrorMessage}
</p>
)}
{showModelOptions && (
<>
<div className="dropdown-container">
@@ -265,7 +313,7 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({
{selectedProvider === "anthropic" && createDropdown(anthropicModels)}
{selectedProvider === "openrouter" && createDropdown(openRouterModels)}
{selectedProvider === "bedrock" && createDropdown(bedrockModels)}
{selectedProvider === "maestro" && createDropdown(maestroModels)}
{selectedProvider === "kodu" && createDropdown(koduModels)}
</div>
<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 (
<p style={{ fontSize: "12px", marginTop: "2px", color: "var(--vscode-descriptionForeground)" }}>
<ModelInfoSupportsItem
@@ -369,8 +417,8 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
return getProviderData(openRouterModels, openRouterDefaultModelId)
case "bedrock":
return getProviderData(bedrockModels, bedrockDefaultModelId)
case "maestro":
return getProviderData(maestroModels, maestroDefaultModelId)
case "kodu":
return getProviderData(koduModels, koduDefaultModelId)
default:
return getProviderData(anthropicModels, anthropicDefaultModelId)
}

View File

@@ -4,13 +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>>
koduCredits?: number
maxRequestsPerTask: string
setMaxRequestsPerTask: React.Dispatch<React.SetStateAction<string>>
customInstructions: string
@@ -21,8 +20,8 @@ type SettingsViewProps = {
const SettingsView = ({
version,
apiConfiguration,
maestroUser,
setApiConfiguration,
koduCredits,
maxRequestsPerTask,
setMaxRequestsPerTask,
customInstructions,
@@ -96,20 +95,11 @@ const SettingsView = ({
<div style={{ marginBottom: 5 }}>
<ApiOptions
apiConfiguration={apiConfiguration}
maestroUser={maestroUser}
setApiConfiguration={setApiConfiguration}
showModelOptions={true}
koduCredits={koduCredits}
apiErrorMessage={apiErrorMessage}
/>
{apiErrorMessage && (
<p
style={{
margin: "-5px 0 12px 0",
fontSize: "12px",
color: "var(--vscode-errorForeground)",
}}>
{apiErrorMessage}
</p>
)}
</div>
<div style={{ marginBottom: 5 }}>

View File

@@ -18,10 +18,10 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s
return "You must provide a valid API key or choose a different provider."
}
break
case "maestro":
// if (!apiConfiguration.maestroApiKey) {
// return "You must provide a valid API key or choose a different provider."
// }
case "kodu":
if (!apiConfiguration.koduApiKey) {
return "You must sign in to Kodu to use it as an API provider."
}
break
}
}