diff --git a/CHANGELOG.md b/CHANGELOG.md index de3538ce8..95f4b2024 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## [Unreleased] +## [v0.51.432] — 2026-06-15 — Release OS (TUI sessions discoverable in the sidebar) + +### Fixed + +- **TUI sessions now show up in the sidebar like CLI sessions do.** The session source-classification only recognized `cli` (and friends), so `tui`-sourced sessions weren't tagged as CLI-origin and could be filtered out or mislabeled. `tui` is now treated alongside `cli` everywhere the source is classified, and a TUI continuation tip keeps the navigation pointed at the latest tip with its visible TUI title (rather than an opaque compression-snapshot title), so the newest TUI conversation is findable by name. (#4213) + ## [v0.51.431] — 2026-06-15 — Release OR (make request logging non-fatal) ### Fixed diff --git a/api/agent_sessions.py b/api/agent_sessions.py index 955b4418b..6a152e3f9 100644 --- a/api/agent_sessions.py +++ b/api/agent_sessions.py @@ -31,6 +31,7 @@ SOURCE_LABELS = { 'slack': 'Slack', 'telegram': 'Telegram', 'tool': 'Tool', + 'tui': 'TUI', 'webui': 'WebUI', 'weixin': 'Weixin', } @@ -47,7 +48,7 @@ def normalize_agent_session_source(raw_source: str | None) -> dict: if raw == 'webui': session_source = 'webui' - elif raw == 'cli': + elif raw in {'cli', 'tui'}: session_source = 'cli' elif raw in MESSAGING_SOURCES: session_source = 'messaging' @@ -176,7 +177,12 @@ def is_cli_session_row(row: dict) -> bool: return False if source == "cli": return True - if source_tag == "cli" or raw_source == "cli" or source_name == "cli" or source_label == "cli": + if ( + source_tag in {"cli", "tui"} + or raw_source in {"cli", "tui"} + or source_name in {"cli", "tui"} + or source_label in {"cli", "tui"} + ): return True # Legacy imported CLI rows may only be marked as CLI in sidebar metadata. @@ -202,6 +208,14 @@ def is_cli_session_row_visible(row: dict) -> bool: if message_count <= 0: return False + if "tui" in { + _normalize_source_name(row.get("source")), + _normalize_source_name(row.get("source_tag")), + _normalize_source_name(row.get("raw_source")), + _normalize_source_name(row.get("source_label")), + }: + return True + if _has_cli_lineage(row): return True @@ -375,10 +389,20 @@ def _project_agent_session_rows(rows: list[dict]) -> list[dict]: ): if key in tip: merged[key] = tip[key] - if not merged.get('title'): - merged['title'] = tip.get('title') - if not merged.get('source'): - merged['source'] = tip.get('source') + if str(tip.get('source') or '').strip().lower() == 'tui': + # TUI continuation rows are user-visible session segments (#6, #17, + # ...), not opaque compression snapshots. Keep navigation pointed at + # the latest tip and show that tip's title so the newest conversation + # can be found by its visible TUI name. + if tip.get('title'): + merged['title'] = tip.get('title') + if tip.get('source'): + merged['source'] = tip.get('source') + else: + if not merged.get('title'): + merged['title'] = tip.get('title') + if not merged.get('source'): + merged['source'] = tip.get('source') merged['_lineage_root_id'] = row['id'] merged['_lineage_tip_id'] = tip['id'] merged['_compression_segment_count'] = segment_count diff --git a/static/sessions.js b/static/sessions.js index f589e80eb..9255a0112 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -1418,7 +1418,7 @@ function _isCliSession(session) { || session.source_label || '' ).toLowerCase(); - if (raw === 'cli') return true; + if (raw === 'cli' || raw === 'tui') return true; // If messaging-like, don't classify as legacy CLI even when is_cli_session is true. if (_isMessagingSession(session)) return false; return session.is_cli_session === true; diff --git a/tests/test_1466_sidebar_cancel_clarify.py b/tests/test_1466_sidebar_cancel_clarify.py index 35651eb65..2bac9203a 100644 --- a/tests/test_1466_sidebar_cancel_clarify.py +++ b/tests/test_1466_sidebar_cancel_clarify.py @@ -71,6 +71,7 @@ class TestSidebarCancelAction: assert "session.source" in body assert "session.source_label" in body assert "if (_isMessagingSession(session)) return false;" in body + assert "raw === 'tui'" in body assert "return session.is_cli_session === true;" in body def test_cli_sessions_hide_duplicate_and_delete_in_action_menu(self): diff --git a/tests/test_issue2351_cli_session_source_filter.py b/tests/test_issue2351_cli_session_source_filter.py index 71312b694..e78df8b87 100644 --- a/tests/test_issue2351_cli_session_source_filter.py +++ b/tests/test_issue2351_cli_session_source_filter.py @@ -125,3 +125,74 @@ def test_real_cli_sidebar_cli_flag_is_preserved_before_frontend_response(): ) assert normalized["is_cli_session"] is True + + +def test_tui_state_db_rows_are_cli_sidebar_rows(): + """Hermes TUI state.db rows belong in the CLI/agent sidebar bucket. + + TUI sessions are projected from state.db with raw/source_tag='tui'. If they + stay session_source='other' and is_cli_session=false, the two-tab sidebar + partition can make active TUI continuations disappear from both the WebUI + and CLI views. + """ + from api.agent_sessions import is_cli_session_row, normalize_agent_session_source + from api.routes import _normalize_sidebar_source_flags + + normalized_source = normalize_agent_session_source("tui") + assert normalized_source["session_source"] == "cli" + assert normalized_source["source_label"] == "TUI" + + tui_row = { + "session_id": "tui-tip", + "title": "Podcast work #17", + "source_tag": "tui", + "raw_source": "tui", + "session_source": "other", + "source_label": "Tui", + "message_count": 281, + } + + assert is_cli_session_row(tui_row) is True + assert _normalize_sidebar_source_flags(tui_row)["is_cli_session"] is True + + +def test_tui_continuation_projection_uses_latest_tip_title(): + """TUI continuation rows should surface under the latest segment title.""" + from api.agent_sessions import _project_agent_session_rows + + rows = [ + { + "id": "tui_parent", + "source": "tui", + "title": "Podcast work #6", + "started_at": 100.0, + "last_activity": 150.0, + "message_count": 10, + "actual_message_count": 10, + "actual_user_message_count": 5, + "parent_session_id": None, + "ended_at": 199.0, + "end_reason": "cli_close", + }, + { + "id": "tui_tip", + "source": "tui", + "title": "Podcast work #17", + "started_at": 200.0, + "last_activity": 250.0, + "message_count": 8, + "actual_message_count": 8, + "actual_user_message_count": 4, + "parent_session_id": "tui_parent", + "ended_at": None, + "end_reason": None, + }, + ] + + projected = _project_agent_session_rows(rows) + + assert len(projected) == 1 + assert projected[0]["id"] == "tui_tip" + assert projected[0]["title"] == "Podcast work #17" + assert projected[0]["_lineage_root_id"] == "tui_parent" + assert projected[0]["_lineage_tip_id"] == "tui_tip"