mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-08 06:25:34 +08:00
refactor: improve session limit and offset validation in provider routes
This commit is contained in:
@@ -338,21 +338,28 @@ router.get(
|
|||||||
const limitRaw = readOptionalQueryString(req.query.limit);
|
const limitRaw = readOptionalQueryString(req.query.limit);
|
||||||
const offsetRaw = readOptionalQueryString(req.query.offset);
|
const offsetRaw = readOptionalQueryString(req.query.offset);
|
||||||
|
|
||||||
const limit = limitRaw === undefined ? null : Number.parseInt(limitRaw, 10);
|
let limit: number | null = null;
|
||||||
const offset = offsetRaw === undefined ? 0 : Number.parseInt(offsetRaw, 10);
|
if (limitRaw !== undefined) {
|
||||||
|
const parsedLimit = Number.parseInt(limitRaw, 10);
|
||||||
if (limitRaw !== undefined && Number.isNaN(limit)) {
|
if (Number.isNaN(parsedLimit) || parsedLimit < 0) {
|
||||||
throw new AppError('limit must be a valid integer.', {
|
throw new AppError('limit must be a non-negative integer.', {
|
||||||
code: 'INVALID_QUERY_PARAMETER',
|
code: 'INVALID_QUERY_PARAMETER',
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
limit = parsedLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (offsetRaw !== undefined && Number.isNaN(offset)) {
|
let offset = 0;
|
||||||
throw new AppError('offset must be a valid integer.', {
|
if (offsetRaw !== undefined) {
|
||||||
code: 'INVALID_QUERY_PARAMETER',
|
const parsedOffset = Number.parseInt(offsetRaw, 10);
|
||||||
statusCode: 400,
|
if (Number.isNaN(parsedOffset) || parsedOffset < 0) {
|
||||||
});
|
throw new AppError('offset must be a non-negative integer.', {
|
||||||
|
code: 'INVALID_QUERY_PARAMETER',
|
||||||
|
statusCode: 400,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
offset = parsedOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await sessionsService.fetchHistory(sessionId, {
|
const result = await sessionsService.fetchHistory(sessionId, {
|
||||||
|
|||||||
@@ -196,10 +196,14 @@ function createWordMatcher(
|
|||||||
return phraseRegex.test(text);
|
return phraseRegex.test(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (phraseRegex.test(text) || words.length === 1) {
|
if (phraseRegex.test(text)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (words.length === 1) {
|
||||||
|
return allWordsMatch(text.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
return allWordsMatch(text.toLowerCase());
|
return allWordsMatch(text.toLowerCase());
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -534,31 +538,47 @@ async function findMatchedFileKeys(
|
|||||||
const requireExactPhrase = words.length > 1 && normalizedQuery.length > 0;
|
const requireExactPhrase = words.length > 1 && normalizedQuery.length > 0;
|
||||||
|
|
||||||
if (requireExactPhrase) {
|
if (requireExactPhrase) {
|
||||||
const matchedForPhrase = new Set<string>();
|
let matchedForPhrase = searchablePathEntries.slice();
|
||||||
const fileChunks = chunkArray(
|
|
||||||
searchablePathEntries.map((entry) => entry.absolutePath),
|
|
||||||
RIPGREP_FILE_CHUNK_SIZE,
|
|
||||||
);
|
|
||||||
|
|
||||||
let nextChunkIndex = 0;
|
// Keep ripgrep as an over-approximation for exact phrase mode by requiring
|
||||||
const workerCount = Math.min(RIPGREP_CHUNK_CONCURRENCY, fileChunks.length);
|
// each word to appear somewhere in the file, then defer strict phrase
|
||||||
const workers = Array.from({ length: workerCount }, async () => {
|
// validation to the in-memory matcher.
|
||||||
while (nextChunkIndex < fileChunks.length && !signal?.aborted) {
|
for (const word of words) {
|
||||||
const currentIndex = nextChunkIndex;
|
if (signal?.aborted) {
|
||||||
nextChunkIndex += 1;
|
return new Set();
|
||||||
const chunkMatches = await runRipgrepFilesWithMatches(normalizedQuery, fileChunks[currentIndex], signal);
|
|
||||||
for (const matchedPath of chunkMatches) {
|
|
||||||
matchedForPhrase.add(matchedPath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(workers);
|
const matchedForWord = new Set<string>();
|
||||||
if (signal?.aborted) {
|
const fileChunks = chunkArray(
|
||||||
return new Set();
|
matchedForPhrase.map((entry) => entry.absolutePath),
|
||||||
|
RIPGREP_FILE_CHUNK_SIZE,
|
||||||
|
);
|
||||||
|
|
||||||
|
let nextChunkIndex = 0;
|
||||||
|
const workerCount = Math.min(RIPGREP_CHUNK_CONCURRENCY, fileChunks.length);
|
||||||
|
const workers = Array.from({ length: workerCount }, async () => {
|
||||||
|
while (nextChunkIndex < fileChunks.length && !signal?.aborted) {
|
||||||
|
const currentIndex = nextChunkIndex;
|
||||||
|
nextChunkIndex += 1;
|
||||||
|
const chunkMatches = await runRipgrepFilesWithMatches(word, fileChunks[currentIndex], signal);
|
||||||
|
for (const matchedPath of chunkMatches) {
|
||||||
|
matchedForWord.add(matchedPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(workers);
|
||||||
|
if (signal?.aborted) {
|
||||||
|
return new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
matchedForPhrase = matchedForPhrase.filter((entry) => matchedForWord.has(entry.normalizedPath));
|
||||||
|
if (matchedForPhrase.length === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return matchedForPhrase;
|
return new Set(matchedForPhrase.map((entry) => entry.normalizedPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
let remainingEntries = searchablePathEntries.slice();
|
let remainingEntries = searchablePathEntries.slice();
|
||||||
|
|||||||
@@ -120,6 +120,8 @@ export function useSidebarController({
|
|||||||
const searchSeqRef = useRef(0);
|
const searchSeqRef = useRef(0);
|
||||||
const eventSourceRef = useRef<EventSource | null>(null);
|
const eventSourceRef = useRef<EventSource | null>(null);
|
||||||
const starToggleSequenceByProjectRef = useRef<Map<string, number>>(new Map());
|
const starToggleSequenceByProjectRef = useRef<Map<string, number>>(new Map());
|
||||||
|
const migrationStartedRef = useRef(false);
|
||||||
|
const onRefreshRef = useRef(onRefresh);
|
||||||
|
|
||||||
const isSidebarCollapsed = !isMobile && !sidebarVisible;
|
const isSidebarCollapsed = !isMobile && !sidebarVisible;
|
||||||
|
|
||||||
@@ -194,19 +196,25 @@ export function useSidebarController({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
onRefreshRef.current = onRefresh;
|
||||||
|
}, [onRefresh]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (migrationStartedRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const legacyStarredProjectIds = readLegacyStarredProjectIds();
|
const legacyStarredProjectIds = readLegacyStarredProjectIds();
|
||||||
if (legacyStarredProjectIds.length === 0) {
|
if (legacyStarredProjectIds.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let active = true;
|
migrationStartedRef.current = true;
|
||||||
|
|
||||||
const migrateLegacyStars = async () => {
|
const migrateLegacyStars = async () => {
|
||||||
try {
|
try {
|
||||||
await api.migrateLegacyProjectStars(legacyStarredProjectIds);
|
await api.migrateLegacyProjectStars(legacyStarredProjectIds);
|
||||||
if (active) {
|
await onRefreshRef.current();
|
||||||
await onRefresh();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Sidebar] Failed to migrate legacy starred projects:', error);
|
console.error('[Sidebar] Failed to migrate legacy starred projects:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -215,10 +223,6 @@ export function useSidebarController({
|
|||||||
};
|
};
|
||||||
|
|
||||||
void migrateLegacyStars();
|
void migrateLegacyStars();
|
||||||
|
|
||||||
return () => {
|
|
||||||
active = false;
|
|
||||||
};
|
|
||||||
}, [onRefresh]);
|
}, [onRefresh]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -446,16 +450,26 @@ export function useSidebarController({
|
|||||||
const getProjectSessions = useCallback((project: Project) => getAllSessions(project), []);
|
const getProjectSessions = useCallback((project: Project) => getAllSessions(project), []);
|
||||||
|
|
||||||
const loadMoreSessionsForProject = useCallback(async (projectId: string) => {
|
const loadMoreSessionsForProject = useCallback(async (projectId: string) => {
|
||||||
if (!onLoadMoreSessions || loadingMoreProjects.has(projectId)) {
|
if (!onLoadMoreSessions) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let shouldLoad = false;
|
||||||
setLoadingMoreProjects((previous) => {
|
setLoadingMoreProjects((previous) => {
|
||||||
|
if (previous.has(projectId)) {
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldLoad = true;
|
||||||
const next = new Set(previous);
|
const next = new Set(previous);
|
||||||
next.add(projectId);
|
next.add(projectId);
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!shouldLoad) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await onLoadMoreSessions(projectId);
|
await onLoadMoreSessions(projectId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -468,7 +482,7 @@ export function useSidebarController({
|
|||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [loadingMoreProjects, onLoadMoreSessions, t]);
|
}, [onLoadMoreSessions, t]);
|
||||||
|
|
||||||
const projectsWithResolvedStarState = useMemo(() => {
|
const projectsWithResolvedStarState = useMemo(() => {
|
||||||
if (optimisticStarByProjectId.size === 0) {
|
if (optimisticStarByProjectId.size === 0) {
|
||||||
|
|||||||
@@ -605,12 +605,28 @@ export function useProjectsState({
|
|||||||
|
|
||||||
setProjects((prevProjects) =>
|
setProjects((prevProjects) =>
|
||||||
prevProjects.map((project) => {
|
prevProjects.map((project) => {
|
||||||
|
const sessions = project.sessions?.filter((session) => session.id !== sessionIdToDelete) ?? [];
|
||||||
|
const cursorSessions = project.cursorSessions?.filter((session) => session.id !== sessionIdToDelete) ?? [];
|
||||||
|
const codexSessions = project.codexSessions?.filter((session) => session.id !== sessionIdToDelete) ?? [];
|
||||||
|
const geminiSessions = project.geminiSessions?.filter((session) => session.id !== sessionIdToDelete) ?? [];
|
||||||
|
|
||||||
|
const removedFromProject = (
|
||||||
|
sessions.length !== (project.sessions?.length ?? 0)
|
||||||
|
|| cursorSessions.length !== (project.cursorSessions?.length ?? 0)
|
||||||
|
|| codexSessions.length !== (project.codexSessions?.length ?? 0)
|
||||||
|
|| geminiSessions.length !== (project.geminiSessions?.length ?? 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!removedFromProject) {
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
|
||||||
const updatedProject: Project = {
|
const updatedProject: Project = {
|
||||||
...project,
|
...project,
|
||||||
sessions: project.sessions?.filter((session) => session.id !== sessionIdToDelete) ?? [],
|
sessions,
|
||||||
cursorSessions: project.cursorSessions?.filter((session) => session.id !== sessionIdToDelete) ?? [],
|
cursorSessions,
|
||||||
codexSessions: project.codexSessions?.filter((session) => session.id !== sessionIdToDelete) ?? [],
|
codexSessions,
|
||||||
geminiSessions: project.geminiSessions?.filter((session) => session.id !== sessionIdToDelete) ?? [],
|
geminiSessions,
|
||||||
};
|
};
|
||||||
|
|
||||||
const totalSessions = Math.max(0, Number(project.sessionMeta?.total ?? 0) - 1);
|
const totalSessions = Math.max(0, Number(project.sessionMeta?.total ?? 0) - 1);
|
||||||
|
|||||||
Reference in New Issue
Block a user