From 049ce9baaed553ff11e982568a4020c7426a6566 Mon Sep 17 00:00:00 2001 From: "xzq.xu" Date: Wed, 8 Apr 2026 13:12:47 +0800 Subject: [PATCH] fix(tool-hints): deduplicate by formatted string + per-line inline display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two display fixes based on real-world Feishu testing: 1. tool_hints.py: format_tool_hints now deduplicates by comparing the fully formatted hint string instead of tool name alone. This fixes `ls /Desktop` and `ls /Downloads` being incorrectly merged as `ls /Desktop Ɨ 2`. Truly identical calls still fold correctly. (_group_consecutive and all abbreviation logic preserved unchanged.) 2. feishu.py: inline tool hints now display one tool per line with šŸ”§ prefix, and use double-newline trailing to prevent Setext heading rendering when followed by markdown `---`. Made-with: Cursor --- nanobot/channels/feishu.py | 4 +++- tests/channels/test_feishu_streaming.py | 10 +++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/nanobot/channels/feishu.py b/nanobot/channels/feishu.py index 2441c20c5..365732595 100644 --- a/nanobot/channels/feishu.py +++ b/nanobot/channels/feishu.py @@ -1379,7 +1379,9 @@ class FeishuChannel(BaseChannel): if buf and buf.card_id: if buf.tool_hint_len > 0: buf.text = buf.text[:-buf.tool_hint_len] - suffix = f"\n\n---\nšŸ”§ {hint}" + lines = self._format_tool_hint_lines(hint).split("\n") + formatted = "\n".join(f"šŸ”§ {ln}" for ln in lines if ln.strip()) + suffix = f"\n\n{formatted}\n\n" buf.text += suffix buf.tool_hint_len = len(suffix) buf.sequence += 1 diff --git a/tests/channels/test_feishu_streaming.py b/tests/channels/test_feishu_streaming.py index 1559ef8d3..6f5fc0580 100644 --- a/tests/channels/test_feishu_streaming.py +++ b/tests/channels/test_feishu_streaming.py @@ -309,7 +309,7 @@ class TestToolHintInlineStreaming: await ch.send(msg) buf = ch._stream_bufs["oc_chat1"] - assert buf.text.endswith('šŸ”§ web_fetch("https://example.com")') + assert 'šŸ”§ web_fetch("https://example.com")' in buf.text assert buf.tool_hint_len > 0 assert buf.sequence == 3 ch._client.cardkit.v1.card_element.content.assert_called_once() @@ -319,7 +319,7 @@ class TestToolHintInlineStreaming: async def test_tool_hint_preserved_on_next_delta(self): """When new delta arrives, the tool hint is kept as permanent content and delta appends after it.""" ch = _make_channel() - suffix = "\n\n---\nšŸ”§ web_fetch(\"url\")" + suffix = "\n\nšŸ”§ web_fetch(\"url\")\n\n" ch._stream_bufs["oc_chat1"] = _FeishuStreamBuf( text="Partial answer" + suffix, card_id="card_1", sequence=3, last_edit=0.0, @@ -376,13 +376,13 @@ class TestToolHintInlineStreaming: assert buf.text.count("$ cd /project") == 0 assert buf.text.count("$ git status") == 1 assert buf.text.startswith("Partial answer") - assert buf.text.endswith("šŸ”§ $ git status") + assert "šŸ”§ $ git status" in buf.text @pytest.mark.asyncio async def test_tool_hint_preserved_on_resuming_flush(self): """When _resuming flushes the buffer, tool hint is kept as permanent content.""" ch = _make_channel() - suffix = "\n\n---\nšŸ”§ $ cd /project" + suffix = "\n\nšŸ”§ $ cd /project\n\n" ch._stream_bufs["oc_chat1"] = _FeishuStreamBuf( text="Partial answer" + suffix, card_id="card_1", sequence=2, last_edit=0.0, @@ -401,7 +401,7 @@ class TestToolHintInlineStreaming: async def test_tool_hint_preserved_on_final_stream_end(self): """When final _stream_end closes the card, tool hint is kept in the final text.""" ch = _make_channel() - suffix = "\n\n---\nšŸ”§ web_fetch(\"url\")" + suffix = "\n\nšŸ”§ web_fetch(\"url\")\n\n" ch._stream_bufs["oc_chat1"] = _FeishuStreamBuf( text="Final content" + suffix, card_id="card_1", sequence=3, last_edit=0.0,