Skip to content

Commit d395525

Browse files
authored
feat(webview): add OpenAI Codex speed selector (#1076)
* feat(webview): add OpenAI Codex speed selector * test(webview): add Codex speed selector visual coverage * test(webview): exclude visual fixture from unit coverage * test(webview): cover codex speed themes * test(webview): combine codex speed snapshots * fix(webview): label codex speed selector
1 parent 05ab11f commit d395525

27 files changed

Lines changed: 402 additions & 1 deletion

webview-ui/playwright/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import "./vscode-theme-dark.css"
22
import "@vscode/codicons/dist/codicon.css"
33
import "../src/index.css"
4+
import "./vscode-theme-light.css"
45

56
// Components read `window.IMAGES_BASE_URI` at mount time to resolve extension
67
// image assets. Under Playwright CT the extension host isn't present, so seed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
.vscode-light {
2+
color-scheme: light;
3+
--vscode-foreground: #3b3b3b;
4+
--vscode-disabledForeground: #61616180;
5+
--vscode-descriptionForeground: #717171;
6+
--vscode-errorForeground: #a1260d;
7+
--vscode-focusBorder: #0090f1;
8+
--vscode-editor-foreground: #333333;
9+
--vscode-editor-background: #ffffff;
10+
--vscode-button-foreground: #ffffff;
11+
--vscode-button-background: #007acc;
12+
--vscode-button-hoverBackground: #0062a3;
13+
--vscode-dropdown-foreground: #3b3b3b;
14+
--vscode-dropdown-background: #ffffff;
15+
--vscode-dropdown-border: #cecece;
16+
--vscode-input-foreground: #3b3b3b;
17+
--vscode-input-background: #ffffff;
18+
--vscode-input-border: #cecece;
19+
--vscode-list-hoverForeground: #3b3b3b;
20+
--vscode-list-hoverBackground: #e8e8e8;
21+
--vscode-list-focusBackground: #d6ebff;
22+
--vscode-list-activeSelectionBackground: #0060c0;
23+
--vscode-list-activeSelectionForeground: #ffffff;
24+
--vscode-toolbar-hoverBackground: #e8e8e8;
25+
--vscode-widget-border: #d4d4d4;
26+
--vscode-widget-shadow: #00000029;
27+
--vscode-menu-foreground: #3b3b3b;
28+
--vscode-menu-background: #ffffff;
29+
--vscode-editorHoverWidget-foreground: #3b3b3b;
30+
--vscode-editorHoverWidget-background: #f8f8f8;
31+
--vscode-editorHoverWidget-border: #c8c8c8;
32+
--vscode-scrollbarSlider-background: #64646466;
33+
--vscode-scrollbarSlider-hoverBackground: #64646499;
34+
--vscode-scrollbarSlider-activeBackground: #00000099;
35+
}

webview-ui/src/components/settings/providers/OpenAICodex.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
import React from "react"
22

3-
import { type ProviderSettings, openAiCodexDefaultModelId, openAiCodexModels } from "@roo-code/types"
3+
import {
4+
OPEN_AI_CODEX_SERVICE_TIER_KEY,
5+
type ProviderSettings,
6+
openAiCodexDefaultModelId,
7+
openAiCodexModels,
8+
} from "@roo-code/types"
49

510
import { useAppTranslation } from "@src/i18n/TranslationContext"
611
import { Button } from "@src/components/ui"
712
import { vscode } from "@src/utils/vscode"
813

914
import { ModelPicker } from "../ModelPicker"
1015
import { OpenAICodexRateLimitDashboard } from "./OpenAICodexRateLimitDashboard"
16+
import { OpenAICodexSpeedSelector } from "./OpenAICodexSpeedSelector"
1117

1218
interface OpenAICodexProps {
1319
apiConfiguration: ProviderSettings
@@ -66,6 +72,11 @@ export const OpenAICodex: React.FC<OpenAICodexProps> = ({
6672
simplifySettings={simplifySettings}
6773
hidePricing
6874
/>
75+
76+
<OpenAICodexSpeedSelector
77+
value={apiConfiguration[OPEN_AI_CODEX_SERVICE_TIER_KEY]}
78+
onValueChange={(value) => setApiConfigurationField(OPEN_AI_CODEX_SERVICE_TIER_KEY, value)}
79+
/>
6980
</div>
7081
)
7182
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React from "react"
2+
3+
import {
4+
OpenAiCodexServiceTier,
5+
type OpenAiCodexServiceTier as OpenAiCodexServiceTierValue,
6+
} from "@roo-code/types/model"
7+
8+
import { useAppTranslation } from "@src/i18n/TranslationContext"
9+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, StandardTooltip } from "@src/components/ui"
10+
11+
interface OpenAICodexSpeedSelectorProps {
12+
value?: OpenAiCodexServiceTierValue
13+
onValueChange: (value: OpenAiCodexServiceTierValue) => void
14+
}
15+
16+
export const OpenAICodexSpeedSelector: React.FC<OpenAICodexSpeedSelectorProps> = ({ value, onValueChange }) => {
17+
const { t } = useAppTranslation()
18+
const selectId = React.useId()
19+
20+
return (
21+
<div className="flex flex-col gap-1" data-testid="openai-codex-service-tier">
22+
<div className="flex items-center gap-1">
23+
<label htmlFor={selectId} className="block font-medium">
24+
{t("settings:openAiCodexSpeed.label")}
25+
</label>
26+
<StandardTooltip content={t("settings:openAiCodexSpeed.tooltip")}>
27+
<i className="codicon codicon-info text-vscode-descriptionForeground text-xs" />
28+
</StandardTooltip>
29+
</div>
30+
<Select value={value ?? OpenAiCodexServiceTier.Default} onValueChange={onValueChange}>
31+
<SelectTrigger id={selectId} className="w-full">
32+
<SelectValue placeholder={t("settings:common.select")} />
33+
</SelectTrigger>
34+
<SelectContent>
35+
<SelectItem value={OpenAiCodexServiceTier.Default}>
36+
{t("settings:openAiCodexSpeed.standard")}
37+
</SelectItem>
38+
<SelectItem value={OpenAiCodexServiceTier.Priority}>
39+
{t("settings:openAiCodexSpeed.fast")}
40+
</SelectItem>
41+
</SelectContent>
42+
</Select>
43+
</div>
44+
)
45+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import React from "react"
2+
3+
import {
4+
OPEN_AI_CODEX_SERVICE_TIER_KEY,
5+
OpenAiCodexServiceTier,
6+
providerIdentifiers,
7+
type ProviderSettings,
8+
} from "@roo-code/types"
9+
10+
import { fireEvent, render, screen } from "@/utils/test-utils"
11+
import { vscode } from "@src/utils/vscode"
12+
13+
import { OpenAICodex } from "../OpenAICodex"
14+
15+
vi.mock("@src/i18n/TranslationContext", () => ({
16+
useAppTranslation: () => ({
17+
t: (key: string) =>
18+
({
19+
"settings:openAiCodexSpeed.label": "Speed",
20+
"settings:openAiCodexSpeed.tooltip":
21+
"Fast uses Codex priority processing for about 1.5x speed and consumes more subscription quota.",
22+
"settings:openAiCodexSpeed.standard": "Standard",
23+
"settings:openAiCodexSpeed.fast": "Fast (1.5x speed, increased usage)",
24+
})[key] ?? key,
25+
}),
26+
}))
27+
28+
vi.mock("@src/components/ui", () => ({
29+
Button: ({ children, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) => (
30+
<button {...props}>{children}</button>
31+
),
32+
Select: ({ children, value, onValueChange }: any) => (
33+
<select aria-label="Speed" value={value} onChange={(event) => onValueChange(event.target.value)}>
34+
{children}
35+
</select>
36+
),
37+
SelectContent: ({ children }: any) => <>{children}</>,
38+
SelectItem: ({ children, value }: any) => <option value={value}>{children}</option>,
39+
SelectTrigger: ({ children }: any) => <>{children}</>,
40+
SelectValue: () => null,
41+
StandardTooltip: ({ children, content }: any) => <span title={content}>{children}</span>,
42+
}))
43+
44+
vi.mock("../../ModelPicker", () => ({
45+
ModelPicker: () => <div data-testid="model-picker" />,
46+
}))
47+
48+
vi.mock("../OpenAICodexRateLimitDashboard", () => ({
49+
OpenAICodexRateLimitDashboard: () => null,
50+
}))
51+
52+
vi.mock("@src/utils/vscode", () => ({
53+
vscode: { postMessage: vi.fn() },
54+
}))
55+
56+
describe("OpenAICodex speed selector", () => {
57+
const renderSelector = (apiConfiguration: ProviderSettings, setApiConfigurationField = vi.fn()) => {
58+
render(<OpenAICodex apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />)
59+
return { setApiConfigurationField, selector: screen.getByRole("combobox", { name: "Speed" }) }
60+
}
61+
62+
it("defaults to Standard and clearly explains the Fast quota trade-off", () => {
63+
const { selector } = renderSelector({ apiProvider: providerIdentifiers.openaiCodex })
64+
65+
expect(selector).toHaveValue(OpenAiCodexServiceTier.Default)
66+
expect(screen.getByRole("option", { name: "Standard" })).toBeInTheDocument()
67+
expect(screen.getByRole("option", { name: "Fast (1.5x speed, increased usage)" })).toBeInTheDocument()
68+
expect(
69+
screen.getByTitle(
70+
"Fast uses Codex priority processing for about 1.5x speed and consumes more subscription quota.",
71+
),
72+
).toBeInTheDocument()
73+
})
74+
75+
it("selects Fast from a saved preference and persists changes through the settings callback", () => {
76+
const { selector, setApiConfigurationField } = renderSelector({
77+
apiProvider: providerIdentifiers.openaiCodex,
78+
[OPEN_AI_CODEX_SERVICE_TIER_KEY]: OpenAiCodexServiceTier.Priority,
79+
})
80+
const postMessage = vi.mocked(vscode.postMessage)
81+
82+
expect(selector).toHaveValue(OpenAiCodexServiceTier.Priority)
83+
84+
fireEvent.change(selector, { target: { value: OpenAiCodexServiceTier.Default } })
85+
expect(setApiConfigurationField).toHaveBeenLastCalledWith(
86+
OPEN_AI_CODEX_SERVICE_TIER_KEY,
87+
OpenAiCodexServiceTier.Default,
88+
)
89+
90+
fireEvent.change(selector, { target: { value: OpenAiCodexServiceTier.Priority } })
91+
expect(setApiConfigurationField).toHaveBeenLastCalledWith(
92+
OPEN_AI_CODEX_SERVICE_TIER_KEY,
93+
OpenAiCodexServiceTier.Priority,
94+
)
95+
expect(postMessage).not.toHaveBeenCalled()
96+
})
97+
})
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/* v8 ignore file -- Playwright component fixture is covered by the visual test. */
2+
import React from "react"
3+
4+
import { OpenAiCodexServiceTier } from "@roo-code/types/model"
5+
6+
import { TranslationContext } from "@src/i18n/TranslationContext"
7+
import { TooltipProvider } from "@src/components/ui/tooltip"
8+
import { OpenAICodexSpeedSelector } from "../OpenAICodexSpeedSelector"
9+
10+
const translations: Record<string, string> = {
11+
"settings:common.select": "Select",
12+
"settings:openAiCodexSpeed.label": "Speed",
13+
"settings:openAiCodexSpeed.tooltip":
14+
"Fast uses Codex priority processing for about 1.5x speed and consumes more subscription quota.",
15+
"settings:openAiCodexSpeed.standard": "Standard",
16+
"settings:openAiCodexSpeed.fast": "Fast (1.5x speed, increased usage)",
17+
}
18+
19+
export const OpenAICodexFixture = () => (
20+
<TranslationContext.Provider
21+
value={{
22+
t: (key) => translations[key] ?? key,
23+
i18n: null as unknown as typeof import("../../../../i18n/setup").default,
24+
}}>
25+
<TooltipProvider>
26+
<div className="flex w-[480px] flex-col gap-4 bg-vscode-editor-background p-4 text-vscode-foreground">
27+
<OpenAICodexSpeedSelector value={OpenAiCodexServiceTier.Default} onValueChange={() => {}} />
28+
<OpenAICodexSpeedSelector value={OpenAiCodexServiceTier.Priority} onValueChange={() => {}} />
29+
</div>
30+
</TooltipProvider>
31+
</TranslationContext.Provider>
32+
)
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import React from "react"
2+
3+
import { expect, test } from "../../../../../playwright/coverage-fixture"
4+
import { OpenAICodexFixture } from "./OpenAICodex.visual.fixture"
5+
6+
const themes = [
7+
{
8+
name: "dark",
9+
bodyClass: "vscode-dark",
10+
themeId: "Default Dark Modern",
11+
editorBackground: "#1e1e1e",
12+
dropdownBackground: "#3c3c3c",
13+
triggerBackground: "rgb(60, 60, 60)",
14+
},
15+
{
16+
name: "light",
17+
bodyClass: "vscode-light",
18+
themeId: "Default Light Modern",
19+
editorBackground: "#ffffff",
20+
dropdownBackground: "#ffffff",
21+
triggerBackground: "rgb(255, 255, 255)",
22+
},
23+
] as const
24+
25+
for (const theme of themes) {
26+
test(`renders both OpenAI Codex speeds in the VS Code ${theme.name} theme`, async ({ mount }) => {
27+
const component = await mount(<OpenAICodexFixture />)
28+
const selectors = component.getByTestId("openai-codex-service-tier")
29+
const comboboxes = component.getByRole("combobox", { name: "Speed" })
30+
const selector = selectors.first()
31+
32+
await expect(selectors).toHaveCount(2)
33+
await expect(comboboxes).toHaveCount(2)
34+
await selector.evaluate((element, { bodyClass, themeId }) => {
35+
const { document } = element.ownerDocument.defaultView!
36+
37+
document.documentElement.className = bodyClass
38+
document.body.className = bodyClass
39+
document.body.dataset.vscodeThemeId = themeId
40+
}, theme)
41+
await expect
42+
.poll(() =>
43+
selector.evaluate((element) => {
44+
const body = element.ownerDocument.body
45+
const styles = getComputedStyle(body)
46+
const trigger = element.querySelector("button")!
47+
48+
return {
49+
documentClass: element.ownerDocument.documentElement.className,
50+
bodyClass: body.className,
51+
editorBackground: styles.getPropertyValue("--vscode-editor-background").trim(),
52+
dropdownBackground: styles.getPropertyValue("--vscode-dropdown-background").trim(),
53+
triggerBackground: getComputedStyle(trigger).backgroundColor,
54+
}
55+
}),
56+
)
57+
.toEqual({
58+
documentClass: theme.bodyClass,
59+
bodyClass: theme.bodyClass,
60+
editorBackground: theme.editorBackground,
61+
dropdownBackground: theme.dropdownBackground,
62+
triggerBackground: theme.triggerBackground,
63+
})
64+
65+
await component.evaluate(async () => {
66+
await document.fonts.ready
67+
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
68+
})
69+
70+
await expect(component).toHaveScreenshot(`openai-codex-speed-selector-states-${theme.name}.png`)
71+
})
72+
}
8.02 KB
Loading
7.67 KB
Loading

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

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)