Compare commits

...

5 Commits

6 changed files with 62 additions and 17 deletions
+5 -5
View File
@@ -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(() => {
@@ -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
+4 -2
View File
@@ -424,6 +424,7 @@ async function authWithUserCode(client: OAuth2Client): Promise<boolean> {
'\n\n',
);
let authTimeoutId: NodeJS.Timeout | undefined;
const code = await new Promise<string>((resolve, reject) => {
const rl = readline.createInterface({
input: process.stdin,
@@ -431,7 +432,7 @@ async function authWithUserCode(client: OAuth2Client): Promise<boolean> {
terminal: true,
});
const timeout = setTimeout(() => {
authTimeoutId = setTimeout(() => {
rl.close();
reject(
new FatalAuthenticationError(
@@ -441,10 +442,11 @@ async function authWithUserCode(client: OAuth2Client): Promise<boolean> {
}, 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) {
@@ -577,6 +577,12 @@ export class ChatRecordingService {
) {
const conversation = this.readConversation();
updateFn(conversation);
// Enforce a hard limit of 1000 items to prevent unbounded memory and file growth
if (conversation.messages.length > 1000) {
conversation.messages = conversation.messages.slice(
conversation.messages.length - 1000,
);
}
this.writeConversation(conversation);
}
+11 -2
View File
@@ -116,6 +116,8 @@ export function startCallbackServer(
portReject = reject;
});
let timeoutId: NodeJS.Timeout | undefined;
const responsePromise = new Promise<OAuthAuthorizationResponse>(
(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);
}
}),
};
}
/**
+30 -7
View File
@@ -108,6 +108,8 @@ export async function resolveExecutable(
let bashLanguage: Language | null = null;
let treeSitterInitialization: Promise<void> | null = null;
let treeSitterInitializationError: Error | null = null;
let parserState: 'uninitialized' | 'initializing' | 'initialized' | 'error' =
'uninitialized';
class ShellParserInitializationError extends Error {
constructor(cause: Error) {
@@ -163,16 +165,37 @@ async function loadBashLanguage(): Promise<void> {
}
export async function initializeShellParsers(): Promise<void> {
if (!treeSitterInitialization) {
treeSitterInitialization = loadBashLanguage().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.
debugLogger.debug('Failed to initialize shell parsers:', error);
if (parserState === 'uninitialized') {
parserState = 'initializing';
let timerId: NodeJS.Timeout | undefined;
const timeoutPromise = new Promise<void>((_, reject) => {
timerId = setTimeout(
() => reject(new Error('Tree-sitter initialization timed out')),
30000,
);
});
treeSitterInitialization = Promise.race([
loadBashLanguage(),
timeoutPromise,
])
.finally(() => {
if (timerId) clearTimeout(timerId);
})
.then(() => {
parserState = 'initialized';
})
.catch((error) => {
parserState = '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.
debugLogger.debug('Failed to initialize shell parsers:', error);
});
}
await treeSitterInitialization;
if (treeSitterInitialization) {
await treeSitterInitialization;
}
}
export interface ParsedCommandDetail {