diff --git a/the-matrix/index.html b/the-matrix/index.html
index 52cd6d0..27f3b9f 100644
--- a/the-matrix/index.html
+++ b/the-matrix/index.html
@@ -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 @@
OFFLINE
+
diff --git a/the-matrix/js/session.js b/the-matrix/js/session.js
index a52cbb9..49c1cd5 100644
--- a/the-matrix/js/session.js
+++ b/the-matrix/js/session.js
@@ -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 => {
diff --git a/the-matrix/js/ui.js b/the-matrix/js/ui.js
index b646057..fd8ccc3 100644
--- a/the-matrix/js/ui.js
+++ b/the-matrix/js/ui.js
@@ -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);
+ }
+}
diff --git a/the-matrix/js/websocket.js b/the-matrix/js/websocket.js
index 8ef4a04..0cdcb86 100644
--- a/the-matrix/js/websocket.js
+++ b/the-matrix/js/websocket.js
@@ -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);