mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-03 14:10:43 +00:00
feat(cli): add Ctrl+Y shortcut to retry failed requests (#2011)
* feat: add Ctrl+Y shortcut to retry failed requests
- Add Ctrl+Y keyboard shortcut for retrying the last failed request
- Add isNetworkError() to detect transient network failures (ECONNREFUSED, ETIMEDOUT, etc.)
- Add DashScope 1305 error code to rate limit detection
- Add error hint \"Press Ctrl+Y to retry\" in error messages
- Support user-defined error codes for retry via config
- Add retryLastPrompt() hook in useGeminiStream
- Update keyboard shortcuts documentation
* feat: improve Ctrl+Y retry feature with tests, docs, and rate limit config
- Add comprehensive tests for Ctrl+Y retry shortcut in InputPrompt
- Add unit tests for retryLastPrompt in useGeminiStream hook
- Add detailed JSDoc comments for retryLastPrompt function and Ctrl+Y shortcut
- Extend isRateLimitError to support custom error codes via retryErrorCodes config
- Fix rate limit retry log variable reference (RATE_LIMIT_RETRY_OPTIONS → maxRateLimitRetries)
- Add Eclipse IDE files to .gitignore
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* refactor(ui): consolidate retry countdown as inline hint in error messages
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* feat(cli): enhance error handling with improved retry mechanism
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
- Modify ErrorMessage component to remove dim color from hint text
- Update useGeminiStream hook to improve retry countdown behavior with option to preserve or clear hints
- Adjust tests to match new error handling implementation
* feat: add Ctrl+Y shortcut to retry the last failed request
When a request errors out, the error message shows an inline hint
"(Press Ctrl+Y to retry.)" in secondary color. Pressing Ctrl+Y
re-submits the same prompt, commits the error text to history
(without the hint), and clears the hint from the UI.
- Add retryLastPrompt action wired to Ctrl+Y via keyBindings and InputPrompt
- Track last submitted prompt and error state in useGeminiStream refs
- Show retry hint inline with error text in ErrorMessage component,
wrapping naturally on narrow terminals while preserving hint color
- Expose retryLastPrompt through UIActionsContext
- Add keyboard shortcut entry in KeyboardShortcuts display
- Add i18n strings for hint and no-retry-available message
- Document Ctrl+Y in keyboard-shortcuts.md
* docs(configuration): Update model provider configuration document
* chore: remove YOLO mode code from core
* fix: prevent Ctrl+Y hint from overriding auto-retry countdown
When an auto-retry countdown is active (retryCountdownTimerRef is set),
handleErrorEvent should not overwrite it with the Ctrl+Y hint. The auto-retry
hint ("retrying in Xs...") and manual retry hint ("Press Ctrl+Y to retry.")
are mutually exclusive:
- Auto-retry errors (e.g., rate limits): show countdown hint
- Other errors: show Ctrl+Y hint
Also removed retryErrorCodes from ContentGeneratorConfig as it's not part
of the minimal Ctrl+Y feature scope.
* simplify: remove complex options from clearRetryCountdown
Revert clearRetryCountdown to simplest form without options parameter.
The function now just clears the timer and pending item without any
automatic history commit logic.
* fix: restore pendingRetryCountdownItem as separate state from pendingRetryErrorItem
Auto-retry countdown and manual retry hint are now independent:
- pendingRetryErrorItem: displays error message with optional hint
- pendingRetryCountdownItem: displays separate countdown line for auto-retry
This ensures both can be shown simultaneously without overriding each other.
* fix: restore RetryCountdownMessage rendering in HistoryItemDisplay
The retry_countdown type should be rendered as a separate message,
not inline in ErrorMessage. This allows auto-retry countdown and
manual retry hint to coexist properly.
* fix(cli): properly commit retry error item to history before clearing
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(cli): remove trailing period from retry hint translations
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Remove unnecessary period from 'Press Ctrl+Y to retry' translation strings in both en.js and zh.js locales. Also update the corresponding usage in useGeminiStream hook.
* chore(sdk-java): add Eclipse project configuration files
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Add .project configuration files for client and qwencode modules to support Eclipse IDE development environment.
* feat(cli): add retry countdown hint to error message
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* Revert "chore(sdk-java): add Eclipse project configuration files"
This reverts commit da83b5e571.
---------
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
6fdd715458
commit
c353fbbfa3
18 changed files with 355 additions and 76 deletions
|
|
@ -169,12 +169,17 @@ export const useGeminiStream = (
|
|||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const turnCancelledRef = useRef(false);
|
||||
const isSubmittingQueryRef = useRef(false);
|
||||
const lastPromptRef = useRef<PartListUnion | null>(null);
|
||||
const lastPromptErroredRef = useRef(false);
|
||||
const [isResponding, setIsResponding] = useState<boolean>(false);
|
||||
const [thought, setThought] = useState<ThoughtSummary | null>(null);
|
||||
const [pendingHistoryItem, pendingHistoryItemRef, setPendingHistoryItem] =
|
||||
useStateAndRef<HistoryItemWithoutId | null>(null);
|
||||
const [pendingRetryErrorItem, setPendingRetryErrorItem] =
|
||||
useState<HistoryItemWithoutId | null>(null);
|
||||
const [
|
||||
pendingRetryErrorItem,
|
||||
pendingRetryErrorItemRef,
|
||||
setPendingRetryErrorItem,
|
||||
] = useStateAndRef<HistoryItemWithoutId | null>(null);
|
||||
const [
|
||||
pendingRetryCountdownItem,
|
||||
pendingRetryCountdownItemRef,
|
||||
|
|
@ -254,11 +259,18 @@ export const useGeminiStream = (
|
|||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Clears the retry countdown timer and pending retry items.
|
||||
*/
|
||||
const clearRetryCountdown = useCallback(() => {
|
||||
stopRetryCountdownTimer();
|
||||
setPendingRetryErrorItem(null);
|
||||
setPendingRetryCountdownItem(null);
|
||||
}, [setPendingRetryCountdownItem, stopRetryCountdownTimer]);
|
||||
}, [
|
||||
setPendingRetryErrorItem,
|
||||
setPendingRetryCountdownItem,
|
||||
stopRetryCountdownTimer,
|
||||
]);
|
||||
|
||||
const startRetryCountdown = useCallback(
|
||||
(retryInfo: {
|
||||
|
|
@ -273,18 +285,21 @@ export const useGeminiStream = (
|
|||
const retryReasonText =
|
||||
message ?? t('Rate limit exceeded. Please wait and try again.');
|
||||
|
||||
// Error line stays static (red with ✕ prefix)
|
||||
setPendingRetryErrorItem({
|
||||
type: MessageType.ERROR,
|
||||
text: retryReasonText,
|
||||
});
|
||||
|
||||
// Countdown line updates every second (dim/secondary color)
|
||||
const updateCountdown = () => {
|
||||
const elapsedMs = Date.now() - startTime;
|
||||
const remainingMs = Math.max(0, delayMs - elapsedMs);
|
||||
const remainingSec = Math.ceil(remainingMs / 1000);
|
||||
|
||||
// Update error item with hint containing countdown info (short format)
|
||||
const hintText = `Retrying in ${remainingSec}s… (attempt ${attempt}/${maxRetries})`;
|
||||
|
||||
setPendingRetryErrorItem({
|
||||
type: MessageType.ERROR,
|
||||
text: retryReasonText,
|
||||
hint: hintText,
|
||||
});
|
||||
|
||||
setPendingRetryCountdownItem({
|
||||
type: 'retry_countdown',
|
||||
text: t(
|
||||
|
|
@ -305,7 +320,11 @@ export const useGeminiStream = (
|
|||
updateCountdown();
|
||||
retryCountdownTimerRef.current = setInterval(updateCountdown, 1000);
|
||||
},
|
||||
[setPendingRetryCountdownItem, stopRetryCountdownTimer],
|
||||
[
|
||||
setPendingRetryErrorItem,
|
||||
setPendingRetryCountdownItem,
|
||||
stopRetryCountdownTimer,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => () => stopRetryCountdownTimer(), [stopRetryCountdownTimer]);
|
||||
|
|
@ -693,6 +712,7 @@ export const useGeminiStream = (
|
|||
return;
|
||||
}
|
||||
|
||||
lastPromptErroredRef.current = false;
|
||||
if (pendingHistoryItemRef.current) {
|
||||
if (pendingHistoryItemRef.current.type === 'tool_group') {
|
||||
const updatedTools = pendingHistoryItemRef.current.tools.map(
|
||||
|
|
@ -732,27 +752,36 @@ export const useGeminiStream = (
|
|||
|
||||
const handleErrorEvent = useCallback(
|
||||
(eventValue: GeminiErrorEventValue, userMessageTimestamp: number) => {
|
||||
lastPromptErroredRef.current = true;
|
||||
if (pendingHistoryItemRef.current) {
|
||||
addItem(pendingHistoryItemRef.current, userMessageTimestamp);
|
||||
setPendingHistoryItem(null);
|
||||
}
|
||||
addItem(
|
||||
{
|
||||
type: MessageType.ERROR,
|
||||
// Only show Ctrl+Y hint if not already showing an auto-retry countdown
|
||||
// (auto-retry countdown is shown when retryCountdownTimerRef is active)
|
||||
const isShowingAutoRetry = retryCountdownTimerRef.current !== null;
|
||||
clearRetryCountdown();
|
||||
if (!isShowingAutoRetry) {
|
||||
const retryHint = t('Press Ctrl+Y to retry');
|
||||
// Store error with hint as a pending item (not in history).
|
||||
// This allows the hint to be removed when the user retries with Ctrl+Y,
|
||||
// since pending items are in the dynamic rendering area (not <Static>).
|
||||
setPendingRetryErrorItem({
|
||||
type: 'error' as const,
|
||||
text: parseAndFormatApiError(
|
||||
eventValue.error,
|
||||
config.getContentGeneratorConfig()?.authType,
|
||||
),
|
||||
},
|
||||
userMessageTimestamp,
|
||||
);
|
||||
clearRetryCountdown();
|
||||
hint: retryHint,
|
||||
});
|
||||
}
|
||||
setThought(null); // Reset thought when there's an error
|
||||
},
|
||||
[
|
||||
addItem,
|
||||
pendingHistoryItemRef,
|
||||
setPendingHistoryItem,
|
||||
setPendingRetryErrorItem,
|
||||
config,
|
||||
setThought,
|
||||
clearRetryCountdown,
|
||||
|
|
@ -816,7 +845,10 @@ export const useGeminiStream = (
|
|||
userMessageTimestamp,
|
||||
);
|
||||
}
|
||||
clearRetryCountdown();
|
||||
// Only clear auto-retry countdown errors (those with active timer)
|
||||
if (retryCountdownTimerRef.current) {
|
||||
clearRetryCountdown();
|
||||
}
|
||||
},
|
||||
[addItem, clearRetryCountdown],
|
||||
);
|
||||
|
|
@ -1023,7 +1055,7 @@ export const useGeminiStream = (
|
|||
const submitQuery = useCallback(
|
||||
async (
|
||||
query: PartListUnion,
|
||||
options?: { isContinuation: boolean },
|
||||
options?: { isContinuation: boolean; skipPreparation?: boolean },
|
||||
prompt_id?: string,
|
||||
) => {
|
||||
// Prevent concurrent executions of submitQuery, but allow continuations
|
||||
|
|
@ -1047,7 +1079,11 @@ export const useGeminiStream = (
|
|||
// Reset quota error flag when starting a new query (not a continuation)
|
||||
if (!options?.isContinuation) {
|
||||
setModelSwitchedFromQuotaError(false);
|
||||
// No quota-error / fallback routing mechanism currently; keep state minimal.
|
||||
// Commit any pending retry error to history (without hint) since the
|
||||
// user is starting a new conversation turn
|
||||
if (pendingRetryCountdownItemRef.current) {
|
||||
clearRetryCountdown();
|
||||
}
|
||||
}
|
||||
|
||||
abortControllerRef.current = new AbortController();
|
||||
|
|
@ -1059,12 +1095,14 @@ export const useGeminiStream = (
|
|||
}
|
||||
|
||||
return promptIdContext.run(prompt_id, async () => {
|
||||
const { queryToSend, shouldProceed } = await prepareQueryForGemini(
|
||||
query,
|
||||
userMessageTimestamp,
|
||||
abortSignal,
|
||||
prompt_id!,
|
||||
);
|
||||
const { queryToSend, shouldProceed } = options?.skipPreparation
|
||||
? { queryToSend: query, shouldProceed: true }
|
||||
: await prepareQueryForGemini(
|
||||
query,
|
||||
userMessageTimestamp,
|
||||
abortSignal,
|
||||
prompt_id!,
|
||||
);
|
||||
|
||||
if (!shouldProceed || queryToSend === null) {
|
||||
isSubmittingQueryRef.current = false;
|
||||
|
|
@ -1086,6 +1124,8 @@ export const useGeminiStream = (
|
|||
}
|
||||
|
||||
const finalQueryToSend = queryToSend;
|
||||
lastPromptRef.current = finalQueryToSend;
|
||||
lastPromptErroredRef.current = false;
|
||||
|
||||
if (!options?.isContinuation) {
|
||||
// trigger new prompt event for session stats in CLI
|
||||
|
|
@ -1134,6 +1174,12 @@ export const useGeminiStream = (
|
|||
addItem(pendingHistoryItemRef.current, userMessageTimestamp);
|
||||
setPendingHistoryItem(null);
|
||||
}
|
||||
// Only clear auto-retry countdown errors (those with an active timer).
|
||||
// Do NOT clear static error+hint from handleErrorEvent — those should
|
||||
// remain visible until the user presses Ctrl+Y to retry.
|
||||
if (retryCountdownTimerRef.current) {
|
||||
clearRetryCountdown();
|
||||
}
|
||||
if (loopDetectedRef.current) {
|
||||
loopDetectedRef.current = false;
|
||||
handleLoopDetectedEvent();
|
||||
|
|
@ -1142,16 +1188,17 @@ export const useGeminiStream = (
|
|||
if (error instanceof UnauthorizedError) {
|
||||
onAuthError('Session expired or is unauthorized.');
|
||||
} else if (!isNodeError(error) || error.name !== 'AbortError') {
|
||||
addItem(
|
||||
{
|
||||
type: MessageType.ERROR,
|
||||
text: parseAndFormatApiError(
|
||||
getErrorMessage(error) || 'Unknown error',
|
||||
config.getContentGeneratorConfig()?.authType,
|
||||
),
|
||||
},
|
||||
userMessageTimestamp,
|
||||
);
|
||||
lastPromptErroredRef.current = true;
|
||||
const retryHint = t('Press Ctrl+Y to retry');
|
||||
// Store error with hint as a pending item (same as handleErrorEvent)
|
||||
setPendingRetryErrorItem({
|
||||
type: 'error' as const,
|
||||
text: parseAndFormatApiError(
|
||||
getErrorMessage(error) || 'Unknown error',
|
||||
config.getContentGeneratorConfig()?.authType,
|
||||
),
|
||||
hint: retryHint,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setIsResponding(false);
|
||||
|
|
@ -1174,9 +1221,71 @@ export const useGeminiStream = (
|
|||
startNewPrompt,
|
||||
getPromptCount,
|
||||
handleLoopDetectedEvent,
|
||||
clearRetryCountdown,
|
||||
pendingRetryCountdownItemRef,
|
||||
setPendingRetryErrorItem,
|
||||
],
|
||||
);
|
||||
|
||||
/**
|
||||
* Retries the last failed prompt when the user presses Ctrl+Y.
|
||||
*
|
||||
* Activation conditions for Ctrl+Y shortcut:
|
||||
* 1. ✅ The last request must have failed (lastPromptErroredRef.current === true)
|
||||
* 2. ✅ Current streaming state must NOT be "Responding" (avoid interrupting ongoing stream)
|
||||
* 3. ✅ Current streaming state must NOT be "WaitingForConfirmation" (avoid conflicting with tool confirmation flow)
|
||||
* 4. ✅ There must be a stored lastPrompt in lastPromptRef.current
|
||||
*
|
||||
* When conditions are not met:
|
||||
* - If streaming is active (Responding/WaitingForConfirmation): silently return without action
|
||||
* - If no failed request exists: display "No failed request to retry." info message
|
||||
*
|
||||
* When conditions are met:
|
||||
* - Clears any pending auto-retry countdown to avoid duplicate retries
|
||||
* - Re-submits the last query with skipPreparation: true for faster retry
|
||||
*
|
||||
* This function is exposed via UIActionsContext and triggered by InputPrompt
|
||||
* when the user presses Ctrl+Y (bound to Command.RETRY_LAST in keyBindings.ts).
|
||||
*/
|
||||
const retryLastPrompt = useCallback(async () => {
|
||||
if (
|
||||
streamingState === StreamingState.Responding ||
|
||||
streamingState === StreamingState.WaitingForConfirmation
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastPrompt = lastPromptRef.current;
|
||||
if (!lastPrompt || !lastPromptErroredRef.current) {
|
||||
addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: t('No failed request to retry.'),
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Commit the error to history (without hint) before clearing
|
||||
const errorItem = pendingRetryErrorItemRef.current;
|
||||
if (errorItem) {
|
||||
addItem({ type: errorItem.type, text: errorItem.text }, Date.now());
|
||||
}
|
||||
clearRetryCountdown();
|
||||
|
||||
await submitQuery(lastPrompt, {
|
||||
isContinuation: false,
|
||||
skipPreparation: true,
|
||||
});
|
||||
}, [
|
||||
streamingState,
|
||||
addItem,
|
||||
clearRetryCountdown,
|
||||
submitQuery,
|
||||
pendingRetryErrorItemRef,
|
||||
]);
|
||||
|
||||
const handleApprovalModeChange = useCallback(
|
||||
async (newApprovalMode: ApprovalMode) => {
|
||||
// Auto-approve pending tool calls when switching to auto-approval modes
|
||||
|
|
@ -1480,6 +1589,7 @@ export const useGeminiStream = (
|
|||
pendingHistoryItems,
|
||||
thought,
|
||||
cancelOngoingRequest,
|
||||
retryLastPrompt,
|
||||
pendingToolCalls: toolCalls,
|
||||
handleApprovalModeChange,
|
||||
activePtyId,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue