Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -429,13 +429,15 @@ 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`. |

```bash
ocx service
ocx service install
ocx service status
ocx service repair
ocx service uninstall
```

Expand Down
20 changes: 13 additions & 7 deletions gui/src/components/provider-catalog/ProviderCatalog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
43 changes: 35 additions & 8 deletions gui/src/components/provider-workspace/ProviderAuthPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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,
}: {
Expand All @@ -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<string>();
const doctorCopy = useCopyFeedback<string>();

// Soft &quota=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]);
Comment on lines +57 to +76

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file structure first, then inspect the relevant slices with line numbers.
ast-grep outline gui/src/components/provider-workspace/ProviderAuthPanel.tsx --view expanded || true

echo
echo "===== lines 1-140 ====="
sed -n '1,140p' gui/src/components/provider-workspace/ProviderAuthPanel.tsx | cat -n

echo
echo "===== lines 220-320 ====="
sed -n '220,320p' gui/src/components/provider-workspace/ProviderAuthPanel.tsx | cat -n

Repository: lidge-jun/opencodex

Length of output: 13710


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline gui/src/components/provider-workspace/ProviderAuthPanel.tsx --view expanded || true

echo
echo "===== lines 1-140 ====="
sed -n '1,140p' gui/src/components/provider-workspace/ProviderAuthPanel.tsx | cat -n

echo
echo "===== lines 220-320 ====="
sed -n '220,320p' gui/src/components/provider-workspace/ProviderAuthPanel.tsx | cat -n

Repository: lidge-jun/opencodex

Length of output: 13710


Reserve the quota placeholder on the initial render. In gui/src/components/provider-workspace/ProviderAuthPanel.tsx:55-74, reserveQuotaSlots is only enabled inside useEffect, so accounts that already need quota enrichment paint once without the reserved quota row and then insert it after paint. If the goal is to prevent layout shift, initialize the state from accounts.some(a => a.quota == null && !a.quotaUnavailable) and keep the timeout in the effect, or derive the placeholder directly from that condition.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/src/components/provider-workspace/ProviderAuthPanel.tsx` around lines 55
- 74, Initialize reserveQuotaSlots from the current accounts list using the
existing quota-enrichment condition so the placeholder is reserved on the first
render, rather than only after useEffect runs. Update the useEffect around
reserveQuotaSlots to retain the existing timeout and cleanup behavior for
clearing the reservation after QUOTA_ENRICH_RESERVE_MS.

Source: Path instructions


