Skip to content

Commit d92ec7a

Browse files
committed
feat(ui): add Poe provider settings UI
Add Poe settings component with API key input, base URL configuration, model picker with dynamic model list from the Poe API, and manual model refresh. Integrate into ApiOptions provider selection and the useSelectedModel hook.
1 parent 88cc81b commit d92ec7a

File tree

6 files changed

+199
-2
lines changed

6 files changed

+199
-2
lines changed

webview-ui/src/components/settings/ApiOptions.tsx

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { memo, useCallback, useEffect, useMemo, useState } from "react"
22
import { convertHeadersToObject } from "./utils/headers"
33
import { useDebounce } from "react-use"
4-
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
4+
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
55
import { ExternalLinkIcon } from "@radix-ui/react-icons"
66

77
import {
@@ -10,6 +10,7 @@ import {
1010
isRetiredProvider,
1111
DEFAULT_CONSECUTIVE_MISTAKE_LIMIT,
1212
openRouterDefaultModelId,
13+
poeDefaultModelId,
1314
requestyDefaultModelId,
1415
litellmDefaultModelId,
1516
openAiNativeDefaultModelId,
@@ -80,6 +81,7 @@ import {
8081
OpenAICompatible,
8182
OpenAICodex,
8283
OpenRouter,
84+
Poe,
8385
QwenCode,
8486
Requesty,
8587
Roo,
@@ -238,7 +240,7 @@ const ApiOptions = ({
238240
vscode.postMessage({ type: "requestLmStudioModels" })
239241
} else if (selectedProvider === "vscode-lm") {
240242
vscode.postMessage({ type: "requestVsCodeLmModels" })
241-
} else if (selectedProvider === "litellm" || selectedProvider === "roo") {
243+
} else if (selectedProvider === "litellm" || selectedProvider === "roo" || selectedProvider === "poe") {
242244
vscode.postMessage({ type: "requestRouterModels" })
243245
}
244246
},
@@ -252,6 +254,8 @@ const ApiOptions = ({
252254
apiConfiguration?.lmStudioBaseUrl,
253255
apiConfiguration?.litellmBaseUrl,
254256
apiConfiguration?.litellmApiKey,
257+
apiConfiguration?.poeApiKey,
258+
apiConfiguration?.poeBaseUrl,
255259
customHeaders,
256260
],
257261
)
@@ -356,6 +360,7 @@ const ApiOptions = ({
356360
: internationalZAiDefaultModelId,
357361
},
358362
fireworks: { field: "apiModelId", default: fireworksDefaultModelId },
363+
poe: { field: "apiModelId", default: poeDefaultModelId },
359364
roo: { field: "apiModelId", default: rooDefaultModelId },
360365
"vercel-ai-gateway": { field: "vercelAiGatewayModelId", default: vercelAiGatewayDefaultModelId },
361366
openai: { field: "openAiModelId" },
@@ -703,6 +708,16 @@ const ApiOptions = ({
703708
/>
704709
)}
705710

711+
{selectedProvider === "poe" && (
712+
<Poe
713+
apiConfiguration={apiConfiguration}
714+
setApiConfigurationField={setApiConfigurationField}
715+
organizationAllowList={organizationAllowList}
716+
modelValidationError={modelValidationError}
717+
simplifySettings={fromWelcomeView}
718+
/>
719+
)}
720+
706721
{selectedProvider === "roo" && (
707722
<Roo
708723
apiConfiguration={apiConfiguration}
@@ -800,6 +815,17 @@ const ApiOptions = ({
800815
}
801816
onChange={(value) => setApiConfigurationField("consecutiveMistakeLimit", value)}
802817
/>
818+
{selectedProvider === "poe" && (
819+
<VSCodeTextField
820+
value={apiConfiguration?.poeBaseUrl || ""}
821+
onInput={handleInputChange("poeBaseUrl")}
822+
placeholder="https://api.poe.com/v1"
823+
className="w-full">
824+
<label className="block font-medium mb-1">
825+
{t("settings:providers.poeBaseUrl")}
826+
</label>
827+
</VSCodeTextField>
828+
)}
803829
{selectedProvider === "openrouter" &&
804830
openRouterModelProviders &&
805831
Object.keys(openRouterModelProviders).length > 0 && (

webview-ui/src/components/settings/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,5 @@ export const PROVIDERS = [
6565
{ value: "minimax", label: "MiniMax", proxy: false },
6666
{ value: "baseten", label: "Baseten", proxy: false },
6767
{ value: "unbound", label: "Unbound", proxy: false },
68+
{ value: "poe", label: "Poe", proxy: false },
6869
].sort((a, b) => a.label.localeCompare(b.label))
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { useCallback, useState, useEffect, useRef } from "react"
2+
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
3+
4+
import {
5+
type ProviderSettings,
6+
type OrganizationAllowList,
7+
type ExtensionMessage,
8+
poeDefaultModelId,
9+
type ProviderName,
10+
} from "@roo-code/types"
11+
12+
import { RouterName } from "@roo/api"
13+
14+
import { useAppTranslation } from "@src/i18n/TranslationContext"
15+
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
16+
import { useExtensionState } from "@src/context/ExtensionStateContext"
17+
import { vscode } from "@src/utils/vscode"
18+
import { Button } from "@src/components/ui"
19+
20+
import { inputEventTransform } from "../transforms"
21+
import { ModelPicker } from "../ModelPicker"
22+
import { handleModelChangeSideEffects } from "../utils/providerModelConfig"
23+
24+
type PoeProps = {
25+
apiConfiguration: ProviderSettings
26+
setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
27+
organizationAllowList: OrganizationAllowList
28+
modelValidationError?: string
29+
simplifySettings?: boolean
30+
}
31+
32+
export const Poe = ({
33+
apiConfiguration,
34+
setApiConfigurationField,
35+
organizationAllowList,
36+
modelValidationError,
37+
simplifySettings,
38+
}: PoeProps) => {
39+
const { t } = useAppTranslation()
40+
const { routerModels } = useExtensionState()
41+
const [refreshStatus, setRefreshStatus] = useState<"idle" | "loading" | "success" | "error">("idle")
42+
const [refreshError, setRefreshError] = useState<string | undefined>()
43+
const poeErrorJustReceived = useRef(false)
44+
45+
useEffect(() => {
46+
const handleMessage = (event: MessageEvent<ExtensionMessage>) => {
47+
const message = event.data
48+
if (message.type === "singleRouterModelFetchResponse" && !message.success) {
49+
const providerName = message.values?.provider as RouterName
50+
if (providerName === "poe") {
51+
poeErrorJustReceived.current = true
52+
setRefreshStatus("error")
53+
setRefreshError(message.error)
54+
}
55+
} else if (message.type === "routerModels") {
56+
if (refreshStatus === "loading") {
57+
if (!poeErrorJustReceived.current) {
58+
setRefreshStatus("success")
59+
}
60+
}
61+
}
62+
}
63+
64+
window.addEventListener("message", handleMessage)
65+
return () => {
66+
window.removeEventListener("message", handleMessage)
67+
}
68+
}, [refreshStatus])
69+
70+
const handleInputChange = useCallback(
71+
<K extends keyof ProviderSettings, E>(
72+
field: K,
73+
transform: (event: E) => ProviderSettings[K] = inputEventTransform,
74+
) =>
75+
(event: E | Event) => {
76+
setApiConfigurationField(field, transform(event as E))
77+
},
78+
[setApiConfigurationField],
79+
)
80+
81+
const handleRefreshModels = useCallback(() => {
82+
poeErrorJustReceived.current = false
83+
setRefreshStatus("loading")
84+
setRefreshError(undefined)
85+
86+
const key = apiConfiguration.poeApiKey
87+
88+
if (!key) {
89+
setRefreshStatus("error")
90+
setRefreshError(t("settings:providers.refreshModels.missingConfig"))
91+
return
92+
}
93+
94+
vscode.postMessage({
95+
type: "requestRouterModels",
96+
values: { poeApiKey: key, poeBaseUrl: apiConfiguration.poeBaseUrl },
97+
})
98+
}, [apiConfiguration, t])
99+
100+
return (
101+
<>
102+
<VSCodeTextField
103+
value={apiConfiguration?.poeApiKey || ""}
104+
type="password"
105+
onInput={handleInputChange("poeApiKey")}
106+
placeholder={t("settings:placeholders.apiKey")}
107+
className="w-full">
108+
<label className="block font-medium mb-1">{t("settings:providers.poeApiKey")}</label>
109+
</VSCodeTextField>
110+
<div className="text-sm text-vscode-descriptionForeground -mt-2">
111+
{t("settings:providers.apiKeyStorageNotice")}
112+
</div>
113+
{!apiConfiguration?.poeApiKey && (
114+
<VSCodeButtonLink href="https://poe.com/api_key" appearance="secondary">
115+
{t("settings:providers.getPoeApiKey")}
116+
</VSCodeButtonLink>
117+
)}
118+
<Button
119+
variant="outline"
120+
onClick={handleRefreshModels}
121+
disabled={refreshStatus === "loading" || !apiConfiguration.poeApiKey}>
122+
<div className="flex items-center gap-2">
123+
{refreshStatus === "loading" ? (
124+
<span className="codicon codicon-loading codicon-modifier-spin" />
125+
) : (
126+
<span className="codicon codicon-refresh" />
127+
)}
128+
{t("settings:providers.refreshModels.label")}
129+
</div>
130+
</Button>
131+
{refreshStatus === "loading" && (
132+
<div className="text-sm text-vscode-descriptionForeground">
133+
{t("settings:providers.refreshModels.loading")}
134+
</div>
135+
)}
136+
{refreshStatus === "success" && (
137+
<div className="text-sm text-vscode-foreground">{t("settings:providers.refreshModels.success")}</div>
138+
)}
139+
{refreshStatus === "error" && (
140+
<div className="text-sm text-vscode-errorForeground">
141+
{refreshError || t("settings:providers.refreshModels.error")}
142+
</div>
143+
)}
144+
<ModelPicker
145+
apiConfiguration={apiConfiguration}
146+
setApiConfigurationField={setApiConfigurationField}
147+
defaultModelId={poeDefaultModelId}
148+
models={routerModels?.poe ?? {}}
149+
modelIdKey="apiModelId"
150+
serviceName="Poe"
151+
serviceUrl="https://poe.com"
152+
organizationAllowList={organizationAllowList}
153+
errorMessage={modelValidationError}
154+
simplifySettings={simplifySettings}
155+
onModelChange={(modelId) =>
156+
handleModelChangeSideEffects("poe" as ProviderName, modelId, setApiConfigurationField)
157+
}
158+
/>
159+
</>
160+
)
161+
}

webview-ui/src/components/settings/providers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export { OpenAI } from "./OpenAI"
1010
export { OpenAICodex } from "./OpenAICodex"
1111
export { OpenAICompatible } from "./OpenAICompatible"
1212
export { OpenRouter } from "./OpenRouter"
13+
export { Poe } from "./Poe"
1314
export { QwenCode } from "./QwenCode"
1415
export { Roo } from "./Roo"
1516
export { Requesty } from "./Requesty"

webview-ui/src/components/ui/hooks/useSelectedModel.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,11 @@ function getSelectedModel({
317317
const info = routerModels.roo?.[id]
318318
return { id, info }
319319
}
320+
case "poe": {
321+
const id = apiConfiguration.apiModelId ?? defaultModelId
322+
const info = routerModels.poe?.[id]
323+
return { id, info }
324+
}
320325
case "qwen-code": {
321326
const id = apiConfiguration.apiModelId ?? defaultModelId
322327
const info = qwenCodeModels[id as keyof typeof qwenCodeModels]

webview-ui/src/i18n/locales/en/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,9 @@
443443
"vertex1MContextBetaDescription": "Extends context window to 1 million tokens for Claude Sonnet 4.x / Claude Opus 4.6",
444444
"basetenApiKey": "Baseten API Key",
445445
"getBasetenApiKey": "Get Baseten API Key",
446+
"poeApiKey": "Poe API Key",
447+
"getPoeApiKey": "Get Poe API Key",
448+
"poeBaseUrl": "Poe Base URL",
446449
"fireworksApiKey": "Fireworks API Key",
447450
"getFireworksApiKey": "Get Fireworks API Key",
448451
"deepSeekApiKey": "DeepSeek API Key",

0 commit comments

Comments
 (0)