2025-12-20 02:06:00 +02:00
|
|
|
import React from 'react';
|
2026-01-03 13:39:59 +02:00
|
|
|
import type { Session } from '@opencode-ai/sdk/v2';
|
2025-12-20 02:06:00 +02:00
|
|
|
import { useSessionStore } from '@/stores/useSessionStore';
|
|
|
|
|
import { useUIStore } from '@/stores/useUIStore';
|
|
|
|
|
|
|
|
|
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
|
|
|
const AUTO_DELETE_KEEP_RECENT = 5;
|
|
|
|
|
const AUTO_DELETE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
|
|
|
|
|
|
|
|
const getSessionLastActivity = (session: Session): number => {
|
|
|
|
|
return session.time?.updated ?? session.time?.created ?? 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type BuildAutoDeleteCandidatesOptions = {
|
|
|
|
|
sessions: Session[];
|
|
|
|
|
currentSessionId: string | null;
|
|
|
|
|
cutoffDays: number;
|
|
|
|
|
keepRecent?: number;
|
|
|
|
|
now?: number;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const buildAutoDeleteCandidates = ({
|
|
|
|
|
sessions,
|
|
|
|
|
currentSessionId,
|
|
|
|
|
cutoffDays,
|
|
|
|
|
keepRecent = AUTO_DELETE_KEEP_RECENT,
|
|
|
|
|
now = Date.now(),
|
|
|
|
|
}: BuildAutoDeleteCandidatesOptions): string[] => {
|
|
|
|
|
if (!Array.isArray(sessions) || cutoffDays <= 0) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const cutoffTime = now - cutoffDays * DAY_MS;
|
|
|
|
|
const sorted = [...sessions].sort(
|
|
|
|
|
(a, b) => getSessionLastActivity(b) - getSessionLastActivity(a)
|
|
|
|
|
);
|
|
|
|
|
const protectedIds = new Set(sorted.slice(0, keepRecent).map((session) => session.id));
|
|
|
|
|
|
|
|
|
|
return sorted
|
|
|
|
|
.filter((session) => {
|
|
|
|
|
if (!session?.id) return false;
|
|
|
|
|
if (protectedIds.has(session.id)) return false;
|
|
|
|
|
if (session.id === currentSessionId) return false;
|
|
|
|
|
if (session.share) return false;
|
|
|
|
|
const lastActivity = getSessionLastActivity(session);
|
|
|
|
|
if (!lastActivity) return false;
|
|
|
|
|
return lastActivity < cutoffTime;
|
|
|
|
|
})
|
|
|
|
|
.map((session) => session.id);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type CleanupResult = {
|
|
|
|
|
deletedIds: string[];
|
|
|
|
|
failedIds: string[];
|
|
|
|
|
skippedReason?: 'disabled' | 'loading' | 'cooldown' | 'no-candidates' | 'running';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type CleanupOptions = {
|
|
|
|
|
autoRun?: boolean;
|
2026-03-02 02:11:33 +02:00
|
|
|
enabled?: boolean;
|
2025-12-20 02:06:00 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const useSessionAutoCleanup = (options?: CleanupOptions) => {
|
|
|
|
|
const autoRun = options?.autoRun !== false;
|
2026-03-02 02:11:33 +02:00
|
|
|
const enabled = options?.enabled ?? true;
|
2025-12-20 02:06:00 +02:00
|
|
|
|
|
|
|
|
const sessions = useSessionStore((state) => state.sessions);
|
|
|
|
|
const currentSessionId = useSessionStore((state) => state.currentSessionId);
|
|
|
|
|
const isLoading = useSessionStore((state) => state.isLoading);
|
|
|
|
|
const deleteSessions = useSessionStore((state) => state.deleteSessions);
|
|
|
|
|
|
|
|
|
|
const autoDeleteEnabled = useUIStore((state) => state.autoDeleteEnabled);
|
|
|
|
|
const autoDeleteAfterDays = useUIStore((state) => state.autoDeleteAfterDays);
|
|
|
|
|
const autoDeleteLastRunAt = useUIStore((state) => state.autoDeleteLastRunAt);
|
|
|
|
|
const setAutoDeleteLastRunAt = useUIStore((state) => state.setAutoDeleteLastRunAt);
|
|
|
|
|
|
|
|
|
|
const [isRunning, setIsRunning] = React.useState(false);
|
|
|
|
|
const runningRef = React.useRef(false);
|
|
|
|
|
|
|
|
|
|
const candidates = React.useMemo(() => {
|
|
|
|
|
if (autoDeleteAfterDays <= 0) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
return buildAutoDeleteCandidates({
|
|
|
|
|
sessions,
|
|
|
|
|
currentSessionId,
|
|
|
|
|
cutoffDays: autoDeleteAfterDays,
|
|
|
|
|
});
|
|
|
|
|
}, [autoDeleteAfterDays, currentSessionId, sessions]);
|
|
|
|
|
|
|
|
|
|
const runCleanup = React.useCallback(
|
|
|
|
|
async ({ force = false }: { force?: boolean } = {}): Promise<CleanupResult> => {
|
|
|
|
|
if (runningRef.current) {
|
|
|
|
|
return { deletedIds: [], failedIds: [], skippedReason: 'running' };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!autoDeleteEnabled || autoDeleteAfterDays <= 0) {
|
|
|
|
|
if (!force) {
|
|
|
|
|
return { deletedIds: [], failedIds: [], skippedReason: 'disabled' };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isLoading) {
|
|
|
|
|
return { deletedIds: [], failedIds: [], skippedReason: 'loading' };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
if (!force && autoDeleteLastRunAt && now - autoDeleteLastRunAt < AUTO_DELETE_INTERVAL_MS) {
|
|
|
|
|
return { deletedIds: [], failedIds: [], skippedReason: 'cooldown' };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (sessions.length === 0) {
|
|
|
|
|
return { deletedIds: [], failedIds: [], skippedReason: 'no-candidates' };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const candidateIds = buildAutoDeleteCandidates({
|
|
|
|
|
sessions,
|
|
|
|
|
currentSessionId,
|
|
|
|
|
cutoffDays: autoDeleteAfterDays,
|
|
|
|
|
now,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (candidateIds.length === 0) {
|
|
|
|
|
setAutoDeleteLastRunAt(now);
|
|
|
|
|
return { deletedIds: [], failedIds: [], skippedReason: 'no-candidates' };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runningRef.current = true;
|
|
|
|
|
setIsRunning(true);
|
|
|
|
|
try {
|
|
|
|
|
const result = await deleteSessions(candidateIds, { silent: true });
|
|
|
|
|
return result;
|
|
|
|
|
} finally {
|
|
|
|
|
runningRef.current = false;
|
|
|
|
|
setIsRunning(false);
|
|
|
|
|
setAutoDeleteLastRunAt(Date.now());
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[
|
|
|
|
|
autoDeleteAfterDays,
|
|
|
|
|
autoDeleteEnabled,
|
|
|
|
|
autoDeleteLastRunAt,
|
|
|
|
|
currentSessionId,
|
|
|
|
|
deleteSessions,
|
|
|
|
|
isLoading,
|
|
|
|
|
sessions,
|
|
|
|
|
setAutoDeleteLastRunAt,
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
2026-03-02 02:11:33 +02:00
|
|
|
if (!enabled) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-20 02:06:00 +02:00
|
|
|
if (!autoRun) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!autoDeleteEnabled || autoDeleteAfterDays <= 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (isLoading || sessions.length === 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
if (autoDeleteLastRunAt && now - autoDeleteLastRunAt < AUTO_DELETE_INTERVAL_MS) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
void runCleanup();
|
|
|
|
|
}, [
|
|
|
|
|
autoDeleteAfterDays,
|
|
|
|
|
autoDeleteEnabled,
|
|
|
|
|
autoDeleteLastRunAt,
|
|
|
|
|
autoRun,
|
2026-03-02 02:11:33 +02:00
|
|
|
enabled,
|
2025-12-20 02:06:00 +02:00
|
|
|
isLoading,
|
2025-12-25 02:55:08 +02:00
|
|
|
sessions.length,
|
2025-12-20 02:06:00 +02:00
|
|
|
runCleanup,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
candidates,
|
|
|
|
|
isRunning,
|
|
|
|
|
runCleanup,
|
|
|
|
|
keepRecentCount: AUTO_DELETE_KEEP_RECENT,
|
|
|
|
|
};
|
|
|
|
|
};
|