import { useCallback, useEffect, useRef, useState } from 'react'; export interface UsePromptHistoryOptions { /** * Storage key — should be product-scoped (e.g. `notes:chat:history`). * Pass `null` to disable persistence (in-memory only). */ storageKey?: string | null; /** Maximum number of prompts retained. Default 50. */ capacity?: number; } export interface UsePromptHistoryHelpers { /** The full history array, newest LAST. */ history: string[]; /** Append a prompt — dedupes consecutive duplicates + trims to capacity. */ push: (prompt: string) => void; /** Clear all stored prompts. */ clear: () => void; /** Cursor-up — returns the previous prompt (or null at the top). */ prev: () => string | null; /** Cursor-down — returns the next prompt (or null at the bottom). */ next: () => string | null; /** Reset the cursor — call when the user starts typing fresh. */ resetCursor: () => void; } /** * `usePromptHistory` — bash-style ↑/↓ recall for prompt composers. * * Wave 9.E.4. Persistence is `localStorage` by default, scoped via * `storageKey`. Pass `null` for in-memory only (useful for tests + * SSR-prerender safety). */ export function usePromptHistory( options: UsePromptHistoryOptions = {}, ): UsePromptHistoryHelpers { const { storageKey = 'bytelyst:prompt-history', capacity = 50 } = options; // Lazy initial value — never touches `localStorage` during SSR. const [history, setHistory] = useState(() => { if (!storageKey || typeof window === 'undefined') return []; try { const raw = window.localStorage.getItem(storageKey); if (!raw) return []; const parsed = JSON.parse(raw); return Array.isArray(parsed) ? (parsed as string[]) : []; } catch { return []; } }); // Cursor: -1 means 'not navigating'. >=0 indexes from the END (so 0 // is the newest, history.length-1 is the oldest). This matches the // bash ↑ recall convention. const cursorRef = useRef(-1); useEffect(() => { if (!storageKey || typeof window === 'undefined') return; try { window.localStorage.setItem(storageKey, JSON.stringify(history)); } catch { /* quota / disabled — ignore silently */ } }, [storageKey, history]); const push = useCallback( (prompt: string) => { const trimmed = prompt.trim(); if (!trimmed) return; setHistory((cur) => { // De-dupe consecutive duplicate. if (cur[cur.length - 1] === trimmed) return cur; const next = [...cur, trimmed]; return next.length > capacity ? next.slice(next.length - capacity) : next; }); cursorRef.current = -1; }, [capacity], ); const clear = useCallback(() => { setHistory([]); cursorRef.current = -1; }, []); const prev = useCallback((): string | null => { if (history.length === 0) return null; const newIdx = Math.min(history.length - 1, cursorRef.current + 1); cursorRef.current = newIdx; return history[history.length - 1 - newIdx] ?? null; }, [history]); const next = useCallback((): string | null => { if (history.length === 0) return null; if (cursorRef.current <= 0) { cursorRef.current = -1; return null; } cursorRef.current -= 1; return history[history.length - 1 - cursorRef.current] ?? null; }, [history]); const resetCursor = useCallback(() => { cursorRef.current = -1; }, []); return { history, push, clear, prev, next, resetCursor }; }