Interactive session picker and numeric selection for /resume
Cluster 4988 · hermes cli. Data as of
Nothing open on GitHub. Fix merged in #31709 on 24 May 2026. Check it covers your case.
The short answer
- Known?
- Yes. 4 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #31709. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
unresolved. That is separate from the fix event. - Still open
- Nothing open on GitHub.
These members all concern making /resume easier to use, mainly by showing an interactive session picker when no arguments are given. Some also mention selecting a session by position number, including in the gateway, but the titles alone don't show how far any of this has been implemented.
Open issues0
Open PRs0
Fixes on record
-
#31709 feat(cli,gateway): /resume accepts position numbers
files:
cli.py,gateway/run.py,locales/af.yaml,locales/de.yaml,locales/en.yaml,locales/es.yaml,locales/fr.yaml,locales/ga.yaml,locales/hu.yaml,locales/it.yaml,locales/ja.yaml,locales/ko.yaml,locales/pt.yaml,locales/ru.yaml,locales/tr.yaml,locales/uk.yaml,locales/zh-hant.yaml,locales/zh.yaml,scripts/release.py,tests/cli/test_cli_init.py(+2 more)Show the diff
-
cli.py@@ -6172,15 +6172,16 @@ def _show_recent_sessions(self, *, reason: str = "history", limit: int = 10) -> else: print(" Recent sessions:") print() - print(f" {'Title':<32} {'Preview':<40} {'Last Active':<13} {'ID'}") - print(f" {'─' * 32} {'─' * 40} {'─' * 13} {'─' * 24}") - for session in sessions: + print(f" {'#':<3} {'Title':<32} {'Preview':<40} {'Last Active':<13} {'ID'}") + print(f" {'─' * 3} {'─' * 32} {'─' * 40} {'─' * 13} {'─' * 24}") + for idx, session in enumerate(sessions, start=1): title = session.get("title") or "—" preview = (session.get("preview") or "")[:38] last_active = _relative_time(session.get("last_active")) - print(f" {title:<32} {preview:<40} {last_active:<13} {session['id']}") + print(f" {idx:<3} {title:<32} {preview:<40} {last_active:<13} {session['id']}") print() - print(" Use /resume <session id or title> to continue where you left off.") + print(" Use /resume <number>, /resume <session id>, or /resume <session title> to continue.") + print(" Example: /resume 2") print() return True @@ -6525,7 +6526,7 @@ def _handle_resume_command(self, cmd_original: str) -> None: target = parts[1].strip() if len(parts) > 1 else "" if not target: - _cprint(" Usage: /resume <session_id_or_title>") + _cprint(" Usage: /resume <number|session_id_or_title>") if self._show_recent_sessions(reason="resume"): return _cprint(" Tip: Use /history or `hermes sessions list` to find sessions.") @@ -6536,10 +6537,20 @@ def _handle_resume_command(self, cmd_original: str) -> None: _cprint(f" {format_session_db_unavailable()}") return - # Resolve title or ID - from hermes_cli.main import _resolve_session_by_name_or_id - resolved = _resolve_session_by_name_or_id(target) - target_id = resolved or target + # Resolve numbered selection, title, or ID + if target.isdigit(): + sessions = self._list_recent_sessions(limit=10) + index = int(target) + if index < 1 or index > len(sessions): + _cprint(f" Resume index {index} is out of range.") + _cprint(" Use /resume with no arguments to see available sessions.") + return + selected = sessions[index - 1] + target_id = selected["id"] + else: + from hermes_cli.main import _resolve_session_by_name_or_id + resolved = _resolve_session_by_name_or_id(target) + target_id = resolved or target session_meta = self._session_db.get_session(target_id) if not session_meta: -
gateway/run.py@@ -12741,7 +12741,7 @@ async def _handle_title_command(self, event: MessageEvent) -> str: return t("gateway.title.current_no_title", session_id=session_id) async def _handle_resume_command(self, event: MessageEvent) -> str: - """Handle /resume command — switch to a previously-named session.""" + """Handle /resume command — list or switch to a previous session.""" if not self._session_db: from hermes_state import format_session_db_unavailable return format_session_db_unavailable(prefix=t("gateway.shared.session_db_unavailable_prefix")) @@ -12750,30 +12750,44 @@ async def _handle_resume_command(self, event: MessageEvent) -> str: session_key = self._session_key_for_source(source) name = event.get_command_args().strip() + def _list_titled_sessions() -> list[dict]: + user_source = source.platform.value if source.platform else None + sessions = self._session_db.list_sessions_rich(source=user_source, limit=10) + return [s for s in sessions if s.get("title")][:10] + if not name: # List recent titled sessions for this user/platform try: - user_source = source.platform.value if source.platform else None - sessions = self._session_db.list_sessions_rich( - source=user_source, limit=10 - ) - titled = [s for s in sessions if s.get("title")] + titled = _list_titled_sessions() if not titled: return t("gateway.resume.no_named_sessions") lines = [t("gateway.resume.list_header")] - for s in titled[:10]: + for idx, s in enumerate(titled[:10], start=1): title = s["title"] preview = s.get("preview", "")[:40] preview_part = t("gateway.resume.list_preview_suffix", preview=preview) if preview else "" - lines.append(t("gateway.resume.list_item", title=title, preview_part=preview_part)) - lines.append(t("gateway.resume.list_footer")) + lines.append(t("gateway.resume.list_item_numbered", index=idx, title=title, preview_part=preview_part)) + lines.append(t("gateway.resume.list_footer_numbered")) return "\n".join(lines) except Exception as e: logger.debug("Failed to list titled sessions: %s", e) return t("gateway.resume.list_failed", error=e) - # Resolve the name to a session ID. - target_id = self._session_db.resolve_session_by_title(name) + # Resolve a numbered choice or a title to a session ID. + if name.isdigit(): + try: + titled = _list_titled_sessions() + except Exception as e: + logger.debug("Failed to list titled sessions for numeric resume: %s", e) + return t("gateway.resume.list_failed", error=e) + index = int(name) + if index < 1 or index > len(titled): + return t("gateway.resume.out_of_range", index=index) + target = titled[index - 1] + target_id = target.get("id") + name = target.get("title") or name + else: + target_id = self._session_db.resolve_session_by_title(name) if not target_id: return t("gateway.resume.not_found", name=name) # Compression creates child continuations that hold the live transcript. -
locales/af.yaml@@ -222,9 +222,12 @@ gateway: no_named_sessions: "Geen benoemde sessies gevind nie.\nGebruik `/title My Sessie` om jou huidige sessie 'n naam te gee, en dan `/resume My Sessie` om later daarheen terug te keer." list_header: "📋 **Benoemde Sessies**\n" list_item: "• **{title}**{preview_part}" + list_item_numbered: "{index}. **{title}**{preview_part}" list_preview_suffix: " — _{preview}_" list_footer: "\nGebruik: `/resume <session name>`" + list_footer_numbered: "\nGebruik: `/resume <sessienaam>` of `/resume <nommer>` (bv. `/resume 1` vir die mees onlangse)" list_failed: "Kon nie sessies lys nie: {error}" + out_of_range: "Hervat-indeks {index} is buite bereik.\nGebruik `/resume` sonder argumente om beskikbare sessies te sien." not_found: "Geen sessie gevind wat by '**{name}**' pas nie.\nGebruik `/resume` sonder argumente om beskikbare sessies te sien." already_on: "📌 Reeds op sessie **{name}**." switch_failed: "Kon nie sessie verander nie." -
locales/de.yaml@@ -222,9 +222,12 @@ gateway: no_named_sessions: "Keine benannten Sitzungen gefunden.\nVerwenden Sie `/title Meine Sitzung`, um die aktuelle Sitzung zu benennen, dann `/resume Meine Sitzung`, um später dorthin zurückzukehren." list_header: "📋 **Benannte Sitzungen**\n" list_item: "• **{title}**{preview_part}" + list_item_numbered: "{index}. **{title}**{preview_part}" list_preview_suffix: " — _{preview}_" list_footer: "\nVerwendung: `/resume <Sitzungsname>`" + list_footer_numbered: "\nVerwendung: `/resume <Sitzungsname>` oder `/resume <Nummer>` (z. B. `/resume 1` für die zuletzt verwendete)" list_failed: "Sitzungen konnten nicht aufgelistet werden: {error}" + out_of_range: "Wiederaufnahme-Index {index} liegt außerhalb des gültigen Bereichs.\nVerwenden Sie `/resume` ohne Argumente, um verfügbare Sitzungen anzuzeigen." not_found: "Keine Sitzung passend zu '**{name}**' gefunden.\nVerwenden Sie `/resume` ohne Argumente, um verfügbare Sitzungen zu sehen." already_on: "📌 Bereits in Sitzung **{name}**." switch_failed: "Sitzungswechsel fehlgeschlagen." -
locales/en.yaml@@ -237,9 +237,12 @@ gateway: no_named_sessions: "No named sessions found.\nUse `/title My Session` to name your current session, then `/resume My Session` to return to it later." list_header: "📋 **Named Sessions**\n" list_item: "• **{title}**{preview_part}" + list_item_numbered: "{index}. **{title}**{preview_part}" list_preview_suffix: " — _{preview}_" list_footer: "\nUsage: `/resume <session name>`" + list_footer_numbered: "\nUsage: `/resume <session name>` or `/resume <number>` (e.g. `/resume 1` for the most recent)" list_failed: "Could not list sessions: {error}" + out_of_range: "Resume index {index} is out of range.\nUse `/resume` with no arguments to see available sessions." not_found: "No session found matching '**{name}**'.\nUse `/resume` with no arguments to see available sessions." already_on: "📌 Already on session **{name}**." switch_failed: "Failed to switch session." -
locales/es.yaml@@ -222,9 +222,12 @@ gateway: no_named_sessions: "No se encontraron sesiones con nombre.\nUsa `/title Mi sesión` para nombrar la sesión actual y luego `/resume Mi sesión` para volver a ella." list_header: "📋 **Sesiones con nombre**\n" list_item: "• **{title}**{preview_part}" + list_item_numbered: "{index}. **{title}**{preview_part}" list_preview_suffix: " — _{preview}_" list_footer: "\nUso: `/resume <nombre de sesión>`" + list_footer_numbered: "\nUso: `/resume <nombre de sesión>` o `/resume <número>` (p. ej. `/resume 1` para la más reciente)" list_failed: "No se pudieron listar las sesiones: {error}" + out_of_range: "El índice de reanudación {index} está fuera de rango.\nUsa `/resume` sin argumentos para ver las sesiones disponibles." not_found: "No se encontró ninguna sesión que coincida con '**{name}**'.\nUsa `/resume` sin argumentos para ver las sesiones disponibles." already_on: "📌 Ya estás en la sesión **{name}**." switch_failed: "No se pudo cambiar de sesión." -
locales/fr.yaml@@ -222,9 +222,12 @@ gateway: no_named_sessions: "Aucune session nommée trouvée.\nUtilisez `/title Ma session` pour nommer la session actuelle, puis `/resume Ma session` pour y revenir plus tard." list_header: "📋 **Sessions nommées**\n" list_item: "• **{title}**{preview_part}" + list_item_numbered: "{index}. **{title}**{preview_part}" list_preview_suffix: " — _{preview}_" list_footer: "\nUsage : `/resume <nom de session>`" + list_footer_numbered: "\nUtilisation : `/resume <nom de session>` ou `/resume <numéro>` (par exemple `/resume 1` pour la plus récente)" list_failed: "Impossible de lister les sessions : {error}" + out_of_range: "L'index de reprise {index} est hors limites.\nUtilisez `/resume` sans arguments pour voir les sessions disponibles." not_found: "Aucune session correspondant à '**{name}**' trouvée.\nUtilisez `/resume` sans argument pour voir les sessions disponibles." already_on: "📌 Déjà sur la session **{name}**." switch_failed: "Échec du changement de session." -
locales/ga.yaml@@ -226,9 +226,12 @@ gateway: no_named_sessions: "Níor aimsíodh aon seisiún ainmnithe.\nÚsáid `/title M'Ainm Seisiúin` chun do sheisiún reatha a ainmniú, ansin `/resume M'Ainm Seisiúin` chun filleadh air níos déanaí." list_header: "📋 **Seisiúin Ainmnithe**\n" list_item: "• **{title}**{preview_part}" + list_item_numbered: "{index}. **{title}**{preview_part}" list_preview_suffix: " — _{preview}_" list_footer: "\nÚsáid: `/resume <session name>`" + list_footer_numbered: "\nÚsáid: `/resume <ainm seisiúin>` nó `/resume <uimhir>` (m.sh. `/resume 1` don cheann is déanaí)" list_failed: "Níorbh fhéidir seisiúin a liostáil: {error}" + out_of_range: "Tá an t-innéacs atosaithe {index} as raon.\nÚsáid `/resume` gan argóintí chun na seisiúin atá ar fáil a fheiceáil." not_found: "Níor aimsíodh aon seisiún ag teacht le '**{name}**'.\nÚsáid `/resume` gan argóintí chun seisiúin atá ar fáil a fheiceáil." already_on: "📌 Cheana ar an seisiún **{name}**." switch_failed: "Theip ar athrú seisiúin." -
locales/hu.yaml@@ -222,9 +222,12 @@ gateway: no_named_sessions: "Nem található elnevezett munkamenet.\nHasználd a `/title Saját munkamenet` parancsot a jelenlegi munkamenet elnevezéséhez, majd a `/resume Saját munkamenet` paranccsal térhetsz vissza hozzá." list_header: "📋 **Elnevezett munkamenetek**\n" list_item: "• **{title}**{preview_part}" + list_item_numbered: "{index}. **{title}**{preview_part}" list_preview_suffix: " — _{preview}_" list_footer: "\nHasználat: `/resume <munkamenet neve>`" + list_footer_numbered: "\nHasználat: `/resume <munkamenet neve>` vagy `/resume <szám>` (pl. `/resume 1` a legutóbbihoz)" list_failed: "Nem sikerült listázni a munkameneteket: {error}" + out_of_range: "A folytatási index ({index}) tartományon kívül esik.\nA `/resume` argumentumok nélküli használata megjeleníti az elérhető munkameneteket." not_found: "Nem található '**{name}**' nevű munkamenet.\nArgumentumok nélkül használd a `/resume` parancsot az elérhető munkamenetek megtekintéséhez." already_on: "📌 Már a **{name}** munkamenetben vagy." switch_failed: "Nem sikerült munkamenetet váltani." -
locales/it.yaml@@ -222,9 +222,12 @@ gateway: no_named_sessions: "Nessuna sessione con nome trovata.\nUsa `/title My Session` per dare un nome alla sessione attuale, poi `/resume My Session` per tornare a essa in seguito." list_header: "📋 **Sessioni con nome**\n" list_item: "• **{title}**{preview_part}" + list_item_numbered: "{index}. **{title}**{preview_part}" list_preview_suffix: " — _{preview}_" list_footer: "\nUso: `/resume <session name>`" + list_footer_numbered: "\nUso: `/resume <nome sessione>` o `/resume <numero>` (es. `/resume 1` per la più recente)" list_failed: "Impossibile elencare le sessioni: {error}" + out_of_range: "L'indice di ripresa {index} è fuori intervallo.\nUsa `/resume` senza argomenti per vedere le sessioni disponibili." not_found: "Nessuna sessione trovata corrispondente a '**{name}**'.\nUsa `/resume` senza argomenti per vedere le sessioni disponibili." already_on: "📌 Già nella sessione **{name}**." switch_failed: "Cambio di sessione non riuscito." -
locales/ja.yaml@@ -222,9 +222,12 @@ gateway: no_named_sessions: "名前付きセッションが見つかりません。\n`/title セッション名` で現在のセッションに名前を付けると、後で `/resume セッション名` で戻れます。" list_header: "📋 **名前付きセッション**\n" list_item: "• **{title}**{preview_part}" + list_item_numbered: "{index}. **{title}**{preview_part}" list_preview_suffix: " — _{preview}_" list_footer: "\n使い方: `/resume <セッション名>`" + list_footer_numbered: "\n使い方: `/resume <セッション名>` または `/resume <番号>`(例: 最新のセッションには `/resume 1`)" list_failed: "セッションを一覧表示できませんでした: {error}" + out_of_range: "再開インデックス {index} は範囲外です。\n引数なしで `/resume` を実行すると、利用可能なセッションが表示されます。" not_found: "'**{name}**' に一致するセッションが見つかりません。\n引数なしで `/resume` を実行すると利用可能なセッションを表示します。" already_on: "📌 既にセッション **{name}** にいます。" switch_failed: "セッションの切り替えに失敗しました。" -
locales/ko.yaml@@ -222,9 +222,12 @@ gateway: no_named_sessions: "이름이 지정된 세션이 없습니다.\n현재 세션에 이름을 지정하려면 `/title 내 세션`을 사용하고, 나중에 `/resume 내 세션`으로 돌아오세요." list_header: "📋 **이름이 지정된 세션**\n" list_item: "• **{title}**{preview_part}" + list_item_numbered: "{index}. **{title}**{preview_part}" list_preview_suffix: " — _{preview}_" list_footer: "\n사용법: `/resume <session name>`" + list_footer_numbered: "\n사용법: `/resume <세션 이름>` 또는 `/resume <번호>` (예: 가장 최근 세션은 `/resume 1`)" list_failed: "세션 목록을 가져올 수 없습니다: {error}" + out_of_range: "재개 인덱스 {index}이(가) 범위를 벗어났습니다.\n인자 없이 `/resume`을 실행하면 사용 가능한 세션이 표시됩니다." not_found: "'**{name}**'와 일치하는 세션이 없습니다.\n사용 가능한 세션을 보려면 인수 없이 `/resume`을 사용하세요." already_on: "📌 이미 **{name}** 세션에 있습니다." switch_failed: "세션 전환에 실패했습니다." -
locales/pt.yaml@@ -222,9 +222,12 @@ gateway: no_named_sessions: "Não foram encontradas sessões com nome.\nUsa `/title A minha sessão` para nomear a sessão atual e depois `/resume A minha sessão` para voltar a ela." list_header: "📋 **Sessões com nome**\n" list_item: "• **{title}**{preview_part}" + list_item_numbered: "{index}. **{title}**{preview_part}" list_preview_suffix: " — _{preview}_" list_footer: "\nUso: `/resume <nome da sessão>`" + list_footer_numbered: "\nUso: `/resume <nome da sessão>` ou `/resume <número>` (ex.: `/resume 1` para a mais recente)" list_failed: "Não foi possível listar as sessões: {error}" + out_of_range: "O índice de retomada {index} está fora do intervalo.\nUse `/resume` sem argumentos para ver as sessões disponíveis." not_found: "Não foi encontrada nenhuma sessão correspondente a '**{name}**'.\nUsa `/resume` sem argumentos para ver as sessões disponíveis." already_on: "📌 Já estás na sessão **{name}**." switch_failed: "Falha ao mudar de sessão." -
locales/ru.yaml@@ -222,9 +222,12 @@ gateway: no_named_sessions: "Именованных сеансов не найдено.\nИспользуйте `/title Мой сеанс`, чтобы назвать текущий сеанс, затем `/resume Мой сеанс`, чтобы вернуться к нему позже." list_header: "📋 **Именованные сеансы**\n" list_item: "• **{title}**{preview_part}" + list_item_numbered: "{index}. **{title}**{preview_part}" list_preview_suffix: " — _{preview}_" list_footer: "\nИспользование: `/resume <название сеанса>`" + list_footer_numbered: "\nИспользование: `/resume <имя сеанса>` или `/resume <номер>` (например, `/resume 1` для самого недавнего)" list_failed: "Не удалось получить список сеансов: {error}" + out_of_range: "Индекс возобновления {index} вне диапазона.\nИспользуйте `/resume` без аргументов, чтобы увидеть доступные сеансы." not_found: "Сеанс, соответствующий '**{name}**', не найден.\nИспользуйте `/resume` без аргументов, чтобы увидеть доступные сеансы." already_on: "📌 Уже в сеансе **{name}**." switch_failed: "Не удалось переключить сеанс." -
locales/tr.yaml@@ -222,9 +222,12 @@ gateway: no_named_sessions: "Adlandırılmış oturum bulunamadı.\nMevcut oturumu adlandırmak için `/title Oturumum`, daha sonra geri dönmek için `/resume Oturumum` kullanın." list_header: "📋 **Adlandırılmış Oturumlar**\n" list_item: "• **{title}**{preview_part}" + list_item_numbered: "{index}. **{title}**{preview_part}" list_preview_suffix: " — _{preview}_" list_footer: "\nKullanım: `/resume <oturum adı>`" + list_footer_numbered: "\nKullanım: `/resume <oturum adı>` veya `/resume <numara>` (örn. en yenisi için `/resume 1`)" list_failed: "Oturumlar listelenemedi: {error}" + out_of_range: "Devam endeksi {index} aralık dışında.\nKullanılabilir oturumları görmek için `/resume` komutunu argümansız çalıştırın." not_found: "'**{name}**' ile eşleşen oturum bulunamadı.\nKullanılabilir oturumları görmek için argümansız `/resume` kullanın." already_on: "📌 Zaten **{name}** oturumundasınız." switch_failed: "Oturum değiştirilemedi." -
locales/uk.yaml@@ -222,9 +222,12 @@ gateway: no_named_sessions: "Іменованих сеансів не знайдено.\nВикористайте `/title Мій сеанс`, щоб назвати поточний сеанс, потім `/resume Мій сеанс`, щоб повернутися до нього." list_header: "📋 **Іменовані сеанси**\n" list_item: "• **{title}**{preview_part}" + list_item_numbered: "{index}. **{title}**{preview_part}" list_preview_suffix: " — _{preview}_" list_footer: "\nВикористання: `/resume <назва сеансу>`" + list_footer_numbered: "\nВикористання: `/resume <назва сесії>` або `/resume <номер>` (наприклад, `/resume 1` для найновішої)" list_failed: "Не вдалося отримати список сеансів: {error}" + out_of_range: "Індекс відновлення {index} поза межами діапазону.\nВикористовуйте `/resume` без аргументів, щоб переглянути доступні сесії." not_found: "Сеанс, що відповідає '**{name}**', не знайдено.\nВикористайте `/resume` без аргументів, щоб побачити доступні сеанси." already_on: "📌 Уже в сеансі **{name}**." switch_failed: "Не вдалося переключити сеанс." -
locales/zh-hant.yaml@@ -222,9 +222,12 @@ gateway: no_named_sessions: "找不到已命名的工作階段。\n使用 `/title 我的工作階段` 為目前工作階段命名,然後使用 `/resume 我的工作階段` 返回。" list_header: "📋 **已命名工作階段**\n" list_item: "• **{title}**{preview_part}" + list_item_numbered: "{index}. **{title}**{preview_part}" list_preview_suffix: " — _{preview}_" list_footer: "\n用法:`/resume <工作階段名稱>`" + list_footer_numbered: "\n用法:`/resume <會話名稱>` 或 `/resume <編號>`(例如,`/resume 1` 表示最近的會話)" list_failed: "無法列出工作階段:{error}" + out_of_range: "恢復索引 {index} 超出範圍。\n請使用不帶參數的 `/resume` 查看可用會話。" not_found: "找不到符合 '**{name}**' 的工作階段。\n使用不帶參數的 `/resume` 檢視可用的工作階段。" already_on: "📌 已在工作階段 **{name}** 上。" switch_failed: "切換工作階段失敗。" -
locales/zh.yaml@@ -222,9 +222,12 @@ gateway: no_named_sessions: "未找到已命名的会话。\n使用 `/title 我的会话` 为当前会话命名,然后用 `/resume 我的会话` 返回。" list_header: "📋 **已命名会话**\n" list_item: "• **{title}**{preview_part}" + list_item_numbered: "{index}. **{title}**{preview_part}" list_preview_suffix: " — _{preview}_" list_footer: "\n用法:`/resume <会话名称>`" + list_footer_numbered: "\n用法:`/resume <会话名称>` 或 `/resume <编号>`(例如,`/resume 1` 表示最近的会话)" list_failed: "无法列出会话:{error}" + out_of_range: "恢复索引 {index} 超出范围。\n请使用不带参数的 `/resume` 查看可用会话。" not_found: "未找到匹配 '**{name}**' 的会话。\n使用不带参数的 `/resume` 查看可用会话。" already_on: "📌 已在会话 **{name}** 上。" switch_failed: "切换会话失败。" -
scripts/release.py@@ -1283,6 +1283,7 @@ "[redacted:email]": "egilewski", # PR #30432 (MEDIA path traversal fix, GHSA-jmf9-9729-7pp8) "[redacted:email]": "McClean-Edison", # PR #29817 (register_auxiliary_task plugin API) "[redacted:email]": "SamuelZ12", # PR #7480 (show recap after in-session resume) + "[redacted:email]": "daizhonggeng", # PR #9020 (numbered /resume selection) } -
tests/cli/test_cli_init.py@@ -331,7 +331,8 @@ def test_resume_without_target_lists_recent_sessions(self, capsys): assert "Recent sessions" in output assert "Checking Running Hermes Agent" in output - assert "Use /resume <session id or title> to continue" in output + assert "Use /resume" in output + assert "session title" in output def test_resume_updates_hermes_session_id_env_and_context(self, tmp_path): from gateway.session_context import _UNSET, _VAR_MAP, get_session_env -
tests/cli/test_cli_resume_command.py@@ -0,0 +1,77 @@ +from unittest.mock import MagicMock, patch + +from cli import HermesCLI + + +def _make_cli(): + cli_obj = HermesCLI.__new__(HermesCLI) + cli_obj.session_id = "current_session" + cli_obj._resumed = False + cli_obj._pending_title = None + cli_obj.conversation_history = [] + cli_obj.agent = None + cli_obj._session_db = MagicMock() + # _handle_resume_command now triggers _display_resumed_history (#31695), + # which reads self.resume_display. "minimal" short-circuits the recap so + # the test only exercises session-switch behavior. + cli_obj.resume_display = "minimal" + return cli_obj + + +class TestCliResumeCommand: + def test_show_recent_sessions_includes_indexes_and_resume_hint(self, capsys): + cli_obj = _make_cli() + cli_obj._list_recent_sessions = MagicMock(return_value=[ + {"id": "sess_002", "title": "Coding", "preview": "build feature", "last_active": None}, + {"id": "sess_001", "title": "Research", "preview": "read docs", "last_active": None}, + ]) + + shown = cli_obj._show_recent_sessions(reason="resume") + output = capsys.readouterr().out + + assert shown is True + assert "1" in output + assert "2" in output + assert "Coding" in output + assert "Research" in output + assert "/resume 2" in output + assert "/resume <session title>" in output + + def test_handle_resume_by_index_switches_to_numbered_session(self): + cli_obj = _make_cli() + cli_obj._list_recent_sessions = MagicMock(return_value=[ + {"id": "sess_002", "title": "Coding"}, + {"id": "sess_001", "title": "Research"}, + ]) + cli_obj._session_db.get_session.return_value = {"id": "sess_001", "title": "Research"} + cli_obj._session_db.get_messages_as_conversation.return_value = [ + {"role": "user", "content": "hello"}, + {"role": "assistant", "content": "hi"}, + ] + # resolve_resume_session_id passes the id through when no compression chain. + cli_obj._session_db.resolve_resume_session_id.return_value = "sess_001" + + with ( + patch("hermes_cli.main._resolve_session_by_name_or_id", return_value=None), + patch("cli._cprint") as mock_cprint, + ): + cli_obj._handle_resume_command("/resume 2") + + printed = " ".join(str(call) for call in mock_cprint.call_args_list) + assert cli_obj.session_id == "sess_001" + assert "Resumed session sess_001" in printed + assert "Research" in printed + + def test_handle_resume_by_index_out_of_range(self): + cli_obj = _make_cli() + cli_obj._list_recent_sessions = MagicMock(return_value=[ + {"id": "sess_002", "title": "Coding"}, + ]) + + with patch("cli._cprint") as mock_cprint: + cli_obj._handle_resume_command("/resume 9") + + printed = " ".join(str(call) for call in mock_cprint.call_args_list) + assert "out of range" in printed.lower() + assert "/resume" in printed + assert cli_obj.session_id == "current_session" -
tests/gateway/test_resume_command.py@@ -88,6 +88,9 @@ async def test_list_named_sessions_when_no_arg(self, tmp_path): assert "Research" in result assert "Coding" in result assert "Named Sessions" in result + assert "1." in result + assert "2." in result + assert "/resume 1" in result db.close() @pytest.mark.asyncio @@ -104,6 +107,47 @@ async def test_list_shows_usage_when_no_titled(self, tmp_path): assert "/title" in result db.close() + @pytest.mark.asyncio + async def test_resume_by_index(self, tmp_path): + """Numeric argument resumes the indexed titled session from the list.""" + from hermes_state import SessionDB + db = SessionDB(db_path=tmp_path / "state.db") + db.create_session("sess_001", "telegram") + db.create_session("sess_002", "telegram") + db.set_session_title("sess_001", "Research") + db.set_session_title("sess_002", "Coding") + db.create_session("current_session_001", "telegram") + + event = _make_event(text="/resume 2") + runner = _make_runner(session_db=db, current_session_id="current_session_001", + event=event) + result = await runner._handle_resume_command(event) + + assert "Resumed" in result + runner.session_store.switch_session.assert_called_once() + call_args = runner.session_store.switch_session.call_args + assert call_args[0][1] == "sess_001" + db.close() + + @pytest.mark.asyncio + async def test_resume_index_out_of_range(self, tmp_path): + """Out-of-range numeric arguments show a helpful error.""" + from hermes_state import SessionDB + db = SessionDB(db_path=tmp_path / "state.db") + db.create_session("sess_001", "telegram") + db.set_session_title("sess_001", "Research") + db.create_session("current_session_001", "telegram") + + event = _make_event(text="/resume 9") + runner = _make_runner(session_db=db, current_session_id="current_session_001", + event=event) + result = await runner._handle_resume_command(event) + + assert "out of range" in result.lower() + assert "/resume" in result + runner.session_store.switch_session.assert_not_called() + db.close() + @pytest.mark.asyncio async def test_resume_by_name(self, tmp_path): """Resolves a title and switches to that session."""
-
2 PRs closed without merging
From the close comments: duplicate 1 · merged via another change 1
Closed issues · 1
- Closed#4988[Feature]: /resume with no arguments should show an interactive session picker
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.50: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Subsystem
- hermes cli
- Merged PRs
- #31709
- Closed issues
- 1
- Attempts
- 2 PRs closed without merging
- Fix link checked
- 24 May 2026