import { useCallback, useEffect, useRef, useState } from "react"; import type { Message } from "../types"; const STORAGE_KEY_PREFIX = "storykit-chat-history:"; const LIMIT_KEY_PREFIX = "storykit-chat-history-limit:"; const DEFAULT_LIMIT = 200; function storageKey(projectPath: string): string { return `${STORAGE_KEY_PREFIX}${projectPath}`; } function limitKey(projectPath: string): string { return `${LIMIT_KEY_PREFIX}${projectPath}`; } function loadLimit(projectPath: string): number { try { const raw = localStorage.getItem(limitKey(projectPath)); if (raw === null) return DEFAULT_LIMIT; const parsed = Number(raw); if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_LIMIT; return Math.floor(parsed); } catch { return DEFAULT_LIMIT; } } function saveLimit(projectPath: string, limit: number): void { try { localStorage.setItem(limitKey(projectPath), String(limit)); } catch { // Ignore — quota or security errors. } } function loadMessages(projectPath: string): Message[] { try { const raw = localStorage.getItem(storageKey(projectPath)); if (!raw) return []; const parsed: unknown = JSON.parse(raw); if (!Array.isArray(parsed)) return []; return parsed as Message[]; } catch { return []; } } function pruneMessages(messages: Message[], limit: number): Message[] { if (limit === 0 || messages.length <= limit) return messages; return messages.slice(-limit); } function saveMessages( projectPath: string, messages: Message[], limit: number, ): void { try { const pruned = pruneMessages(messages, limit); if (pruned.length === 0) { localStorage.removeItem(storageKey(projectPath)); } else { localStorage.setItem(storageKey(projectPath), JSON.stringify(pruned)); } } catch (e) { console.warn("Failed to persist chat history to localStorage:", e); } } export function useChatHistory(projectPath: string) { const [messages, setMessagesState] = useState(() => loadMessages(projectPath), ); const [maxMessages, setMaxMessagesState] = useState(() => loadLimit(projectPath), ); const projectPathRef = useRef(projectPath); // Keep the ref in sync so the effect closure always has the latest path. projectPathRef.current = projectPath; // Persist whenever messages or limit change. useEffect(() => { saveMessages(projectPathRef.current, messages, maxMessages); }, [messages, maxMessages]); const setMessages = useCallback( (update: Message[] | ((prev: Message[]) => Message[])) => { setMessagesState(update); }, [], ); const setMaxMessages = useCallback((limit: number) => { setMaxMessagesState(limit); saveLimit(projectPathRef.current, limit); }, []); const clearMessages = useCallback(() => { setMessagesState([]); // Eagerly remove from storage so clearSession doesn't depend on the // effect firing before the component unmounts or re-renders. try { localStorage.removeItem(storageKey(projectPathRef.current)); } catch { // Ignore — quota or security errors. } }, []); return { messages, setMessages, clearMessages, maxMessages, setMaxMessages, } as const; }