Repository navigation
GUI tabs show no loading state on switch; providers tab issues 38 requests per switch #753
Description
Summary
Switching sidebar tabs in the dashboard shows an empty surface before data appears, and several tabs give no loading indication at all while that happens. On the Codex Auth page the account list is blank for the first ~80ms of every revisit, with no skeleton or spinner, which reads as "the accounts did not load". Data does appear after an explicit action such as the quota-refresh button, which makes the explicit action look like the only working path.
Separately, one #providers tab switch issues 38 /api/* requests, including 8 duplicate /api/provider-quotas calls.
This issue asks for one shared loading contract across every tab, plus the request-count reduction.
Environment
- opencodex
v2.7.43(npm global install), macOS, proxy on127.0.0.1:10100 - Served assets:
index-cmds12BG.js/index-Czw-jpTU.css
What actually happens
Requests are issued correctly on every tab switch. Measured with CDP Network against the running dashboard:
| Switch | Requests | Status / latency |
|---|---|---|
| to Codex Auth | /api/codex-auth/active, /api/codex-auth/accounts, /api/codex-auth/active, /api/config |
200, 2-6ms |
| to Models | 7 requests | 200, 3-127ms |
| to Providers | 38 requests, incl. /api/provider-quotas x8 |
200 |
So this is not a missing-request bug. It is a missing-loading-state bug plus a request-count problem.
DOM sampling during tab switches (rows = account rows, skel = skeleton nodes):
to codex-auth (1st): 19ms rows=0 ... 82ms rows=7 <- first account row at 82ms
to codex-auth (2nd): 18ms rows=0 ... 82ms rows=7
cold reload: 113ms rows=0 skel=0 ... 480ms rows=7
skel=0 throughout: no skeleton is rendered in the shipped build during that window.
Forced refresh behaves differently, and that is why it looks like the only working path:
/api/codex-auth/accounts?refresh=1 200 908ms
During those 908ms the existing rows stay visible, because the forced path runs on an already-populated surface and skips the loading transition entirely.
Root cause
Two independent things.
1. No cache seed across route remounts. App.tsx renders one page at a time, so leaving a tab unmounts its controller. Returning starts from an empty account array, and the account-pool loading UI only renders when accountsCount === 0 — which is exactly that moment. There is no skeleton in the shipped build, so the user sees a bare surface.
2. No shared loading contract. There is no exported Spinner or Skeleton primitive. Each of the 15 surfaces implements its own first-load behavior: shimmer skeleton, EmptyState text, plain muted text, or nothing. Two surfaces cannot distinguish loading from failed at all, so a failed fetch can look like an indefinite load. Ten surfaces announce nothing to screen readers during first load.
The 38-request #providers burst comes from a derived cache key: provider account responses each update quotaRefreshKey in Providers.tsx, and the workspace shell's quota effect depends on that key, so it re-runs once per provider response.
Server side is not the cause
Local loopback timings stay under 1.1ms for /healthz, /api/config, /api/codex-auth/accounts, and the document route. /api/codex-auth/active and /api/config are in-memory projections. There is no rate limiter, ETag, or admission gate that could return stale or empty data on rapid switching, and no canceled requests were observed under rapid switching (6 switches at 180ms intervals, 0 loadingFailed events, 0 console errors).
One genuine server-side latency finding is worth tracking: /api/codex-auth/accounts fans out to one upstream usage request per credentialed account when its 5-minute cache is cold, with concurrency 4 and an 8s per-request timeout, so the cold bound is roughly 8s * ceil(accounts / 4). There is no request-level coalescing.
Requested changes
- A shared loading contract used by all 15 surfaces: immediate fetch on mount, a layout-shaped loading indicator, and a real distinction between loading, empty, and failed.
- Consistent skeleton/spinner primitives instead of four different per-page patterns, with
role="status"/aria-busycoverage. - Cache seeding plus background revalidation so a tab revisit does not blank out, without letting a stale-but-wrong list persist silently.
- Request de-duplication on the providers surface (stabilize the derived quota key, share in-flight reads) while preserving genuine refresh triggers: account switch, account mutation, manual refresh, cache invalidation, login/logout, config save.
- Coalescing and stale-while-revalidate for the
/accountsupstream fan-out.
Affected surfaces
All sidebar tabs: Dashboard, Codex Auth, Providers, Models, Subagents, Logs (plus the Debug subview), Usage, Storage, API, Claude (Code and Desktop), Grok, Startup, Combos.
Notes
Some of this is already partially addressed on dev but not released. The dev work also introduced a related defect worth avoiding in the final design: a successful empty account list is recorded as hydrated, so a later failed load cannot surface an error state.
Activity
github-actions commentedon Jul 30, 2026
Automated translation bookkeeping — detected language: unknown.
lidge-jun commentedon Jul 30, 2026
Patch plan
The work is split into six phases. WP1 (root cause + this issue) is done; the rest are planned to diff level.
useKeyedClientResource, plus skeleton and status primitives with correctrole="status"/aria-busysemantics"a=1re-reads/accountscoalescing and stale-while-revalidateContract shape (WP2)
Six states, so that a failure after a prior success can never look like a normal ready state:
cold, loading-with-stale-data, ready-empty, ready-populated, failed-cold, failed-with-stale.
"Has ever succeeded" and "last attempt succeeded" are tracked separately — this is what prevents a
successful empty response from suppressing a later error.
Mount fetch moves off the
setTimeout(..., 0)pattern used by 13 of the 15 surfaces, because amount-then-unmount currently drops the request with no retry.
Request targets (WP4)
Derived arithmetic rather than a round number: cold
#providersgoes from 38 measured to 25 or fewer(
38 - 7for provider-quotas in-flight sharing,- 6for merging the"a=1account re-reads).Warm revisit target is 8 or fewer. Four scenarios are explicitly excluded from reduction and get
preservation tests: explicit refresh, OAuth account switch, login/logout, config save. Two more
preservation triggers cover concurrent mounts and in-flight followers.
Any target below 25 requires naming a specific request from the measured list and proving its
display or refresh behavior survives.
Alignment fix (WP6)
The threshold control and its switch are bottom-aligned today (
align-items: flex-end), leaving a32px numeric control and a 20px toggle with centers 6px apart. The fix centers the switch inside a
32px slot so both centers land on the same line, and converges the hand-rolled toggle onto the
shared
Switchcomponent. Verified by measuring both elements' box centers, not by eyeballing.Verification
Each phase re-runs the same CDP Network plus DOM-sampling procedure used to produce the measurements
in this issue, so before and after are directly comparable. Screenshots are captured per surface for
WP3 and for the card and switch alignment in WP6.
Detailed diff-level documents live in the maintainer's private planning repo; this comment is the
public summary.
fix(gui): close the WP3 audit blockers and stop the startup chip wait…
plan(gui): fold the wp4 audit into the revalidation policy
perf(gui): stop passive polling in a hidden tab, with an opt-out
fix(gui): make Usage, Storage and Subagents readable by scrolling again
fix(gui): drop the duplicated Usage section heading
docs(plan): record the hidden-tab poll gate and the tab layout revert
fix(gui): uncap the Usage model and provider tables
perf(gui): read provider quotas once per cold load, not once per prov…
docs(plan): record the measured quota duplication and its fix
fix(gui): centre the auto-switch toggle and name the rotation strateg…
docs(plan): record the wp6 alignment measurements and the kept descri…
docs(cli): document every per-model ocx models operation
10 remaining items
plan(gui): fold the wp4 audit into the revalidation policy
perf(gui): stop passive polling in a hidden tab, with an opt-out
fix(gui): make Usage, Storage and Subagents readable by scrolling again
fix(gui): drop the duplicated Usage section heading
docs(plan): record the hidden-tab poll gate and the tab layout revert
fix(gui): uncap the Usage model and provider tables
perf(gui): read provider quotas once per cold load, not once per prov…
docs(plan): record the measured quota duplication and its fix
fix(gui): centre the auto-switch toggle and name the rotation strateg…
docs(plan): record the wp6 alignment measurements and the kept descri…
docs(cli): document every per-model ocx models operation
fix(api): reject out-of-enum input modalities at the custom-model routes
docs(plan): record wp7 as PARTIAL and what the doc audit uncovered
fix(gui): cap the two long lists without trapping the wheel
docs(plan): record the list caps, the sticky header retry and the cac…