Skip to content

feat: expose credential expiry and reset credits in list output - #159

Merged
Loongphy merged 7 commits into
Loongphy:mainfrom
zfaustk:feat/list-account-lifecycle-details
Oct 22, 2026
Merged

Loongphy merged 7 commits into
Loongphy:mainfrom
zfaustk:feat/list-account-lifecycle-details

Conversation

@zfaustk

@zfaustk zfaustk commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Account listing now surfaces the credential and reset-credit state needed to operate multiple ChatGPT accounts:

  • Persist the access-token JWT exp claim as access_token_expires_at and show it in the new AUTH EXPIRE column (local time).
  • Fetch and persist the reset-credit cards returned by GET wham/rate-limit-reset-credits as slim records {id, status, expires_at, title}, sorted ascending by expires_at at parse time (entries without an expiry last).
  • Add the RESET CREDITS column: the count comes from rate_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 with status == "available". The cell appends the earliest available card's expiry as N (exp MMM-DD) in local time.
  • JSON output keeps the full reset-credit records plus access_token_expires_at.

Design notes

The registry schema stays at v4. The new fields are additive and optional: access_token_expires_at and last_usage.reset_credit are absent in older files and are filled on the next --api refresh, which already rewrites the registry, so a schema bump would break older binaries without adding any correctness guarantee.

The previous NEXT RESET column and the per-card (C1 EXP, C2 EXP, …) columns were dropped: both reset windows already embed their future reset time in the 5H/WEEKLY cells, 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 no LOCAL/UTC suffix is needed.

access_token_expires_at describes credential lifetime only. It is not a subscription or account-entitlement expiry, which is not provided by the current data sources.

available_count is the backend's authoritative usable count. The credits list can contain more available entries than that count (upstream truncates with take(available_count)), so consumers should treat available_count as the usable limit rather than credits.len().

Validation

  • zig fmt --check on all changed Zig files
  • zig build -j1
  • zig build run -- list
  • zig build test -j1 — all tests pass
  • pty checks of the live table with the reset-credits cell at multiple terminal widths

@pkg-pr-new

pkg-pr-new Bot commented Sep 22, 2026

Copy link
Copy Markdown

Open in StackBlitz

@loongphy/codex-auth-darwin-arm64

npx https://pkg.pr.new/@loongphy/codex-auth-darwin-arm64@94ee2dc

@loongphy/codex-auth-darwin-x64

npx https://pkg.pr.new/@loongphy/codex-auth-darwin-x64@94ee2dc

@loongphy/codex-auth-linux-arm64

npx https://pkg.pr.new/@loongphy/codex-auth-linux-arm64@94ee2dc

@loongphy/codex-auth-linux-x64

npx https://pkg.pr.new/@loongphy/codex-auth-linux-x64@94ee2dc

@loongphy/codex-auth-win32-arm64

npx https://pkg.pr.new/@loongphy/codex-auth-win32-arm64@94ee2dc

@loongphy/codex-auth-win32-x64

npx https://pkg.pr.new/@loongphy/codex-auth-win32-x64@94ee2dc

@loongphy/codex-auth

npx https://pkg.pr.new/@loongphy/codex-auth@94ee2dc

commit: 94ee2dc

@Loongphy Loongphy left a comment •

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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 Loongphy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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.

Comment thread src/registry/common.zig Outdated
account_name: ?[]u8,
plan: ?PlanType,
auth_mode: ?AuthMode,
auth_expires_at: ?i64 = null,

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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.

Comment thread src/registry/storage_parse.zig Outdated
.account_name = try parseOptionalStoredStringAlloc(allocator, obj.get("account_name")),
.plan = null,
.auth_mode = null,
.auth_expires_at = readInt(obj.get("auth_expires_at")),

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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.

Comment thread src/api/usage.zig Outdated
status_code: ?u16,
error_code: ?ResponseErrorCode = null,
missing_auth: bool = false,
auth_expires_at: ?i64 = null,

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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.

Comment thread src/workflows/results.zig Outdated
account_name: ?[]u8,
plan: ?registry.PlanType,
auth_mode: ?registry.AuthMode,
auth_expires_at: ?i64,

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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]
Loongphy added a commit that referenced this pull request Oct 22, 2026
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]
@Loongphy Loongphy changed the title feat: expose account lifecycle details in list output feat: expose credential expiry and reset credits in list output Oct 22, 2026
@Loongphy
Loongphy merged commit ada9b7e into Loongphy:main Oct 22, 2026
Loongphy added a commit that referenced this pull request Oct 22, 2026
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]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants