Per-tool emoji on tool cards (gateway-resolved via hermes get_tool_emoji)
tool.start gains an optional cosmetic 'emoji' field, resolved server-side through hermes' own display layer (active-skin overrides, then the tool registry's per-tool emoji) so icons track the user's hermes theme and new/plugin tools get their glyph for free. Omitted for unknown tools so the app falls back to its default wrench. - protocol.py: tool_start(emoji=...) kwarg, payload field when set - adapter.py: _tool_emoji() helper (lazy import, None on unknown/failure) - frames.schema.json + docs/04: field documented - app: ToolStartPayload.emoji -> ToolItem.emoji -> ToolCard header - tests: frame shape, resolution/fallback, end-to-end tool.start emoji
This commit is contained in:
1 parent
e6015033b6
commit
7f0bdcbbc1
8 files changed
+98
-3
No files matched your search
@@ -2285,6 +2285,67 @@ def test_parse_tool_line_or_block_verbose(plugin):
|
||||
assert name2 == "web_search" and preview2 == "x" and args2 is None
|
||||
|
||||
|
||||
def test_tool_start_frame_emoji_field(plugin):
|
||||
"""``tool.start`` carries the cosmetic emoji when given, omits it when
|
||||
None (the app then falls back to its own default glyph)."""
|
||||
pf = plugin.protocol.tool_start
|
||||
f = pf("android:default", 3, "terminal", emoji="💻")
|
||||
assert f.payload["emoji"] == "💻"
|
||||
f2 = pf("android:default", 3, "terminal")
|
||||
assert "emoji" not in f2.payload
|
||||
f3 = pf("android:default", 3, "terminal", emoji=None)
|
||||
assert "emoji" not in f3.payload
|
||||
|
||||
|
||||
def test_tool_emoji_resolves_via_hermes_display(plugin, monkeypatch):
|
||||
"""``_tool_emoji`` delegates to hermes' ``get_tool_emoji`` (skin overrides
|
||||
-> registry) and maps unknown tools / failures to None (field omitted)."""
|
||||
a = plugin.adapter
|
||||
seen = {}
|
||||
|
||||
def fake_get_tool_emoji(name, default="⚡"):
|
||||
seen[name] = default
|
||||
return {"terminal": "💻", "read_file": "📖"}.get(name, default)
|
||||
|
||||
monkeypatch.setattr("agent.display.get_tool_emoji", fake_get_tool_emoji)
|
||||
assert a._tool_emoji("terminal") == "💻"
|
||||
assert a._tool_emoji("read_file") == "📖"
|
||||
# Unknown tool: the adapter asks for an empty default and drops it.
|
||||
assert a._tool_emoji("no_such_tool") is None
|
||||
assert seen["no_such_tool"] == ""
|
||||
# A display-layer failure degrades to None, never a crash.
|
||||
def boom(name, default="⚡"):
|
||||
raise RuntimeError("no display")
|
||||
|
||||
monkeypatch.setattr("agent.display.get_tool_emoji", boom)
|
||||
assert a._tool_emoji("terminal") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tool_start_frame_carries_emoji(plugin, adapter, ws_client, monkeypatch):
|
||||
"""A tool-progress line emits ``tool.start`` with the per-tool emoji
|
||||
resolved from hermes' display layer."""
|
||||
ws, _ = ws_client
|
||||
monkeypatch.setattr(
|
||||
"agent.display.get_tool_emoji",
|
||||
lambda name, default="⚡": "💻" if name == "terminal" else default,
|
||||
)
|
||||
res = await adapter.send('android:default', '💻 terminal: "ls -la"')
|
||||
assert res.success
|
||||
frames = await recv_until(ws, lambda f: f.get("type") == "tool.start")
|
||||
payload = frames[-1]["payload"]
|
||||
assert payload["name"] == "terminal"
|
||||
assert payload["emoji"] == "💻"
|
||||
# Unknown tool -> field omitted (app falls back to its default glyph).
|
||||
monkeypatch.setattr("agent.display.get_tool_emoji", lambda name, default="⚡": default)
|
||||
res2 = await adapter.send('android:default', '🔧 patch: "x"')
|
||||
assert res2.success
|
||||
frames2 = await recv_until(
|
||||
ws, lambda f: f.get("type") == "tool.start" and f["payload"]["name"] == "patch"
|
||||
)
|
||||
assert "emoji" not in frames2[-1]["payload"]
|
||||
|
||||
|
||||
# ── Slash-command catalog (the app's "/" drawer) ──────────────────────────
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user