fix(ui): humanize provider retry delay in the banner copy - #3402
Conversation
jackwener
left a comment
There was a problem hiding this comment.
Automated review of exact head ef7097b50a91c7dc5284f00180db0ffe15a95fa4.
I confirmed the current main copy still renders provider waits as raw seconds, and the patch fixes that at the presentation boundary without changing retry policy. The d/h/m/s formatter is bounded, locale-specific, and the focused UI build plus new locale test passed locally (1/1). I found no P0-P2 correctness issue.
Merge readiness: not ready yet. This head has no hosted check result and still requires an independent human review; both gates should be present on this exact head before merge.
Astro-Han
left a comment
There was a problem hiding this comment.
Thanks for this — the five-digit second count really does read as a hang, and fixing it per locale rather than with a generic duration formatter is the right call for copy that sits inside a sentence.
I verified the fix itself and it behaves as described. Two [P2]s and two [P3]s below, none of them about the desktop change being wrong.
The one I would like you to look at first is the premise in the PR description:
The TUI strip already humanizes the same wait as
4h 28m 3s(#3393).
At current main it does not. renderMakaPiActivityStrip (packages/cli/src/pi-transcript.ts:1407) still renders the retry countdown as Retrying in ${Math.max(1, Math.ceil(retry.delayMs / 1_000))}s, so the same quota window that produced 13565 秒后重试 in the desktop banner produces Retrying in 13565s in the TUI strip. What #3393 humanized is the elapsed counter on the line below (Working… <elapsed>, via formatElapsedDuration), which is a different string. That is line 1403-1407 and it is the only provider-retry render in the CLI, so the surface is not covered anywhere else.
That matters twice over, because formatElapsedDuration (pi-transcript.ts:1415) is the algorithm this PR just re-implemented — same [['d', 86_400], ['h', 3_600], ['m', 60]] table, same loop, same if (remaining > 0 || parts.length === 0) tail, same join(' '). The new formatProviderRetryDelay is that function with the unit strings lifted into a parameter.
I want to be fair about the cost of de-duplicating: formatElapsedDuration lives in packages/cli and the new one in packages/ui, so reuse is not a local import — the algorithm would have to be hoisted somewhere both can reach. The zh unit strings genuinely differ, so what is shared is the ladder, not the copy. That is real work, which is why this is a [P2] and not a merge blocker.
Nothing here blocks: no [P0]/[P1]. If you would rather land the desktop fix now and take the TUI strip and the shared ladder as a follow-up, that seems entirely reasonable to me — I would just ask that the PR description be corrected either way, since the sentence about the TUI currently describes a state the repository is not in, and the next person to touch this will trust it.
Review assisted by AI (Claude Opus 5). The findings above were verified against the files at this head; the reviewer is accountable for them.
| */ | ||
| function formatProviderRetryDelay(seconds: number, units: ProviderRetryDelayUnits): string { | ||
| let remaining = Math.max(0, Math.floor(seconds)); | ||
| const parts: string[] = []; |
There was a problem hiding this comment.
[P2] This duplicates formatElapsedDuration in packages/cli/src/pi-transcript.ts:1415 exactly — same unit table, same loop, same zero/empty tail, same separator. The only real difference is that the unit strings became a parameter so zh can supply its own.
Simpler equivalent solution, spelled out: hoist this ladder to a package both ui and cli can import, taking the unit strings and separator as the parameter you already designed here, then have formatElapsedDuration and formatProviderRetryDelay both call it. That also makes the [P2] on the TUI strip a one-line change instead of a third copy.
Acknowledging the cost honestly: this is a cross-package move, not a local import, so it is more work than it looks. Not a blocker — but with the ladder now written twice, the third copy is the one that will drift.
| minute: '分', | ||
| hour: '小时', | ||
| day: '天', | ||
| separator: ' ', |
There was a problem hiding this comment.
[P3] separator: ' ' is shared by both locales, so zh renders 4小时 28分 3秒后重试. Chinese typography does not normally space between a number-unit group and the next one — 4小时28分3秒后重试 is the conventional form. Since separator is already per-locale in this interface, zh can just carry '' and en keep ' '; the test expectations would move with it.
Cosmetic, and a native reader should overrule me if they disagree.
| assert.equal(zh(1, 2, 10), '1秒后重试(2/10)'); | ||
| assert.equal(en(1, 2, 10), 'Retrying in 1s (2/10)'); | ||
| assert.equal(zh(45, 2, 10), '45秒后重试(2/10)'); | ||
| assert.equal(en(45, 2, 10), 'Retrying in 45s (2/10)'); |
There was a problem hiding this comment.
[P3] The description says short delays “keep the familiar seconds-only form (45秒后重试)”, but the previous zh copy was ${seconds} 秒后重试 — with a space before 秒. This assertion pins '45秒后重试(2/10)', so the short-delay zh string does change, just subtly.
I think dropping the space is the better copy and I am not asking you to restore it. Only flagging that the description says this path is unchanged when it is not, so a reader diffing screenshots is not left confused.
Separately, and to the test's credit: pinning both locales across second/minute/hour/day scales is exactly the right shape here — it fails if the ladder regresses, rather than restating the implementation.
ef7097b to
d18d0c2
Compare
A subscription quota window hands the runtime an hour-scale Retry-After,
and the desktop banner rendered it as a raw five-digit second count
('13565 秒后重试(2/10)') that reads as a frozen hang. Format the delay
in d/h/m/s units per locale ('4小时 28分 3秒后重试', 'Retrying in 4h 28m
3s') so the countdown stays legible and keeps ticking every second,
unlike the goal chip's minute-granularity ladder.
Generated-by: Maka
d18d0c2 to
efd19db
Compare
Summary
With a long provider
Retry-After(subscription quota window reset, e.g. kimi-k3 / OpenCode Go 5h quota), the desktop retry banner rendered a raw five-digit second count —13565 秒后重试(2/10)— illegible and visually indistinguishable from a hang. The TUI strip humanizes the same wait as4h 28m 3s(#3393).providerRetrySchedulednow formats in d/h/m/s units per locale:4小时 28分 3秒后重试(2/10)/Retrying in 4h 28m 3s (2/10). One-second granularity is kept so the banner visibly ticks every second — the goal chip's minute-granularity ladder would reintroduce the frozen look between minute boundaries. Short delays keep the familiar seconds-only form.Independent of #3400 (countdown ticking); either merge order works.
Fixes #3401
Verification
conversation-copy.test.tspins both locales across second/minute/hour/day scales and fails without the change.composer-plus-menu › a loading catalog holds the row still, reproduces on the base commit (9a661a183) without this change — unrelated.AI use
Tool(s) and scope: Maka (AI agent) prepared the change end to end — implementation, tests, and PR text. The commit carries a
Generated-by: Makatrailer.Checklist
Does this PR entail a change in behavior?