Kaynağa Gözat

Merge pull request #5426 from Kilo-Org/jl-cherry-pick-10813

Add ChatGPT usage dashboard to settings pane (cherry pick upstream #10813)
Kevin van Dijk 3 hafta önce
ebeveyn
işleme
bceee83068
33 değiştirilmiş dosya ile 1273 ekleme ve 1 silme
  1. 5 0
      .changeset/eleven-bulldogs-live.md
  2. 1 0
      packages/types/src/providers/index.ts
  3. 29 0
      packages/types/src/providers/openai-codex-rate-limits.ts
  4. 17 1
      packages/types/src/vscode-extension-host.ts
  5. 53 0
      src/core/webview/__tests__/webviewMessageHandler.spec.ts
  6. 32 0
      src/core/webview/webviewMessageHandler.ts
  7. 47 0
      src/integrations/openai-codex/__tests__/rate-limits.spec.ts
  8. 96 0
      src/integrations/openai-codex/rate-limits.ts
  9. 4 0
      webview-ui/src/components/settings/providers/OpenAICodex.tsx
  10. 228 0
      webview-ui/src/components/settings/providers/OpenAICodexRateLimitDashboard.tsx
  11. 79 0
      webview-ui/src/components/settings/providers/__tests__/OpenAICodexRateLimitDashboard.spec.tsx
  12. 31 0
      webview-ui/src/i18n/locales/ar/settings.json
  13. 31 0
      webview-ui/src/i18n/locales/ca/settings.json
  14. 31 0
      webview-ui/src/i18n/locales/cs/settings.json
  15. 31 0
      webview-ui/src/i18n/locales/de/settings.json
  16. 31 0
      webview-ui/src/i18n/locales/en/settings.json
  17. 31 0
      webview-ui/src/i18n/locales/es/settings.json
  18. 31 0
      webview-ui/src/i18n/locales/fr/settings.json
  19. 31 0
      webview-ui/src/i18n/locales/hi/settings.json
  20. 31 0
      webview-ui/src/i18n/locales/id/settings.json
  21. 31 0
      webview-ui/src/i18n/locales/it/settings.json
  22. 31 0
      webview-ui/src/i18n/locales/ja/settings.json
  23. 31 0
      webview-ui/src/i18n/locales/ko/settings.json
  24. 31 0
      webview-ui/src/i18n/locales/nl/settings.json
  25. 31 0
      webview-ui/src/i18n/locales/pl/settings.json
  26. 31 0
      webview-ui/src/i18n/locales/pt-BR/settings.json
  27. 31 0
      webview-ui/src/i18n/locales/ru/settings.json
  28. 31 0
      webview-ui/src/i18n/locales/th/settings.json
  29. 31 0
      webview-ui/src/i18n/locales/tr/settings.json
  30. 31 0
      webview-ui/src/i18n/locales/uk/settings.json
  31. 31 0
      webview-ui/src/i18n/locales/vi/settings.json
  32. 31 0
      webview-ui/src/i18n/locales/zh-CN/settings.json
  33. 31 0
      webview-ui/src/i18n/locales/zh-TW/settings.json

+ 5 - 0
.changeset/eleven-bulldogs-live.md

@@ -0,0 +1,5 @@
+---
+"kilo-code": patch
+---
+
+OpenAI Codex: Add ChatGPT subscription usage limits dashboard

+ 1 - 0
packages/types/src/providers/index.ts

@@ -27,6 +27,7 @@ export * from "./nano-gpt.js" // kilocode_change
 export * from "./ollama.js"
 export * from "./openai.js"
 export * from "./openai-codex.js"
+export * from "./openai-codex-rate-limits.js"
 export * from "./openrouter.js"
 export * from "./qwen-code.js"
 export * from "./requesty.js"

+ 29 - 0
packages/types/src/providers/openai-codex-rate-limits.ts

@@ -0,0 +1,29 @@
+/**
+ * OpenAI Codex usage/rate limit information (ChatGPT subscription)
+ */
+export interface OpenAiCodexRateLimitInfo {
+	primary?: {
+		/** Used percent in 0–100 */
+		usedPercent: number
+		/** Window length in minutes, when provided */
+		windowMinutes?: number
+		/** Reset time (unix ms since epoch), when provided */
+		resetsAt?: number
+	}
+	secondary?: {
+		/** Used percent in 0–100 */
+		usedPercent: number
+		/** Window length in minutes, when provided */
+		windowMinutes?: number
+		/** Reset time (unix ms since epoch), when provided */
+		resetsAt?: number
+	}
+	credits?: {
+		hasCredits: boolean
+		unlimited: boolean
+		balance?: string
+	}
+	planType?: string
+	/** Timestamp when this was fetched (unix ms since epoch) */
+	fetchedAt: number
+}

+ 17 - 1
packages/types/src/vscode-extension-host.ts

@@ -21,6 +21,7 @@ import type { GitCommit } from "./git.js"
 import type { McpServer } from "./mcp.js"
 import type { ModelRecord, RouterModels, ModelInfo } from "./model.js"
 import type { CommitRange } from "./kilocode/kilocode.js"
+import type { OpenAiCodexRateLimitInfo } from "./providers/openai-codex-rate-limits.js"
 
 // kilocode_change start: Type definitions for Kilo Code-specific features
 // SAP AI Core deployment types
@@ -258,6 +259,7 @@ export interface ExtensionMessage {
 		| "taskWithAggregatedCosts"
 		| "skillsData"
 		| "askReviewScope" // kilocode_change: Review mode scope selection
+		| "openAiCodexRateLimits"
 	text?: string
 	// kilocode_change start
 	completionRequestId?: string // Correlation ID from request
@@ -338,7 +340,9 @@ export interface ExtensionMessage {
 	customMode?: ModeConfig
 	slug?: string
 	success?: boolean
-	values?: Record<string, any> // eslint-disable-line @typescript-eslint/no-explicit-any
+	/** Generic payload for extension messages that use `values` */
+	// eslint-disable-next-line @typescript-eslint/no-explicit-any
+	values?: Record<string, any>
 	requestId?: string
 	promptText?: string
 	results?:
@@ -462,6 +466,12 @@ export interface ExtensionMessage {
 	// kilocode_change end: Review mode
 }
 
+export interface OpenAiCodexRateLimitsMessage {
+	type: "openAiCodexRateLimits"
+	values?: OpenAiCodexRateLimitInfo
+	error?: string
+}
+
 export type ExtensionState = Pick<
 	GlobalSettings,
 	| "currentApiConfigName"
@@ -943,6 +953,7 @@ export interface WebviewMessage {
 		| "chatCompletionAccepted" // kilocode_change: User accepted a chat completion suggestion
 		| "downloadErrorDiagnostics"
 		| "requestClaudeCodeRateLimits"
+		| "requestOpenAiCodexRateLimits"
 		| "refreshCustomTools"
 		| "requestModes"
 		| "switchMode"
@@ -995,6 +1006,7 @@ export interface WebviewMessage {
 	promptMode?: string | "enhance"
 	customPrompt?: PromptComponent
 	dataUrls?: string[]
+	/** Generic payload for webview messages that use `values` */
 	// eslint-disable-next-line @typescript-eslint/no-explicit-any
 	values?: Record<string, any>
 	query?: string
@@ -1142,6 +1154,10 @@ export interface TaskHistoryResponsePayload {
 }
 // kilocode_change end
 
+export interface RequestOpenAiCodexRateLimitsMessage {
+	type: "requestOpenAiCodexRateLimits"
+}
+
 export const checkoutDiffPayloadSchema = z.object({
 	ts: z.number().optional(),
 	previousCommitHash: z.string().optional(),

+ 53 - 0
src/core/webview/__tests__/webviewMessageHandler.spec.ts

@@ -5,6 +5,17 @@ import type { Mock } from "vitest"
 // Mock dependencies - must come before imports
 vi.mock("../../../api/providers/fetchers/modelCache")
 
+vi.mock("../../../integrations/openai-codex/oauth", () => ({
+	openAiCodexOAuthManager: {
+		getAccessToken: vi.fn(),
+		getAccountId: vi.fn(),
+	},
+}))
+
+vi.mock("../../../integrations/openai-codex/rate-limits", () => ({
+	fetchOpenAiCodexRateLimitInfo: vi.fn(),
+}))
+
 // Mock the diagnosticsHandler module
 vi.mock("../diagnosticsHandler", () => ({
 	generateErrorDiagnostics: vi.fn().mockResolvedValue({ success: true, filePath: "/tmp/diagnostics.json" }),
@@ -15,8 +26,13 @@ import type { ModelRecord } from "@roo-code/types"
 import { webviewMessageHandler } from "../webviewMessageHandler"
 import type { ClineProvider } from "../ClineProvider"
 import { getModels } from "../../../api/providers/fetchers/modelCache"
+const { openAiCodexOAuthManager } = await import("../../../integrations/openai-codex/oauth")
+const { fetchOpenAiCodexRateLimitInfo } = await import("../../../integrations/openai-codex/rate-limits")
 
 const mockGetModels = getModels as Mock<typeof getModels>
+const mockGetAccessToken = vi.mocked(openAiCodexOAuthManager.getAccessToken)
+const mockGetAccountId = vi.mocked(openAiCodexOAuthManager.getAccountId)
+const mockFetchOpenAiCodexRateLimitInfo = vi.mocked(fetchOpenAiCodexRateLimitInfo)
 
 // Mock ClineProvider
 const mockClineProvider = {
@@ -740,6 +756,43 @@ describe("webviewMessageHandler - requestRouterModels", () => {
 	})
 })
 
+describe("webviewMessageHandler - requestOpenAiCodexRateLimits", () => {
+	beforeEach(() => {
+		vi.clearAllMocks()
+		mockGetAccessToken.mockResolvedValue(null)
+		mockGetAccountId.mockResolvedValue(null)
+	})
+
+	it("posts error when not authenticated", async () => {
+		await webviewMessageHandler(mockClineProvider, { type: "requestOpenAiCodexRateLimits" } as any)
+
+		expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
+			type: "openAiCodexRateLimits",
+			error: "Not authenticated with OpenAI Codex",
+		})
+	})
+
+	it("posts values when authenticated", async () => {
+		mockGetAccessToken.mockResolvedValue("token")
+		mockGetAccountId.mockResolvedValue("acct_123")
+		mockFetchOpenAiCodexRateLimitInfo.mockResolvedValue({
+			primary: { usedPercent: 10, resetsAt: 1700000000000 },
+			fetchedAt: 1700000000000,
+		})
+
+		await webviewMessageHandler(mockClineProvider, { type: "requestOpenAiCodexRateLimits" } as any)
+
+		expect(mockFetchOpenAiCodexRateLimitInfo).toHaveBeenCalledWith("token", { accountId: "acct_123" })
+		expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
+			type: "openAiCodexRateLimits",
+			values: {
+				primary: { usedPercent: 10, resetsAt: 1700000000000 },
+				fetchedAt: 1700000000000,
+			},
+		})
+	})
+})
+
 describe("webviewMessageHandler - deleteCustomMode", () => {
 	beforeEach(() => {
 		vi.clearAllMocks()

+ 32 - 0
src/core/webview/webviewMessageHandler.ts

@@ -4514,6 +4514,38 @@ export const webviewMessageHandler = async (
 			break
 		}
 
+		case "requestOpenAiCodexRateLimits": {
+			try {
+				const { openAiCodexOAuthManager } = await import("../../integrations/openai-codex/oauth")
+				const accessToken = await openAiCodexOAuthManager.getAccessToken()
+
+				if (!accessToken) {
+					provider.postMessageToWebview({
+						type: "openAiCodexRateLimits",
+						error: "Not authenticated with OpenAI Codex",
+					})
+					break
+				}
+
+				const accountId = await openAiCodexOAuthManager.getAccountId()
+				const { fetchOpenAiCodexRateLimitInfo } = await import("../../integrations/openai-codex/rate-limits")
+				const rateLimits = await fetchOpenAiCodexRateLimitInfo(accessToken, { accountId })
+
+				provider.postMessageToWebview({
+					type: "openAiCodexRateLimits",
+					values: rateLimits,
+				})
+			} catch (error) {
+				const errorMessage = error instanceof Error ? error.message : String(error)
+				provider.log(`Error fetching OpenAI Codex rate limits: ${errorMessage}`)
+				provider.postMessageToWebview({
+					type: "openAiCodexRateLimits",
+					error: errorMessage,
+				})
+			}
+			break
+		}
+
 		case "openDebugApiHistory":
 		case "openDebugUiHistory": {
 			const currentTask = provider.getCurrentTask()

+ 47 - 0
src/integrations/openai-codex/__tests__/rate-limits.spec.ts

@@ -0,0 +1,47 @@
+import { describe, it, expect } from "vitest"
+
+import { parseOpenAiCodexUsagePayload } from "../rate-limits"
+
+describe("parseOpenAiCodexUsagePayload()", () => {
+	it("maps primary/secondary windows", () => {
+		const fetchedAt = 1234567890000
+		const payload = {
+			rate_limit: {
+				primary_window: { used_percent: 12.34, limit_window_seconds: 300 * 60, reset_at: 1700000000 },
+				secondary_window: { used_percent: 99.9, limit_window_seconds: 10080 * 60, reset_at: 1700000000 },
+			},
+			plan_type: "plus",
+		}
+
+		const out = parseOpenAiCodexUsagePayload(payload, fetchedAt)
+
+		expect(out).toEqual({
+			primary: {
+				usedPercent: 12.34,
+				windowMinutes: 300,
+				resetsAt: 1700000000 * 1000,
+			},
+			secondary: {
+				usedPercent: 99.9,
+				windowMinutes: 10080,
+				resetsAt: 1700000000 * 1000,
+			},
+			planType: "plus",
+			fetchedAt,
+		})
+	})
+
+	it("clamps used_percent to 0–100 and tolerates missing fields", () => {
+		const fetchedAt = 1
+		const payload = {
+			rate_limit: {
+				primary_window: { used_percent: 1000 },
+				secondary_window: { used_percent: -5 },
+			},
+		}
+		const out = parseOpenAiCodexUsagePayload(payload, fetchedAt)
+		expect(out.primary?.usedPercent).toBe(100)
+		expect(out.secondary?.usedPercent).toBe(0)
+		expect(out.fetchedAt).toBe(fetchedAt)
+	})
+})

+ 96 - 0
src/integrations/openai-codex/rate-limits.ts

@@ -0,0 +1,96 @@
+import type { OpenAiCodexRateLimitInfo } from "@roo-code/types"
+
+const WHAM_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage"
+
+type WhamUsageResponse = {
+	rate_limit?: {
+		primary_window?: {
+			limit_window_seconds?: number
+			used_percent?: number
+			reset_at?: number
+		}
+		secondary_window?: {
+			limit_window_seconds?: number
+			used_percent?: number
+			reset_at?: number
+		}
+	}
+	plan_type?: string
+}
+
+function clampPercent(value: number): number {
+	if (!Number.isFinite(value)) return 0
+	return Math.max(0, Math.min(100, value))
+}
+
+function secondsToMs(value: number | undefined): number | undefined {
+	return typeof value === "number" && Number.isFinite(value) ? Math.round(value * 1000) : undefined
+}
+
+export function parseOpenAiCodexUsagePayload(payload: unknown, fetchedAt: number): OpenAiCodexRateLimitInfo {
+	const data = (payload && typeof payload === "object" ? payload : {}) as WhamUsageResponse
+	const primaryRaw = data.rate_limit?.primary_window
+	const secondaryRaw = data.rate_limit?.secondary_window
+
+	const primary: OpenAiCodexRateLimitInfo["primary"] | undefined =
+		primaryRaw && typeof primaryRaw.used_percent === "number"
+			? {
+					usedPercent: clampPercent(primaryRaw.used_percent),
+					...(typeof primaryRaw.limit_window_seconds === "number"
+						? { windowMinutes: Math.round(primaryRaw.limit_window_seconds / 60) }
+						: {}),
+					...(secondsToMs(primaryRaw.reset_at) !== undefined
+						? { resetsAt: secondsToMs(primaryRaw.reset_at) }
+						: {}),
+				}
+			: undefined
+
+	const secondary: OpenAiCodexRateLimitInfo["secondary"] | undefined =
+		secondaryRaw && typeof secondaryRaw.used_percent === "number"
+			? {
+					usedPercent: clampPercent(secondaryRaw.used_percent),
+					...(typeof secondaryRaw.limit_window_seconds === "number"
+						? { windowMinutes: Math.round(secondaryRaw.limit_window_seconds / 60) }
+						: {}),
+					...(secondsToMs(secondaryRaw.reset_at) !== undefined
+						? { resetsAt: secondsToMs(secondaryRaw.reset_at) }
+						: {}),
+				}
+			: undefined
+
+	return {
+		...(primary ? { primary } : {}),
+		...(secondary ? { secondary } : {}),
+		...(typeof data.plan_type === "string" ? { planType: data.plan_type } : {}),
+		fetchedAt,
+	}
+}
+
+export async function fetchOpenAiCodexRateLimitInfo(
+	accessToken: string,
+	options?: { accountId?: string | null },
+): Promise<OpenAiCodexRateLimitInfo> {
+	const fetchedAt = Date.now()
+	const headers: Record<string, string> = {
+		Authorization: `Bearer ${accessToken}`,
+		Accept: "application/json",
+	}
+	if (options?.accountId) {
+		headers["ChatGPT-Account-Id"] = options.accountId
+	}
+
+	const response = await fetch(WHAM_USAGE_URL, { method: "GET", headers })
+	if (!response.ok) {
+		const text = await response.text().catch(() => "")
+		throw new Error(
+			`OpenAI Codex WHAM usage request failed: ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`,
+		)
+	}
+
+	const json = (await response.json()) as unknown
+	const parsed = parseOpenAiCodexUsagePayload(json, fetchedAt)
+	if (!parsed.primary && !parsed.secondary) {
+		throw new Error("OpenAI Codex WHAM usage response did not include rate_limit windows")
+	}
+	return parsed
+}

+ 4 - 0
webview-ui/src/components/settings/providers/OpenAICodex.tsx

@@ -7,6 +7,7 @@ import { Button } from "@src/components/ui"
 import { vscode } from "@src/utils/vscode"
 
 import { ModelPicker } from "../ModelPicker"
+import { OpenAICodexRateLimitDashboard } from "./OpenAICodexRateLimitDashboard"
 
 interface OpenAICodexProps {
 	apiConfiguration: ProviderSettings
@@ -50,6 +51,9 @@ export const OpenAICodex: React.FC<OpenAICodexProps> = ({
 				)}
 			</div>
 
+			{/* Rate Limit Dashboard - only shown when authenticated */}
+			<OpenAICodexRateLimitDashboard isAuthenticated={openAiCodexIsAuthenticated} />
+
 			{/* Model Picker */}
 			<ModelPicker
 				apiConfiguration={apiConfiguration}

+ 228 - 0
webview-ui/src/components/settings/providers/OpenAICodexRateLimitDashboard.tsx

@@ -0,0 +1,228 @@
+import React, { useCallback, useEffect, useState } from "react"
+import type { OpenAiCodexRateLimitInfo } from "@roo-code/types"
+
+import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { vscode } from "@src/utils/vscode"
+
+interface OpenAICodexRateLimitDashboardProps {
+	isAuthenticated: boolean
+}
+
+type Translate = (key: string, options?: Record<string, any>) => string
+
+function formatDurationSeconds(totalSeconds: number, t: Translate): string {
+	const days = Math.floor(totalSeconds / 86400)
+	const hours = Math.floor((totalSeconds % 86400) / 3600)
+	const minutes = Math.floor((totalSeconds % 3600) / 60)
+
+	if (days > 0) {
+		return t("settings:providers.openAiCodexRateLimits.duration.daysHours", { days, hours })
+	}
+	if (hours > 0) {
+		return t("settings:providers.openAiCodexRateLimits.duration.hoursMinutes", { hours, minutes })
+	}
+	return t("settings:providers.openAiCodexRateLimits.duration.minutes", { minutes })
+}
+
+function formatTimeRemainingMs(ms: number | undefined, t: Translate): string {
+	if (ms === undefined) return ""
+	if (ms <= 0) return t("settings:providers.openAiCodexRateLimits.time.now")
+	const totalSeconds = Math.max(0, Math.floor(ms / 1000))
+	return formatDurationSeconds(totalSeconds, t)
+}
+
+function formatResetTimeMs(resetMs: number | undefined, t: Translate): string {
+	if (!resetMs) return t("settings:providers.openAiCodexRateLimits.time.notAvailable")
+	const diffMs = resetMs - Date.now()
+	if (diffMs <= 0) return t("settings:providers.openAiCodexRateLimits.time.now")
+
+	const diffSec = Math.floor(diffMs / 1000)
+	return formatDurationSeconds(diffSec, t)
+}
+
+function formatWindowLabel(windowMinutes: number | undefined, t: Translate): string | undefined {
+	if (!windowMinutes) return undefined
+	if (windowMinutes === 60) return t("settings:providers.openAiCodexRateLimits.window.oneHour")
+	if (windowMinutes === 24 * 60) return t("settings:providers.openAiCodexRateLimits.window.daily")
+	if (windowMinutes === 7 * 24 * 60) return t("settings:providers.openAiCodexRateLimits.window.weekly")
+	if (windowMinutes === 5 * 60) return t("settings:providers.openAiCodexRateLimits.window.fiveHour")
+	if (windowMinutes % (24 * 60) === 0) {
+		return t("settings:providers.openAiCodexRateLimits.window.days", { days: windowMinutes / (24 * 60) })
+	}
+	if (windowMinutes % 60 === 0) {
+		return t("settings:providers.openAiCodexRateLimits.window.hours", { hours: windowMinutes / 60 })
+	}
+	return t("settings:providers.openAiCodexRateLimits.window.minutes", { minutes: windowMinutes })
+}
+
+function formatPlanLabel(planType: string | undefined, t: Translate): string {
+	if (!planType) return t("settings:providers.openAiCodexRateLimits.plan.default")
+	return t("settings:providers.openAiCodexRateLimits.plan.withType", { planType })
+}
+
+const UsageProgressBar: React.FC<{ usedPercent: number; label?: string }> = ({ usedPercent, label }) => {
+	const percentage = Math.max(0, Math.min(100, usedPercent))
+	const isWarning = percentage >= 70
+	const isCritical = percentage >= 90
+
+	return (
+		<div className="w-full">
+			{label ? <div className="text-xs text-vscode-descriptionForeground mb-1">{label}</div> : null}
+			<div className="w-full bg-vscode-input-background rounded-sm h-2 overflow-hidden">
+				<div
+					className={`h-full transition-all duration-300 ${
+						isCritical
+							? "bg-vscode-errorForeground"
+							: isWarning
+								? "bg-vscode-editorWarning-foreground"
+								: "bg-vscode-button-background"
+					}`}
+					style={{ width: `${percentage}%` }}
+				/>
+			</div>
+		</div>
+	)
+}
+
+export const OpenAICodexRateLimitDashboard: React.FC<OpenAICodexRateLimitDashboardProps> = ({ isAuthenticated }) => {
+	const { t } = useAppTranslation()
+	const [rateLimits, setRateLimits] = useState<OpenAiCodexRateLimitInfo | null>(null)
+	const [isLoading, setIsLoading] = useState(false)
+	const [error, setError] = useState<string | null>(null)
+
+	const fetchRateLimits = useCallback(() => {
+		if (!isAuthenticated) {
+			setRateLimits(null)
+			setError(null)
+			return
+		}
+		setIsLoading(true)
+		setError(null)
+		vscode.postMessage({ type: "requestOpenAiCodexRateLimits" })
+	}, [isAuthenticated])
+
+	useEffect(() => {
+		const handleMessage = (event: MessageEvent) => {
+			const message = event.data
+			if (message.type === "openAiCodexRateLimits") {
+				setIsLoading(false)
+				if (message.error) {
+					setError(message.error)
+					setRateLimits(null)
+				} else if (message.values) {
+					setRateLimits(message.values)
+					setError(null)
+				}
+			}
+		}
+		window.addEventListener("message", handleMessage)
+		return () => window.removeEventListener("message", handleMessage)
+	}, [])
+
+	useEffect(() => {
+		if (isAuthenticated) {
+			fetchRateLimits()
+		}
+	}, [isAuthenticated, fetchRateLimits])
+
+	if (!isAuthenticated) return null
+
+	if (isLoading && !rateLimits) {
+		return (
+			<div className="bg-vscode-editor-background border border-vscode-panel-border rounded-md p-3">
+				<div className="text-sm text-vscode-descriptionForeground">
+					{t("settings:providers.openAiCodexRateLimits.loading")}
+				</div>
+			</div>
+		)
+	}
+
+	if (error) {
+		return (
+			<div className="bg-vscode-editor-background border border-vscode-panel-border rounded-md p-3">
+				<div className="flex items-center justify-between">
+					<div className="text-sm text-vscode-errorForeground">
+						{t("settings:providers.openAiCodexRateLimits.loadError")}
+					</div>
+					<button
+						onClick={fetchRateLimits}
+						className="text-xs text-vscode-textLink-foreground hover:text-vscode-textLink-activeForeground cursor-pointer bg-transparent border-none">
+						{t("settings:providers.openAiCodexRateLimits.retry")}
+					</button>
+				</div>
+				<div className="mt-2 text-xs text-vscode-descriptionForeground break-words">{error}</div>
+			</div>
+		)
+	}
+
+	if (!rateLimits) return null
+
+	const primary = rateLimits.primary
+	const secondary = rateLimits.secondary
+	const planType = rateLimits.planType
+
+	const planLabel = formatPlanLabel(planType, t)
+
+	const primaryWindowLabel = primary ? formatWindowLabel(primary.windowMinutes, t) : undefined
+	const primaryTimeRemaining = primary?.resetsAt ? formatTimeRemainingMs(primary.resetsAt - Date.now(), t) : ""
+	const primaryUsed = primary ? Math.round(primary.usedPercent) : undefined
+
+	const secondaryWindowLabel = secondary ? formatWindowLabel(secondary.windowMinutes, t) : undefined
+	const secondaryTimeRemaining = secondary?.resetsAt ? formatTimeRemainingMs(secondary.resetsAt - Date.now(), t) : ""
+	const secondaryUsed = secondary ? Math.round(secondary.usedPercent) : undefined
+
+	const getUsageStatusLabel = (used: number | undefined, timeRemaining: string, resetAt?: number) => {
+		const usedLabel =
+			used !== undefined ? t("settings:providers.openAiCodexRateLimits.usedPercent", { percent: used }) : ""
+		const resetLabel = timeRemaining
+			? t("settings:providers.openAiCodexRateLimits.resetsIn", { time: timeRemaining })
+			: resetAt
+				? t("settings:providers.openAiCodexRateLimits.resetsIn", {
+						time: formatResetTimeMs(resetAt, t),
+					})
+				: ""
+
+		if (usedLabel && resetLabel) return `${usedLabel} • ${resetLabel}`
+		return usedLabel || resetLabel
+	}
+
+	return (
+		<div className="bg-vscode-editor-background border border-vscode-panel-border rounded-md p-3">
+			<div className="mb-3">
+				<div className="text-sm font-medium text-vscode-foreground">
+					{t("settings:providers.openAiCodexRateLimits.title", { planLabel })}
+				</div>
+			</div>
+
+			<div className="space-y-3">
+				{primary ? (
+					<div className="space-y-1">
+						<div className="flex items-center justify-between text-xs">
+							<span className="text-vscode-foreground">
+								{primaryWindowLabel ?? t("settings:providers.openAiCodexRateLimits.window.usage")}
+							</span>
+							<span className="text-vscode-descriptionForeground">
+								{getUsageStatusLabel(primaryUsed, primaryTimeRemaining, primary.resetsAt)}
+							</span>
+						</div>
+						<UsageProgressBar usedPercent={primary.usedPercent} label={undefined} />
+					</div>
+				) : null}
+
+				{secondary ? (
+					<div className="space-y-1">
+						<div className="flex items-center justify-between text-xs">
+							<span className="text-vscode-foreground">
+								{secondaryWindowLabel ?? t("settings:providers.openAiCodexRateLimits.window.usage")}
+							</span>
+							<span className="text-vscode-descriptionForeground">
+								{getUsageStatusLabel(secondaryUsed, secondaryTimeRemaining, secondary.resetsAt)}
+							</span>
+						</div>
+						<UsageProgressBar usedPercent={secondary.usedPercent} />
+					</div>
+				) : null}
+			</div>
+		</div>
+	)
+}

+ 79 - 0
webview-ui/src/components/settings/providers/__tests__/OpenAICodexRateLimitDashboard.spec.tsx

@@ -0,0 +1,79 @@
+import { render, screen, waitFor } from "@/utils/test-utils"
+
+import { OpenAICodexRateLimitDashboard } from "../OpenAICodexRateLimitDashboard"
+
+vi.mock("@src/i18n/TranslationContext", () => ({
+	useAppTranslation: () => ({
+		t: (key: string, options?: Record<string, any>) => {
+			switch (key) {
+				case "settings:providers.openAiCodexRateLimits.title":
+					return `Usage Limits for Codex${options?.planLabel ?? ""}`
+				case "settings:providers.openAiCodexRateLimits.plan.withType":
+					return ` (${options?.planType ?? ""})`
+				case "settings:providers.openAiCodexRateLimits.plan.default":
+					return ""
+				case "settings:providers.openAiCodexRateLimits.window.fiveHour":
+					return "5h limit"
+				case "settings:providers.openAiCodexRateLimits.window.weekly":
+					return "Weekly limit"
+				case "settings:providers.openAiCodexRateLimits.usedPercent":
+					return `${options?.percent ?? ""}% used`
+				default:
+					return key
+			}
+		},
+	}),
+}))
+
+const { postMessageMock } = vi.hoisted(() => ({
+	postMessageMock: vi.fn(),
+}))
+
+vi.mock("@src/utils/vscode", () => ({
+	vscode: {
+		postMessage: postMessageMock,
+	},
+}))
+
+describe("OpenAICodexRateLimitDashboard", () => {
+	beforeEach(() => {
+		postMessageMock.mockClear()
+	})
+
+	it("hides when not authenticated", () => {
+		const { container } = render(<OpenAICodexRateLimitDashboard isAuthenticated={false} />)
+		expect(container.firstChild).toBeNull()
+	})
+
+	it("sends request message when authenticated", () => {
+		render(<OpenAICodexRateLimitDashboard isAuthenticated={true} />)
+		expect(postMessageMock).toHaveBeenCalledWith({ type: "requestOpenAiCodexRateLimits" })
+	})
+
+	it("renders usage values from payload", () => {
+		render(<OpenAICodexRateLimitDashboard isAuthenticated={true} />)
+
+		window.dispatchEvent(
+			new MessageEvent("message", {
+				data: {
+					type: "openAiCodexRateLimits",
+					values: {
+						primary: { usedPercent: 12.3, windowMinutes: 300, resetsAt: Date.now() + 60_000 },
+						secondary: { usedPercent: 45.6, windowMinutes: 10080, resetsAt: Date.now() + 120_000 },
+						credits: { hasCredits: true, unlimited: true },
+						fetchedAt: Date.now(),
+						planType: "pro",
+					},
+				},
+			}),
+		)
+
+		return waitFor(() => {
+			expect(screen.getByText(/Usage Limits for Codex \(pro\)/)).toBeInTheDocument()
+			expect(screen.getByText(/5h limit/)).toBeInTheDocument()
+			expect(screen.getByText(/Weekly limit/)).toBeInTheDocument()
+			expect(screen.getByText(/12% used/)).toBeInTheDocument()
+			expect(screen.getByText(/46% used/)).toBeInTheDocument()
+		})
+	})
+})

+ 31 - 0
webview-ui/src/i18n/locales/ar/settings.json

@@ -349,6 +349,37 @@
 		"apiKeyStorageNotice": "المفاتيح تُحفظ بأمان في مخزن أسرار VSCode",
 		"glamaApiKey": "مفتاح Glama",
 		"getGlamaApiKey": "احصل على مفتاح Glama",
+		"openAiCodexRateLimits": {
+			"title": "حدود الاستخدام لـ Codex{{planLabel}}",
+			"loading": "جاري تحميل حدود الاستخدام...",
+			"loadError": "فشل تحميل حدود الاستخدام",
+			"retry": "إعادة المحاولة",
+			"usedPercent": "{{percent}}% مستخدم",
+			"resetsIn": "يُعاد التعيين خلال {{time}}",
+			"plan": {
+				"default": "",
+				"withType": " ({{planType}})"
+			},
+			"time": {
+				"now": "الآن",
+				"notAvailable": "غير متوفر"
+			},
+			"duration": {
+				"daysHours": "{{days}} يوم {{hours}} ساعة",
+				"hoursMinutes": "{{hours}} ساعة {{minutes}} دقيقة",
+				"minutes": "{{minutes}} دقيقة"
+			},
+			"window": {
+				"usage": "الاستخدام",
+				"fiveHour": "حد 5 ساعات",
+				"oneHour": "حد ساعة واحدة",
+				"daily": "الحد اليومي",
+				"weekly": "الحد الأسبوعي",
+				"days": "حد {{days}} يوم",
+				"hours": "حد {{hours}} ساعة",
+				"minutes": "حد {{minutes}} دقيقة"
+			}
+		},
 		"useCustomBaseUrl": "استخدم رابط أساسي مخصص",
 		"useReasoning": "تفعيل الـ Reasoning",
 		"useHostHeader": "استخدم ترويسة Host مخصصة",

+ 31 - 0
webview-ui/src/i18n/locales/ca/settings.json

@@ -313,6 +313,37 @@
 		"glamaApiKey": "Clau API de Glama",
 		"getGlamaApiKey": "Obtenir clau API de Glama",
 		"apiKeyStorageNotice": "Les claus API s'emmagatzemen de forma segura a l'Emmagatzematge Secret de VSCode",
+		"openAiCodexRateLimits": {
+			"title": "Usage Limits for Codex{{planLabel}}",
+			"loading": "Loading usage limits...",
+			"loadError": "Failed to load usage limits",
+			"retry": "Retry",
+			"usedPercent": "{{percent}}% used",
+			"resetsIn": "Resets in {{time}}",
+			"plan": {
+				"default": "",
+				"withType": " ({{planType}})"
+			},
+			"time": {
+				"now": "Now",
+				"notAvailable": "N/A"
+			},
+			"duration": {
+				"daysHours": "{{days}}d {{hours}}h",
+				"hoursMinutes": "{{hours}}h {{minutes}}m",
+				"minutes": "{{minutes}}m"
+			},
+			"window": {
+				"usage": "Usage",
+				"fiveHour": "5h limit",
+				"oneHour": "1h limit",
+				"daily": "Daily limit",
+				"weekly": "Weekly limit",
+				"days": "{{days}}d limit",
+				"hours": "{{hours}}h limit",
+				"minutes": "{{minutes}}m limit"
+			}
+		},
 		"useCustomBaseUrl": "Utilitzar URL base personalitzada",
 		"useReasoning": "Activar raonament",
 		"useHostHeader": "Utilitzar capçalera Host personalitzada",

+ 31 - 0
webview-ui/src/i18n/locales/cs/settings.json

@@ -335,6 +335,37 @@
 		"apiKeyStorageNotice": "Klíče API jsou bezpečně uloženy v tajném úložišti VSCode",
 		"glamaApiKey": "Klíč API Glama",
 		"getGlamaApiKey": "Získat klíč API Glama",
+		"openAiCodexRateLimits": {
+			"title": "Limity využití pro Codex{{planLabel}}",
+			"loading": "Načítání limitů využití...",
+			"loadError": "Nepodařilo se načíst limity využití",
+			"retry": "Zkusit znovu",
+			"usedPercent": "{{percent}}% použito",
+			"resetsIn": "Obnoví se za {{time}}",
+			"plan": {
+				"default": "",
+				"withType": " ({{planType}})"
+			},
+			"time": {
+				"now": "Nyní",
+				"notAvailable": "Nedostupné"
+			},
+			"duration": {
+				"daysHours": "{{days}}d {{hours}}h",
+				"hoursMinutes": "{{hours}}h {{minutes}}m",
+				"minutes": "{{minutes}}m"
+			},
+			"window": {
+				"usage": "Využití",
+				"fiveHour": "5h limit",
+				"oneHour": "1h limit",
+				"daily": "Denní limit",
+				"weekly": "Týdenní limit",
+				"days": "{{days}}d limit",
+				"hours": "{{hours}}h limit",
+				"minutes": "{{minutes}}m limit"
+			}
+		},
 		"useCustomBaseUrl": "Použít vlastní základní URL",
 		"useReasoning": "Povolit uvažování",
 		"useHostHeader": "Použít vlastní hlavičku Host",

+ 31 - 0
webview-ui/src/i18n/locales/de/settings.json

@@ -324,6 +324,37 @@
 		"apiKeyStorageNotice": "API-Schlüssel werden sicher im VSCode Secret Storage gespeichert",
 		"glamaApiKey": "Glama API-Schlüssel",
 		"getGlamaApiKey": "Glama API-Schlüssel erhalten",
+		"openAiCodexRateLimits": {
+			"title": "Usage Limits for Codex{{planLabel}}",
+			"loading": "Loading usage limits...",
+			"loadError": "Failed to load usage limits",
+			"retry": "Retry",
+			"usedPercent": "{{percent}}% used",
+			"resetsIn": "Resets in {{time}}",
+			"plan": {
+				"default": "",
+				"withType": " ({{planType}})"
+			},
+			"time": {
+				"now": "Now",
+				"notAvailable": "N/A"
+			},
+			"duration": {
+				"daysHours": "{{days}}d {{hours}}h",
+				"hoursMinutes": "{{hours}}h {{minutes}}m",
+				"minutes": "{{minutes}}m"
+			},
+			"window": {
+				"usage": "Usage",
+				"fiveHour": "5h limit",
+				"oneHour": "1h limit",
+				"daily": "Daily limit",
+				"weekly": "Weekly limit",
+				"days": "{{days}}d limit",
+				"hours": "{{hours}}h limit",
+				"minutes": "{{minutes}}m limit"
+			}
+		},
 		"useCustomBaseUrl": "Benutzerdefinierte Basis-URL verwenden",
 		"useReasoning": "Reasoning aktivieren",
 		"useHostHeader": "Benutzerdefinierten Host-Header verwenden",

+ 31 - 0
webview-ui/src/i18n/locales/en/settings.json

@@ -331,6 +331,37 @@
 		"apiKeyStorageNotice": "API keys are stored securely in VSCode's Secret Storage",
 		"glamaApiKey": "Glama API Key",
 		"getGlamaApiKey": "Get Glama API Key",
+		"openAiCodexRateLimits": {
+			"title": "Usage Limits for Codex{{planLabel}}",
+			"loading": "Loading usage limits...",
+			"loadError": "Failed to load usage limits",
+			"retry": "Retry",
+			"usedPercent": "{{percent}}% used",
+			"resetsIn": "Resets in {{time}}",
+			"plan": {
+				"default": "",
+				"withType": " ({{planType}})"
+			},
+			"time": {
+				"now": "Now",
+				"notAvailable": "N/A"
+			},
+			"duration": {
+				"daysHours": "{{days}}d {{hours}}h",
+				"hoursMinutes": "{{hours}}h {{minutes}}m",
+				"minutes": "{{minutes}}m"
+			},
+			"window": {
+				"usage": "Usage",
+				"fiveHour": "5h limit",
+				"oneHour": "1h limit",
+				"daily": "Daily limit",
+				"weekly": "Weekly limit",
+				"days": "{{days}}d limit",
+				"hours": "{{hours}}h limit",
+				"minutes": "{{minutes}}m limit"
+			}
+		},
 		"useCustomBaseUrl": "Use custom base URL",
 		"useReasoning": "Enable reasoning",
 		"useHostHeader": "Use custom Host header",

+ 31 - 0
webview-ui/src/i18n/locales/es/settings.json

@@ -322,6 +322,37 @@
 		"apiKeyStorageNotice": "Las claves API se almacenan de forma segura en el Almacenamiento Secreto de VSCode",
 		"glamaApiKey": "Clave API de Glama",
 		"getGlamaApiKey": "Obtener clave API de Glama",
+		"openAiCodexRateLimits": {
+			"title": "Usage Limits for Codex{{planLabel}}",
+			"loading": "Loading usage limits...",
+			"loadError": "Failed to load usage limits",
+			"retry": "Retry",
+			"usedPercent": "{{percent}}% used",
+			"resetsIn": "Resets in {{time}}",
+			"plan": {
+				"default": "",
+				"withType": " ({{planType}})"
+			},
+			"time": {
+				"now": "Now",
+				"notAvailable": "N/A"
+			},
+			"duration": {
+				"daysHours": "{{days}}d {{hours}}h",
+				"hoursMinutes": "{{hours}}h {{minutes}}m",
+				"minutes": "{{minutes}}m"
+			},
+			"window": {
+				"usage": "Usage",
+				"fiveHour": "5h limit",
+				"oneHour": "1h limit",
+				"daily": "Daily limit",
+				"weekly": "Weekly limit",
+				"days": "{{days}}d limit",
+				"hours": "{{hours}}h limit",
+				"minutes": "{{minutes}}m limit"
+			}
+		},
 		"useCustomBaseUrl": "Usar URL base personalizada",
 		"useReasoning": "Habilitar razonamiento",
 		"useHostHeader": "Usar encabezado Host personalizado",

+ 31 - 0
webview-ui/src/i18n/locales/fr/settings.json

@@ -322,6 +322,37 @@
 		"apiKeyStorageNotice": "Les clés API sont stockées en toute sécurité dans le stockage sécurisé de VSCode",
 		"glamaApiKey": "Clé API Glama",
 		"getGlamaApiKey": "Obtenir la clé API Glama",
+		"openAiCodexRateLimits": {
+			"title": "Usage Limits for Codex{{planLabel}}",
+			"loading": "Loading usage limits...",
+			"loadError": "Failed to load usage limits",
+			"retry": "Retry",
+			"usedPercent": "{{percent}}% used",
+			"resetsIn": "Resets in {{time}}",
+			"plan": {
+				"default": "",
+				"withType": " ({{planType}})"
+			},
+			"time": {
+				"now": "Now",
+				"notAvailable": "N/A"
+			},
+			"duration": {
+				"daysHours": "{{days}}d {{hours}}h",
+				"hoursMinutes": "{{hours}}h {{minutes}}m",
+				"minutes": "{{minutes}}m"
+			},
+			"window": {
+				"usage": "Usage",
+				"fiveHour": "5h limit",
+				"oneHour": "1h limit",
+				"daily": "Daily limit",
+				"weekly": "Weekly limit",
+				"days": "{{days}}d limit",
+				"hours": "{{hours}}h limit",
+				"minutes": "{{minutes}}m limit"
+			}
+		},
 		"useCustomBaseUrl": "Utiliser une URL de base personnalisée",
 		"useReasoning": "Activer le raisonnement",
 		"useHostHeader": "Utiliser un en-tête Host personnalisé",

+ 31 - 0
webview-ui/src/i18n/locales/hi/settings.json

@@ -321,6 +321,37 @@
 		"apiKeyStorageNotice": "API कुंजियाँ VSCode के सुरक्षित स्टोरेज में सुरक्षित रूप से संग्रहीत हैं",
 		"glamaApiKey": "Glama API कुंजी",
 		"getGlamaApiKey": "Glama API कुंजी प्राप्त करें",
+		"openAiCodexRateLimits": {
+			"title": "Usage Limits for Codex{{planLabel}}",
+			"loading": "Loading usage limits...",
+			"loadError": "Failed to load usage limits",
+			"retry": "Retry",
+			"usedPercent": "{{percent}}% used",
+			"resetsIn": "Resets in {{time}}",
+			"plan": {
+				"default": "",
+				"withType": " ({{planType}})"
+			},
+			"time": {
+				"now": "Now",
+				"notAvailable": "N/A"
+			},
+			"duration": {
+				"daysHours": "{{days}}d {{hours}}h",
+				"hoursMinutes": "{{hours}}h {{minutes}}m",
+				"minutes": "{{minutes}}m"
+			},
+			"window": {
+				"usage": "Usage",
+				"fiveHour": "5h limit",
+				"oneHour": "1h limit",
+				"daily": "Daily limit",
+				"weekly": "Weekly limit",
+				"days": "{{days}}d limit",
+				"hours": "{{hours}}h limit",
+				"minutes": "{{minutes}}m limit"
+			}
+		},
 		"useCustomBaseUrl": "कस्टम बेस URL का उपयोग करें",
 		"useReasoning": "तर्क सक्षम करें",
 		"useHostHeader": "कस्टम होस्ट हेडर का उपयोग करें",

+ 31 - 0
webview-ui/src/i18n/locales/id/settings.json

@@ -313,6 +313,37 @@
 		"glamaApiKey": "Glama API Key",
 		"getGlamaApiKey": "Dapatkan Glama API Key",
 		"apiKeyStorageNotice": "API key disimpan dengan aman di Secret Storage VSCode",
+		"openAiCodexRateLimits": {
+			"title": "Usage Limits for Codex{{planLabel}}",
+			"loading": "Loading usage limits...",
+			"loadError": "Failed to load usage limits",
+			"retry": "Retry",
+			"usedPercent": "{{percent}}% used",
+			"resetsIn": "Resets in {{time}}",
+			"plan": {
+				"default": "",
+				"withType": " ({{planType}})"
+			},
+			"time": {
+				"now": "Now",
+				"notAvailable": "N/A"
+			},
+			"duration": {
+				"daysHours": "{{days}}d {{hours}}h",
+				"hoursMinutes": "{{hours}}h {{minutes}}m",
+				"minutes": "{{minutes}}m"
+			},
+			"window": {
+				"usage": "Usage",
+				"fiveHour": "5h limit",
+				"oneHour": "1h limit",
+				"daily": "Daily limit",
+				"weekly": "Weekly limit",
+				"days": "{{days}}d limit",
+				"hours": "{{hours}}h limit",
+				"minutes": "{{minutes}}m limit"
+			}
+		},
 		"useCustomBaseUrl": "Gunakan base URL kustom",
 		"useReasoning": "Aktifkan reasoning",
 		"useHostHeader": "Gunakan Host header kustom",

+ 31 - 0
webview-ui/src/i18n/locales/it/settings.json

@@ -327,6 +327,37 @@
 		"glamaApiKey": "Chiave API Glama",
 		"getGlamaApiKey": "Ottieni chiave API Glama",
 		"apiKeyStorageNotice": "Le chiavi API sono memorizzate in modo sicuro nell'Archivio Segreto di VSCode",
+		"openAiCodexRateLimits": {
+			"title": "Usage Limits for Codex{{planLabel}}",
+			"loading": "Loading usage limits...",
+			"loadError": "Failed to load usage limits",
+			"retry": "Retry",
+			"usedPercent": "{{percent}}% used",
+			"resetsIn": "Resets in {{time}}",
+			"plan": {
+				"default": "",
+				"withType": " ({{planType}})"
+			},
+			"time": {
+				"now": "Now",
+				"notAvailable": "N/A"
+			},
+			"duration": {
+				"daysHours": "{{days}}d {{hours}}h",
+				"hoursMinutes": "{{hours}}h {{minutes}}m",
+				"minutes": "{{minutes}}m"
+			},
+			"window": {
+				"usage": "Usage",
+				"fiveHour": "5h limit",
+				"oneHour": "1h limit",
+				"daily": "Daily limit",
+				"weekly": "Weekly limit",
+				"days": "{{days}}d limit",
+				"hours": "{{hours}}h limit",
+				"minutes": "{{minutes}}m limit"
+			}
+		},
 		"useCustomBaseUrl": "Usa URL base personalizzato",
 		"useReasoning": "Abilita ragionamento",
 		"useHostHeader": "Usa intestazione Host personalizzata",

+ 31 - 0
webview-ui/src/i18n/locales/ja/settings.json

@@ -318,6 +318,37 @@
 		"glamaApiKey": "Glama APIキー",
 		"getGlamaApiKey": "Glama APIキーを取得",
 		"apiKeyStorageNotice": "APIキーはVSCodeのシークレットストレージに安全に保存されます",
+		"openAiCodexRateLimits": {
+			"title": "Usage Limits for Codex{{planLabel}}",
+			"loading": "Loading usage limits...",
+			"loadError": "Failed to load usage limits",
+			"retry": "Retry",
+			"usedPercent": "{{percent}}% used",
+			"resetsIn": "Resets in {{time}}",
+			"plan": {
+				"default": "",
+				"withType": " ({{planType}})"
+			},
+			"time": {
+				"now": "Now",
+				"notAvailable": "N/A"
+			},
+			"duration": {
+				"daysHours": "{{days}}d {{hours}}h",
+				"hoursMinutes": "{{hours}}h {{minutes}}m",
+				"minutes": "{{minutes}}m"
+			},
+			"window": {
+				"usage": "Usage",
+				"fiveHour": "5h limit",
+				"oneHour": "1h limit",
+				"daily": "Daily limit",
+				"weekly": "Weekly limit",
+				"days": "{{days}}d limit",
+				"hours": "{{hours}}h limit",
+				"minutes": "{{minutes}}m limit"
+			}
+		},
 		"useCustomBaseUrl": "カスタムベースURLを使用",
 		"useReasoning": "推論を有効化",
 		"useHostHeader": "カスタムHostヘッダーを使用",

+ 31 - 0
webview-ui/src/i18n/locales/ko/settings.json

@@ -313,6 +313,37 @@
 		"glamaApiKey": "Glama API 키",
 		"getGlamaApiKey": "Glama API 키 받기",
 		"apiKeyStorageNotice": "API 키는 VSCode의 보안 저장소에 안전하게 저장됩니다",
+		"openAiCodexRateLimits": {
+			"title": "Usage Limits for Codex{{planLabel}}",
+			"loading": "Loading usage limits...",
+			"loadError": "Failed to load usage limits",
+			"retry": "Retry",
+			"usedPercent": "{{percent}}% used",
+			"resetsIn": "Resets in {{time}}",
+			"plan": {
+				"default": "",
+				"withType": " ({{planType}})"
+			},
+			"time": {
+				"now": "Now",
+				"notAvailable": "N/A"
+			},
+			"duration": {
+				"daysHours": "{{days}}d {{hours}}h",
+				"hoursMinutes": "{{hours}}h {{minutes}}m",
+				"minutes": "{{minutes}}m"
+			},
+			"window": {
+				"usage": "Usage",
+				"fiveHour": "5h limit",
+				"oneHour": "1h limit",
+				"daily": "Daily limit",
+				"weekly": "Weekly limit",
+				"days": "{{days}}d limit",
+				"hours": "{{hours}}h limit",
+				"minutes": "{{minutes}}m limit"
+			}
+		},
 		"useCustomBaseUrl": "사용자 정의 기본 URL 사용",
 		"useReasoning": "추론 활성화",
 		"useHostHeader": "사용자 정의 Host 헤더 사용",

+ 31 - 0
webview-ui/src/i18n/locales/nl/settings.json

@@ -313,6 +313,37 @@
 		"glamaApiKey": "Glama API-sleutel",
 		"getGlamaApiKey": "Glama API-sleutel ophalen",
 		"apiKeyStorageNotice": "API-sleutels worden veilig opgeslagen in de geheime opslag van VSCode",
+		"openAiCodexRateLimits": {
+			"title": "Usage Limits for Codex{{planLabel}}",
+			"loading": "Loading usage limits...",
+			"loadError": "Failed to load usage limits",
+			"retry": "Retry",
+			"usedPercent": "{{percent}}% used",
+			"resetsIn": "Resets in {{time}}",
+			"plan": {
+				"default": "",
+				"withType": " ({{planType}})"
+			},
+			"time": {
+				"now": "Now",
+				"notAvailable": "N/A"
+			},
+			"duration": {
+				"daysHours": "{{days}}d {{hours}}h",
+				"hoursMinutes": "{{hours}}h {{minutes}}m",
+				"minutes": "{{minutes}}m"
+			},
+			"window": {
+				"usage": "Usage",
+				"fiveHour": "5h limit",
+				"oneHour": "1h limit",
+				"daily": "Daily limit",
+				"weekly": "Weekly limit",
+				"days": "{{days}}d limit",
+				"hours": "{{hours}}h limit",
+				"minutes": "{{minutes}}m limit"
+			}
+		},
 		"useCustomBaseUrl": "Aangepaste basis-URL gebruiken",
 		"useReasoning": "Redenering inschakelen",
 		"useHostHeader": "Aangepaste Host-header gebruiken",

+ 31 - 0
webview-ui/src/i18n/locales/pl/settings.json

@@ -313,6 +313,37 @@
 		"glamaApiKey": "Klucz API Glama",
 		"getGlamaApiKey": "Uzyskaj klucz API Glama",
 		"apiKeyStorageNotice": "Klucze API są bezpiecznie przechowywane w Tajnym Magazynie VSCode",
+		"openAiCodexRateLimits": {
+			"title": "Usage Limits for Codex{{planLabel}}",
+			"loading": "Loading usage limits...",
+			"loadError": "Failed to load usage limits",
+			"retry": "Retry",
+			"usedPercent": "{{percent}}% used",
+			"resetsIn": "Resets in {{time}}",
+			"plan": {
+				"default": "",
+				"withType": " ({{planType}})"
+			},
+			"time": {
+				"now": "Now",
+				"notAvailable": "N/A"
+			},
+			"duration": {
+				"daysHours": "{{days}}d {{hours}}h",
+				"hoursMinutes": "{{hours}}h {{minutes}}m",
+				"minutes": "{{minutes}}m"
+			},
+			"window": {
+				"usage": "Usage",
+				"fiveHour": "5h limit",
+				"oneHour": "1h limit",
+				"daily": "Daily limit",
+				"weekly": "Weekly limit",
+				"days": "{{days}}d limit",
+				"hours": "{{hours}}h limit",
+				"minutes": "{{minutes}}m limit"
+			}
+		},
 		"useCustomBaseUrl": "Użyj niestandardowego URL bazowego",
 		"useReasoning": "Włącz rozumowanie",
 		"useHostHeader": "Użyj niestandardowego nagłówka Host",

+ 31 - 0
webview-ui/src/i18n/locales/pt-BR/settings.json

@@ -313,6 +313,37 @@
 		"glamaApiKey": "Chave API do Glama",
 		"getGlamaApiKey": "Obter chave API do Glama",
 		"apiKeyStorageNotice": "As chaves de API são armazenadas com segurança no Armazenamento Secreto do VSCode",
+		"openAiCodexRateLimits": {
+			"title": "Usage Limits for Codex{{planLabel}}",
+			"loading": "Loading usage limits...",
+			"loadError": "Failed to load usage limits",
+			"retry": "Retry",
+			"usedPercent": "{{percent}}% used",
+			"resetsIn": "Resets in {{time}}",
+			"plan": {
+				"default": "",
+				"withType": " ({{planType}})"
+			},
+			"time": {
+				"now": "Now",
+				"notAvailable": "N/A"
+			},
+			"duration": {
+				"daysHours": "{{days}}d {{hours}}h",
+				"hoursMinutes": "{{hours}}h {{minutes}}m",
+				"minutes": "{{minutes}}m"
+			},
+			"window": {
+				"usage": "Usage",
+				"fiveHour": "5h limit",
+				"oneHour": "1h limit",
+				"daily": "Daily limit",
+				"weekly": "Weekly limit",
+				"days": "{{days}}d limit",
+				"hours": "{{hours}}h limit",
+				"minutes": "{{minutes}}m limit"
+			}
+		},
 		"useCustomBaseUrl": "Usar URL base personalizado",
 		"useReasoning": "Habilitar raciocínio",
 		"useHostHeader": "Usar cabeçalho Host personalizado",

+ 31 - 0
webview-ui/src/i18n/locales/ru/settings.json

@@ -313,6 +313,37 @@
 		"glamaApiKey": "API-ключ Glama",
 		"getGlamaApiKey": "Получить API-ключ Glama",
 		"apiKeyStorageNotice": "API-ключи хранятся безопасно в Secret Storage VSCode",
+		"openAiCodexRateLimits": {
+			"title": "Usage Limits for Codex{{planLabel}}",
+			"loading": "Loading usage limits...",
+			"loadError": "Failed to load usage limits",
+			"retry": "Retry",
+			"usedPercent": "{{percent}}% used",
+			"resetsIn": "Resets in {{time}}",
+			"plan": {
+				"default": "",
+				"withType": " ({{planType}})"
+			},
+			"time": {
+				"now": "Now",
+				"notAvailable": "N/A"
+			},
+			"duration": {
+				"daysHours": "{{days}}d {{hours}}h",
+				"hoursMinutes": "{{hours}}h {{minutes}}m",
+				"minutes": "{{minutes}}m"
+			},
+			"window": {
+				"usage": "Usage",
+				"fiveHour": "5h limit",
+				"oneHour": "1h limit",
+				"daily": "Daily limit",
+				"weekly": "Weekly limit",
+				"days": "{{days}}d limit",
+				"hours": "{{hours}}h limit",
+				"minutes": "{{minutes}}m limit"
+			}
+		},
 		"useCustomBaseUrl": "Использовать пользовательский базовый URL",
 		"useReasoning": "Включить рассуждения",
 		"useHostHeader": "Использовать пользовательский Host-заголовок",

+ 31 - 0
webview-ui/src/i18n/locales/th/settings.json

@@ -345,6 +345,37 @@
 		"apiKeyStorageNotice": "คีย์ API จะถูกเก็บไว้อย่างปลอดภัยในที่เก็บข้อมูลลับของ VSCode",
 		"glamaApiKey": "คีย์ API ของ Glama",
 		"getGlamaApiKey": "รับคีย์ API ของ Glama",
+		"openAiCodexRateLimits": {
+			"title": "ขีดจำกัดการใช้งานสำหรับ Codex{{planLabel}}",
+			"loading": "กำลังโหลดขีดจำกัดการใช้งาน...",
+			"loadError": "ไม่สามารถโหลดขีดจำกัดการใช้งาน",
+			"retry": "ลองอีกครั้ง",
+			"usedPercent": "ใช้ไป {{percent}}%",
+			"resetsIn": "รีเซ็ตใน {{time}}",
+			"plan": {
+				"default": "",
+				"withType": " ({{planType}})"
+			},
+			"time": {
+				"now": "ตอนนี้",
+				"notAvailable": "ไม่พร้อมใช้งาน"
+			},
+			"duration": {
+				"daysHours": "{{days}} วัน {{hours}} ชม.",
+				"hoursMinutes": "{{hours}} ชม. {{minutes}} นาที",
+				"minutes": "{{minutes}} นาที"
+			},
+			"window": {
+				"usage": "การใช้งาน",
+				"fiveHour": "ขีดจำกัด 5 ชม.",
+				"oneHour": "ขีดจำกัด 1 ชม.",
+				"daily": "ขีดจำกัดรายวัน",
+				"weekly": "ขีดจำกัดรายสัปดาห์",
+				"days": "ขีดจำกัด {{days}} วัน",
+				"hours": "ขีดจำกัด {{hours}} ชม.",
+				"minutes": "ขีดจำกัด {{minutes}} นาที"
+			}
+		},
 		"useCustomBaseUrl": "ใช้ URL พื้นฐานที่กำหนดเอง",
 		"useReasoning": "เปิดใช้งานการให้เหตุผล",
 		"useHostHeader": "ใช้ส่วนหัวโฮสต์ที่กำหนดเอง",

+ 31 - 0
webview-ui/src/i18n/locales/tr/settings.json

@@ -313,6 +313,37 @@
 		"glamaApiKey": "Glama API Anahtarı",
 		"getGlamaApiKey": "Glama API Anahtarı Al",
 		"apiKeyStorageNotice": "API anahtarları VSCode'un Gizli Depolamasında güvenli bir şekilde saklanır",
+		"openAiCodexRateLimits": {
+			"title": "Usage Limits for Codex{{planLabel}}",
+			"loading": "Loading usage limits...",
+			"loadError": "Failed to load usage limits",
+			"retry": "Retry",
+			"usedPercent": "{{percent}}% used",
+			"resetsIn": "Resets in {{time}}",
+			"plan": {
+				"default": "",
+				"withType": " ({{planType}})"
+			},
+			"time": {
+				"now": "Now",
+				"notAvailable": "N/A"
+			},
+			"duration": {
+				"daysHours": "{{days}}d {{hours}}h",
+				"hoursMinutes": "{{hours}}h {{minutes}}m",
+				"minutes": "{{minutes}}m"
+			},
+			"window": {
+				"usage": "Usage",
+				"fiveHour": "5h limit",
+				"oneHour": "1h limit",
+				"daily": "Daily limit",
+				"weekly": "Weekly limit",
+				"days": "{{days}}d limit",
+				"hours": "{{hours}}h limit",
+				"minutes": "{{minutes}}m limit"
+			}
+		},
 		"useCustomBaseUrl": "Özel temel URL kullan",
 		"useReasoning": "Akıl yürütmeyi etkinleştir",
 		"useHostHeader": "Özel Host başlığı kullan",

+ 31 - 0
webview-ui/src/i18n/locales/uk/settings.json

@@ -350,6 +350,37 @@
 		"apiKeyStorageNotice": "Ключі API надійно зберігаються в Secret Storage VSCode",
 		"glamaApiKey": "Ключ API Glama",
 		"getGlamaApiKey": "Отримати ключ API Glama",
+		"openAiCodexRateLimits": {
+			"title": "Ліміти використання для Codex{{planLabel}}",
+			"loading": "Завантаження лімітів використання...",
+			"loadError": "Не вдалося завантажити ліміти використання",
+			"retry": "Спробувати знову",
+			"usedPercent": "{{percent}}% використано",
+			"resetsIn": "Скинеться через {{time}}",
+			"plan": {
+				"default": "",
+				"withType": " ({{planType}})"
+			},
+			"time": {
+				"now": "Зараз",
+				"notAvailable": "Недоступно"
+			},
+			"duration": {
+				"daysHours": "{{days}}д {{hours}}г",
+				"hoursMinutes": "{{hours}}г {{minutes}}хв",
+				"minutes": "{{minutes}}хв"
+			},
+			"window": {
+				"usage": "Використання",
+				"fiveHour": "5-годинний ліміт",
+				"oneHour": "1-годинний ліміт",
+				"daily": "Денний ліміт",
+				"weekly": "Тижневий ліміт",
+				"days": "{{days}}д ліміт",
+				"hours": "{{hours}}г ліміт",
+				"minutes": "{{minutes}}хв ліміт"
+			}
+		},
 		"useCustomBaseUrl": "Використовувати власний базовий URL",
 		"useReasoning": "Увімкнути міркування",
 		"useHostHeader": "Використовувати власний заголовок Host",

+ 31 - 0
webview-ui/src/i18n/locales/vi/settings.json

@@ -321,6 +321,37 @@
 		"glamaApiKey": "Khóa API Glama",
 		"getGlamaApiKey": "Lấy khóa API Glama",
 		"apiKeyStorageNotice": "Khóa API được lưu trữ an toàn trong Bộ lưu trữ bí mật của VSCode",
+		"openAiCodexRateLimits": {
+			"title": "Usage Limits for Codex{{planLabel}}",
+			"loading": "Loading usage limits...",
+			"loadError": "Failed to load usage limits",
+			"retry": "Retry",
+			"usedPercent": "{{percent}}% used",
+			"resetsIn": "Resets in {{time}}",
+			"plan": {
+				"default": "",
+				"withType": " ({{planType}})"
+			},
+			"time": {
+				"now": "Now",
+				"notAvailable": "N/A"
+			},
+			"duration": {
+				"daysHours": "{{days}}d {{hours}}h",
+				"hoursMinutes": "{{hours}}h {{minutes}}m",
+				"minutes": "{{minutes}}m"
+			},
+			"window": {
+				"usage": "Usage",
+				"fiveHour": "5h limit",
+				"oneHour": "1h limit",
+				"daily": "Daily limit",
+				"weekly": "Weekly limit",
+				"days": "{{days}}d limit",
+				"hours": "{{hours}}h limit",
+				"minutes": "{{minutes}}m limit"
+			}
+		},
 		"useCustomBaseUrl": "Sử dụng URL cơ sở tùy chỉnh",
 		"useReasoning": "Bật lý luận",
 		"useHostHeader": "Sử dụng tiêu đề Host tùy chỉnh",

+ 31 - 0
webview-ui/src/i18n/locales/zh-CN/settings.json

@@ -313,6 +313,37 @@
 		"glamaApiKey": "Glama API 密钥",
 		"getGlamaApiKey": "获取 Glama API 密钥",
 		"apiKeyStorageNotice": "API 密钥安全存储在 VSCode 的密钥存储中",
+		"openAiCodexRateLimits": {
+			"title": "Usage Limits for Codex{{planLabel}}",
+			"loading": "Loading usage limits...",
+			"loadError": "Failed to load usage limits",
+			"retry": "Retry",
+			"usedPercent": "{{percent}}% used",
+			"resetsIn": "Resets in {{time}}",
+			"plan": {
+				"default": "",
+				"withType": " ({{planType}})"
+			},
+			"time": {
+				"now": "Now",
+				"notAvailable": "N/A"
+			},
+			"duration": {
+				"daysHours": "{{days}}d {{hours}}h",
+				"hoursMinutes": "{{hours}}h {{minutes}}m",
+				"minutes": "{{minutes}}m"
+			},
+			"window": {
+				"usage": "Usage",
+				"fiveHour": "5h limit",
+				"oneHour": "1h limit",
+				"daily": "Daily limit",
+				"weekly": "Weekly limit",
+				"days": "{{days}}d limit",
+				"hours": "{{hours}}h limit",
+				"minutes": "{{minutes}}m limit"
+			}
+		},
 		"useCustomBaseUrl": "使用自定义基础 URL",
 		"useReasoning": "启用推理",
 		"useHostHeader": "使用自定义 Host 标头",

+ 31 - 0
webview-ui/src/i18n/locales/zh-TW/settings.json

@@ -323,6 +323,37 @@
 		"glamaApiKey": "Glama API 金鑰",
 		"getGlamaApiKey": "取得 Glama API 金鑰",
 		"apiKeyStorageNotice": "API 金鑰會安全地儲存在 VS Code 的 Secret Storage 中",
+		"openAiCodexRateLimits": {
+			"title": "Usage Limits for Codex{{planLabel}}",
+			"loading": "Loading usage limits...",
+			"loadError": "Failed to load usage limits",
+			"retry": "Retry",
+			"usedPercent": "{{percent}}% used",
+			"resetsIn": "Resets in {{time}}",
+			"plan": {
+				"default": "",
+				"withType": " ({{planType}})"
+			},
+			"time": {
+				"now": "Now",
+				"notAvailable": "N/A"
+			},
+			"duration": {
+				"daysHours": "{{days}}d {{hours}}h",
+				"hoursMinutes": "{{hours}}h {{minutes}}m",
+				"minutes": "{{minutes}}m"
+			},
+			"window": {
+				"usage": "Usage",
+				"fiveHour": "5h limit",
+				"oneHour": "1h limit",
+				"daily": "Daily limit",
+				"weekly": "Weekly limit",
+				"days": "{{days}}d limit",
+				"hours": "{{hours}}h limit",
+				"minutes": "{{minutes}}m limit"
+			}
+		},
 		"useCustomBaseUrl": "使用自訂基礎 URL",
 		"useReasoning": "啟用推理",
 		"useHostHeader": "使用自訂 Host 標頭",