[claude] Matrix chat history persistence — localStorage per-agent, 100-msg cap, Clear button (#43) (#63)

Co-authored-by: Claude (Opus 4.6) <claude@hermes.local>
Co-committed-by: Claude (Opus 4.6) <claude@hermes.local>
This commit was merged in pull request #63.
This commit is contained in:
2026-03-23 20:40:12 +00:00
committed by rockachopa
parent 4fdb77c53d
commit eed37885fb
4 changed files with 105 additions and 13 deletions

View File

@@ -71,6 +71,15 @@
pointer-events: none; z-index: 10;
}
.log-entry { opacity: 0.7; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#clear-history-btn {
position: fixed; bottom: 80px; left: 300px;
background: transparent; border: 1px solid #1a1a2e;
color: #334466; font-family: 'Courier New', monospace;
font-size: 9px; letter-spacing: 1px; padding: 2px 8px;
cursor: pointer; z-index: 10;
transition: color 0.2s, border-color 0.2s;
}
#clear-history-btn:hover { color: #6688bb; border-color: #4466aa; }
/* ── Top button bar ───────────────────────────────────────────────── */
#top-buttons {
@@ -495,6 +504,7 @@
<div id="connection-status">OFFLINE</div>
<div id="event-log"></div>
<button id="clear-history-btn">CLEAR HISTORY</button>
<!-- ── Timmy identity card ────────────────────────────────────────── -->
<div id="timmy-id-card">

View File

