fix(ui): hover-only footer chrome with timestamps for both user and assistant — v0.50.110 (fixes #680) (#758)

Squash merge of PR #717 — rebased on behalf of @franksong2702.

## What it does

Fixes #680. Footer chrome (timestamps, copy, edit, regenerate) is now hover-only for both user and assistant message rows, consistent throughout the conversation. The last assistant turn keeps cumulative usage visible at rest; timestamp and actions are revealed inline on hover in the same row.

Key changes:
- `static/ui.js`: new `_formatMessageFooterTimestamp()` (local timezone, cross-day fuller format); `timeHtml` no longer gated to user-only; last assistant usage moved from separate `.msg-usage` div to inline `.msg-usage-inline` span in the footer
- `static/style.css`: `.msg-foot-with-usage` class + rules; assistant footer opacity changed from 0.45 to 0 (hover-only); `:focus-within` alongside `:hover` for keyboard users
- `api/streaming.py`: `_restore_reasoning_metadata()` now preserves `_ts`/`timestamp` for unchanged historical messages
- `tests/test_sprint49.py`: 8 new tests covering rendering contract, hover CSS, timestamp preservation

Tests: 1518 passed. QA: 20/20. Browser verified. Reviewed and approved by @nesquena and @aronprins.
This commit is contained in:
nesquena-hermes
2026-04-20 00:53:19 -07:00
committed by GitHub
parent a1c5c395e5
commit 711d8bb6c0
5 changed files with 198 additions and 17 deletions

View File

@@ -619,11 +619,16 @@ def _api_safe_message_positions(messages):
def _restore_reasoning_metadata(previous_messages, updated_messages):
"""Carry forward assistant reasoning metadata lost during API-safe history sanitization.
"""Carry forward display-only metadata lost during API-safe history sanitization.
The provider-facing history strips WebUI-only fields like `reasoning`. When the
agent returns its new full message history, prior assistant messages come back
without that metadata unless we merge it back in by API-history position.
This also preserves existing timestamps for unchanged historical messages.
Without that, older turns that come back from the agent without `_ts` /
`timestamp` can be re-stamped with the current time on every new assistant
response, making prior messages appear to "move" in time.
"""
if not previous_messages or not updated_messages:
return updated_messages
@@ -651,6 +656,10 @@ def _restore_reasoning_metadata(previous_messages, updated_messages):
if isinstance(prev_msg, dict) and isinstance(cur_msg, dict) and _safe_projection(prev_msg) == _safe_projection(cur_msg):
if prev_msg.get('role') == 'assistant' and prev_msg.get('reasoning') and not cur_msg.get('reasoning'):
cur_msg['reasoning'] = prev_msg['reasoning']
if prev_msg.get('timestamp') and not cur_msg.get('timestamp'):
cur_msg['timestamp'] = prev_msg['timestamp']
elif prev_msg.get('_ts') and not cur_msg.get('_ts') and not cur_msg.get('timestamp'):
cur_msg['_ts'] = prev_msg['_ts']
safe_pos += 1
continue