refactor(discord): simplify tool preview links

This commit is contained in:
Coffee☕️
2026-08-02 22:29:19 -04:00
committed by Teknium
parent df9e039d2d
commit 911d8dfbf4
6 changed files with 99 additions and 125 deletions
+28 -17
View File
@@ -188,6 +188,15 @@ def _truncate_preview(text: str, max_len: int | None) -> str:
return text
@dataclass(frozen=True)
class ToolPreview:
"""A compact tool preview plus presentation facts lost to truncation."""
text: str
truncated: bool = False
url: str | None = None
_SHELL_SILENT_HEADS = {"cd", "pushd", "popd", "export", "set", "unset", "source", ".", "true", "false", ":"}
_SHELL_PIPE_TAIL_HEADS = {"head", "tail", "wc", "sort", "uniq"}
@@ -557,30 +566,33 @@ def build_tool_preview(tool_name: str, args: dict, max_len: int | None = None) -
return preview
def get_tool_preview_url(
def prepare_tool_preview(
tool_name: str,
args: dict | None,
*,
fallback: str | None = None,
) -> str | None:
"""Return the uncapped HTTP(S) URL represented by a tool preview.
fallback: str,
max_len: int,
) -> ToolPreview:
"""Build one canonical compact preview before platform formatting.
Rebuild the preview from the tool's primary display argument so callers
can recover information lost to an earlier display-length cap. ``fallback``
covers callback paths that carry a preview but no argument dictionary.
The uncapped preview is rebuilt from the tool arguments when possible so
an upstream display cap cannot discard its link target. Platforms then
receive explicit truncation and URL metadata instead of inferring either
fact from the rendered text.
"""
expanded = build_tool_preview(tool_name, args, max_len=0)
for value in (expanded, fallback):
candidate = _display_url(value)
if not candidate:
continue
full_text = build_tool_preview(tool_name, args, max_len=0) or fallback
text = _truncate_preview(full_text, max_len)
truncated = text != full_text
url = None
if truncated:
candidate = _display_url(full_text)
try:
parsed = urlsplit(candidate)
except ValueError:
continue
if parsed.scheme.lower() in {"http", "https"} and parsed.netloc:
return candidate
return None
parsed = None
if parsed and parsed.scheme.lower() in {"http", "https"} and parsed.netloc:
url = candidate
return ToolPreview(text=text, truncated=truncated, url=url)
# =========================================================================
@@ -1533,4 +1545,3 @@ def get_cute_tool_message(
# =========================================================================
# Honcho session line (one-liner with clickable OSC 8 hyperlink)
# =========================================================================
+18 -25
View File
@@ -541,7 +541,7 @@ import dataclasses
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Any, Callable, Awaitable, Tuple, Union
from typing import TYPE_CHECKING, Dict, List, Optional, Any, Callable, Awaitable, Tuple, Union
from enum import Enum
from pathlib import Path as _Path
@@ -551,6 +551,9 @@ from gateway.config import Platform, PlatformConfig
from gateway.session import SessionSource, build_session_key
from hermes_constants import get_default_hermes_root, get_hermes_dir, get_hermes_home
if TYPE_CHECKING:
from agent.display import ToolPreview
# ---------------------------------------------------------------------------
# Streaming TTS format descriptor and handle (#60671)
@@ -3095,37 +3098,27 @@ class BasePlatformAdapter(ABC):
# progress bubbles compact — they persist as permanent messages).
preview = event.preview
if preview:
full_preview = preview
from agent.display import prepare_tool_preview
cap = preview_max_len if preview_max_len > 0 else 40
if len(preview) > cap:
preview = preview[:cap - 3] + "..."
preview = self.format_tool_preview(
preview,
full_preview=full_preview,
tool_name=event.tool_name,
args=event.args,
prepared = prepare_tool_preview(
event.tool_name,
event.args,
fallback=preview,
max_len=cap,
)
return f"{emoji} {event.tool_name}: \"{preview}\""
rendered = self.format_tool_preview(prepared)
return f"{emoji} {event.tool_name}: \"{rendered}\""
return f"{emoji} {event.tool_name}..."
def format_tool_preview(
self,
preview: str,
*,
full_preview: Optional[str] = None,
tool_name: Optional[str] = None,
args: Optional[Dict[str, Any]] = None,
) -> str:
def format_tool_preview(self, preview: "ToolPreview") -> str:
"""Apply platform-native formatting to a compact tool preview.
The gateway calls this after imposing its display-length cap, while
still supplying the original preview and tool arguments. Most
platforms should preserve the compact text unchanged. Markdown
adapters may use the extra context to retain information that would
otherwise be lost during truncation (for example, Discord keeps a
truncated URL label linked to its full destination).
Most adapters only need the compact text. Rich-text adapters can use
the preview's explicit metadata to preserve details such as a URL that
was shortened for display.
"""
return preview
return preview.text
@property
def has_fatal_error(self) -> bool:
+10 -9
View File
@@ -3777,21 +3777,22 @@ class TurnRunner:
from agent.display import (
get_tool_preview_max_len,
get_tool_verb,
prepare_tool_preview,
tool_verb_connector,
verb_drops_preview,
)
_pl = get_tool_preview_max_len()
_cap = _pl if _pl > 0 else 40
_full_preview = preview
if len(preview) > _cap:
preview = preview[:_cap - 3] + "..."
_prepared_preview = prepare_tool_preview(
tool_name,
args,
fallback=preview,
max_len=_cap,
)
if _progress_adapter is not None:
preview = _progress_adapter.format_tool_preview(
preview,
full_preview=_full_preview,
tool_name=tool_name,
args=args,
)
preview = _progress_adapter.format_tool_preview(_prepared_preview)
else:
preview = _prepared_preview.text
# Friendly labels: render a human-phrased line for built-in
# tools ("🔍 Searching the web for ...") by prefixing the verb
# onto the preview the callback already computed (so the
+6 -33
View File
@@ -31,7 +31,7 @@ from urllib.parse import quote, urljoin
from agent.async_utils import (
consume_detached_task_result as _consume_background_task_result,
)
from agent.display import get_tool_preview_url
from agent.display import ToolPreview
logger = logging.getLogger(__name__)
@@ -977,40 +977,13 @@ class DiscordAdapter(BasePlatformAdapter):
PLAYBACK_TIMEOUT = 120
PLAYBACK_TIMEOUT_PADDING = 30
def format_tool_preview(
self,
preview: str,
*,
full_preview: Optional[str] = None,
tool_name: Optional[str] = None,
args: Optional[Dict[str, Any]] = None,
) -> str:
def format_tool_preview(self, preview: ToolPreview) -> str:
"""Keep a truncated URL preview clickable in Discord markdown."""
if preview.endswith("..."):
visible_prefix = preview[:-3]
elif preview.endswith(_DISCORD_ELLIPSIS):
visible_prefix = preview[:-1]
else:
return preview
if not preview.url:
return preview.text
destination = get_tool_preview_url(
tool_name or "",
args,
fallback=full_preview,
)
if (
not destination
or destination == preview
or not destination.startswith(visible_prefix)
):
return preview
# Escape the label's link delimiters and URL-encode characters that
# can prematurely terminate Discord's markdown destination.
label = (
preview.replace("\\", "\\\\").replace("[", "\\[").replace("]", "\\]")
)
href = quote(destination, safe=":/?#[]@!$&'*+,;=%")
label = re.sub(r"([\\\[\]])", r"\\\1", preview.text)
href = quote(preview.url, safe=":/?#[]@!$&'*+,;=%")
return f"[{label}]({href})"
def __init__(self, config: PlatformConfig):
+28 -9
View File
@@ -10,7 +10,7 @@ from agent.display import (
capture_local_edit_snapshot,
extract_edit_diff,
get_cute_tool_message,
get_tool_preview_url,
prepare_tool_preview,
redact_tool_args_for_display,
set_tool_preview_max_len,
_render_inline_unified_diff,
@@ -107,23 +107,42 @@ class TestBuildToolPreview:
assert build_tool_preview("terminal", []) is None
class TestGetToolPreviewUrl:
def test_recovers_uncapped_web_extract_url(self):
class TestPrepareToolPreview:
def test_recovers_and_describes_truncated_url(self):
url = "https://example.com/a/very/long/path/to/a/page"
set_tool_preview_max_len(20)
assert get_tool_preview_url("web_extract", {"urls": [url]}) == url
preview = prepare_tool_preview(
"web_extract",
{"urls": [url]},
fallback=url[:17] + "...",
max_len=20,
)
def test_uses_callback_preview_when_args_are_missing(self):
assert preview.text == url[:17] + "..."
assert preview.truncated is True
assert preview.url == url
def test_untruncated_url_has_no_link_target(self):
url = "https://example.com/page"
preview = prepare_tool_preview(
"browser_navigate", None, fallback=url, max_len=40
)
assert get_tool_preview_url("browser_navigate", None, fallback=url) == url
assert preview.text == url
assert preview.truncated is False
assert preview.url is None
def test_rejects_non_url_preview(self):
assert get_tool_preview_url(
def test_truncated_non_url_has_no_link_target(self):
preview = prepare_tool_preview(
"web_search",
{"query": "how to parse a URL"},
) is None
fallback="how to parse a URL",
max_len=12,
)
assert preview.truncated is True
assert preview.url is None
class TestCuteToolMessagePreviewLength:
+9 -32
View File
@@ -46,16 +46,13 @@ class TestDiscordFormatMessage:
class TestDiscordToolPreviewFormatting:
def test_truncated_url_keeps_full_click_target(self):
from agent.display import ToolPreview
adapter = _make_discord_adapter()
url = "https://hermes-agent.nousresearch.com/docs/gateway/discord/tool-progress"
visible = "https://hermes-agent.nousresearch..."
out = adapter.format_tool_preview(
visible,
full_preview=url,
tool_name="web_extract",
args={"urls": [url]},
)
out = adapter.format_tool_preview(ToolPreview(visible, truncated=True, url=url))
assert out == f"[{visible}]({url})"
@@ -75,42 +72,22 @@ class TestDiscordToolPreviewFormatting:
assert out is not None
assert f"[{visible}]({url})" in out
def test_url_already_shortened_upstream_uses_full_url_from_args(self):
adapter = _make_discord_adapter()
url = "https://example.com/a/very/long/path/to/a/page"
visible = "https://example.com/a/very..."
out = adapter.format_tool_preview(
visible,
full_preview=visible,
tool_name="browser_navigate",
args={"url": url},
)
assert out == f"[{visible}]({url})"
def test_untruncated_url_remains_plain(self):
from agent.display import ToolPreview
adapter = _make_discord_adapter()
url = "https://example.com/page"
out = adapter.format_tool_preview(
url,
full_preview=url,
tool_name="browser_navigate",
args={"url": url},
)
out = adapter.format_tool_preview(ToolPreview(url))
assert out == url
def test_truncated_non_url_remains_plain(self):
from agent.display import ToolPreview
adapter = _make_discord_adapter()
visible = "a long search query that was trunc..."
out = adapter.format_tool_preview(
visible,
full_preview="a long search query that was truncated for display",
tool_name="web_search",
args={"query": "a long search query that was truncated for display"},
)
out = adapter.format_tool_preview(ToolPreview(visible, truncated=True))
assert out == visible