@@ -13,7 +13,7 @@
*/
import { setSpeechBubble, setMood } from './agents.js';
import { appendSystemMessage, setSessionSendHandler, setInputBarSessionMode } from './ui.js';
import { appendSystemMessage, appendChatMessage, setSessionSendHandler, setInputBarSessionMode } from './ui.js';
import { getOrRefreshToken } from './nostr-identity.js';
import { sentiment } from './edge-worker-client.js';
import qrcode from 'qrcode-generator';
@@ -111,7 +111,7 @@ export async function sessionSendHandler(text) {
_inFlight = true;
_setSendBusy(true);
appendSystemMessage(`you: ${text}`);
appendChatMessage('you', `you: ${text}`, null, 'timmy');
// Attach Nostr token if available
const nostrToken = await getOrRefreshToken('/api');
@@ -159,7 +159,7 @@ export async function sessionSendHandler(text) {
const reply = data.result || data.reason || '…';
setSpeechBubble(reply);
appendSystemMessage('Timmy: ' + reply.slice(0, 80));
appendChatMessage('Timmy', 'Timmy: ' + reply.slice(0, 80), null, 'timmy');
// Sentiment-driven mood on inbound Timmy reply
sentiment(reply).then(s => {

View File

@@ -138,6 +138,15 @@ export function initUI() {
if (uiInitialized) return;
uiInitialized = true;
initInputBar();
// Restore chat history from localStorage on load
renderChatHistory('timmy');
// Wire up Clear History button
const $clearBtn = document.getElementById('clear-history-btn');
if ($clearBtn) {
$clearBtn.addEventListener('click', () => {
clearChatHistory('timmy');
});
}
}
function initInputBar() {
@@ -159,7 +168,8 @@ function initInputBar() {
if (cls.complexity === 'trivial' && cls.localReply) {
// Greeting / small-talk → answer locally, 0 sats, no network call in any mode
appendSystemMessage(`you: ${text}`);
appendChatMessage('you', `you: ${text}`, null, 'timmy');
appendChatMessage('timmy', `${cls.localReply} ⚡ local`, null, 'timmy');
setSpeechBubble(`${cls.localReply} ⚡ local`);
_showCostPreview('answered locally ⚡ 0 sats', '#44dd88');
setTimeout(_hideCostPreview, 3000);
@@ -184,7 +194,7 @@ function initInputBar() {
// Route to server via WebSocket
sendVisitorMessage(text);
appendSystemMessage(`you: ${text}`);
appendChatMessage('you', `you: ${text}`, null, 'timmy');
}
$sendBtn.addEventListener('click', send);
@@ -226,9 +236,81 @@ export function appendSystemMessage(text) {
}
export function appendChatMessage(agentLabel, message, cssColor, agentId) {
void agentLabel; void cssColor; void agentId;
appendSystemMessage(message);
const id = agentId || 'timmy';
const entry = { agentLabel, message, cssColor, agentId: id, timestamp: Date.now() };
const history = loadChatHistory(id);
history.push(entry);
saveChatHistory(id, history);
_renderChatEntry(entry);
}
export function loadChatHistory() { return []; }
export function saveChatHistory() {}
// ── Chat history persistence (localStorage, 100-msg cap per agent) ───────────
const CHAT_MAX = 100;
function _chatKey(agentId) {
return `matrix:chat:${agentId}`;
}
export function loadChatHistory(agentId) {
try {
const raw = localStorage.getItem(_chatKey(agentId));
if (!raw) return [];
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}
export function saveChatHistory(agentId, messages) {
const capped = messages.length > CHAT_MAX
? messages.slice(messages.length - CHAT_MAX)
: messages;
try {
localStorage.setItem(_chatKey(agentId), JSON.stringify(capped));
} catch { /* storage full — silently drop */ }
}
export function clearChatHistory(agentId) {
localStorage.removeItem(_chatKey(agentId));
// Clear rendered log entries
if ($log) {
while ($log.firstChild) $log.removeChild($log.firstChild);
logEntries.length = 0;
}
}
function _formatTimestamp(ts) {
const d = new Date(ts);
const hh = String(d.getHours()).padStart(2, '0');
const mm = String(d.getMinutes()).padStart(2, '0');
return `${hh}:${mm}`;
}
function _renderChatEntry(entry) {
if (!$log) return;
const el = document.createElement('div');
el.className = 'log-entry';
const time = entry.timestamp ? `[${_formatTimestamp(entry.timestamp)}] ` : '';
el.textContent = `${time}${entry.message}`;
if (entry.cssColor) el.style.color = entry.cssColor;
logEntries.push(el);
if (logEntries.length > MAX_LOG) {
const removed = logEntries.shift();
$log.removeChild(removed);
}
$log.appendChild(el);
$log.scrollTop = $log.scrollHeight;
}
/**
* Render stored chat history for an agent into the event log.
* Call on panel open / page load to restore conversation continuity.
*/
export function renderChatHistory(agentId) {
const history = loadChatHistory(agentId);
for (const entry of history) {
_renderChatEntry(entry);
}
}

View File

@@ -1,5 +1,5 @@
import { setAgentState, setSpeechBubble, applyAgentStates, setMood } from './agents.js';
import { appendSystemMessage } from './ui.js';
import { appendSystemMessage, appendChatMessage } from './ui.js';
import { sentiment } from './edge-worker-client.js';
import { setLabelState } from './hud-labels.js';
@@ -110,9 +110,9 @@ function handleMessage(msg) {
case 'chat': {
if (msg.agentId === 'timmy') {
// Timmy's AI reply: show in speech bubble + event log
// Timmy's AI reply: show in speech bubble + persist to chat history
if (msg.text) setSpeechBubble(msg.text);
appendSystemMessage('Timmy: ' + (msg.text || '').slice(0, 80));
appendChatMessage('Timmy', 'Timmy: ' + (msg.text || '').slice(0, 80), null, 'timmy');
// Sentiment-driven facial expression on inbound Timmy messages
if (msg.text) {
sentiment(msg.text).then(s => {
@@ -122,7 +122,7 @@ function handleMessage(msg) {
}
} else if (msg.agentId === 'visitor') {
// Another visitor's message: event log only (don't hijack the speech bubble)
appendSystemMessage((msg.text || '').slice(0, 80));
appendChatMessage('visitor', (msg.text || '').slice(0, 80), null, 'timmy');
} else {
// System agent messages (delta payment confirmations, etc.): speech bubble
if (msg.text) setSpeechBubble(msg.text);