fix(tool-hints): deduplicate by formatted string + per-line inline display

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
This commit is contained in:
xzq.xu 2026-04-08 13:12:47 +08:00 committed by Xubin Ren
parent 512c3b88e3
commit 049ce9baae
2 changed files with 8 additions and 6 deletions

View File

@ -1379,7 +1379,9 @@ class FeishuChannel(BaseChannel):
if buf and buf.card_id: if buf and buf.card_id:
if buf.tool_hint_len > 0: if buf.tool_hint_len > 0:
buf.text = buf.text[:-buf.tool_hint_len] 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.text += suffix
buf.tool_hint_len = len(suffix) buf.tool_hint_len = len(suffix)
buf.sequence += 1 buf.sequence += 1

View File

@ -309,7 +309,7 @@ class TestToolHintInlineStreaming:
await ch.send(msg) await ch.send(msg)
buf = ch._stream_bufs["oc_chat1"] 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.tool_hint_len > 0
assert buf.sequence == 3 assert buf.sequence == 3
ch._client.cardkit.v1.card_element.content.assert_called_once() 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): 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.""" """When new delta arrives, the tool hint is kept as permanent content and delta appends after it."""
ch = _make_channel() ch = _make_channel()
suffix = "\n\n---\n🔧 web_fetch(\"url\")" suffix = "\n\n🔧 web_fetch(\"url\")\n\n"
ch._stream_bufs["oc_chat1"] = _FeishuStreamBuf( ch._stream_bufs["oc_chat1"] = _FeishuStreamBuf(
text="Partial answer" + suffix, text="Partial answer" + suffix,
card_id="card_1", sequence=3, last_edit=0.0, 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("$ cd /project") == 0
assert buf.text.count("$ git status") == 1 assert buf.text.count("$ git status") == 1
assert buf.text.startswith("Partial answer") assert buf.text.startswith("Partial answer")
assert buf.text.endswith("🔧 $ git status") assert "🔧 $ git status" in buf.text
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_tool_hint_preserved_on_resuming_flush(self): async def test_tool_hint_preserved_on_resuming_flush(self):
"""When _resuming flushes the buffer, tool hint is kept as permanent content.""" """When _resuming flushes the buffer, tool hint is kept as permanent content."""
ch = _make_channel() ch = _make_channel()
suffix = "\n\n---\n🔧 $ cd /project" suffix = "\n\n🔧 $ cd /project\n\n"
ch._stream_bufs["oc_chat1"] = _FeishuStreamBuf( ch._stream_bufs["oc_chat1"] = _FeishuStreamBuf(
text="Partial answer" + suffix, text="Partial answer" + suffix,
card_id="card_1", sequence=2, last_edit=0.0, 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): 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.""" """When final _stream_end closes the card, tool hint is kept in the final text."""
ch = _make_channel() ch = _make_channel()
suffix = "\n\n---\n🔧 web_fetch(\"url\")" suffix = "\n\n🔧 web_fetch(\"url\")\n\n"
ch._stream_bufs["oc_chat1"] = _FeishuStreamBuf( ch._stream_bufs["oc_chat1"] = _FeishuStreamBuf(
text="Final content" + suffix, text="Final content" + suffix,
card_id="card_1", sequence=3, last_edit=0.0, card_id="card_1", sequence=3, last_edit=0.0,