From 2b886ffd1fef43a682e087dddd8bbdca2b1f3566 Mon Sep 17 00:00:00 2001 From: Xubin Ren Date: Mon, 27 Apr 2026 10:22:06 +0000 Subject: [PATCH] fix(command): expose history in chat command menus Made-with: Cursor --- nanobot/channels/discord.py | 1 + nanobot/channels/telegram.py | 10 ++++++- tests/channels/test_discord_channel.py | 2 +- tests/channels/test_telegram_channel.py | 1 + tests/cli/test_restart_command.py | 35 +++++++++++++++++++++++++ 5 files changed, 47 insertions(+), 2 deletions(-) diff --git a/nanobot/channels/discord.py b/nanobot/channels/discord.py index f32158dae..94be6a907 100644 --- a/nanobot/channels/discord.py +++ b/nanobot/channels/discord.py @@ -195,6 +195,7 @@ if DISCORD_AVAILABLE: ("stop", "Stop the current task", "/stop"), ("restart", "Restart the bot", "/restart"), ("status", "Show bot status", "/status"), + ("history", "Show recent conversation messages", "/history"), ) for name, description, command_text in commands: diff --git a/nanobot/channels/telegram.py b/nanobot/channels/telegram.py index 1e392dd16..fdc578e83 100644 --- a/nanobot/channels/telegram.py +++ b/nanobot/channels/telegram.py @@ -12,7 +12,14 @@ from typing import Any, Literal from loguru import logger from pydantic import Field -from telegram import BotCommand, InlineKeyboardButton, InlineKeyboardMarkup, ReactionTypeEmoji, ReplyParameters, Update +from telegram import ( + BotCommand, + InlineKeyboardButton, + InlineKeyboardMarkup, + ReactionTypeEmoji, + ReplyParameters, + Update, +) from telegram.error import BadRequest, NetworkError, TimedOut from telegram.ext import Application, CallbackQueryHandler, ContextTypes, MessageHandler, filters from telegram.request import HTTPXRequest @@ -253,6 +260,7 @@ class TelegramChannel(BaseChannel): BotCommand("stop", "Stop the current task"), BotCommand("restart", "Restart the bot"), BotCommand("status", "Show bot status"), + BotCommand("history", "Show recent conversation messages"), BotCommand("dream", "Run Dream memory consolidation now"), BotCommand("dream_log", "Show the latest Dream memory change"), BotCommand("dream_restore", "Restore Dream memory to an earlier version"), diff --git a/tests/channels/test_discord_channel.py b/tests/channels/test_discord_channel.py index 356e94d0e..d5882ca99 100644 --- a/tests/channels/test_discord_channel.py +++ b/tests/channels/test_discord_channel.py @@ -865,7 +865,7 @@ async def test_slash_new_is_blocked_for_disallowed_user() -> None: assert handled == [] -@pytest.mark.parametrize("slash_name", ["stop", "restart", "status"]) +@pytest.mark.parametrize("slash_name", ["stop", "restart", "status", "history"]) @pytest.mark.asyncio async def test_slash_commands_forward_via_handle_message(slash_name: str) -> None: channel = DiscordChannel(DiscordConfig(enabled=True, allow_from=["*"]), MessageBus()) diff --git a/tests/channels/test_telegram_channel.py b/tests/channels/test_telegram_channel.py index 1fbfe2439..e45059222 100644 --- a/tests/channels/test_telegram_channel.py +++ b/tests/channels/test_telegram_channel.py @@ -193,6 +193,7 @@ async def test_start_creates_separate_pools_with_proxy(monkeypatch) -> None: assert builder.get_updates_request_value is poll_req assert callable(app.updater.start_polling_kwargs["error_callback"]) assert any(cmd.command == "status" for cmd in app.bot.commands) + assert any(cmd.command == "history" for cmd in app.bot.commands) assert any(cmd.command == "dream" for cmd in app.bot.commands) assert any(cmd.command == "dream_log" for cmd in app.bot.commands) assert any(cmd.command == "dream_restore" for cmd in app.bot.commands) diff --git a/tests/cli/test_restart_command.py b/tests/cli/test_restart_command.py index 1e4ba5179..f61e18923 100644 --- a/tests/cli/test_restart_command.py +++ b/tests/cli/test_restart_command.py @@ -282,6 +282,41 @@ class TestRestartCommand: assert "message 19" in response.content # most recent assert "message 0" not in response.content # too old + @pytest.mark.asyncio + async def test_history_clamps_count_and_extracts_text_blocks(self): + loop, _bus = _make_loop() + session = MagicMock() + session.get_history.return_value = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "visible text"}, + {"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}}, + ], + }, + *({"role": "assistant", "content": f"reply {i}"} for i in range(60)), + ] + loop.sessions.get_or_create.return_value = session + + msg = InboundMessage(channel="telegram", sender_id="u1", chat_id="c1", content="/history 999") + response = await loop._process_message(msg) + + assert response is not None + assert "Last 50 message(s)" in response.content + assert "visible text" not in response.content + assert "reply 59" in response.content + assert "reply 9" not in response.content + + @pytest.mark.asyncio + async def test_history_invalid_count_returns_usage(self): + loop, _bus = _make_loop() + + msg = InboundMessage(channel="telegram", sender_id="u1", chat_id="c1", content="/history nope") + response = await loop._process_message(msg) + + assert response is not None + assert response.content.startswith("Usage: /history [count]") + @pytest.mark.asyncio async def test_history_empty_session(self): loop, _bus = _make_loop()