From c21498bb376c7766a7386795653cebd767d9cc14 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 2 Sep 2026 20:19:53 -0700 Subject: [PATCH] refactor(hermes_cli): split HTML export into sidebar/session-view helpers, compact message renderer --- hermes_cli/session_export_html.py | 184 ++++++++++++------------------ 1 file changed, 73 insertions(+), 111 deletions(-) diff --git a/hermes_cli/session_export_html.py b/hermes_cli/session_export_html.py index 6e05856267..bfd6dee1c4 100644 --- a/hermes_cli/session_export_html.py +++ b/hermes_cli/session_export_html.py @@ -1,7 +1,4 @@ -"""HTML Export generator for Hermes sessions. Generates a standalone, beautiful HTML file with all -messages embedded. Supports single and multi-session exports with a professional sidebar. No remote -dependencies. Enhanced with UI-UX-PRO-MAX design intelligence. -""" +"""Standalone single-file HTML export (single or multi-session with sidebar); no remote deps.""" import datetime import secrets @@ -645,14 +642,16 @@ HTML_TEMPLATE = """ """ -def _escape_html(text: str) -> str: - if not isinstance(text, str): - text = str(text) - return text.replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """).replace("'", "'") +def _escape_html(text: Any) -> str: + return ( + str(text).replace("&", "&").replace("<", "<").replace(">", ">") + .replace('"', """).replace("'", "'") + ) + def _format_timestamp(ts: float) -> str: - if not ts: return "N/A" - return datetime.datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S") + return datetime.datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S") if ts else "N/A" + _ROLE_ICONS = {"user": ICON_USER, "assistant": ICON_BOT, "system": ICON_SHIELD} _CHEVRON_HTML = ICON_CHEVRON_RIGHT.replace('class="', 'class="chevron ') @@ -675,7 +674,7 @@ def _content_text(content: Any) -> str: def _collapsible(kind: str, icon: str, title: str, inner: str, indent: str, outer_class: str = "") -> str: """A chevron-headed collapsible block (tool-call / reasoning / system-prompt).""" - return f''' + return f""" {indent}
{indent}
{indent} {_CHEVRON_HTML} @@ -685,98 +684,66 @@ def _collapsible(kind: str, icon: str, title: str, inner: str, indent: str, oute {indent} {inner} {indent}
{indent}
-{indent}''' +{indent}""" def _generate_messages_html(messages: List[Dict[str, Any]]) -> str: html_list = [] for i, msg in enumerate(messages): role = msg.get("role", "unknown") - - # Skip internal metadata messages - if role == "session_meta": + if role == "session_meta": # internal metadata, never rendered continue - content = _content_text(msg.get("content") or "") - timestamp = _format_timestamp(msg.get("timestamp", 0)) - role_icon = _ROLE_ICONS.get(role, ICON_TERMINAL) - - # Build message HTML. The role feeds two sinks and for tool/MCP messages - # is externally influenced, so treat each sink on its own terms: - # - display text: HTML-escape (prevents markup/JS injection). - # - class attribute: reduce to a single safe CSS token (alnum/-/_), - # so a crafted role can neither break out of the attribute nor split - # into several unintended classes. Real roles (user/assistant/system/ - # tool) are unchanged, so the `.message-` rules still match. - safe_role = _escape_html(role) + # The role feeds two sinks and is externally influenced for tool/MCP messages: HTML-escape + # the display text; reduce the class token to alnum/-/_ so a crafted role can neither break + # out of the attribute nor split into extra classes (real roles keep matching `.message-`). role_class = "".join(c if c.isalnum() or c in "-_" else "-" for c in str(role).lower()) - msg_class = f"message message-{role_class} active" - # Delay animation for initial items - delay_style = f' style="animation-delay: {min(i * 0.05, 1.0)}s"' if i < 10 else "" - - html = f'
' - html += f'
' - html += f'
{_CHEVRON_HTML} {role_icon} {safe_role}
' - html += f'
{timestamp}
' - html += '
' - html += '
' - + delay_style = f' style="animation-delay: {min(i * 0.05, 1.0)}s"' if i < 10 else "" # stagger first items + html = ( + f'
' + f'
' + f'
{_CHEVRON_HTML} {_ROLE_ICONS.get(role, ICON_TERMINAL)} {_escape_html(role)}
' + f'
{_format_timestamp(msg.get("timestamp", 0))}
' + '
' + '
' + ) for tc in msg.get("tool_calls") or (): - fn_name = tc.get("function", {}).get("name", "unknown") - args = tc.get("function", {}).get("arguments", "{}") + fn = tc.get("function", {}) html += _collapsible( - "tool-call", ICON_WRENCH, f"Tool Call: {_escape_html(fn_name)}", - f"
{_escape_html(args)}
", " " * 16, + "tool-call", ICON_WRENCH, f"Tool Call: {_escape_html(fn.get('name', 'unknown'))}", + f"
{_escape_html(fn.get('arguments', '{}'))}
", " " * 16, ) - if content: - if role == "tool": - html += f'
{_escape_html(content)}
' - else: - html += f'
{_escape_html(content)}
' - - reasoning = msg.get("reasoning") or msg.get("reasoning_content") - if reasoning: + escaped = _escape_html(content) + body = f"
{escaped}
" if role == "tool" else escaped + html += f'
{body}
' + if reasoning := msg.get("reasoning") or msg.get("reasoning_content"): html += _collapsible( "reasoning", ICON_SPARKLES, "Reasoning", f'
{_escape_html(reasoning)}
', " " * 12, ) - - html += '
' - html += '
' - html_list.append(html) + html_list.append(html + "
") return "\n".join(html_list) -def generate_multi_session_html_export(sessions: List[Dict[str, Any]]) -> str: - if not sessions: - return "

No sessions to export.

" - is_multi = len(sessions) > 1 - generated_at = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - - # Sidebar - sidebar_html = "" - if is_multi: - sidebar_items = [] - for s in sessions: - sid = str(s.get("id", "N/A")) - escaped_sid = _escape_html(sid) - title = s.get("title") or s.get("preview") or "Untitled Session" - if len(title) > 50: title = title[:47] + "..." - date = _format_timestamp(s.get("started_at", 0)).split(" ")[0] - - item = f''' - +def _sidebar_html(sessions: List[Dict[str, Any]]) -> str: + items = [] + for s in sessions: + sid = str(s.get("id", "N/A")) + title = s.get("title") or s.get("preview") or "Untitled Session" + if len(title) > 50: + title = title[:47] + "..." + date = _format_timestamp(s.get("started_at", 0)).split(" ")[0] + items.append(f""" +
{_escape_html(title)}
{_escape_html(sid[:8])} {date}
- ''' - sidebar_items.append(item) - - sidebar_html = f''' + """) + return f""" - ''' + """ - # Main Content - sessions_html_list = [] - for s in sessions: - sid = str(s.get("id", "N/A")) - escaped_sid = _escape_html(sid) - title = s.get("title") or "Hermes Session" - model = s.get("model") or "Unknown" - started_at = _format_timestamp(s.get("started_at", 0)) - messages_html = _generate_messages_html(s.get("messages", [])) - view_class = "session-view" if is_multi else "session-view active" - - system_prompt = s.get("system_prompt") - system_html = "" - if system_prompt: - system_html = _collapsible( - "system-prompt", ICON_SHIELD, "System Prompt (Persona)", - f'
{_escape_html(system_prompt)}
', " " * 12, - outer_class="system-prompt-section active", - ) - - session_html = f''' -
+ +def _session_view_html(s: Dict[str, Any], is_multi: bool) -> str: + escaped_sid = _escape_html(str(s.get("id", "N/A"))) + system_html = "" + if system_prompt := s.get("system_prompt"): + system_html = _collapsible( + "system-prompt", ICON_SHIELD, "System Prompt (Persona)", + f'
{_escape_html(system_prompt)}
', " " * 12, + outer_class="system-prompt-section active", + ) + return f""" +
-

{_escape_html(title)}

+

{_escape_html(s.get("title") or "Hermes Session")}

ID: {escaped_sid}
-
Model: {_escape_html(model)}
-
Started: {started_at}
+
Model: {_escape_html(s.get("model") or "Unknown")}
+
Started: {_format_timestamp(s.get("started_at", 0))}
{system_html}
- {messages_html} + {_generate_messages_html(s.get("messages", []))}
- ''' - sessions_html_list.append(session_html) + """ + +def generate_multi_session_html_export(sessions: List[Dict[str, Any]]) -> str: + if not sessions: + return "

No sessions to export.

" + is_multi = len(sessions) > 1 return HTML_TEMPLATE.format( page_title="Hermes Session Export" if is_multi else _escape_html(sessions[0].get("title") or "Hermes Session"), - sidebar_html=sidebar_html, - sessions_html="\n".join(sessions_html_list), + sidebar_html=_sidebar_html(sessions) if is_multi else "", + sessions_html="\n".join(_session_view_html(s, is_multi) for s in sessions), main_margin="var(--sidebar-width)" if is_multi else "0", layout_class="layout-multi" if is_multi else "layout-single", - generated_at=generated_at, + generated_at=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), script_nonce=secrets.token_urlsafe(16), ) + def generate_html_export(session_data: Dict[str, Any]) -> str: - """Legacy wrapper for single session export.""" + """Single-session export (legacy entry point).""" return generate_multi_session_html_export([session_data])