feat(cli): /timestamps command + timestamps in /history (#50506)

display.timestamps already drove the [HH:MM] suffix on live submitted and
streamed message labels, but there was no runtime command to toggle it and
/history ignored the setting entirely. Add /timestamps [on|off|status]
(alias /ts) and render [HH:MM] in /history for turns that carry a stored
unix timestamp (resumed sessions). Live unsaved turns without a stored time
are never given a fabricated one. Uses the existing sanctioned non-wire
'timestamp' message key (stripped before the API call in chat_completions),
so message-alternation and prompt-cache invariants are untouched.
This commit is contained in:
Teknium 2026-06-21 22:44:25 -07:00 committed by GitHub
parent b9b4756ab4
commit 5ff11a689b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 171 additions and 2 deletions

22
cli.py
View file

@ -6216,6 +6216,22 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
preview_limit = 400
visible_index = 0
hidden_tool_messages = 0
show_ts = bool(getattr(self, "show_timestamps", False))
def _ts_suffix(message: dict) -> str:
# Messages restored from SessionDB carry a unix `timestamp`; live
# unsaved turns may not. Only annotate when both the toggle is on
# and the turn actually has a stored time — never fabricate one.
if not show_ts:
return ""
ts = message.get("timestamp")
if not ts:
return ""
try:
from datetime import datetime
return f" [{datetime.fromtimestamp(float(ts)).strftime('%H:%M')}]"
except (ValueError, OSError, TypeError):
return ""
def flush_tool_summary():
nonlocal hidden_tool_messages
@ -6249,13 +6265,13 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
content_text = "" if content is None else str(content)
if role == "user":
print(f"\n [You #{visible_index}]")
print(f"\n [You #{visible_index}]{_ts_suffix(msg)}")
print(
f" {content_text[:preview_limit]}{'...' if len(content_text) > preview_limit else ''}"
)
continue
print(f"\n [Hermes #{visible_index}]")
print(f"\n [Hermes #{visible_index}]{_ts_suffix(msg)}")
tool_calls = msg.get("tool_calls") or []
if content_text:
preview = content_text[:preview_limit]
@ -7978,6 +7994,8 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
self._status_bar_visible = not self._status_bar_visible
state = "visible" if self._status_bar_visible else "hidden"
self._console_print(f" Status bar {state}")
elif canonical == "timestamps":
self._handle_timestamps_command(cmd_original)
elif canonical == "verbose":
self._toggle_verbose()
elif canonical == "footer":

View file

@ -2086,6 +2086,56 @@ class CLICommandsMixin:
else:
_cprint(" Failed to save runtime_footer setting to config.yaml")
def _handle_timestamps_command(self, cmd_original: str) -> None:
"""Toggle or inspect ``display.timestamps`` from the CLI.
When on, submitted and streamed message labels carry an ``[HH:MM]``
suffix and ``/history`` prefixes each turn with its time (for turns
that carry a stored timestamp).
Usage:
/timestamps toggle
/timestamps on|off explicit
/timestamps status show current state
"""
from cli import _cprint, save_config_value
from hermes_cli.colors import Colors as _Colors
arg = ""
try:
parts = (cmd_original or "").strip().split(None, 1)
if len(parts) > 1:
arg = parts[1].strip().lower()
except Exception:
arg = ""
current = bool(getattr(self, "show_timestamps", False))
if arg in {"status", "?"}:
state = "ON" if current else "OFF"
_cprint(f" {_Colors.BOLD}Message timestamps:{_Colors.RESET} {state}")
return
if arg in {"on", "enable", "true", "1"}:
new_state = True
elif arg in {"off", "disable", "false", "0"}:
new_state = False
elif arg == "":
new_state = not current
else:
_cprint(" Usage: /timestamps [on|off|status]")
return
self.show_timestamps = new_state
if save_config_value("display.timestamps", new_state):
state = (
f"{_Colors.GREEN}ON{_Colors.RESET}" if new_state
else f"{_Colors.DIM}OFF{_Colors.RESET}"
)
_cprint(f" Message timestamps: {state}")
else:
_cprint(" Failed to save timestamps setting to config.yaml")
def _handle_reasoning_command(self, cmd: str):
"""Handle /reasoning — manage effort level and display toggle.

View file

@ -135,6 +135,9 @@ COMMAND_REGISTRY: list[CommandDef] = [
args_hint="[name]"),
CommandDef("statusbar", "Toggle the context/model status bar", "Configuration",
cli_only=True, aliases=("sb",)),
CommandDef("timestamps", "Toggle [HH:MM] timestamps on messages and /history", "Configuration",
cli_only=True, args_hint="[on|off|status]",
subcommands=("on", "off", "status"), aliases=("ts",)),
CommandDef("verbose", "Cycle tool progress display: off -> new -> all -> verbose",
"Configuration", cli_only=True,
gateway_config_gate="display.tool_progress_command"),

View file

@ -0,0 +1,98 @@
"""Tests for the CLI `/timestamps` toggle and timestamps in `/history`.
`display.timestamps` already drove the live `[HH:MM]` label suffix on
submitted/streamed messages but had no runtime toggle and `/history`
ignored it. These assert the new `/timestamps` command flips and persists
the flag and that `/history` renders `[HH:MM]` only for turns that carry a
stored unix `timestamp` (never fabricating one for live unsaved turns).
"""
import io
import sys
import time
from datetime import datetime
import yaml
from hermes_cli.cli_commands_mixin import CLICommandsMixin
class _Stub(CLICommandsMixin):
def __init__(self):
self.show_timestamps = False
def _seed(tmp_path, monkeypatch, value=False):
hh = tmp_path / ".hermes"
hh.mkdir()
(hh / "config.yaml").write_text(f"display:\n timestamps: {str(value).lower()}\n")
monkeypatch.setenv("HERMES_HOME", str(hh))
import cli
monkeypatch.setattr(cli, "_hermes_home", hh, raising=False)
return hh
def test_timestamps_on_sets_and_persists(tmp_path, monkeypatch):
hh = _seed(tmp_path, monkeypatch)
s = _Stub()
s._handle_timestamps_command("/timestamps on")
assert s.show_timestamps is True
assert yaml.safe_load((hh / "config.yaml").read_text())["display"]["timestamps"] is True
def test_timestamps_bare_toggles(tmp_path, monkeypatch):
_seed(tmp_path, monkeypatch)
s = _Stub()
s.show_timestamps = True
s._handle_timestamps_command("/timestamps")
assert s.show_timestamps is False
def test_timestamps_status_is_noop(tmp_path, monkeypatch):
_seed(tmp_path, monkeypatch)
s = _Stub()
s.show_timestamps = True
s._handle_timestamps_command("/timestamps status")
assert s.show_timestamps is True
def _render_history(history, show_ts):
from cli import HermesCLI
h = HermesCLI.__new__(HermesCLI)
h.show_timestamps = show_ts
h.conversation_history = history
h._show_recent_sessions = lambda reason="history", limit=10: True
buf = io.StringIO()
old = sys.stdout
sys.stdout = buf
try:
h.show_history()
finally:
sys.stdout = old
return buf.getvalue()
def test_history_shows_timestamp_for_stored_turns():
ts = time.time()
hist = [
{"role": "user", "content": "hello", "timestamp": ts},
{"role": "assistant", "content": "hi", "timestamp": ts + 60},
{"role": "user", "content": "live turn, no ts"},
]
out = _render_history(hist, show_ts=True)
hhmm = datetime.fromtimestamp(ts).strftime("%H:%M")
assert f"[You #1] [{hhmm}]" in out
assert "[Hermes #2] [" in out
# a turn with no stored timestamp must NOT get a fabricated time
assert "[You #3]\n" in out
def test_history_hides_timestamps_when_off():
ts = time.time()
hist = [{"role": "user", "content": "hello", "timestamp": ts}]
out = _render_history(hist, show_ts=False)
# label present, no [HH:MM] suffix
first_label_line = out.split("[You #1]")[1].split("\n")[0]
assert "[" not in first_label_line