feat: allow Ctrl+Y to skip rate-limit retry delay immediately

Add skippableDelay utility that resolves early via a skip() callback,
replacing the plain setTimeout in rate-limit retry paths. The skipDelay
callback is included in RetryInfo, flowing naturally through turn.ts to
the UI via startRetryCountdown/clearRetryCountdown with symmetric
lifecycle management. When the user presses Ctrl+Y during a rate-limit
countdown, retryLastPrompt calls skipDelay() to resolve the delay
promise, letting the generator continue its retry loop naturally
without aborting or re-submitting the query.
This commit is contained in:
胡玮文 2026-03-16 17:07:41 +08:00
parent 02ac895544
commit 73ab82afb9
6 changed files with 122 additions and 9 deletions

View file

@ -9,6 +9,7 @@ import type {
Config,
EditorType,
GeminiClient,
RetryInfo,
ServerGeminiChatCompressedEvent,
ServerGeminiContentEvent as ContentEvent,
ServerGeminiFinishedEvent,
@ -268,6 +269,7 @@ export const useGeminiStream = (
*/
const clearRetryCountdown = useCallback(() => {
stopRetryCountdownTimer();
skipRetryDelayRef.current = null;
setPendingRetryErrorItem(null);
setPendingRetryCountdownItem(null);
}, [
@ -276,14 +278,14 @@ export const useGeminiStream = (
stopRetryCountdownTimer,
]);
// Holds the skipDelay callback from the current rate-limit RetryInfo.
// Managed symmetrically: set in startRetryCountdown, cleared in clearRetryCountdown.
const skipRetryDelayRef = useRef<(() => void) | null>(null);
const startRetryCountdown = useCallback(
(retryInfo: {
message?: string;
attempt: number;
maxRetries: number;
delayMs: number;
}) => {
(retryInfo: RetryInfo) => {
stopRetryCountdownTimer();
skipRetryDelayRef.current = retryInfo.skipDelay;
const startTime = Date.now();
const { message, attempt, maxRetries, delayMs } = retryInfo;
const retryReasonText =
@ -1293,6 +1295,15 @@ export const useGeminiStream = (
* when the user presses Ctrl+Y (bound to Command.RETRY_LAST in keyBindings.ts).
*/
const retryLastPrompt = useCallback(async () => {
// During a rate-limit retry countdown, skip the delay so the generator
// retries immediately — no abort/re-submit needed.
if (skipRetryDelayRef.current) {
skipRetryDelayRef.current();
skipRetryDelayRef.current = null;
clearRetryCountdown();
return;
}
if (
streamingState === StreamingState.Responding ||
streamingState === StreamingState.WaitingForConfirmation