feat: persistent chat history with clear button

- Add dashboard/store.py: MessageLog dataclass singleton tracking
  user/agent/error messages for the lifetime of the server process
- agents.py: write each chat turn to MessageLog; add GET and DELETE
  /agents/timmy/history routes returning the history.html partial
- partials/history.html: render stored messages by role (YOU / TIMMY /
  SYSTEM); falls back to the Mission Control init message when empty
- index.html: chat-log loads history via hx-get on page start; new
  CLEAR button in panel header sends hx-delete to reset the log
- style.css: add .mc-btn-clear (muted, red-on-hover for the header)
- tests: autouse reset_message_log fixture in conftest; 5 new history
  tests covering empty state, recording, offline errors, clear, and
  post-clear state → 32 tests total, all passing

https://claude.ai/code/session_01KZMfwBpLuiv6x9GbzTqbys
This commit is contained in:
Claude
2026-02-20 14:00:16 +00:00
parent c9ac2d9d17
commit 0d14be291a
7 changed files with 173 additions and 7 deletions

View File

@@ -6,6 +6,7 @@ from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from timmy.agent import create_timmy
from dashboard.store import message_log
router = APIRouter(prefix="/agents", tags=["agents"])
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
@@ -27,6 +28,25 @@ async def list_agents():
return {"agents": list(AGENT_REGISTRY.values())}
@router.get("/timmy/history", response_class=HTMLResponse)
async def get_history(request: Request):
return templates.TemplateResponse(
request,
"partials/history.html",
{"messages": message_log.all()},
)
@router.delete("/timmy/history", response_class=HTMLResponse)
async def clear_history(request: Request):
message_log.clear()
return templates.TemplateResponse(
request,
"partials/history.html",
{"messages": []},
)
@router.post("/timmy/chat", response_class=HTMLResponse)
async def chat_timmy(request: Request, message: str = Form(...)):
timestamp = datetime.now().strftime("%H:%M:%S")
@@ -40,6 +60,12 @@ async def chat_timmy(request: Request, message: str = Form(...)):
except Exception as exc:
error_text = f"Timmy is offline: {exc}"
message_log.append(role="user", content=message, timestamp=timestamp)
if response_text is not None:
message_log.append(role="agent", content=response_text, timestamp=timestamp)
else:
message_log.append(role="error", content=error_text, timestamp=timestamp)
return templates.TemplateResponse(
request,
"partials/chat_message.html",

31
src/dashboard/store.py Normal file
View File

@@ -0,0 +1,31 @@
from dataclasses import dataclass, field
@dataclass
class Message:
role: str # "user" | "agent" | "error"
content: str
timestamp: str
class MessageLog:
"""In-memory chat history for the lifetime of the server process."""
def __init__(self) -> None:
self._entries: list[Message] = []
def append(self, role: str, content: str, timestamp: str) -> None:
self._entries.append(Message(role=role, content=content, timestamp=timestamp))
def all(self) -> list[Message]:
return list(self._entries)
def clear(self) -> None:
self._entries.clear()
def __len__(self) -> int:
return len(self._entries)
# Module-level singleton shared across the app
message_log = MessageLog()

View File

@@ -46,15 +46,20 @@
<!-- Chat Panel -->
<div class="col-12 col-md-9 d-flex flex-column mc-chat-panel">
<div class="card mc-panel flex-grow-1 d-flex flex-column min-h-0">
<div class="card-header mc-panel-header">// TIMMY INTERFACE</div>
<div class="chat-log flex-grow-1 overflow-auto p-3" id="chat-log">
<div class="chat-message agent">
<div class="msg-meta">TIMMY // SYSTEM</div>
<div class="msg-body">Mission Control initialized. Timmy ready — awaiting input.</div>
</div>
<div class="card-header mc-panel-header d-flex justify-content-between align-items-center">
<span>// TIMMY INTERFACE</span>
<button class="mc-btn-clear"
hx-delete="/agents/timmy/history"
hx-target="#chat-log"
hx-swap="innerHTML"
hx-confirm="Clear conversation history?">CLEAR</button>
</div>
<div class="chat-log flex-grow-1 overflow-auto p-3" id="chat-log"
hx-get="/agents/timmy/history"
hx-trigger="load"
hx-swap="innerHTML"></div>
<div class="card-footer mc-chat-footer">
<form hx-post="/agents/timmy/chat"
hx-target="#chat-log"

View File

@@ -0,0 +1,25 @@
{% if messages %}
{% for msg in messages %}
{% if msg.role == "user" %}
<div class="chat-message user">
<div class="msg-meta">YOU // {{ msg.timestamp }}</div>
<div class="msg-body">{{ msg.content }}</div>
</div>
{% elif msg.role == "agent" %}
<div class="chat-message agent">
<div class="msg-meta">TIMMY // {{ msg.timestamp }}</div>
<div class="msg-body">{{ msg.content }}</div>
</div>
{% else %}
<div class="chat-message error-msg">
<div class="msg-meta">SYSTEM // {{ msg.timestamp }}</div>
<div class="msg-body">{{ msg.content }}</div>
</div>
{% endif %}
{% endfor %}
{% else %}
<div class="chat-message agent">
<div class="msg-meta">TIMMY // SYSTEM</div>
<div class="msg-body">Mission Control initialized. Timmy ready — awaiting input.</div>
</div>
{% endif %}