resolve comment

This commit is contained in:
DennisYu07 2026-03-02 06:11:11 -08:00
commit e4e21bb6b7
93 changed files with 3360 additions and 1604 deletions

View file

@ -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],
);
@ -1032,7 +1064,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
@ -1056,7 +1088,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();
@ -1068,12 +1104,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;
@ -1095,6 +1133,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
@ -1143,6 +1183,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();
@ -1151,16 +1197,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);
@ -1183,9 +1230,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
@ -1489,6 +1598,7 @@ export const useGeminiStream = (
pendingHistoryItems,
thought,
cancelOngoingRequest,
retryLastPrompt,
pendingToolCalls: toolCalls,
handleApprovalModeChange,
activePtyId,