Repository navigation
feat: expose credential expiry and reset credits in list output - #159
Conversation
@loongphy/codex-auth-darwin-arm64
@loongphy/codex-auth-darwin-x64
@loongphy/codex-auth-linux-arm64
@loongphy/codex-auth-linux-x64
@loongphy/codex-auth-win32-arm64
@loongphy/codex-auth-win32-x64
@loongphy/codex-auth
commit: |
There was a problem hiding this comment.
Display and schema changes requested before merge:
1. Drop the NEXT RESET column — fully redundant.
The 5H and WEEKLY cells already embed their own reset times when in the future (58% (14:20 on 22 Oct)). The earliest future reset is therefore always already visible in one of those two cells: if the 5h window is unexpired, its time sits in the 5H cell; once it expires (cell shows 100%), the earliest future reset is the weekly one, whose time sits in the WEEKLY cell. When both have passed, next_reset_at is null and the column shows -.
2. Rename ACCESS EXP LOCAL → ACCESS EXPIRE, and drop the LOCAL suffix pattern.
With the credit columns removed (point 3), every timestamp in the table is local and the label carries no information.
3. Remove the per-credit columns (C1 EXP, C2 EXP, …). Keep the rate-limit-reset-credits fetch, with a slimmer persisted record.
The fetch itself is correct — keep it, keep persisting the detail records, and keep the full JSON output. Only the table columns go: each credit column is a fixed 23 chars (credit_width) and grows with credits.len (bounded only by the 64-column array), so a few cards squeeze out the identity/usage columns while extra cards silently disappear with no "+N more" hint; they show expiry without card identity (title/status); and they pin the backend timezone while every other timestamp in the table is local.
Slim the persisted record to the four fields anything actually reads:
"reset_credit": [
{ "id": "credit-1", "status": "available", "expires_at": "2026-11-01T00:00:00Z",
"title": "Full reset (Weekly + 5 hr)" },
{ "id": "credit-2", "status": "available", "expires_at": null, "title": null }
]Dropped: reset_type, granted_at, description (nothing renders or acts on them), the wrapper available_count (duplicates last_usage.reset_credits on disk), and total_earned_count (upstream drops it too). Keep status: upstream retains redeemed rows in the list (enum RateLimitResetCreditStatus: available/redeeming/redeemed), so the persisted file stays a faithful snapshot of the API response, and rendering filters to status == available the same way upstream's picker does. Keep id: the upstream-defined opaque identity, useful for stable dedup/ordering across refreshes.
Persist and render in ascending expires_at order — soonest first, cards without an expiry last (upstream's picker sorts the same way). With storage pre-sorted, the RESET CREDITS cell takes its (exp MMM-DD) from the first element and the expanded panel renders in stored order.
4. Keep schema_version at 4 — do not bump.
The new fields (access_token_expires_at, last_usage.reset_credit) are optional additive data: older binaries ignore unknown keys on read, and the data self-heals on the next --api refresh if an older binary rewrites the file. Bumping to 5 would hard-break older binaries against files this version writes, for no correctness gain here. Drop the v4→v5 migration and the schema-migration.md history entry.
The requested changes are implemented on the maintainer side as #173 (draft, stacked on this branch), and the per-card interactive detail view is prototyped in #171 (also stacked) — both will be retargeted to main after this PR merges. Until then the details remain available in the JSON output (reset_credit).
Docs: docs/commands/list.md and docs/json-api.md describe the LOCAL/UTC labeling and the credit columns — update them to match.
Loongphy
left a comment
There was a problem hiding this comment.
Field naming: rename auth_expires_at → access_token_expires_at at each inline anchor (struct field, persisted JSON key, fetch/view layers). Everything else in the earlier review stands.
| account_name: ?[]u8, | ||
| plan: ?PlanType, | ||
| auth_mode: ?AuthMode, | ||
| auth_expires_at: ?i64 = null, |
There was a problem hiding this comment.
Rename to access_token_expires_at (field and, where applicable, the persisted JSON key — safe since nothing released has written this field yet). Registry timestamp fields are uniformly _at seconds: created_at, last_used_at, last_usage_at, resets_at. auth_expires_at is ambiguous — this is specifically the access token's JWT exp, not a general auth/subscription lifetime (the docs even have to spell that out). The in-memory counterpart you added in auth.zig is already named access_token_expires_at; please align this layer with it.
| .account_name = try parseOptionalStoredStringAlloc(allocator, obj.get("account_name")), | ||
| .plan = null, | ||
| .auth_mode = null, | ||
| .auth_expires_at = readInt(obj.get("auth_expires_at")), |
There was a problem hiding this comment.
Same rename: the persisted JSON key becomes access_token_expires_at, matching the AccountRecord field above and the registry _at convention. No migration concern — this key is brand-new in this unmerged PR.
| status_code: ?u16, | ||
| error_code: ?ResponseErrorCode = null, | ||
| missing_auth: bool = false, | ||
| auth_expires_at: ?i64 = null, |
There was a problem hiding this comment.
Same rename here (and the mirror field at the BatchUsageFetchResult below): keep the fetch layer aligned with AuthInfo.access_token_expires_at and the registry field.
| account_name: ?[]u8, | ||
| plan: ?registry.PlanType, | ||
| auth_mode: ?registry.AuthMode, | ||
| auth_expires_at: ?i64, |
There was a problem hiding this comment.
Same rename; the list --json output key changes with it (json_output.zig:169).
…a stays v4
- remove the NEXT RESET column and the next_reset_at field (the value is
always one of the two timestamps already shown in the 5H/WEEKLY cells)
- rename ACCESS EXP LOCAL -> ACCESS EXPIRE and drop the LOCAL suffix
pattern; credit expiry renders in local time (UTC mixing removed)
- remove the per-credit columns (C1/C2 EXP) and their width machinery
- RESET CREDITS cell shows 'N (exp MMM-DD)': count plus the earliest
available-card expiry in local form
- slim persisted credit records to {id, status, expires_at, title};
wrapper available_count/total_earned_count and per-card reset_type/
granted_at/description dropped (upstream drops total_earned_count too)
- rename auth_expires_at -> access_token_expires_at across the
persistence/fetch/JSON layers
- schema_version stays 4: additive optional fields, older binaries
ignore unknown keys, data self-heals on the next --api refresh
- docs and tests updated accordingly
[skip ci]
Adds a read-only RESET CREDITS detail block to the list --live screen: - 'c' toggles the block under the cursor row; j/k move the cursor row - lists available reset credits sorted by expires_at ascending, one line per card (title, local expiry, relative countdown), bounded to terminal width - read-only: redemption stays in codex sessions (contextual picker on rate-limit hit); no changes to the data layer from #159 Stacked on #159; draft for design review of the panel interaction. [skip ci]
The registry stores the slimmed flat array; the wire object shape (from the details endpoint) is still accepted for the API path. Without this, loading a registry written by the slimmed build drops the card records on the next rewrite (lossy proxy). [skip ci]
…only The reset-credit parser served two callers with two shapes (registry flat array, backend wire object). Move the wire-object unpacking to the API layer (parseResetCreditResponse extracts credits[] itself) so the registry parser only ever sees the persisted flat array — no dual-shape compat branch. [skip ci]
…edit - table header ACCESS EXPIRE -> AUTH EXPIRE (the expiry is the stored auth credential's, matching the field name) - docs/schema-migration.md: no changes in this PR (the schema stays at the documented v4 policy) [skip ci]
…mary is absent The RESET CREDITS cell counts status==available cards from the persisted reset_credit list when rate_limit_reset_credits.available_count is absent from the usage snapshot — the same backend number, matching the upstream picker's Available filter. [skip ci]
Adds a read-only RESET CREDITS detail block to the list --live screen: - 'c' toggles the block under the cursor row; j/k move the cursor row - lists available reset credits sorted by expires_at ascending, one line per card (title, local expiry, relative countdown), bounded to terminal width - read-only: redemption stays in codex sessions (contextual picker on rate-limit hit); no changes to the data layer from #159 Stacked on #159; draft for design review of the panel interaction. [skip ci]
Summary
Account listing now surfaces the credential and reset-credit state needed to operate multiple ChatGPT accounts:
expclaim asaccess_token_expires_atand show it in the newAUTH EXPIREcolumn (local time).GET wham/rate-limit-reset-creditsas slim records{id, status, expires_at, title}, sorted ascending byexpires_atat parse time (entries without an expiry last).RESET CREDITScolumn: the count comes fromrate_limit_reset_credits.available_count— the same summary field the upstream TUI reads. When that summary is absent, the count is derived from the persisted cards withstatus == "available". The cell appends the earliest available card's expiry asN (exp MMM-DD)in local time.access_token_expires_at.Design notes
The registry schema stays at v4. The new fields are additive and optional:
access_token_expires_atandlast_usage.reset_creditare absent in older files and are filled on the next--apirefresh, which already rewrites the registry, so a schema bump would break older binaries without adding any correctness guarantee.The previous
NEXT RESETcolumn and the per-card (C1 EXP,C2 EXP, …) columns were dropped: both reset windows already embed their future reset time in the5H/WEEKLYcells, and the reset-credit list is rendered as a single aggregate cell instead of one column per card. With the credit columns gone, the remaining timestamps are all local, so noLOCAL/UTCsuffix is needed.access_token_expires_atdescribes credential lifetime only. It is not a subscription or account-entitlement expiry, which is not provided by the current data sources.available_countis the backend's authoritative usable count. Thecreditslist can contain moreavailableentries than that count (upstream truncates withtake(available_count)), so consumers should treatavailable_countas the usable limit rather thancredits.len().Validation
zig fmt --checkon all changed Zig fileszig build -j1zig build run -- listzig build test -j1— all tests pass