Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit ce22e97

Browse files
committed
feat(webview): add Firmware provider settings UI with quota display
- Add firmwareQuota and requestFirmwareQuota message types - Create useFirmwareQuota hook for fetching quota info - Create FirmwareQuotaDisplay component showing remaining balance - Create Firmware settings component with API key, model picker, and quota - Add firmwareModelId to ModelPicker's ModelIdKey type - Export Firmware component from providers index
1 parent e0ed6d4 commit ce22e97

6 files changed

Lines changed: 187 additions & 0 deletions

File tree

packages/types/src/vscode-extension-host.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export interface ExtensionMessage {
7777
| "condenseTaskContextResponse"
7878
| "singleRouterModelFetchResponse"
7979
| "rooCreditBalance"
80+
| "firmwareQuota"
8081
| "indexingStatusUpdate"
8182
| "indexCleared"
8283
| "codebaseIndexConfig"
@@ -468,6 +469,7 @@ export interface WebviewMessage {
468469
| "requestLmStudioModels"
469470
| "requestRooModels"
470471
| "requestRooCreditBalance"
472+
| "requestFirmwareQuota"
471473
| "requestVsCodeLmModels"
472474
| "requestHuggingFaceModels"
473475
| "openImage"

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type ModelIdKey = keyof Pick<
3636
| "deepInfraModelId"
3737
| "ioIntelligenceModelId"
3838
| "vercelAiGatewayModelId"
39+
| "firmwareModelId"
3940
| "apiModelId"
4041
| "ollamaModelId"
4142
| "lmStudioModelId"
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { useCallback, useState } from "react"
2+
import { VSCodeTextField, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
3+
4+
import { type OrganizationAllowList, type ProviderSettings, type RouterModels } from "@roo-code/types"
5+
6+
import { vscode } from "@src/utils/vscode"
7+
import { useAppTranslation } from "@src/i18n/TranslationContext"
8+
import { Button } from "@src/components/ui"
9+
10+
import { inputEventTransform } from "../transforms"
11+
import { ModelPicker } from "../ModelPicker"
12+
import { FirmwareQuotaDisplay } from "./FirmwareQuotaDisplay"
13+
14+
const FIRMWARE_DEFAULT_MODEL_ID = "claude-sonnet-4-5"
15+
16+
type FirmwareProps = {
17+
apiConfiguration: ProviderSettings
18+
setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
19+
routerModels?: RouterModels
20+
refetchRouterModels: () => void
21+
organizationAllowList: OrganizationAllowList
22+
modelValidationError?: string
23+
simplifySettings?: boolean
24+
}
25+
26+
export const Firmware = ({
27+
apiConfiguration,
28+
setApiConfigurationField,
29+
routerModels,
30+
refetchRouterModels,
31+
organizationAllowList,
32+
modelValidationError,
33+
simplifySettings,
34+
}: FirmwareProps) => {
35+
const { t } = useAppTranslation()
36+
37+
const [didRefetch, setDidRefetch] = useState<boolean>()
38+
39+
const handleInputChange = useCallback(
40+
<K extends keyof ProviderSettings, E>(
41+
field: K,
42+
transform: (event: E) => ProviderSettings[K] = inputEventTransform,
43+
) =>
44+
(event: E | Event) => {
45+
setApiConfigurationField(field, transform(event as E))
46+
},
47+
[setApiConfigurationField],
48+
)
49+
50+
return (
51+
<>
52+
<div className="flex flex-col gap-1">
53+
<div className="flex items-center justify-between">
54+
<label className="block font-medium">{t("settings:providers.apiKey")}</label>
55+
<div className="flex items-center gap-3">
56+
<FirmwareQuotaDisplay />
57+
<VSCodeLink href="https://app.firmware.ai/api-keys">
58+
{t("settings:providers.getApiKey")}
59+
</VSCodeLink>
60+
</div>
61+
</div>
62+
<VSCodeTextField
63+
value={apiConfiguration?.firmwareApiKey || ""}
64+
type="password"
65+
onInput={handleInputChange("firmwareApiKey")}
66+
placeholder={t("settings:placeholders.apiKey")}
67+
className="w-full"
68+
/>
69+
</div>
70+
71+
<Button
72+
variant="outline"
73+
onClick={() => {
74+
vscode.postMessage({ type: "flushRouterModels", text: "firmware" })
75+
refetchRouterModels()
76+
setDidRefetch(true)
77+
}}>
78+
<div className="flex items-center gap-2">
79+
<span className="codicon codicon-refresh" />
80+
{t("settings:providers.refreshModels.label")}
81+
</div>
82+
</Button>
83+
{didRefetch && (
84+
<div className="flex items-center text-vscode-errorForeground">
85+
{t("settings:providers.refreshModels.hint")}
86+
</div>
87+
)}
88+
89+
<ModelPicker
90+
apiConfiguration={apiConfiguration}
91+
setApiConfigurationField={setApiConfigurationField}
92+
defaultModelId={FIRMWARE_DEFAULT_MODEL_ID}
93+
models={routerModels?.firmware ?? {}}
94+
modelIdKey="firmwareModelId"
95+
serviceName="Firmware"
96+
serviceUrl="https://app.firmware.ai/models"
97+
organizationAllowList={organizationAllowList}
98+
errorMessage={modelValidationError}
99+
simplifySettings={simplifySettings}
100+
/>
101+
</>
102+
)
103+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
2+
3+
import { useFirmwareQuota } from "@/components/ui/hooks/useFirmwareQuota"
4+
5+
export const FirmwareQuotaDisplay = () => {
6+
const { data: quota } = useFirmwareQuota()
7+
8+
if (quota === null || quota === undefined) {
9+
return null
10+
}
11+
12+
const formattedRemaining = quota.remaining.toFixed(2)
13+
const billingUrl = "https://app.firmware.ai/billing"
14+
15+
return (
16+
<VSCodeLink href={billingUrl} className="text-vscode-foreground hover:underline whitespace-nowrap">
17+
${formattedRemaining} remaining
18+
</VSCodeLink>
19+
)
20+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export { XAI } from "./XAI"
2727
export { ZAi } from "./ZAi"
2828
export { LiteLLM } from "./LiteLLM"
2929
export { Fireworks } from "./Fireworks"
30+
export { Firmware } from "./Firmware"
3031
export { Featherless } from "./Featherless"
3132
export { VercelAiGateway } from "./VercelAiGateway"
3233
export { DeepInfra } from "./DeepInfra"
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { useEffect, useState } from "react"
2+
3+
import type { ExtensionMessage } from "@roo-code/types"
4+
5+
import { vscode } from "@src/utils/vscode"
6+
7+
export interface FirmwareQuotaInfo {
8+
remaining: number
9+
windowHours: number
10+
}
11+
12+
export const useFirmwareQuota = () => {
13+
const [quota, setQuota] = useState<FirmwareQuotaInfo | null>(null)
14+
const [isLoading, setIsLoading] = useState(false)
15+
const [error, setError] = useState<string | null>(null)
16+
17+
useEffect(() => {
18+
setIsLoading(true)
19+
const requestId = `firmware-quota-${Date.now()}`
20+
21+
const handleMessage = (event: MessageEvent) => {
22+
const message: ExtensionMessage = event.data
23+
24+
if (message.type === "firmwareQuota" && message.requestId === requestId) {
25+
window.removeEventListener("message", handleMessage)
26+
clearTimeout(timeout)
27+
28+
if (message.values?.remaining !== undefined) {
29+
setQuota({
30+
remaining: message.values.remaining,
31+
windowHours: message.values.windowHours ?? 5,
32+
})
33+
setError(null)
34+
} else if (message.values?.error) {
35+
setError(message.values.error)
36+
setQuota(null)
37+
}
38+
39+
setIsLoading(false)
40+
}
41+
}
42+
43+
const timeout = setTimeout(() => {
44+
window.removeEventListener("message", handleMessage)
45+
setIsLoading(false)
46+
setError("Request timed out")
47+
}, 10000)
48+
49+
window.addEventListener("message", handleMessage)
50+
51+
vscode.postMessage({ type: "requestFirmwareQuota", requestId })
52+
53+
return () => {
54+
window.removeEventListener("message", handleMessage)
55+
clearTimeout(timeout)
56+
}
57+
}, [])
58+
59+
return { data: quota, isLoading, error }
60+
}

0 commit comments

Comments
 (0)