Skip to content

feat: reset credits panel, reset command, and responsive table layout - #171

Open
Loongphy wants to merge 20 commits into
mainfrom
wip/reset-credits-panel
Open

Loongphy wants to merge 20 commits into
mainfrom
wip/reset-credits-panel

Conversation

@Loongphy

@Loongphy Loongphy commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • list --live gains a reset-credits rail: it lists the available reset credits for the cursor row and redeems one in place.
  • New codex-auth reset <selector> command redeems the soonest-expiring reset credit for one account and prints a two-line summary.
  • The consume flow mirrors upstream: POST wham/rate-limit-reset-credits/consume, client-generated UUID v4 idempotency key, and code-based outcomes.
  • Plain list, live details, switch, and remove now share one responsive column allocator (bundled layout unification).

No registry schema change: the persisted shape is the v4 format merged in #159.

codex-auth reset

$ codex-auth reset user@example.com
used "Full reset (Weekly + 5 hr)" · expires 2026-11-01 08:00
remaining: 1 · next: (untitled) · -
  • Resolves one account from the same selectors as switch.
  • Picks the status == "available" credit with the earliest expires_at; credits without an expiry sort last and render -.
  • Prints the consumed credit on one line and a remaining summary (count plus the card that expires next).
  • Exit code 0 after a successful consume (including already_redeemed), 1 otherwise.
  • The registry file is not rewritten by this command; the next list --api refresh reconciles it.

Live reset-credits rail

#*  | ACCOUNT                        | PLAN     | 5H           | WEEKLY       | LAST | AUTH EXPIRE      | RESET CREDITS
*1  | work-main@example…             | Business | 100%         | 58% (14:20 22 O | 1d ago | 2027-10-20 10:40 | 2 (exp 11-01)
    ┌ Available 2 · ⏎ select · esc close
    │ ▸ Full reset (Weekly + 5 hr) · 11-01 08:00 · in 9d · ⏎ again to use
    │   (untitled) · -
    └
  • c expands or collapses the rail under the cursor row; the left rail aligns with the row separator so it nests beneath its account.
  • One compact row per available credit: title, local MM-DD HH:MM expiry, relative countdown; missing expiry renders -.
  • ⏎ selects the highlighted credit (the row shows · ⏎ again to use), the second ⏎ redeems it.
  • esc closes the rail without redeeming; when the rail is collapsed, esc keeps its normal quit behavior.
  • A successful redeem shows ✓ reset credit used and refreshes that account's usage immediately, so the 5H/WEEKLY cells and the remaining list reflect the reset without waiting for the next refresh round. Failures keep their existing messages, and consumed cards are filtered for the session.

Consume semantics

  • Endpoint POST wham/rate-limit-reset-credits/consume with {"redeem_request_id": "<uuid-v4>", "credit_id": "<id>"} and the standard bearer/account headers.
  • The idempotency key is generated from std.Io entropy and reused while retrying the same credit, so a retried request is deduplicated instead of consuming a second credit.
  • The response code is parsed like upstream: reset / already_redeemed count as success, no_credit hides the spent card and asks for a refresh, nothing_to_reset reports that usage does not need a reset, and unknown codes or non-2xx responses fail.

Responsive columns

  • One allocator now drives plain list, live details, switch, and remove while each view keeps its own visual column order.
  • Unsupported fields are hidden and the freed width is redistributed; ACCOUNT grows last.
  • Width-aware percentage cells and compact AUTH EXPIRE / RESET CREDITS headers keep narrow terminals readable.
  • The RESET CREDITS cell appends the earliest available credit expiry as N (exp MMM-DD), or N (-) when no available credit has an expiry.

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]
- ⏎ on the expanded panel arms a two-step confirm (mirrors the upstream
  'Use this reset?' UX); second ⏎ consumes the selected credit
- new api/reset_credits.zig: POST wham/rate-limit-reset-credits/consume
  with a client-generated UUID v4 redeem_request_id (upstream semantics);
  per-account auth via registry.accountAuthPath
- http_curl.zig: POST JSON curl runner (mirrors the GET runner)
- panel: available-card collection (status==available, minus session
  consumed ids, sorted by expiry), cursor/detail/confirm/status lines
- registry is left untouched on use: session-scoped consumed ids filter
  the display; the next --api refresh reconciles
