fix: restore real-time chat responses via WebSocket (#98)
The chat WebSocket return path was broken by two bugs that prevented
Timmy's responses from appearing in the live chat feed:
1. Frontend checked msg.type instead of msg.event for 'timmy_response'
events — the WSEvent dataclass uses 'event' as the field name.
2. Frontend accessed msg.response instead of msg.data.response — the
response payload is nested in the data field.
Additional fixes:
- Queue acknowledgment ("Message queued...") no longer logged as an
agent message in chat history; the real response is logged by the
task processor when it completes, eliminating duplicate messages.
- Chat message template now carries data-task-id so the WS handler
can find and replace the placeholder with the actual response.
- appendMessage() uses DOM APIs (textContent) instead of innerHTML
for safer content insertion before markdown rendering.
- Fixed chat_message.html script targeting when queue-status div is
present between the agent message and the inline script.
https://claude.ai/code/session_011cJfexqBBuGhSRQU8qwKcR
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
d4acaefee9
commit
2e92838033
@@ -176,6 +176,15 @@ async def _task_processor_loop() -> None:
|
||||
context = f"[System: Current date/time is {now.strftime('%A, %B %d, %Y at %I:%M %p')}]\n\n"
|
||||
response = timmy_chat(context + task.description)
|
||||
|
||||
# Log the real agent response to chat history
|
||||
try:
|
||||
from dashboard.store import message_log
|
||||
timestamp = now.strftime("%H:%M:%S")
|
||||
message_log.append(role="agent", content=response, timestamp=timestamp)
|
||||
except Exception as e:
|
||||
logger.debug("Failed to log response to message_log: %s", e)
|
||||
|
||||
# Push response to chat UI via WebSocket
|
||||
try:
|
||||
from infrastructure.ws_manager.handler import ws_manager
|
||||
asyncio.create_task(
|
||||
|
||||
@@ -316,13 +316,16 @@ async def chat_timmy(request: Request, message: str = Form(...)):
|
||||
logger.error("Failed to queue chat message: %s", exc)
|
||||
error_text = f"Failed to queue message: {exc}"
|
||||
|
||||
# Log to message history (for context, even though async)
|
||||
# Log user message to history. For chat_response tasks the real agent
|
||||
# reply is logged by the task processor when it completes, so we only
|
||||
# log the queue acknowledgment for explicit task_request commands.
|
||||
message_log.append(role="user", content=message, timestamp=timestamp)
|
||||
if response_text is not None:
|
||||
if task_info and response_text is not None:
|
||||
# Explicit task queue command — the acknowledgment IS the response
|
||||
message_log.append(role="agent", content=response_text, timestamp=timestamp)
|
||||
else:
|
||||
elif error_text:
|
||||
message_log.append(
|
||||
role="error", content=error_text or "Unknown error", timestamp=timestamp
|
||||
role="error", content=error_text, timestamp=timestamp
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="msg-body">{{ user_message | e }}</div>
|
||||
</div>
|
||||
{% if response %}
|
||||
<div class="chat-message agent">
|
||||
<div class="chat-message agent"{% if task_id %} data-task-id="{{ task_id }}"{% endif %}>
|
||||
<div class="msg-meta">TIMMY // {{ timestamp }}</div>
|
||||
<div class="msg-body timmy-md">{{ response | e }}</div>
|
||||
</div>
|
||||
@@ -14,7 +14,11 @@
|
||||
{% endif %}
|
||||
<script>
|
||||
(function() {
|
||||
var el = document.currentScript.previousElementSibling.querySelector('.timmy-md');
|
||||
var script = document.currentScript;
|
||||
var prev = script.previousElementSibling;
|
||||
// Skip queue-status div to find the agent message div
|
||||
if (prev && prev.classList.contains('queue-status')) prev = prev.previousElementSibling;
|
||||
var el = prev ? prev.querySelector('.timmy-md') : null;
|
||||
if (el && typeof marked !== 'undefined' && typeof DOMPurify !== 'undefined') {
|
||||
el.innerHTML = DOMPurify.sanitize(marked.parse(el.textContent));
|
||||
if (typeof hljs !== 'undefined') {
|
||||
|
||||
@@ -118,15 +118,48 @@
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function appendMessage(role, content, timestamp) {
|
||||
function appendMessage(role, content, timestamp, taskId) {
|
||||
// If this is a response for a queued task, replace the placeholder
|
||||
if (taskId) {
|
||||
var placeholder = chatLog.querySelector('.chat-message.agent[data-task-id="' + taskId + '"]');
|
||||
if (placeholder) {
|
||||
placeholder.removeAttribute('data-task-id');
|
||||
var body = placeholder.querySelector('.msg-body');
|
||||
if (body) {
|
||||
body.textContent = content;
|
||||
body.className = 'msg-body timmy-md';
|
||||
if (typeof marked !== 'undefined' && typeof DOMPurify !== 'undefined') {
|
||||
body.innerHTML = DOMPurify.sanitize(marked.parse(body.textContent));
|
||||
if (typeof hljs !== 'undefined') {
|
||||
body.querySelectorAll('pre code').forEach(function(block) { hljs.highlightElement(block); });
|
||||
}
|
||||
}
|
||||
}
|
||||
placeholder.querySelector('.msg-meta').textContent = 'TIMMY // ' + timestamp;
|
||||
// Remove queue-status indicator if present
|
||||
var qs = placeholder.nextElementSibling;
|
||||
if (qs && qs.classList.contains('queue-status')) qs.remove();
|
||||
chatLog.scrollTop = chatLog.scrollHeight;
|
||||
return;
|
||||
}
|
||||
}
|
||||
var div = document.createElement('div');
|
||||
div.className = 'chat-message ' + role;
|
||||
div.innerHTML = '<div class="msg-meta">' + (role === 'user' ? 'YOU' : 'TIMMY') + ' // ' + timestamp + '</div><div class="msg-body timmy-md">' + content.replace(/</g, '<').replace(/>/g, '>') + '</div>';
|
||||
var meta = document.createElement('div');
|
||||
meta.className = 'msg-meta';
|
||||
meta.textContent = (role === 'user' ? 'YOU' : 'TIMMY') + ' // ' + timestamp;
|
||||
var body = document.createElement('div');
|
||||
body.className = 'msg-body timmy-md';
|
||||
body.textContent = content;
|
||||
div.appendChild(meta);
|
||||
div.appendChild(body);
|
||||
chatLog.appendChild(div);
|
||||
// Render markdown if available
|
||||
if (typeof marked !== 'undefined' && typeof DOMPurify !== 'undefined') {
|
||||
var md = div.querySelector('.timmy-md');
|
||||
md.innerHTML = DOMPurify.sanitize(marked.parse(md.textContent));
|
||||
body.innerHTML = DOMPurify.sanitize(marked.parse(body.textContent));
|
||||
if (typeof hljs !== 'undefined') {
|
||||
body.querySelectorAll('pre code').forEach(function(block) { hljs.highlightElement(block); });
|
||||
}
|
||||
}
|
||||
// Scroll to bottom
|
||||
chatLog.scrollTop = chatLog.scrollHeight;
|
||||
@@ -146,11 +179,12 @@
|
||||
} else if (msg.event === 'task_created' || msg.event === 'task_completed' ||
|
||||
msg.event === 'task_approved') {
|
||||
fetchStatus();
|
||||
} else if (msg.type === 'timmy_response') {
|
||||
// Timmy pushed a response!
|
||||
} else if (msg.event === 'timmy_response' && msg.data) {
|
||||
// Timmy pushed a response via task processor
|
||||
var now = new Date();
|
||||
var ts = now.getHours().toString().padStart(2,'0') + ':' + now.getMinutes().toString().padStart(2,'0');
|
||||
appendMessage('agent', msg.response, ts);
|
||||
var ts = now.getHours().toString().padStart(2,'0') + ':' + now.getMinutes().toString().padStart(2,'0') + ':' + now.getSeconds().toString().padStart(2,'0');
|
||||
appendMessage('agent', msg.data.response, ts, msg.data.task_id);
|
||||
fetchStatus();
|
||||
}
|
||||
} catch(e) {}
|
||||
};
|
||||
|
||||
@@ -143,18 +143,17 @@ def test_history_records_user_and_agent_messages(client):
|
||||
|
||||
response = client.get("/agents/timmy/history")
|
||||
assert "status check" in response.text
|
||||
# In async mode, it records the "Message queued" response
|
||||
assert "Message queued" in response.text
|
||||
# Queue acknowledgment is NOT logged as an agent message; the real
|
||||
# agent response is logged later by the task processor.
|
||||
|
||||
|
||||
def test_history_records_error_when_offline(client):
|
||||
# In async mode, errors during queuing are rare;
|
||||
# if queuing succeeds, it records "Message queued".
|
||||
# In async mode, if queuing succeeds the user message is recorded
|
||||
# and the actual response is logged later by the task processor.
|
||||
client.post("/agents/timmy/chat", data={"message": "ping"})
|
||||
|
||||
response = client.get("/agents/timmy/history")
|
||||
assert "ping" in response.text
|
||||
assert "Message queued" in response.text
|
||||
|
||||
|
||||
def test_history_clear_resets_to_init_message(client):
|
||||
|
||||
Reference in New Issue
Block a user