2026-02-24 17:03:04 +00:00
|
|
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
|
|
import type { Message } from "../types";
|
|
|
|
|
|
|
|
|
|
const STORAGE_KEY_PREFIX = "storykit-chat-history:";
|
2026-02-25 11:40:09 +00:00
|
|
|
const LIMIT_KEY_PREFIX = "storykit-chat-history-limit:";
|
|
|
|
|
const DEFAULT_LIMIT = 200;
|
2026-02-24 17:03:04 +00:00
|
|
|
|
|
|
|
|
function storageKey(projectPath: string): string {
|
|
|
|
|
return `${STORAGE_KEY_PREFIX}${projectPath}`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-25 11:40:09 +00:00
|
|
|
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.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-24 17:03:04 +00:00
|
|
|
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 [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-25 11:40:09 +00:00
|
|
|
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 {
|
2026-02-24 17:03:04 +00:00
|
|
|
try {
|
2026-02-25 11:40:09 +00:00
|
|
|
const pruned = pruneMessages(messages, limit);
|
|
|
|
|
if (pruned.length === 0) {
|
2026-02-24 17:03:04 +00:00
|
|
|
localStorage.removeItem(storageKey(projectPath));
|
|
|
|
|
} else {
|
2026-02-25 11:40:09 +00:00
|
|
|
localStorage.setItem(storageKey(projectPath), JSON.stringify(pruned));
|
2026-02-24 17:03:04 +00:00
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn("Failed to persist chat history to localStorage:", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useChatHistory(projectPath: string) {
|
|
|
|
|
const [messages, setMessagesState] = useState<Message[]>(() =>
|
|
|
|
|
loadMessages(projectPath),
|
|
|
|
|
);
|
2026-02-25 11:40:09 +00:00
|
|
|
const [maxMessages, setMaxMessagesState] = useState<number>(() =>
|
|
|
|
|
loadLimit(projectPath),
|
|
|
|
|
);
|
2026-02-24 17:03:04 +00:00
|
|
|
const projectPathRef = useRef(projectPath);
|
|
|
|
|
|
|
|
|
|
// Keep the ref in sync so the effect closure always has the latest path.
|
|
|
|
|
projectPathRef.current = projectPath;
|
|
|
|
|
|
2026-02-25 11:40:09 +00:00
|
|
|
// Persist whenever messages or limit change.
|
2026-02-24 17:03:04 +00:00
|
|
|
useEffect(() => {
|
2026-02-25 11:40:09 +00:00
|
|
|
saveMessages(projectPathRef.current, messages, maxMessages);
|
|
|
|
|
}, [messages, maxMessages]);
|
2026-02-24 17:03:04 +00:00
|
|
|
|
|
|
|
|
const setMessages = useCallback(
|
|
|
|
|
(update: Message[] | ((prev: Message[]) => Message[])) => {
|
|
|
|
|
setMessagesState(update);
|
|
|
|
|
},
|
|
|
|
|
[],
|
|
|
|
|
);
|
|
|
|
|
|
2026-02-25 11:40:09 +00:00
|
|
|
const setMaxMessages = useCallback((limit: number) => {
|
|
|
|
|
setMaxMessagesState(limit);
|
|
|
|
|
saveLimit(projectPathRef.current, limit);
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-02-24 17:03:04 +00:00
|
|
|
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.
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-02-25 11:40:09 +00:00
|
|
|
return {
|
|
|
|
|
messages,
|
|
|
|
|
setMessages,
|
|
|
|
|
clearMessages,
|
|
|
|
|
maxMessages,
|
|
|
|
|
setMaxMessages,
|
|
|
|
|
} as const;
|
2026-02-24 17:03:04 +00:00
|
|
|
}
|