[skip ci]
@Loongphy
Loongphy force-pushed the wip/reset-credits-panel branch from 3cb072d to 36380a6 Compare October 22, 2026 09:11
@Loongphy Loongphy changed the title wip: reset credits detail view in list --live (stacked on #159) wip: reset credits detail view in list --live Oct 22, 2026
@Loongphy
Loongphy changed the base branch from stack/pr-159-head to main October 22, 2026 09:12
- generate the redeem idempotency key from std.Io entropy (UUID v4) and
  reuse it while retrying the same credit, so the backend deduplicates
  instead of consuming a second credit
- parse the consume response code (reset / already_redeemed /
  nothing_to_reset / no_credit) like upstream instead of trusting any
  2xx status; already_redeemed counts as success and no_credit hides
  the spent card from the panel
- restore workflow_dispatch in CI (unrelated accidental removal)
@pkg-pr-new

pkg-pr-new Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@loongphy/codex-auth-darwin-arm64

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

@loongphy/codex-auth-darwin-x64

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

@loongphy/codex-auth-linux-arm64

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

@loongphy/codex-auth-linux-x64

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

@loongphy/codex-auth-win32-arm64

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

@loongphy/codex-auth-win32-x64

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

@loongphy/codex-auth

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

commit: b771edb

@Loongphy
Loongphy marked this pull request as ready for review October 22, 2026 09:28
`codex-auth reset <selector>` redeems the available reset credit with
the earliest expiry for one account and prints the consumed card plus
the credits that remain. It reuses the panel's card collection, the
shared consume call, and the consume outcome mapping (reset /
already_redeemed succeed; no_credit / nothing_to_reset fail with their
own messages). Help, command docs, and README entries are included,
along with scenario tests for the success and no-credit paths.
@Loongphy Loongphy changed the title wip: reset credits detail view in list --live wip: reset credits live panel and reset command Oct 22, 2026
Print the consumed credit on one line (title + local expiry) and replace
the per-card remaining list with a single summary line: count plus the
card that expires next. Matches the two-line format requested for the
command.
The RESET CREDITS cell now reads N (-) when no available credit has an
expiry, and the reset command's remaining summary prints - instead of
'no expiry'. Docs updated to match.
Keep each view's visual column order while sharing one allocator. Plain list, live details, switch, and remove now hide unsupported fields and redistribute their width/separators.

Use width-aware percentage cells, compact AUTH EXPIRE/RESET CREDITS headers, and leave ACCOUNT growth last. Add coverage for live-detail widths, 100% completeness, and column order.
Render the live panel as a rail: a top line with 'Remaining N', the
⏎ select hint and the esc close hint, compact card rows (title plus
MM-DD HH:MM and a relative countdown, or - when no expiry), the armed
hint on the selected row, and a bare bottom rail. esc now closes the
panel instead of quitting the live view; collapsing with c or esc also
clears the pending confirm state. Unit test covers the rendered rail.
@Loongphy Loongphy changed the title wip: reset credits live panel and reset command feat: reset credits panel, reset command, and responsive table layout Oct 22, 2026
- Indent the rail to the account column so it nests under its row
  (ANSI-aware header scan for the column offset)
- Header now reads 'Available N · ⏎ select · esc close'
- After a successful redeem, refresh that account's usage immediately so
  the 5H/WEEKLY cells and the remaining card list reflect the reset; the
  success message is the compact '✓ reset credit used'
- Unit test covers the indented rail and the new header
Start the rail at the column of the first '|' separator (the one next to
the display number) instead of the ACCOUNT column, so the rail's left
edge continues that separator line.
# Conflicts:
#	docs/commands/list.md
#	docs/table-layout.md
#	src/cli/list_details.zig
#	src/cli/table_layout.zig
#	src/tui/table.zig
#	tests/cli_picker_test.zig
- Render LAST ACTIVITY in list --live when the column is wide enough,
  falling back to LAST like the plain list.
- Bound reset-credit rail text by display columns instead of bytes.
- Add tests for shared column order and multibyte rail truncation.
The live rail cursor mixed two index spaces: it indexed rows.items while the
renderer skips group header rows, so the rail could show one account while
sitting under another, and the consume target followed the wrong account.
renderListBody now shares one predicate with the exported data-row mapping,
and the rail cursor is a rendered data-row index only.

Redemption is irreversible, so the armed confirmation is bound to the exact
(account_key, credit_id) pair instead of a flag: any cursor move, rail close,
selection loss, or registry refresh drops it, and approve() releases the
redeem exactly once. q now closes the open rail like esc, the rail status
line is owned by the selection and cleared when the row changes, and the
panel reuses time.timestamp.parseIso8601 instead of a weaker local parser.

spliceAfterDataLine no longer emits a phantom trailing row (it inflated the
viewport row count) and no longer leaks its buffer on every rendered frame.
credit_id originates at the backend and round-trips through the stored registry, so interpolating it into body text could emit a malformed request or add fields. BuildConsumeBodyAlloc encodes both fields with std.json.Stringify.
docs/tests.md requires production modules to hold no test blocks, so move the shared column order test to its own test root and export cli.table_layout as the module boundary it already is.
docs/tests.md requires production modules to hold no test blocks; relocate the remaining live_tui and text_width tests into their behaviour test root with the descriptions unchanged, so the checklist command rg '^test "' src returns nothing.
…edit

The list viewport only clamped its start, so with more accounts than one screen the rail could hang on a row outside the display: pressing c looked dead, and Enter Enter still redeemed a credit the user could not see along with its result line. listViewportFollowing now scrolls the rail row into view on the body line index that renderListBodyViewport actually slices.

A burst of keys read in one batch could also swallow a queued redeem: the second Enter armed the consume, then a following j moved the rail and cleared the selection before the queued redeem ran, leaving no feedback at all. The batch now stops being consumed once a redeem is committed.

The re-confirm hint was driven by 'armed is not null' while it is drawn on the cursor card, with no identity link. Selection now exposes isPendingFor(identity), and the frame derives the shown credit once and uses it for the hint, the owned copies, and the armed check.
rowIndexForDisplayIndex/dataRowCount/liveViewportStartForDisplayIndex work on rows.items because the switch and remove screens render one line per row, group headers included. The live list body skips header rows and uses isRenderedLiveRow/liveDataRowCount/liveRowIndexForDataIndex. Both bases are correct for their own view; the names gave no hint, which is how a rail cursor nearly got fed to the wrong helper. Say so at each definition.
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.

1 participant