diff --git a/api/agents.py b/api/agents.py index fb131a9..e5867f7 100644 --- a/api/agents.py +++ b/api/agents.py @@ -514,3 +514,86 @@ def get_agent_errors(agent_id: str, limit: int = 20) -> dict: "agent_id": agent_id, "errors": errors, } + + +# ── Chat History ────────────────────────────────────────────────────────────── + +def _get_chat_history(agent_id: str, limit: int = 20) -> list[dict]: + """ + Read chat sessions from JSONL files and return history for a specific agent. + Sessions are sorted newest-first. + Returns list of {session_id, title, message_count, created_at, last_message_at, model}. + """ + sessions_dir = _HERMES_DIR / "sessions" + if not sessions_dir.exists(): + return [] + + sessions = sorted(sessions_dir.glob("*.jsonl"), key=lambda p: p.stat().st_mtime, reverse=True) + history = [] + + for session_file in sessions[:limit * 2]: # overscan + if len(history) >= limit: + break + try: + with open(session_file) as f: + lines = f.readlines() + + if not lines: + continue + + # First line has metadata + metadata = json.loads(lines[0]) + created_at = metadata.get("timestamp", "") + model = metadata.get("model", "unknown") + + # Count messages + message_count = sum(1 for l in lines if l.strip()) + + # Title = first user message preview + title = "Chat" + for line in lines[1:]: + if line.strip(): + try: + msg = json.loads(line) + if msg.get("role") == "user": + content = str(msg.get("content", ""))[:80] + title = content if content else "Chat" + break + except Exception: + pass + + # Last message timestamp + last_msg = None + for line in reversed(lines): + if line.strip(): + try: + last_msg = json.loads(line).get("timestamp", created_at) + break + except Exception: + pass + + session_id = session_file.stem # filename without .jsonl + + history.append({ + "session_id": session_id, + "title": title, + "message_count": message_count, + "created_at": created_at, + "last_message_at": last_msg or created_at, + "model": model, + }) + except Exception: + continue + + return history[:limit] + + +def get_agent_chat_history(agent_id: str, limit: int = 20) -> dict: + """API: GET /api/agents/{id}/chat-history — return chat history for agent.""" + if agent_id not in TIER2_AGENTS and agent_id != "rose": + return {"error": f"Unknown agent: {agent_id}"} + history = _get_chat_history(agent_id, limit) + return { + "agent_id": agent_id, + "sessions": history, + } diff --git a/api/routes.py b/api/routes.py index fad1810..2044d49 100644 --- a/api/routes.py +++ b/api/routes.py @@ -859,6 +859,14 @@ def handle_get(handler, parsed) -> bool: limit = int(parse_qs(parsed.query).get("limit", ["20"])[0]) return j(handler, _agents.get_agent_errors(agent_id, limit=limit)) + # GET /api/agents/{id}/chat-history + if parsed.path.startswith("/api/agents/") and "/chat-history" in parsed.path: + parts = parsed.path.split("/") + if len(parts) == 5 and parts[4] == "chat-history": + agent_id = parts[3] + limit = int(parse_qs(parsed.query).get("limit", ["20"])[0]) + return j(handler, _agents.get_agent_chat_history(agent_id, limit=limit)) + # ── Profile API (GET) ── if parsed.path == "/api/profiles": from api.profiles import list_profiles_api, get_active_profile_name diff --git a/static/panels.js b/static/panels.js index 0f7a2fc..56ef272 100644 --- a/static/panels.js +++ b/static/panels.js @@ -1917,6 +1917,7 @@ async function openAgentDetail(agentId) { +
@@ -1936,7 +1937,7 @@ async function switchAgentTab(tab) { // Update tab buttons document.querySelectorAll('.agent-tab').forEach((el, i) => { - const tabs = ['overview', 'soul', 'memory', 'inbox', 'activity', 'errors']; + const tabs = ['overview', 'soul', 'memory', 'inbox', 'activity', 'errors', 'chat']; el.classList.toggle('active', tabs[i] === tab); }); @@ -1961,6 +1962,9 @@ async function switchAgentTab(tab) { case 'errors': await loadAgentErrors(agentId, content); break; + case 'chat': + await loadAgentChatHistory(agentId, content); + break; } } @@ -2252,6 +2256,51 @@ function toggleInboxMsg(el) { el.classList.toggle('expanded'); } +async function loadAgentChatHistory(agentId, content) { + try { + const data = await api(`/api/agents/${agentId}/chat-history`); + const sessions = data.sessions || []; + + if (sessions.length === 0) { + content.innerHTML = ` +
+
💬
+
No chat history yet
+
Your conversations with ${agentId} appear here
+
`; + return; + } + + const rows = sessions.map(s => { + const created = s.created_at ? new Date(s.created_at).toLocaleString() : 'N/A'; + const rel = s.created_at ? _relTime(s.created_at) : ''; + const model = s.model || 'unknown'; + return ` +
+
${esc(s.title)}
+
+ ${created} · ${rel} + ${esc(model)} + ${s.message_count} msgs +
+
`; + }).join(''); + + content.innerHTML = `
${rows}
`; + + } catch(e) { + content.innerHTML = `
Error: ${esc(e.message)}
`; + } +} + +function openAgentChatSession(agentId, sessionId) { + // Switch to the chat panel and load this session + closeAgentDetail(); + if (typeof switchToChatPanel === 'function') switchToChatPanel(); + if (typeof loadSession === 'function') loadSession(sessionId); + showToast(`Loading chat session...`); +} + // Edit handlers function editAgentSoul(agentId) { document.getElementById('soulView').style.display = 'none'; diff --git a/static/style.css b/static/style.css index 0d84d50..a9cbd9c 100644 --- a/static/style.css +++ b/static/style.css @@ -1630,3 +1630,41 @@ body.resizing{user-select:none;cursor:col-resize;} .error-event-row:hover { background: rgba(244, 67, 54, 0.1); } + +.chat-history-list { + padding: 8px; + display: flex; + flex-direction: column; + gap: 4px; +} + +.chat-history-row { + padding: 10px 12px; + border-radius: 8px; + background: var(--card-bg); + border: 1px solid var(--border); + cursor: pointer; + transition: background 0.15s; +} + +.chat-history-row:hover { + background: var(--row-hover); + border-color: var(--accent); +} + +.chat-history-title { + font-size: 12px; + font-weight: 500; + color: var(--text); + margin-bottom: 4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.chat-history-meta { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +}