Skip to content

Commit 0fab865

Browse files
committed
FEAT: display message timestamps; use server-side timestamps for displayed image timestamps
1 parent 02b7fb4 commit 0fab865

10 files changed

Lines changed: 83 additions & 34 deletions

File tree

packages/eaa-core/src/eaa_core/gui/runtime.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,11 +350,18 @@ def publish_message(self, message: dict[str, Any], conversation_id: str = "prima
350350
if "id" not in payload:
351351
self.message_event_counter += 1
352352
payload["id"] = f"runtime-{self.message_event_counter}"
353+
if "timestamp" not in payload:
354+
payload["timestamp"] = self.runtime_timestamp()
353355
conversation.messages.append(payload)
354356
if conversation_id == "primary":
355357
self.messages.append(payload)
356358
self.publish("message.created", {"conversation_id": conversation_id, "message": payload})
357359

360+
@staticmethod
361+
def runtime_timestamp() -> str:
362+
"""Return an ISO timestamp for runtime-owned display events."""
363+
return datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
364+
358365
def publish_log(
359366
self,
360367
message: str,

packages/eaa-core/src/eaa_core/gui/static/webui/assets/index-2rUbQMUs.js

Lines changed: 0 additions & 23 deletions
This file was deleted.

packages/eaa-core/src/eaa_core/gui/static/webui/assets/index-B3HrSWD9.css

Lines changed: 0 additions & 1 deletion
This file was deleted.

packages/eaa-core/src/eaa_core/gui/static/webui/assets/index-CpwVrxc6.js

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/eaa-core/src/eaa_core/gui/static/webui/assets/index-zUGK0yol.css

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/eaa-core/src/eaa_core/gui/static/webui/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<title>EAA WebUI</title>
7-
<script type="module" crossorigin src="/static/webui/assets/index-2rUbQMUs.js"></script>
8-
<link rel="stylesheet" crossorigin href="/static/webui/assets/index-B3HrSWD9.css">
7+
<script type="module" crossorigin src="/static/webui/assets/index-CpwVrxc6.js"></script>
8+
<link rel="stylesheet" crossorigin href="/static/webui/assets/index-zUGK0yol.css">
99
</head>
1010
<body>
1111
<div id="root"></div>

packages/eaa-core/webui/src/App.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,6 @@ const imagePathTitle = (pathOrSource: unknown) => {
166166
return name || null;
167167
};
168168

169-
const currentImageTimeTitle = () => new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
170-
171169
const isApprovalMessage = (message: WebUIMessage) =>
172170
String(message.role ?? "") === "system" && /Approve\?\s*\[y\/N\]:/i.test(String(message.content ?? ""));
173171

@@ -221,6 +219,11 @@ const formatLogTime = (timestamp: string) => {
221219
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
222220
};
223221

222+
const imageTimestampTitle = (message: WebUIMessage) => {
223+
if (!message.timestamp) return null;
224+
return formatLogTime(message.timestamp) || null;
225+
};
226+
224227
function MarkdownBlock({ content, role }: { content: unknown; role: string }) {
225228
const [expanded, setExpanded] = useState(false);
226229
const lines = String(content ?? "").replace(/\r\n/g, "\n").split("\n");
@@ -296,6 +299,7 @@ function MessageView({
296299
return requestedAt + message.approval_timeout_seconds * 1000;
297300
}, [message.approval_expires_at, message.approval_requested_at, message.approval_timeout_seconds]);
298301
const approvalExpired = approvalRemainingMs !== null && approvalRemainingMs <= 0;
302+
const messageTime = message.timestamp ? formatLogTime(message.timestamp) : "";
299303
const imageSources = useMemo(() => {
300304
const sources: string[] = [];
301305
const seen = new Set<string>();
@@ -341,6 +345,11 @@ function MessageView({
341345
<div className="eaa-message-meta">
342346
<div className={`eaa-avatar eaa-avatar-${role}`}>{roleAvatarLetter(role)}</div>
343347
<div className="eaa-role">{roleLabel(role)}</div>
348+
{messageTime ? (
349+
<time className="eaa-message-time" dateTime={message.timestamp}>
350+
{messageTime}
351+
</time>
352+
) : null}
344353
</div>
345354
<div className="eaa-message-body">
346355
{content ? (
@@ -584,7 +593,6 @@ function App() {
584593
const inputRef = useRef<HTMLTextAreaElement>(null);
585594
const renderedIdsRef = useRef<Set<string>>(new Set());
586595
const pendingMessagesRef = useRef<Map<string, string>>(new Map());
587-
const imageFallbackTitlesRef = useRef<Map<string, string>>(new Map());
588596
const closedConversationIdsRef = useRef<Set<string>>(new Set());
589597
const infoTimeoutRef = useRef<number | null>(null);
590598

@@ -652,12 +660,9 @@ function App() {
652660
if (seen.has(source)) return;
653661
seen.add(source);
654662
const pathTitle = imagePathTitle(image);
655-
if (!pathTitle && !imageFallbackTitlesRef.current.has(source)) {
656-
imageFallbackTitlesRef.current.set(source, currentImageTimeTitle());
657-
}
658663
items.push({
659664
source,
660-
title: pathTitle ?? imageFallbackTitlesRef.current.get(source) ?? currentImageTimeTitle(),
665+
title: pathTitle ?? imageTimestampTitle(message) ?? "Image",
661666
messageDomId: messageDomId(activeConversationId, message, index),
662667
});
663668
};

packages/eaa-core/webui/src/styles.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,13 @@ button {
363363
font-weight: 750;
364364
}
365365

366+
.eaa-message-time {
367+
color: #7b8494;
368+
font-size: 12px;
369+
font-weight: 500;
370+
line-height: 1;
371+
}
372+
366373
.eaa-message-body {
367374
background: #f8fafc;
368375
border: 1px solid #dfe6f0;

packages/eaa-core/webui/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export type WebUIMessage = {
2525
content?: string;
2626
image?: string;
2727
images?: string[];
28+
timestamp?: string;
2829
tool_calls?: unknown;
2930
pending?: boolean;
3031
approval_id?: string;

tests/test_webui_chat.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import sqlite3
22
import time
33
from concurrent.futures import ThreadPoolExecutor
4+
from datetime import datetime
45
from fastapi.testclient import TestClient
56
from fastapi.responses import Response
67

@@ -17,6 +18,11 @@
1718
from eaa_core.tool.base import BaseTool, tool
1819

1920

21+
def assert_runtime_timestamp(timestamp):
22+
parsed = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
23+
assert parsed.tzinfo is not None
24+
25+
2026
def test_parse_images_field_single_data_url():
2127
image = "data:image/png;base64,AAA"
2228
assert parse_persisted_images(image) == [image]
@@ -114,6 +120,24 @@ def test_runtime_controller_publishes_display_logs(tmp_path):
114120
assert snapshot["logs"] == [second_event.payload["log"]]
115121

116122

123+
def test_runtime_controller_stores_message_timestamp(tmp_path):
124+
task_manager = BaseTaskManager(build=False)
125+
controller = WebUIRuntimeController(task_manager)
126+
subscriber = controller.subscribe()
127+
128+
controller.publish_message(
129+
{
130+
"role": "assistant",
131+
"content": "Text result",
132+
}
133+
)
134+
135+
event_message = subscriber.get_nowait().payload["message"]
136+
snapshot_message = controller.snapshot()["messages"][0]
137+
assert_runtime_timestamp(event_message["timestamp"])
138+
assert snapshot_message["timestamp"] == event_message["timestamp"]
139+
140+
117141
def test_task_manager_registers_tools_with_runtime(tmp_path):
118142
class RuntimeAwareTool(BaseTool):
119143
def build(self):
@@ -452,7 +476,10 @@ def test_runtime_state_uses_live_messages_not_transcript_db(tmp_path):
452476

453477
controller.publish_message({"role": "system", "content": "live only"})
454478

455-
assert client.get("/api/state").json()["messages"] == [
479+
messages = client.get("/api/state").json()["messages"]
480+
timestamp = messages[0].pop("timestamp")
481+
assert_runtime_timestamp(timestamp)
482+
assert messages == [
456483
{"role": "system", "content": "live only", "id": "runtime-1"}
457484
]
458485

@@ -480,6 +507,8 @@ def test_runtime_publish_normalizes_structured_content_for_display(tmp_path):
480507

481508
assert task_manager.runtime_controller is not None
482509
messages = task_manager.runtime_controller.snapshot()["messages"]
510+
timestamp = messages[0].pop("timestamp")
511+
assert_runtime_timestamp(timestamp)
483512
assert messages == [
484513
{
485514
"role": "system",

0 commit comments

Comments
 (0)