11# 290-02 Rate-Limit Reset Credits — UI Specification
22
3- ## Design Decision: Workspace Account Behavior
3+ ## Design Decision: Account Plan Behavior
4+
5+ ** Correction (2026-06-27): do not exclude workspace/team plans from reset-credit lookup.**
6+ Live WHAM probes showed ` team ` accounts can return ` rate_limit_reset_credits.available_count `
7+ and can be queried through the same reset-credit endpoint as personal accounts. The UI must
8+ therefore treat the upstream ` resetCredits ` value as authoritative instead of disabling the
9+ ticket badge from ` plan_type ` .
410
511Per codex-rs source (` protocol/src/account.rs:50-59 ` ):
612
@@ -13,18 +19,15 @@ pub fn is_workspace_account(self) -> bool {
1319}
1420```
1521
16- Workspace plans: ` team ` , ` business ` , ` enterprise ` , ` edu ` , ` self_serve_business_usage_based ` , ` enterprise_cbp_usage_based `
17- Workspace aliases (auth.rs): ` hc ` → enterprise, ` education ` → edu
18- Personal plans: ` free ` , ` go ` , ` plus ` , ` pro ` , ` prolite `
19-
20- ** codex-rs TUI behavior** (` usage.rs:13-14 ` ):
21- - Workspace accounts → reset menu item ** visible but disabled** + "No rate-limit resets available."
22- - Personal accounts → reset menu item enabled
22+ The old codex-rs-derived assumption was:
23+ - Workspace accounts always have no reset credits.
24+ - Workspace accounts should show a disabled reset menu.
2325
24- ** opencodex decision** : Match codex-rs — show ticket badge on ALL accounts,
25- but workspace accounts show ` 0 ` count with disabled/grayed styling.
26- Server-side, workspace accounts return ` available_count: 0 ` (or null),
27- so this naturally resolves.
26+ That assumption is not true for current ChatGPT team accounts. Current opencodex behavior:
27+ - If ` resetCredits ` is a number, show a clickable ticket badge for any plan.
28+ - If ` resetCredits > 0 ` , use the amber badge.
29+ - If ` resetCredits === 0 ` , use the muted badge and allow the empty-state popup.
30+ - If ` resetCredits === undefined ` , hide the badge because the value has not been fetched.
2831
2932## Component Architecture
3033
@@ -38,23 +41,21 @@ Placement: between plan badge and NEXT SESSION badge.
3841└──────────────────────────────────────────────────────────┘
3942```
4043
41- For workspace accounts:
44+ For team accounts with credits :
4245```
4346┌─ card-head ──────────────────────────────────────────────┐
44- │ ● p***1@gmail.com [team] [🎫 0 ] [×] │
45- │ ↑ grayed out, no click │
47+ │ ● p***1@gmail.com [team] [🎫 1 ] [×] │
48+ │ ↑ clickable; opens ticket popup │
4649└──────────────────────────────────────────────────────────┘
4750```
4851
4952### Badge Variants
5053
5154| Condition | Style | Clickable |
5255| -----------| -------| -----------|
53- | ` resetCredits > 0 ` + personal plan | amber bg, amber text, ticket icon + count | Yes → opens popup |
54- | ` resetCredits === 0 ` + personal plan | muted bg, muted text, ticket icon + "0" | Yes → opens popup (shows empty state) |
55- | Workspace plan (any count) | muted bg, faint text, ticket icon + "–" | No (disabled) |
56- | Workspace plan + ` resetCredits === undefined ` | muted bg, faint text, ticket icon + "–" | No (disabled) |
57- | Personal plan + ` resetCredits === undefined ` | Hidden | — |
56+ | ` resetCredits > 0 ` | amber bg, amber text, ticket icon + count | Yes → opens popup |
57+ | ` resetCredits === 0 ` | muted bg, muted text, ticket icon + "0" | Yes → opens popup (shows empty state) |
58+ | ` resetCredits === undefined ` | Hidden | — |
5859
5960### Ticket List Popup (Modal)
6061
@@ -186,18 +187,6 @@ interface AccountQuota {
186187}
187188```
188189
189- ### isWorkspaceAccount Helper
190-
191- ``` tsx
192- function isWorkspaceAccount(plan ? : string ): boolean {
193- if (! plan ) return false ;
194- const ws = [" team" , " business" , " enterprise" , " edu" ,
195- " self_serve_business_usage_based" , " enterprise_cbp_usage_based" ,
196- " hc" , " education" ]; // aliases from codex-rs auth.rs
197- return ws .includes (plan .toLowerCase ());
198- }
199- ```
200-
201190### Ticket Badge in card-head (pool cards, line ~ 129-134)
202191
203192Insert after plan badge, before NEXT SESSION badge:
@@ -206,7 +195,7 @@ Insert after plan badge, before NEXT SESSION badge:
206195{/* After: {a.plan && <span className="badge badge-green">{a.plan}</span>} */ }
207196<TicketBadge
208197 account = { a }
209- onClick = { () => ! isWorkspaceAccount ( a . plan ) && setResetPopup (a )}
198+ onClick = { () => setResetPopup (a )}
210199/>
211200{/* Before: {isNext(a.id) && ... NEXT SESSION ...} */ }
212201```
@@ -224,7 +213,7 @@ Insert between `<strong>` (line 106) and CURRENT/NEXT badge (line 107):
224213 { main && (
225214 <TicketBadge
226215 account = { { ... main , id: " __main__" } as AccountEntry }
227- onClick = { () => ! isWorkspaceAccount ( main ?. plan ) && setResetPopup ({ ... main , id: " __main__" } as AccountEntry )}
216+ onClick = { () => setResetPopup ({ ... main , id: " __main__" } as AccountEntry )}
228217 />
229218 )}
230219 <span className = { ` badge ${! activeId ? " badge-primary" : " badge-muted" } ` } >
@@ -238,23 +227,19 @@ not `getValidCodexToken()`. This is already handled in `00_plan.md` §1.2.
238227```tsx
239228function TicketBadge({ account , onClick } : { account : AccountEntry ; onClick : () => void } ) {
240229 const credits = account .quota ?.resetCredits ;
241- const workspace = isWorkspaceAccount (account .plan );
242-
243- // Workspace: always show disabled badge. Personal: hide if not fetched.
244- if (! workspace && credits === undefined ) return null ;
230+ if (credits === undefined ) return null ;
245231
246- const hasCredits = typeof credits === " number" && credits > 0 && ! workspace ;
232+ const hasCredits = typeof credits === " number" && credits > 0 ;
247233
248234 return (
249235 <button
250236 type = " button"
251- className = { ` badge ${hasCredits ? " badge-amber" : " badge-muted" } ${workspace ? " badge-disabled" : " badge-clickable" } ` }
252- onClick = { workspace ? undefined : (e ) => { e .stopPropagation (); onClick (); }}
253- disabled = { workspace }
254- aria-label = { workspace ? " Not available for workspace accounts" : ` ${credits ?? 0 } reset credit(s) ` }
237+ className = { ` badge ${hasCredits ? " badge-amber" : " badge-muted" } badge-clickable ` }
238+ onClick = { (e ) => { e .stopPropagation (); onClick (); }}
239+ aria-label = { ` ${credits } reset credit(s) ` }
255240 >
256241 <IconTicket width = { 12 } />
257- { workspace ? " – " : ( credits ?? 0 ) }
242+ { credits }
258243 </button >
259244 );
260245}
@@ -425,13 +410,13 @@ export const IconTicket = (p: P) => (
425410| # | Severity | Issue | Fix |
426411|---|----------|-------|-----|
427412| 1 | High | Main card insertion point wrong | Corrected: between `<strong >` and CURRENT badge |
428- | 2 | High | Workspace + undefined = hidden, contradicts "always show" | Workspace always shows disabled badge regardless of `resetCredits` |
413+ | 2 | High | Workspace disabled assumption contradicted live team credits | Plan no longer gates reset-credit badge; upstream `resetCredits` is authoritative |
429414| 3 | High | `__main__` consume path | Noted: backend handles via `readCodexTokens()` (see 00_plan.md §1.2) |
430415| 4 | Medium | `handleRedeem` stale closure | Capture `prevCredits` before clearing `resetPopup` |
431416| 5 | Medium | `ko.ts`/`zh.ts` missing | Added note: must add keys to all locale files |
432417| 6 | Medium | IconTicket pattern mismatch | Changed to arrow fn + `P` type + `S()` helper |
433418| 7 | Medium | Dynamic i18n key type violation | Changed to explicit `switch` mapping |
434- | 8 | Low | `isWorkspaceAccount` missing aliases | Added `hc`, `education` |
419+ | 8 | Low | `isWorkspaceAccount` missing aliases | Obsolete: removed workspace gating entirely |
435420| 9 | Low | `resp.ok` check missing | Added `if (!resp.ok)` guard |
436421| 10 | Low | Emoji in modal title | Changed to `<IconTicket >` |
437422| 11 | Low | `<span >` for clickable badge | Changed to `<button >` with `aria-label` |
0 commit comments