diff --git a/docs-site/src/content/docs/reference/cli.md b/docs-site/src/content/docs/reference/cli.md index 1d37ba14b..88b308ab5 100644 --- a/docs-site/src/content/docs/reference/cli.md +++ b/docs-site/src/content/docs/reference/cli.md @@ -429,6 +429,7 @@ Windows **Task Scheduler**) that auto-starts on login and auto-restarts on crash | `start` | Start an installed service. | | `stop` | Stop the service and restore native Codex. | | `status` | Report whether the service is running. | +| `repair` | Refresh installed service assets without re-registering (no Task Scheduler UAC). | | `uninstall` | Remove the service and restore native Codex. | | `remove` | Alias of `uninstall`. | @@ -436,6 +437,7 @@ Windows **Task Scheduler**) that auto-starts on login and auto-restarts on crash ocx service ocx service install ocx service status +ocx service repair ocx service uninstall ``` diff --git a/gui/src/components/provider-catalog/ProviderCatalog.tsx b/gui/src/components/provider-catalog/ProviderCatalog.tsx index 1e8a659e8..dc093bf6a 100644 --- a/gui/src/components/provider-catalog/ProviderCatalog.tsx +++ b/gui/src/components/provider-catalog/ProviderCatalog.tsx @@ -62,13 +62,19 @@ export default function ProviderCatalog({ const catalog = useMemo(() => presets.filter(p => p.id !== "custom"), [presets]); - /** Usage-ranked order: requests desc, then label (050a sortPresets is the no-usage fallback). */ - const ranked = useMemo(() => catalog.toSorted((a, b) => { - const ra = usageRank[a.id] ?? 0; - const rb = usageRank[b.id] ?? 0; - if (rb !== ra) return rb - ra; - return a.label.localeCompare(b.label, undefined, { sensitivity: "base" }) || a.id.localeCompare(b.id); - }), [catalog, usageRank]); + /** Usage-ranked order only after usage arrives; until then keep stable label order + * so a slow /api/usage (~5s cold) cannot flash a catalog resort. */ + const ranked = useMemo(() => { + const hasUsage = Object.keys(usageRank).length > 0; + return catalog.toSorted((a, b) => { + if (hasUsage) { + const ra = usageRank[a.id] ?? 0; + const rb = usageRank[b.id] ?? 0; + if (rb !== ra) return rb - ra; + } + return a.label.localeCompare(b.label, undefined, { sensitivity: "base" }) || a.id.localeCompare(b.id); + }); + }, [catalog, usageRank]); const buckets = useMemo(() => bucketPresets(ranked), [ranked]); const tierList = buckets[tier]; diff --git a/gui/src/components/provider-workspace/ProviderAuthPanel.tsx b/gui/src/components/provider-workspace/ProviderAuthPanel.tsx index efa7bed39..0465947aa 100644 --- a/gui/src/components/provider-workspace/ProviderAuthPanel.tsx +++ b/gui/src/components/provider-workspace/ProviderAuthPanel.tsx @@ -3,7 +3,7 @@ * embedding for the workspace Settings tab (WP091). Consumes WP040+WP060 * handlers via props-down; no internal auth machinery. */ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useT } from "../../i18n/shared"; import { IconLock, IconTrash } from "../../icons"; import type { WorkspaceItem } from "../../provider-workspace/catalog"; @@ -27,9 +27,12 @@ import type { CodexAccountPoolController } from "../../hooks/useCodexAccountPool import type { AccountLoadState, OAuthAccountRow, ApiKeyRow, LoginHint, ProviderAuthHandlers } from "./types"; const DOCTOR_CMD = "ocx doctor"; +const QUOTA_ENRICH_RESERVE_MS = 4_000; +const EMPTY_OAUTH_ACCOUNTS: OAuthAccountRow[] = []; +const EMPTY_API_KEYS: ApiKeyRow[] = []; export default function ProviderAuthPanel({ - item, apiBase, oauth, accounts = [], keys = [], accountLoadState = "ready", + item, apiBase, oauth, accounts = EMPTY_OAUTH_ACCOUNTS, keys = EMPTY_API_KEYS, accountLoadState = "ready", switchingAccountId = null, busy = false, loginHint, authHandlers, onCodexActiveNeedsReauthChange, codexController, }: { @@ -51,9 +54,27 @@ export default function ProviderAuthPanel({ const [addingKey, setAddingKey] = useState(false); const [newKey, setNewKey] = useState(""); const [keyBusy, setKeyBusy] = useState(false); + const [reserveQuotaSlots, setReserveQuotaSlots] = useState(false); const deviceCodeCopy = useCopyFeedback(); const doctorCopy = useCopyFeedback(); + // Soft "a=1 enrichment lands after the local account list. Reserve stacked + // bar height briefly so bars don't shove rows when WHAM returns. + useEffect(() => { + if (accounts.length === 0) { + setReserveQuotaSlots(false); + return; + } + const needsFill = accounts.some(a => a.quota == null && !a.quotaUnavailable); + if (!needsFill) { + setReserveQuotaSlots(false); + return; + } + setReserveQuotaSlots(true); + const timer = window.setTimeout(() => setReserveQuotaSlots(false), QUOTA_ENRICH_RESERVE_MS); + return () => window.clearTimeout(timer); + }, [accounts]); + const surface = providerAuthSurface({ ...item, hasApiKey: item.hasApiKey || keys.length > 0 }); const isOauth = surface === "oauth-accounts"; const isKeyAuth = surface === "api-keys"; @@ -222,7 +243,7 @@ export default function ProviderAuthPanel({ )} {showDoctor && ( - )} @@ -238,13 +259,19 @@ export default function ProviderAuthPanel({