const surface = providerAuthSurface({ ...item, hasApiKey: item.hasApiKey || keys.length > 0 });
const isOauth = surface === "oauth-accounts";
const isKeyAuth = surface === "api-keys";
Expand Down Expand Up @@ -222,7 +243,7 @@ export default function ProviderAuthPanel({
</button>
)}
{showDoctor && (
<button type="button" className="btn btn-ghost btn-sm" onClick={copyDoctor}>
<button type="button" className="btn btn-ghost btn-sm codex-auth-action-btn" onClick={copyDoctor}>
<span aria-live="polite">{doctorCopyButtonLabel(t, doctorCopy.outcomeFor(account.id))}</span>
</button>
)}
Expand All @@ -238,13 +259,19 @@ export default function ProviderAuthPanel({
<IconTrash style={{ width: 13, height: 13 }} aria-hidden="true" />
</button>
</div>
{(account.quota || account.quotaUnavailable) && (
{(account.quota != null || account.quotaUnavailable || (reserveQuotaSlots && account.quota == null)) && (
<div className="pwi-auth-acct-quota">
{account.quota && (
<QuotaBars quota={account.quota} plan={null} threshold={80} t={t} layout="stacked" />
)}
{account.quotaUnavailable && (
{account.quotaUnavailable ? (
<p className="muted pwi-auth-acct-quota-stale">{t("pws.accountQuotaUnavailable")}</p>
) : (
<QuotaBars
quota={account.quota ?? null}
plan={null}
threshold={80}
t={t}
layout="stacked"
pending={account.quota == null}
/>
)}
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,16 @@ export default function ProviderOverviewDashboard({
sections,
quotaReports,
usageTotals,
usageLoading = false,
quotasLoading = false,
onSelectProvider,
onEditConfig,
}: {
sections: WorkspaceSections;
quotaReports: Record<string, ProviderQuotaReportView>;
usageTotals: Record<string, ProviderUsageTotals>;
usageLoading?: boolean;
quotasLoading?: boolean;
onSelectProvider: (name: string) => void;
onEditConfig?: () => void;
}) {
Expand All @@ -47,10 +51,12 @@ export default function ProviderOverviewDashboard({

const attention = useMemo(() => buildAttentionItems(sections, {}), [sections]);
const attentionCount = attention.length;
const reauthCount = useMemo(
() => sections.needsSetup.filter(p => p.activeNeedsReauth).length,
const readyReauthCount = useMemo(
() => sections.ready.filter(p => p.activeNeedsReauth).length,
[sections],
);
const readyCount = sections.ready.length - readyReauthCount;
const needsAttentionCount = sections.needsSetup.length + readyReauthCount;

/* Rate-limit rows: urgency first (highest utilisation), then name */
const quotaProviders = useMemo(() => {
Expand Down Expand Up @@ -96,10 +102,10 @@ export default function ProviderOverviewDashboard({
</div>

<div className="pws-dashboard-summary">
<SummaryCard count={sections.ready.length} label={t("pws.status.ready")} tone="ok" />
<SummaryCard count={readyCount} label={t("pws.status.ready")} tone="ok" />
<SummaryCard
count={sections.needsSetup.length}
label={reauthCount > 0 ? t("pws.status.needsAttention") : t("pws.status.needsSetup")}
count={needsAttentionCount}
label={readyReauthCount > 0 ? t("pws.status.needsAttention") : t("pws.status.needsSetup")}
tone="warn"
/>
<SummaryCard count={sections.disabled.length} label={t("prov.disabledBadge")} tone="muted" />
Expand Down Expand Up @@ -132,9 +138,13 @@ export default function ProviderOverviewDashboard({
)}

<div className="pws-dashboard-columns">
{quotaProviders.length > 0 && (
<section className="pws-dashboard-section" aria-label={t("pws.dashboard.rateLimits")}>
<h3 className="pws-dashboard-section-title">{t("pws.dashboard.rateLimits")}</h3>
<section
className="pws-dashboard-section pws-dashboard-section--rate-limits"
aria-label={t("pws.dashboard.rateLimits")}
aria-busy={quotasLoading || undefined}
>
<h3 className="pws-dashboard-section-title">{t("pws.dashboard.rateLimits")}</h3>
{quotaProviders.length > 0 ? (
<div className="pws-dashboard-rows">
{quotaProviders.map(({ item, report }) => (
<button
Expand All @@ -157,17 +167,39 @@ export default function ProviderOverviewDashboard({
threshold={80}
t={t}
layout="stacked"
pending={quotasLoading && !report.quota}
/>
</div>
</button>
))}
</div>
</section>
)}
) : quotasLoading ? (
<div className="pws-dashboard-rows pws-dashboard-rows--pending" aria-hidden="true">
{Array.from({ length: 3 }, (_, index) => (
<div key={index} className="pws-dashboard-row pws-dashboard-row--skeleton">
<span className="pws-dashboard-row-icon pws-skel" />
<div className="pws-dashboard-row-info">
<span className="pws-skel pws-skel--name" />
<span className="pws-skel pws-skel--meta" />
</div>
<div className="pws-dashboard-row-bars">
<QuotaBars quota={null} threshold={80} t={t} layout="stacked" pending />
</div>
</div>
))}
</div>
) : (
<p className="muted pws-dashboard-empty">{t("pws.dashboard.noRateLimits")}</p>
)}
</section>

{mostUsed.length > 0 ? (
<section className="pws-dashboard-section" aria-label={t("pws.dashboard.recentlyUsed")}>
<h3 className="pws-dashboard-section-title">{t("pws.dashboard.recentlyUsed")}</h3>
<section
className="pws-dashboard-section pws-dashboard-section--recent"
aria-label={t("pws.dashboard.recentlyUsed")}
aria-busy={usageLoading || undefined}
>
<h3 className="pws-dashboard-section-title">{t("pws.dashboard.recentlyUsed")}</h3>
{mostUsed.length > 0 ? (
<div className="pws-dashboard-rows">
{mostUsed.map(provider => (
<button
Expand All @@ -185,13 +217,20 @@ export default function ProviderOverviewDashboard({
</button>
))}
</div>
</section>
) : (
<section className="pws-dashboard-section" aria-label={t("pws.dashboard.recentlyUsed")}>
<h3 className="pws-dashboard-section-title">{t("pws.dashboard.recentlyUsed")}</h3>
<p className="muted">{t("pws.dashboard.noUsage")}</p>
</section>
)}
) : usageLoading ? (
<div className="pws-dashboard-rows pws-dashboard-rows--pending" aria-hidden="true">
{Array.from({ length: 3 }, (_, index) => (
<div key={index} className="pws-dashboard-row pws-dashboard-row--skeleton">
<span className="pws-dashboard-row-icon pws-skel" />
<span className="pws-skel pws-skel--name" />
<span className="pws-skel pws-skel--count" />
</div>
))}
</div>
) : (
<p className="muted pws-dashboard-empty">{t("pws.dashboard.noUsage")}</p>
)}
</section>
</div>
</div>
);
Expand Down
8 changes: 3 additions & 5 deletions gui/src/components/provider-workspace/ProviderRail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,9 @@ export function RailRow({ item, selected, tabbable, modelCount, isDefault, showC
<span className="pwi-rail-badge pwi-rail-badge--free" title={t("pws.freeTitle")}>{t("modal.badge.free")}</span>
) : null}
</span>
{secondaryLabel && (
<span className="providers-workspace-rail-secondary" title={secondaryLabel}>
{secondaryLabel}
</span>
)}
<span className="providers-workspace-rail-secondary" title={secondaryLabel || undefined}>
{secondaryLabel || "\u00a0"}
</span>
</span>
<span className="providers-workspace-rail-trail">
{isDefault && (
Expand Down
Loading
Loading