{"repo":"NousResearch/hermes-agent","public_id":"30351","display_label":"Improve /resume command context display and UX","summary":"These items address how the CLI displays restored context and recap information after using the /resume command, including fixes for in-session behavior and improvements on native Windows 11.","subsystem":"hermes_cli","counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":31695,"merged_at":"2026-05-24T22:36:37+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-05-24T22:36:37+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":4337,"pr_number":31695,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-05-24T22:36:37+00:00"},{"issue_number":30351,"pr_number":31695,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-05-24T22:36:37+00:00"}],"members":[{"kind":"issue","number":30351,"title":"Improve /resume UX and restored context display on native Windows 11","url":null,"state":"closed"},{"kind":"pr","number":31331,"title":"fix(cli): show last-turn preview after /resume so restored context is visible (#30351)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#31695","number":31695}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":31695,"title":"fix(cli): show recap after in-session /resume + tunable recap limits","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["cli.py","hermes_cli/config.py","scripts/release.py","tests/cli/test_resume_display.py"],"fix_hunks":[{"file":"cli.py","hunks":["@@ -415,6 +415,12 @@ def load_cli_config() -> Dict[str, Any]:\n         \"display\": {\n             \"compact\": False,\n             \"resume_display\": \"full\",\n+            # Recap tuning for /resume — see hermes_cli/config.py DEFAULT_CONFIG.\n+            \"resume_exchanges\": 10,\n+            \"resume_max_user_chars\": 300,\n+            \"resume_max_assistant_chars\": 200,\n+            \"resume_max_assistant_lines\": 3,\n+            \"resume_skip_tool_only\": True,\n             \"show_reasoning\": False,\n             \"streaming\": True,\n             \"busy_input_mode\": \"interrupt\",","@@ -5088,10 +5094,13 @@ def _display_resumed_history(self):\n         if self.resume_display == \"minimal\":\n             return\n \n-        MAX_DISPLAY_EXCHANGES = 10   # max user+assistant pairs to show\n-        MAX_USER_LEN = 300           # truncate user messages\n-        MAX_ASST_LEN = 200           # truncate assistant text\n-        MAX_ASST_LINES = 3           # max lines of assistant text\n+        # Read limits from config (with hardcoded defaults)\n+        _disp = CLI_CONFIG.get(\"display\", {})\n+        MAX_DISPLAY_EXCHANGES = int(_disp.get(\"resume_exchanges\", 10))\n+        MAX_USER_LEN = int(_disp.get(\"resume_max_user_chars\", 300))\n+        MAX_ASST_LEN = int(_disp.get(\"resume_max_assistant_chars\", 200))\n+        MAX_ASST_LINES = int(_disp.get(\"resume_max_assistant_lines\", 3))\n+        SKIP_TOOL_ONLY = _disp.get(\"resume_skip_tool_only\", True)\n \n         # Collect displayable entries (skip system, tool-result messages)\n         entries = []  # list of (role, display_text)","@@ -5154,6 +5163,10 @@ def _display_resumed_history(self):\n                 if not parts:\n                     # Skip pure-reasoning messages that have no visible output\n                     continue\n+                # Skip tool-call-only entries when SKIP_TOOL_ONLY is enabled\n+                has_text = bool(text)\n+                if SKIP_TOOL_ONLY and not has_text and tool_calls:\n+                    continue\n                 entries.append((\"assistant\", \" \".join(parts)))\n                 _last_asst_idx = len(entries) - 1\n                 _last_asst_full = \" \".join(full_parts)","@@ -6617,6 +6630,7 @@ def _handle_resume_command(self, cmd_original: str) -> None:\n                 f\" ({msg_count} user message{'s' if msg_count != 1 else ''},\"\n                 f\" {len(self.conversation_history)} total)\"\n             )\n+            self._display_resumed_history()\n         else:\n             _cprint(f\"  ↻ Resumed session {target_id}{title_part} — no messages, starting fresh.\")\n "]},{"file":"hermes_cli/config.py","hunks":["@@ -1008,6 +1008,19 @@ def _ensure_hermes_home_managed(home: Path):\n         \"compact\": False,\n         \"personality\": \"kawaii\",\n         \"resume_display\": \"full\",\n+        # Recap tuning for /resume and startup resume. The defaults match the\n+        # historical hardcoded values; expose them as config so power users can\n+        # widen or tighten the snapshot to taste.\n+        \"resume_exchanges\": 10,            # max user+assistant pairs to show\n+        \"resume_max_user_chars\": 300,      # truncate user message text\n+        \"resume_max_assistant_chars\": 200, # truncate non-last assistant text\n+        \"resume_max_assistant_lines\": 3,   # truncate non-last assistant lines\n+        # When True (default), assistant entries that are *only* tool calls\n+        # (no visible text) are skipped in the recap. This prevents the recap\n+        # from being dominated by `[2 tool calls: terminal, read_file]` lines\n+        # when an exchange was tool-heavy. Set False to restore the legacy\n+        # behavior of showing tool-call summaries inline.\n+        \"resume_skip_tool_only\": True,\n         \"busy_input_mode\": \"interrupt\",  # interrupt | queue | steer\n         # When true, `hermes --tui` auto-resumes the most recent human-\n         # facing session on launch instead of forging a fresh one."]},{"file":"scripts/release.py","hunks":["@@ -1278,6 +1278,7 @@\n     \"[redacted:email]\": \"erikengervall\",  # PR #28774 (firecrawl integration tag)\n     \"[redacted:email]\": \"egilewski\",  # PR #30432 (MEDIA path traversal fix, GHSA-jmf9-9729-7pp8)\n     \"[redacted:email]\": \"McClean-Edison\",  # PR #29817 (register_auxiliary_task plugin API)\n+    \"[redacted:email]\": \"SamuelZ12\",  # PR #7480 (show recap after in-session resume)\n }\n \n "]},{"file":"tests/cli/test_resume_display.py","hunks":["@@ -155,14 +155,34 @@ def test_tool_messages_hidden(self):\n         assert \"Page content\" not in output\n \n     def test_tool_calls_shown_as_summary(self):\n-        cli = _make_cli()\n+        # Disable tool-only skip so the summary line is rendered for this fixture.\n+        cli = _make_cli(config_overrides={\"display\": {\"resume_skip_tool_only\": False}})\n         cli.conversation_history = _tool_call_history()\n-        output = self._capture_display(cli)\n+        import cli as _cli_mod\n+        # CLI_CONFIG is read at call-time inside _display_resumed_history, so\n+        # apply the override for the duration of the capture, not just at init.\n+        with patch.dict(_cli_mod.__dict__, {\"CLI_CONFIG\": {\n+            \"display\": {\"resume_skip_tool_only\": False, \"resume_display\": \"full\"}\n+        }}):\n+            output = self._capture_display(cli)\n \n         assert \"2 tool calls\" in output\n         assert \"web_search\" in output\n         assert \"web_extract\" in output\n \n+    def test_tool_only_message_skipped_by_default(self):\n+        \"\"\"Assistant messages with only tool_calls (no text) are skipped when\n+        resume_skip_tool_only=True (the default). The summary line is hidden.\n+        \"\"\"\n+        cli = _make_cli()\n+        cli.conversation_history = _tool_call_history()\n+        output = self._capture_display(cli)\n+\n+        # The tool-only assistant entry should be skipped\n+        assert \"2 tool calls\" not in output\n+        # The final text reply should still appear\n+        assert \"Here are some great Python tutorials\" in output\n+\n     def test_long_user_message_truncated(self):\n         cli = _make_cli()\n         long_text = \"A\" * 500","@@ -611,6 +631,55 @@ def test_singular_user_message_grammar(self):\n         assert \"1 user messages\" not in output\n \n \n+# ── Tests for _handle_resume_command recap display ───────────────────\n+\n+\n+class TestHandleResumeCommandRecap:\n+    \"\"\"In-session /resume should show the same recap panel as startup resume.\"\"\"\n+\n+    def test_resume_command_displays_recap_when_messages_restored(self):\n+        cli = _make_cli()\n+        cli.session_id = \"current_session\"\n+        messages = _simple_history()\n+\n+        mock_db = MagicMock()\n+        mock_db.get_session.return_value = {\"id\": \"target_session\", \"title\": \"Test Session\"}\n+        mock_db.get_messages_as_conversation.return_value = messages\n+        # resolve_resume_session_id passes the id through when no compression chain.\n+        mock_db.resolve_resume_session_id.return_value = \"target_session\"\n+        cli._session_db = mock_db\n+\n+        with (\n+            patch(\"hermes_cli.main._resolve_session_by_name_or_id\", return_value=\"target_session\"),\n+            patch.object(cli, \"_display_resumed_history\") as display_mock,\n+        ):\n+            cli._handle_resume_command(\"/resume test session\")\n+\n+        assert cli.session_id == \"target_session\"\n+        assert cli.conversation_history == messages\n+        mock_db.end_session.assert_called_once_with(\"current_session\", \"resumed_other\")\n+        mock_db.reopen_session.assert_called_once_with(\"target_session\")\n+        display_mock.assert_called_once_with()\n+\n+    def test_resume_command_skips_recap_when_session_has_no_messages(self):\n+        cli = _make_cli()\n+        cli.session_id = \"current_session\"\n+\n+        mock_db = MagicMock()\n+        mock_db.get_session.return_value = {\"id\": \"target_session\", \"title\": None}\n+        mock_db.get_messages_as_conversation.return_value = []\n+        mock_db.resolve_resume_session_id.return_value = \"target_session\"\n+        cli._session_db = mock_db\n+\n+        with (\n+            patch(\"hermes_cli.main._resolve_session_by_name_or_id\", return_value=\"target_session\"),\n+            patch.object(cli, \"_display_resumed_history\") as display_mock,\n+        ):\n+            cli._handle_resume_command(\"/resume target_session\")\n+\n+        display_mock.assert_not_called()\n+\n+\n # ── Integration: _init_agent skips when preloaded ────────────────────\n \n \n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}