Skip to content

Commit 085f8f1

Browse files
authored
Merge pull request lidge-jun#740 from lidge-jun/stack/gui-dashboard-startup-perf
perf(gui): progressive dashboard paint and Startup safety CLS
2 parents a780281 + a2072d5 commit 085f8f1

34 files changed

Lines changed: 2132 additions & 596 deletions

docs-site/src/content/docs/reference/cli.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,13 +429,15 @@ Windows **Task Scheduler**) that auto-starts on login and auto-restarts on crash
429429
| `start` | Start an installed service. |
430430
| `stop` | Stop the service and restore native Codex. |
431431
| `status` | Report whether the service is running. |
432+
| `repair` | Refresh installed service assets without re-registering (no Task Scheduler UAC). |
432433
| `uninstall` | Remove the service and restore native Codex. |
433434
| `remove` | Alias of `uninstall`. |
434435

435436
```bash
436437
ocx service
437438
ocx service install
438439
ocx service status
440+
ocx service repair
439441
ocx service uninstall
440442
```
441443

gui/src/components/provider-catalog/ProviderCatalog.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,19 @@ export default function ProviderCatalog({
6262

6363
const catalog = useMemo(() => presets.filter(p => p.id !== "custom"), [presets]);
6464

65-
/** Usage-ranked order: requests desc, then label (050a sortPresets is the no-usage fallback). */
66-
const ranked = useMemo(() => catalog.toSorted((a, b) => {
67-
const ra = usageRank[a.id] ?? 0;
68-
const rb = usageRank[b.id] ?? 0;
69-
if (rb !== ra) return rb - ra;
70-
return a.label.localeCompare(b.label, undefined, { sensitivity: "base" }) || a.id.localeCompare(b.id);
71-
}), [catalog, usageRank]);
65+
/** Usage-ranked order only after usage arrives; until then keep stable label order
66+
* so a slow /api/usage (~5s cold) cannot flash a catalog resort. */
67+
const ranked = useMemo(() => {
68+
const hasUsage = Object.keys(usageRank).length > 0;
69+
return catalog.toSorted((a, b) => {
70+
if (hasUsage) {
71+
const ra = usageRank[a.id] ?? 0;
72+
const rb = usageRank[b.id] ?? 0;
73+
if (rb !== ra) return rb - ra;
74+
}
75+
return a.label.localeCompare(b.label, undefined, { sensitivity: "base" }) || a.id.localeCompare(b.id);
76+
});
77+
}, [catalog, usageRank]);
7278

7379
const buckets = useMemo(() => bucketPresets(ranked), [ranked]);
7480
const tierList = buckets[tier];

gui/src/components/provider-workspace/ProviderAuthPanel.tsx

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* embedding for the workspace Settings tab (WP091). Consumes WP040+WP060
44
* handlers via props-down; no internal auth machinery.
55
*/
6-
import { useState } from "react";
6+
import { useEffect, useState } from "react";
77
import { useT } from "../../i18n/shared";
88
import { IconLock, IconTrash } from "../../icons";
99
import type { WorkspaceItem } from "../../provider-workspace/catalog";
@@ -27,9 +27,12 @@ import type { CodexAccountPoolController } from "../../hooks/useCodexAccountPool
2727
import type { AccountLoadState, OAuthAccountRow, ApiKeyRow, LoginHint, ProviderAuthHandlers } from "./types";
2828

2929
const DOCTOR_CMD = "ocx doctor";
30+
const QUOTA_ENRICH_RESERVE_MS = 4_000;
31+
const EMPTY_OAUTH_ACCOUNTS: OAuthAccountRow[] = [];
32+
const EMPTY_API_KEYS: ApiKeyRow[] = [];
3033

3134
export default function ProviderAuthPanel({
32-
item, apiBase, oauth, accounts = [], keys = [], accountLoadState = "ready",
35+
item, apiBase, oauth, accounts = EMPTY_OAUTH_ACCOUNTS, keys = EMPTY_API_KEYS, accountLoadState = "ready",
3336
switchingAccountId = null, busy = false, loginHint, authHandlers, onCodexActiveNeedsReauthChange,
3437
codexController,
3538
}: {
@@ -51,9 +54,27 @@ export default function ProviderAuthPanel({
5154
const [addingKey, setAddingKey] = useState(false);
5255
const [newKey, setNewKey] = useState("");
5356
const [keyBusy, setKeyBusy] = useState(false);
57+
const [reserveQuotaSlots, setReserveQuotaSlots] = useState(false);
5458
const deviceCodeCopy = useCopyFeedback<string>();
5559
const doctorCopy = useCopyFeedback<string>();
5660

61+
// Soft &quota=1 enrichment lands after the local account list. Reserve stacked
62+
// bar height briefly so bars don't shove rows when WHAM returns.
63+
useEffect(() => {
64+
if (accounts.length === 0) {
65+
setReserveQuotaSlots(false);
66+
return;
67+
}
68+
const needsFill = accounts.some(a => a.quota == null && !a.quotaUnavailable);
69+
if (!needsFill) {
70+
setReserveQuotaSlots(false);
71+
return;
72+
}
73+
setReserveQuotaSlots(true);
74+
const timer = window.setTimeout(() => setReserveQuotaSlots(false), QUOTA_ENRICH_RESERVE_MS);
75+
return () => window.clearTimeout(timer);
76+
}, [accounts]);
77+
5778
const surface = providerAuthSurface({ ...item, hasApiKey: item.hasApiKey || keys.length > 0 });
5879
const isOauth = surface === "oauth-accounts";
5980
const isKeyAuth = surface === "api-keys";
@@ -222,7 +243,7 @@ export default function ProviderAuthPanel({
222243
</button>
223244
)}
224245
{showDoctor && (
225-
<button type="button" className="btn btn-ghost btn-sm" onClick={copyDoctor}>
246+
<button type="button" className="btn btn-ghost btn-sm codex-auth-action-btn" onClick={copyDoctor}>
226247
<span aria-live="polite">{doctorCopyButtonLabel(t, doctorCopy.outcomeFor(account.id))}</span>
227248
</button>
228249
)}
@@ -238,13 +259,19 @@ export default function ProviderAuthPanel({
238259
<IconTrash style={{ width: 13, height: 13 }} aria-hidden="true" />
239260
</button>
240261
</div>
241-
{(account.quota || account.quotaUnavailable) && (
262+
{(account.quota != null || account.quotaUnavailable || (reserveQuotaSlots && account.quota == null)) && (
242263
<div className="pwi-auth-acct-quota">
243-
{account.quota && (
244-
<QuotaBars quota={account.quota} plan={null} threshold={80} t={t} layout="stacked" />
245-
)}
246-
{account.quotaUnavailable && (
264+
{account.quotaUnavailable ? (
247265
<p className="muted pwi-auth-acct-quota-stale">{t("pws.accountQuotaUnavailable")}</p>
266+
) : (
267+
<QuotaBars
268+
quota={account.quota ?? null}
269+
plan={null}
270+
threshold={80}
271+
t={t}
272+
layout="stacked"
273+
pending={account.quota == null}
274+
/>
248275
)}
249276
</div>
250277
)}

gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,16 @@ export default function ProviderOverviewDashboard({
2626
sections,
2727
quotaReports,
2828
usageTotals,
29+
usageLoading = false,
30+
quotasLoading = false,
2931
onSelectProvider,
3032
onEditConfig,
3133
}: {
3234
sections: WorkspaceSections;
3335
quotaReports: Record<string, ProviderQuotaReportView>;
3436
usageTotals: Record<string, ProviderUsageTotals>;
37+
usageLoading?: boolean;
38+
quotasLoading?: boolean;
3539
onSelectProvider: (name: string) => void;
3640
onEditConfig?: () => void;
3741
}) {
@@ -47,10 +51,12 @@ export default function ProviderOverviewDashboard({
4751

4852
const attention = useMemo(() => buildAttentionItems(sections, {}), [sections]);
4953
const attentionCount = attention.length;
50-
const reauthCount = useMemo(
51-
() => sections.needsSetup.filter(p => p.activeNeedsReauth).length,
54+
const readyReauthCount = useMemo(
55+
() => sections.ready.filter(p => p.activeNeedsReauth).length,
5256
[sections],
5357
);
58+
const readyCount = sections.ready.length - readyReauthCount;
59+
const needsAttentionCount = sections.needsSetup.length + readyReauthCount;
5460

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

98104
<div className="pws-dashboard-summary">
99-
<SummaryCard count={sections.ready.length} label={t("pws.status.ready")} tone="ok" />
105+
<SummaryCard count={readyCount} label={t("pws.status.ready")} tone="ok" />
100106
<SummaryCard
101-
count={sections.needsSetup.length}
102-
label={reauthCount > 0 ? t("pws.status.needsAttention") : t("pws.status.needsSetup")}
107+
count={needsAttentionCount}
108+
label={readyReauthCount > 0 ? t("pws.status.needsAttention") : t("pws.status.needsSetup")}
103109
tone="warn"
104110
/>
105111
<SummaryCard count={sections.disabled.length} label={t("prov.disabledBadge")} tone="muted" />
@@ -132,9 +138,13 @@ export default function ProviderOverviewDashboard({
132138
)}
133139

134140
<div className="pws-dashboard-columns">
135-
{quotaProviders.length > 0 && (
136-
<section className="pws-dashboard-section" aria-label={t("pws.dashboard.rateLimits")}>
137-
<h3 className="pws-dashboard-section-title">{t("pws.dashboard.rateLimits")}</h3>
141+
<section
142+
className="pws-dashboard-section pws-dashboard-section--rate-limits"
143+
aria-label={t("pws.dashboard.rateLimits")}
144+
aria-busy={quotasLoading || undefined}
145+
>
146+
<h3 className="pws-dashboard-section-title">{t("pws.dashboard.rateLimits")}</h3>
147+
{quotaProviders.length > 0 ? (
138148
<div className="pws-dashboard-rows">
139149
{quotaProviders.map(({ item, report }) => (
140150
<button
@@ -157,17 +167,39 @@ export default function ProviderOverviewDashboard({
157167
threshold={80}
158168
t={t}
159169
layout="stacked"
170+
pending={quotasLoading && !report.quota}
160171
/>
161172
</div>
162173
</button>
163174
))}
164175
</div>
165-
</section>
166-
)}
176+
) : quotasLoading ? (
177+
<div className="pws-dashboard-rows pws-dashboard-rows--pending" aria-hidden="true">
178+
{Array.from({ length: 3 }, (_, index) => (
179+
<div key={index} className="pws-dashboard-row pws-dashboard-row--skeleton">
180+
<span className="pws-dashboard-row-icon pws-skel" />
181+
<div className="pws-dashboard-row-info">
182+
<span className="pws-skel pws-skel--name" />
183+
<span className="pws-skel pws-skel--meta" />
184+
</div>
185+
<div className="pws-dashboard-row-bars">
186+
<QuotaBars quota={null} threshold={80} t={t} layout="stacked" pending />
187+
</div>
188+
</div>
189+
))}
190+
</div>
191+
) : (
192+
<p className="muted pws-dashboard-empty">{t("pws.dashboard.noRateLimits")}</p>
193+
)}
194+
</section>
167195

168-
{mostUsed.length > 0 ? (
169-
<section className="pws-dashboard-section" aria-label={t("pws.dashboard.recentlyUsed")}>
170-
<h3 className="pws-dashboard-section-title">{t("pws.dashboard.recentlyUsed")}</h3>
196+
<section
197+
className="pws-dashboard-section pws-dashboard-section--recent"
198+
aria-label={t("pws.dashboard.recentlyUsed")}
199+
aria-busy={usageLoading || undefined}
200+
>
201+
<h3 className="pws-dashboard-section-title">{t("pws.dashboard.recentlyUsed")}</h3>
202+
{mostUsed.length > 0 ? (
171203
<div className="pws-dashboard-rows">
172204
{mostUsed.map(provider => (
173205
<button
@@ -185,13 +217,20 @@ export default function ProviderOverviewDashboard({
185217
</button>
186218
))}
187219
</div>
188-
</section>
189-
) : (
190-
<section className="pws-dashboard-section" aria-label={t("pws.dashboard.recentlyUsed")}>
191-
<h3 className="pws-dashboard-section-title">{t("pws.dashboard.recentlyUsed")}</h3>
192-
<p className="muted">{t("pws.dashboard.noUsage")}</p>
193-
</section>
194-
)}
220+
) : usageLoading ? (
221+
<div className="pws-dashboard-rows pws-dashboard-rows--pending" aria-hidden="true">
222+
{Array.from({ length: 3 }, (_, index) => (
223+
<div key={index} className="pws-dashboard-row pws-dashboard-row--skeleton">
224+
<span className="pws-dashboard-row-icon pws-skel" />
225+
<span className="pws-skel pws-skel--name" />
226+
<span className="pws-skel pws-skel--count" />
227+
</div>
228+
))}
229+
</div>
230+
) : (
231+
<p className="muted pws-dashboard-empty">{t("pws.dashboard.noUsage")}</p>
232+
)}
233+
</section>
195234
</div>
196235
</div>
197236
);

gui/src/components/provider-workspace/ProviderRail.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,9 @@ export function RailRow({ item, selected, tabbable, modelCount, isDefault, showC
110110
<span className="pwi-rail-badge pwi-rail-badge--free" title={t("pws.freeTitle")}>{t("modal.badge.free")}</span>
111111
) : null}
112112
</span>
113-
{secondaryLabel && (
114-
<span className="providers-workspace-rail-secondary" title={secondaryLabel}>
115-
{secondaryLabel}
116-
</span>
117-
)}
113+
<span className="providers-workspace-rail-secondary" title={secondaryLabel || undefined}>
114+
{secondaryLabel || "\u00a0"}
115+
</span>
118116
</span>
119117
<span className="providers-workspace-rail-trail">
120118
{isDefault && (

0 commit comments

Comments
 (0)