Phase 2: Chat History for Agent Tab
Backend: _get_chat_history() reads JSONL sessions. API: GET /api/agents/{id}/chat-history. Frontend: Chat History tab shows session list with title, model, message count. Click to open session in chat panel.
This commit is contained in:
@@ -514,3 +514,86 @@ def get_agent_errors(agent_id: str, limit: int = 20) -> dict:
|
|||||||
"agent_id": agent_id,
|
"agent_id": agent_id,
|
||||||
"errors": errors,
|
"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,
|
||||||
|
}
|
||||||
|
|||||||
@@ -859,6 +859,14 @@ def handle_get(handler, parsed) -> bool:
|
|||||||
limit = int(parse_qs(parsed.query).get("limit", ["20"])[0])
|
limit = int(parse_qs(parsed.query).get("limit", ["20"])[0])
|
||||||
return j(handler, _agents.get_agent_errors(agent_id, limit=limit))
|
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) ──
|
# ── Profile API (GET) ──
|
||||||
if parsed.path == "/api/profiles":
|
if parsed.path == "/api/profiles":
|
||||||
from api.profiles import list_profiles_api, get_active_profile_name
|
from api.profiles import list_profiles_api, get_active_profile_name
|
||||||
|
|||||||
@@ -1917,6 +1917,7 @@ async function openAgentDetail(agentId) {
|
|||||||
</button>
|
</button>
|
||||||
<button class="agent-tab${_agentTab==='activity'?' active':''}" onclick="switchAgentTab('activity')">Activity</button>
|
<button class="agent-tab${_agentTab==='activity'?' active':''}" onclick="switchAgentTab('activity')">Activity</button>
|
||||||
<button class="agent-tab${_agentTab==='errors'?' active':''}" onclick="switchAgentTab('errors')">Errors</button>
|
<button class="agent-tab${_agentTab==='errors'?' active':''}" onclick="switchAgentTab('errors')">Errors</button>
|
||||||
|
<button class="agent-tab${_agentTab==='chat'?' active':''}" onclick="switchAgentTab('chat')">Chat History</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="agentTabContent" class="agent-tab-content">
|
<div id="agentTabContent" class="agent-tab-content">
|
||||||
@@ -1936,7 +1937,7 @@ async function switchAgentTab(tab) {
|
|||||||
|
|
||||||
// Update tab buttons
|
// Update tab buttons
|
||||||
document.querySelectorAll('.agent-tab').forEach((el, i) => {
|
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);
|
el.classList.toggle('active', tabs[i] === tab);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1961,6 +1962,9 @@ async function switchAgentTab(tab) {
|
|||||||
case 'errors':
|
case 'errors':
|
||||||
await loadAgentErrors(agentId, content);
|
await loadAgentErrors(agentId, content);
|
||||||
break;
|
break;
|
||||||
|
case 'chat':
|
||||||
|
await loadAgentChatHistory(agentId, content);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2252,6 +2256,51 @@ function toggleInboxMsg(el) {
|
|||||||
el.classList.toggle('expanded');
|
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 = `
|
||||||
|
<div style="padding:24px;text-align:center;color:var(--muted);font-size:12px">
|
||||||
|
<div style="font-size:28px;margin-bottom:8px">💬</div>
|
||||||
|
<div>No chat history yet</div>
|
||||||
|
<div style="font-size:10px;margin-top:4px;opacity:0.6">Your conversations with ${agentId} appear here</div>
|
||||||
|
</div>`;
|
||||||
|
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 `
|
||||||
|
<div class="chat-history-row" onclick="openAgentChatSession('${agentId}','${s.session_id}')">
|
||||||
|
<div class="chat-history-title">${esc(s.title)}</div>
|
||||||
|
<div class="chat-history-meta">
|
||||||
|
<span style="font-size:10px;color:var(--muted)">${created} · ${rel}</span>
|
||||||
|
<span style="font-size:9px;padding:1px 5px;background:var(--code-bg);border-radius:4px;color:var(--muted);font-family:monospace">${esc(model)}</span>
|
||||||
|
<span style="font-size:9px;color:var(--muted)">${s.message_count} msgs</span>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
content.innerHTML = `<div class="chat-history-list">${rows}</div>`;
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
content.innerHTML = `<div style="padding:12px;color:var(--accent);font-size:12px">Error: ${esc(e.message)}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// Edit handlers
|
||||||
function editAgentSoul(agentId) {
|
function editAgentSoul(agentId) {
|
||||||
document.getElementById('soulView').style.display = 'none';
|
document.getElementById('soulView').style.display = 'none';
|
||||||
|
|||||||
@@ -1630,3 +1630,41 @@ body.resizing{user-select:none;cursor:col-resize;}
|
|||||||
.error-event-row:hover {
|
.error-event-row:hover {
|
||||||
background: rgba(244, 67, 54, 0.1);
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user