Lint issues fixed: 1. Removed unused error variable in catch block 2. Changed 'ActionTypes' type to enum 3. Removed unnecessary interface extensions 4. Removed unused export component 5. Added the missing dependencies to useMemo 6. Fixed the inline function issue by changing it to useMemo
130 lines
3.1 KiB
TypeScript
130 lines
3.1 KiB
TypeScript
import { useState, useCallback, useEffect, useRef, useMemo } from "react";
|
|
import {
|
|
ResourceReference,
|
|
PromptReference,
|
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
|
|
interface CompletionState {
|
|
completions: Record<string, string[]>;
|
|
loading: Record<string, boolean>;
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
function debounce<T extends (...args: any[]) => PromiseLike<void>>(
|
|
func: T,
|
|
wait: number,
|
|
): (...args: Parameters<T>) => void {
|
|
let timeout: ReturnType<typeof setTimeout>;
|
|
return (...args: Parameters<T>) => {
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(() => {
|
|
void func(...args);
|
|
}, wait);
|
|
};
|
|
}
|
|
|
|
export function useCompletionState(
|
|
handleCompletion: (
|
|
ref: ResourceReference | PromptReference,
|
|
argName: string,
|
|
value: string,
|
|
signal?: AbortSignal,
|
|
) => Promise<string[]>,
|
|
completionsSupported: boolean = true,
|
|
debounceMs: number = 300,
|
|
) {
|
|
const [state, setState] = useState<CompletionState>({
|
|
completions: {},
|
|
loading: {},
|
|
});
|
|
|
|
const abortControllerRef = useRef<AbortController | null>(null);
|
|
|
|
const cleanup = useCallback(() => {
|
|
if (abortControllerRef.current) {
|
|
abortControllerRef.current.abort();
|
|
abortControllerRef.current = null;
|
|
}
|
|
}, []);
|
|
|
|
// Cleanup on unmount
|
|
useEffect(() => {
|
|
return cleanup;
|
|
}, [cleanup]);
|
|
|
|
const clearCompletions = useCallback(() => {
|
|
cleanup();
|
|
setState({
|
|
completions: {},
|
|
loading: {},
|
|
});
|
|
}, [cleanup]);
|
|
|
|
const requestCompletions = useMemo(() => {
|
|
return debounce(
|
|
async (
|
|
ref: ResourceReference | PromptReference,
|
|
argName: string,
|
|
value: string,
|
|
) => {
|
|
if (!completionsSupported) {
|
|
return;
|
|
}
|
|
|
|
cleanup();
|
|
|
|
const abortController = new AbortController();
|
|
abortControllerRef.current = abortController;
|
|
|
|
setState((prev) => ({
|
|
...prev,
|
|
loading: { ...prev.loading, [argName]: true },
|
|
}));
|
|
|
|
try {
|
|
const values = await handleCompletion(
|
|
ref,
|
|
argName,
|
|
value,
|
|
abortController.signal,
|
|
);
|
|
|
|
if (!abortController.signal.aborted) {
|
|
setState((prev) => ({
|
|
...prev,
|
|
completions: { ...prev.completions, [argName]: values },
|
|
loading: { ...prev.loading, [argName]: false },
|
|
}));
|
|
}
|
|
} catch {
|
|
if (!abortController.signal.aborted) {
|
|
setState((prev) => ({
|
|
...prev,
|
|
loading: { ...prev.loading, [argName]: false },
|
|
}));
|
|
}
|
|
} finally {
|
|
if (abortControllerRef.current === abortController) {
|
|
abortControllerRef.current = null;
|
|
}
|
|
}
|
|
},
|
|
debounceMs,
|
|
);
|
|
}, [handleCompletion, completionsSupported, cleanup, debounceMs]);
|
|
|
|
// Clear completions when support status changes
|
|
useEffect(() => {
|
|
if (!completionsSupported) {
|
|
clearCompletions();
|
|
}
|
|
}, [completionsSupported, clearCompletions]);
|
|
|
|
return {
|
|
...state,
|
|
clearCompletions,
|
|
requestCompletions,
|
|
completionsSupported,
|
|
};
|
|
}
|