diff --git a/packages/cli/src/ui/hooks/useAtCompletion.ts b/packages/cli/src/ui/hooks/useAtCompletion.ts index 4a7b9ebc13..67a2e2f984 100644 --- a/packages/cli/src/ui/hooks/useAtCompletion.ts +++ b/packages/cli/src/ui/hooks/useAtCompletion.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { useEffect, useReducer, useRef } from 'react'; +import { useEffect, useReducer, useRef, useCallback } from 'react'; import { setTimeout as setTimeoutPromise } from 'node:timers/promises'; import * as path from 'node:path'; import { @@ -224,15 +224,15 @@ export function useAtCompletion(props: UseAtCompletionProps): void { setIsLoadingSuggestions(state.isLoading); }, [state.isLoading, setIsLoadingSuggestions]); - const resetFileSearchState = () => { + const resetFileSearchState = useCallback(() => { fileSearchMap.current.clear(); initEpoch.current += 1; dispatch({ type: 'RESET' }); - }; + }, []); useEffect(() => { resetFileSearchState(); - }, [cwd, config]); + }, [cwd, config, resetFileSearchState]); useEffect(() => { const workspaceContext = config?.getWorkspaceContext?.(); @@ -242,7 +242,7 @@ export function useAtCompletion(props: UseAtCompletionProps): void { workspaceContext.onDirectoriesChanged(resetFileSearchState); return unsubscribe; - }, [config]); + }, [config, resetFileSearchState]); // Reacts to user input (`pattern`) ONLY. useEffect(() => { diff --git a/packages/cli/src/ui/hooks/useHistoryManager.ts b/packages/cli/src/ui/hooks/useHistoryManager.ts index c6ceabb920..4035264484 100644 --- a/packages/cli/src/ui/hooks/useHistoryManager.ts +++ b/packages/cli/src/ui/hooks/useHistoryManager.ts @@ -83,7 +83,12 @@ export function useHistory({ return prevHistory; // Don't add the duplicate } } - return [...prevHistory, newItem]; + const newHistory = [...prevHistory, newItem]; + // Enforce a hard limit of 1000 items to prevent unbounded memory growth + if (newHistory.length > 1000) { + return newHistory.slice(newHistory.length - 1000); + } + return newHistory; }); // Record UI-specific messages, but don't do it if we're actually loading diff --git a/packages/core/src/code_assist/oauth2.ts b/packages/core/src/code_assist/oauth2.ts index cb4b645ab3..be536d69ce 100644 --- a/packages/core/src/code_assist/oauth2.ts +++ b/packages/core/src/code_assist/oauth2.ts @@ -424,6 +424,7 @@ async function authWithUserCode(client: OAuth2Client): Promise { '\n\n', ); + let authTimeoutId: NodeJS.Timeout | undefined; const code = await new Promise((resolve, reject) => { const rl = readline.createInterface({ input: process.stdin, @@ -431,7 +432,7 @@ async function authWithUserCode(client: OAuth2Client): Promise { terminal: true, }); - const timeout = setTimeout(() => { + authTimeoutId = setTimeout(() => { rl.close(); reject( new FatalAuthenticationError( @@ -441,10 +442,11 @@ async function authWithUserCode(client: OAuth2Client): Promise { }, 300000); // 5 minute timeout rl.question('Enter the authorization code: ', (code) => { - clearTimeout(timeout); rl.close(); resolve(code.trim()); }); + }).finally(() => { + if (authTimeoutId) clearTimeout(authTimeoutId); }); if (!code) { diff --git a/packages/core/src/utils/oauth-flow.ts b/packages/core/src/utils/oauth-flow.ts index e13fd37837..e0fdc0e595 100644 --- a/packages/core/src/utils/oauth-flow.ts +++ b/packages/core/src/utils/oauth-flow.ts @@ -116,6 +116,8 @@ export function startCallbackServer( portReject = reject; }); + let timeoutId: NodeJS.Timeout | undefined; + const responsePromise = new Promise( (resolve, reject) => { let serverPort: number; @@ -222,7 +224,7 @@ export function startCallbackServer( }); // Timeout after 5 minutes - setTimeout( + timeoutId = setTimeout( () => { server.close(); reject(new Error('OAuth callback timeout')); @@ -232,7 +234,14 @@ export function startCallbackServer( }, ); - return { port: portPromise, response: responsePromise }; + return { + port: portPromise, + response: responsePromise.finally(() => { + if (timeoutId) { + clearTimeout(timeoutId); + } + }), + }; } /** diff --git a/packages/core/src/utils/shell-utils.ts b/packages/core/src/utils/shell-utils.ts index 8486be0de9..b35bfd815a 100644 --- a/packages/core/src/utils/shell-utils.ts +++ b/packages/core/src/utils/shell-utils.ts @@ -164,7 +164,16 @@ async function loadBashLanguage(): Promise { export async function initializeShellParsers(): Promise { if (!treeSitterInitialization) { - treeSitterInitialization = loadBashLanguage().catch((error) => { + const timeoutPromise = new Promise((_, reject) => { + setTimeout( + () => reject(new Error('Tree-sitter initialization timed out')), + 5000, + ); + }); + treeSitterInitialization = Promise.race([ + loadBashLanguage(), + timeoutPromise, + ]).catch((error) => { treeSitterInitialization = null; // Log the error but don't throw, allowing the application to fall back to safe defaults (ASK_USER) // or regex checks where appropriate.