fix(web): persist dashboard chat messages across sidebar navigation (#2785)

This commit is contained in:
argenis de la rosa 2026-03-04 21:37:41 -05:00
parent a00ae631e6
commit bd8c191182

View File

@ -10,9 +10,18 @@ interface ChatMessage {
timestamp: Date;
}
interface PersistedChatMessage {
id: string;
role: 'user' | 'agent';
content: string;
timestamp: string;
}
let fallbackMessageIdCounter = 0;
const EMPTY_DONE_FALLBACK =
'Tool execution completed, but no final response text was returned.';
const CHAT_HISTORY_STORAGE_KEY = 'zeroclaw.agent_chat.messages.v1';
const MAX_PERSISTED_MESSAGES = 500;
function makeMessageId(): string {
const uuid = globalThis.crypto?.randomUUID?.();
@ -24,8 +33,74 @@ function makeMessageId(): string {
.slice(2, 10)}`;
}
function loadPersistedMessages(): ChatMessage[] {
if (typeof window === 'undefined') {
return [];
}
try {
const raw = sessionStorage.getItem(CHAT_HISTORY_STORAGE_KEY);
if (!raw) {
return [];
}
const parsed = JSON.parse(raw) as PersistedChatMessage[];
if (!Array.isArray(parsed)) {
return [];
}
return parsed
.map((msg): ChatMessage | null => {
if (!msg || typeof msg !== 'object') {
return null;
}
const timestamp = new Date(msg.timestamp);
if (
typeof msg.id !== 'string' ||
(msg.role !== 'user' && msg.role !== 'agent') ||
typeof msg.content !== 'string' ||
Number.isNaN(timestamp.getTime())
) {
return null;
}
return {
id: msg.id,
role: msg.role,
content: msg.content,
timestamp,
};
})
.filter((msg): msg is ChatMessage => msg !== null)
.slice(-MAX_PERSISTED_MESSAGES);
} catch {
return [];
}
}
function persistMessages(messages: ChatMessage[]): void {
if (typeof window === 'undefined') {
return;
}
try {
const payload: PersistedChatMessage[] = messages
.slice(-MAX_PERSISTED_MESSAGES)
.map((msg) => ({
id: msg.id,
role: msg.role,
content: msg.content,
timestamp: msg.timestamp.toISOString(),
}));
sessionStorage.setItem(CHAT_HISTORY_STORAGE_KEY, JSON.stringify(payload));
} catch {
// sessionStorage may be unavailable in private modes; fail silently.
}
}
export default function AgentChat() {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [messages, setMessages] = useState<ChatMessage[]>(() => loadPersistedMessages());
const [input, setInput] = useState('');
const [typing, setTyping] = useState(false);
const [connected, setConnected] = useState(false);
@ -131,6 +206,10 @@ export default function AgentChat() {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages, typing]);
useEffect(() => {
persistMessages(messages);
}, [messages]);
const handleSend = () => {
const trimmed = input.trim();
if (!trimmed || !wsRef.current?.connected) return;