feat(code): toast when opening a clicked URL (#4368)

Clicking a URL in the interactive TUI now shows a confirmation toast
indicating that the link is opening in the default browser.

---

Clicking links in the TUI previously opened the browser silently, with
no in-app feedback. Shared link handling now posts an informational
toast on successful opens:

`Opening URL in default browser: <url>`

The displayed URL is sanitized with `strip_dangerous_unicode` and
rendered with `markup=False`, matching the existing blocked-URL and
browser-failure notifications.

This covers both Rich style links and Textual Markdown links, including
assistant messages, the startup splash LangSmith tracing links, `/auth`
docs links, MCP login, thread selector, welcome, and similar modal/link
surfaces that share the helper.

The success toast is enabled by default and can be disabled with either:

```toml
[ui]
show_url_open_toast = false
```

or:

```bash
DEEPAGENTS_CODE_SHOW_URL_OPEN_TOAST=0
```

Made by [Open
SWE](https://openswe.vercel.app/agents/0c261ed0-9c77-04cb-35fa-cb8f1c1da3db)

Credit to @tom21100227 for the initial idea

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
Mason Daugherty
2026-07-01 17:20:39 -04:00
committed by GitHub
parent fa5e8b91d6
commit 434f29e5cb
8 changed files with 490 additions and 38 deletions
+7
View File
@@ -218,6 +218,13 @@ config value on launch, so a `/scrollbar` toggle will not appear to "stick"
across restarts while the env var remains set.
"""
SHOW_URL_OPEN_TOAST = "DEEPAGENTS_CODE_SHOW_URL_OPEN_TOAST"
"""Show a confirmation toast after clicking a URL that opens in a browser.
Defaults to enabled; set to a falsy value (`0`, `false`, `no`, `off`, or empty)
to suppress the success toast while still opening URLs normally.
"""
THEME = "DEEPAGENTS_CODE_THEME"
"""Force the CLI to launch with this theme name when set."""
+1 -1
View File
@@ -7809,7 +7809,7 @@ class DeepAgentsApp(App):
# returns True on errors so transient state failures suppress this warning
# rather than showing a false empty-thread note.
parts: list[str | Content | tuple[str, str | TStyle]] = [
f"Opening tracing project {project_name!r}:\n",
f"Opening tracing project {project_name!r} in default browser:\n",
(url, TStyle(dim=True, italic=True, link=url)),
]
if not await self._has_conversation_messages():
@@ -860,6 +860,15 @@ _STATIC_OPTIONS: tuple[ConfigOption, ...] = (
default=False,
env_var=_env_vars.NO_TERMINAL_ESCAPE,
),
ConfigOption(
key="display.show_url_open_toast",
group="Display",
summary="Show a confirmation toast after clicking a URL.",
kind=OptionKind.BOOL,
default=True,
env_var=_env_vars.SHOW_URL_OPEN_TOAST,
toml_keys=("ui", "show_url_open_toast"),
),
ConfigOption(
key="display.onboarding_integrations_screen",
group="Display",
+175 -30
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import ast
import asyncio
import logging
import webbrowser
@@ -13,9 +14,116 @@ if TYPE_CHECKING:
from textual.app import App
from textual.events import Click, MouseMove
def _event_app(event: object, app: App | None = None) -> App | None:
"""Return the app for a click event, including real Textual widgets."""
if app is not None:
return app
widget = getattr(event, "widget", None)
widget_app = getattr(widget, "app", None)
if widget_app is not None:
return widget_app
event_app = getattr(event, "app", None)
return event_app if event_app is not None else None
logger = logging.getLogger(__name__)
def _notify(
app: App | None, message: str, *, severity: str, timeout: int | None = None
) -> None:
"""Post a best-effort Textual toast, tolerating apps without `notify`.
Centralizes the guard/`markup=False`/exception-swallowing pattern shared by
every toast in this module so the call sites cannot drift apart. `markup` is
always disabled so URL content can never be interpreted as Textual markup.
Args:
app: App-like object used to post the toast, or `None`.
message: The toast body. Callers must sanitize any URL with
`strip_dangerous_unicode` before interpolating it here.
severity: Textual notification severity (e.g. `information`, `warning`).
timeout: Optional toast lifetime in seconds; the Textual default is used
when omitted.
"""
notify = getattr(app, "notify", None)
if not callable(notify):
return
kwargs: dict[str, object] = {"severity": severity, "markup": False}
if timeout is not None:
kwargs["timeout"] = timeout
try:
notify(message, **kwargs)
except (AttributeError, TypeError):
logger.debug("Could not send notification", exc_info=True)
def _url_open_toasts_enabled() -> bool:
"""Return whether successful URL-open clicks should show a toast."""
from deepagents_code.config_manifest import (
get_option,
load_config_toml,
resolve_scalar,
)
option = get_option("display.show_url_open_toast")
if option is None:
return True
value, _ = resolve_scalar(option, toml_data=load_config_toml())
return bool(value)
def _notify_url_opened(app: App | None, url: str) -> None:
"""Show the URL-opened toast when the user has not opted out."""
if app is None or not _url_open_toasts_enabled():
return
_notify(
app,
f"Opening URL in default browser: {strip_dangerous_unicode(url)}",
severity="information",
timeout=4,
)
def _link_action_url(click: object) -> str | None:
"""Extract a URL from Textual's Markdown `link(...)` click action.
Args:
click: The `@click` style metadata value to inspect.
Returns:
The parsed URL when the metadata is a quoted `link(...)` action.
"""
if not isinstance(click, str):
return None
if not click.startswith("link(") or not click.endswith(")"):
return None
try:
url = ast.literal_eval(click[len("link(") : -1].strip())
except (SyntaxError, ValueError):
return None
return url if isinstance(url, str) and url else None
def _style_url(style: object) -> str | None:
"""Return a URL from either Rich link style or Textual click metadata.
Args:
style: The Textual event style to inspect.
Returns:
The URL embedded in the style, if one is present.
"""
url = getattr(style, "link", None)
if isinstance(url, str) and url:
return url
meta = getattr(style, "meta", None)
if not isinstance(meta, dict):
return None
return _link_action_url(meta.get("@click"))
def event_targets_link(event: MouseMove) -> bool:
"""Return whether the style under the mouse points to a clickable link.
@@ -29,14 +137,40 @@ def event_targets_link(event: MouseMove) -> bool:
Returns:
`True` when the hovered character belongs to a link span.
"""
style = event.style
if style.link:
return True
click = style.meta.get("@click")
return isinstance(click, str) and click.startswith("link(")
return _style_url(event.style) is not None
async def open_url_async(url: str, *, app: App) -> bool:
async def open_checked_url_async(
url: str, *, app: App, notify_on_success: bool = False
) -> bool:
"""Open a URL after applying the shared URL safety check.
Args:
url: The URL to validate and open.
app: App used to post browser-open notifications.
notify_on_success: Whether to post an informational toast when the
browser accepts the URL.
Returns:
`True` when the URL passed safety checks and the browser accepted it;
`False` when the URL was blocked or the browser could not open it.
"""
safety = check_url_safety(url)
if not safety.safe:
detail = safety.warnings[0] if safety.warnings else "Suspicious URL"
logger.warning("Blocked suspicious URL: %s (%s)", url, detail)
_notify(
app,
f"Blocked suspicious URL: {strip_dangerous_unicode(url)}\n{detail}",
severity="warning",
)
return False
return await open_url_async(url, app=app, notify_on_success=notify_on_success)
async def open_url_async(
url: str, *, app: App, notify_on_success: bool = False
) -> bool:
"""Open url in a browser and toast on failure.
Runs `webbrowser.open` in a thread, catches the platform errors
@@ -46,7 +180,9 @@ async def open_url_async(url: str, *, app: App) -> bool:
Args:
url: The URL to open.
app: App used to post the failure toast.
app: App used to post browser-open notifications.
notify_on_success: Whether to post an informational toast when the
browser accepts the URL.
Returns:
`True` when the browser accepted the URL; `False` otherwise
@@ -58,16 +194,18 @@ async def open_url_async(url: str, *, app: App) -> bool:
logger.warning("webbrowser.open failed for %s: %s", url, exc, exc_info=True)
opened = False
if not opened:
app.notify(
f"Could not open a browser. URL: {url}",
_notify(
app,
f"Could not open a browser. URL: {strip_dangerous_unicode(url)}",
severity="warning",
timeout=8,
markup=False,
)
elif notify_on_success:
_notify_url_opened(app, url)
return opened
def open_style_link(event: Click) -> None:
def open_style_link(event: Click, *, app: App | None = None) -> None:
"""Open the URL from a Rich link style on click, if present.
Rich `Style(link=...)` embeds OSC 8 terminal hyperlinks, but Textual's
@@ -79,14 +217,19 @@ def open_style_link(event: Click) -> None:
homograph domains) are blocked and not opened; the event bubbles and a
warning is logged and displayed as a Textual notification.
On success the event is stopped so it does not bubble further. On failure
(e.g. no browser available in a headless environment) the error is logged at
debug level and the event bubbles normally.
On success the event is stopped so it does not bubble further and, unless
the user has opted out, a best-effort informational toast reports the URL
that was opened. If the browser cannot be launched -- either
`webbrowser.open` raises or the backend declines and returns a falsy value
-- a warning toast with the URL is shown so it can be copied manually, the
failure is logged, and the event bubbles normally.
Args:
event: The Textual click event to inspect.
app: App used to post browser-open notifications.
"""
url = event.style.link
notify_app = _event_app(event, app)
url = _style_url(event.style)
if not url:
return
@@ -94,23 +237,25 @@ def open_style_link(event: Click) -> None:
if not safety.safe:
detail = safety.warnings[0] if safety.warnings else "Suspicious URL"
logger.warning("Blocked suspicious URL: %s (%s)", url, detail)
try:
app = getattr(event, "app", None)
notify = getattr(app, "notify", None)
if callable(notify):
safe_url = strip_dangerous_unicode(url)
notify(
f"Blocked suspicious URL: {safe_url}\n{detail}",
severity="warning",
markup=False,
)
except (AttributeError, TypeError):
logger.debug("Could not send URL-blocked notification", exc_info=True)
_notify(
notify_app,
f"Blocked suspicious URL: {strip_dangerous_unicode(url)}\n{detail}",
severity="warning",
)
return
try:
webbrowser.open(url)
except Exception:
logger.debug("Could not open browser for URL: %s", url, exc_info=True)
opened = webbrowser.open(url)
except (webbrowser.Error, OSError) as exc:
logger.warning("webbrowser.open failed for %s: %s", url, exc, exc_info=True)
opened = False
if not opened:
_notify(
notify_app,
f"Could not open a browser. URL: {strip_dangerous_unicode(url)}",
severity="warning",
timeout=8,
)
return
_notify_url_opened(notify_app, url)
event.stop()
+11 -2
View File
@@ -43,7 +43,11 @@ from deepagents_code.widgets._js_eval_display import (
JsEvalStdout,
parse_js_eval_blocks,
)
from deepagents_code.widgets._links import event_targets_link, open_style_link
from deepagents_code.widgets._links import (
event_targets_link,
open_checked_url_async,
open_style_link,
)
from deepagents_code.widgets.diff import compose_diff_lines
if TYPE_CHECKING:
@@ -718,7 +722,7 @@ class AssistantMessage(Vertical):
"""
from textual.widgets import Markdown
yield Markdown("", id="assistant-content")
yield Markdown("", id="assistant-content", open_links=False)
def on_mount(self) -> None:
"""Store reference to markdown widget."""
@@ -743,6 +747,11 @@ class AssistantMessage(Vertical):
if self._markdown is not None:
self._markdown.styles.pointer = "text"
async def on_markdown_link_clicked(self, event: Markdown.LinkClicked) -> None:
"""Open Markdown links with the same toast feedback as style links."""
event.stop()
await open_checked_url_async(event.href, app=self.app, notify_on_success=True)
def _get_markdown(self) -> Markdown:
"""Get the markdown widget, querying if not cached.
+5 -2
View File
@@ -3885,7 +3885,10 @@ class TestTraceCommand:
mock_open.assert_called_once_with(expected_url)
app_msgs = app.query(AppMessage)
rendered = "\n".join(str(w._content) for w in app_msgs)
assert f"Opening tracing project 'proj':\n{expected_url}" in rendered
assert (
f"Opening tracing project 'proj' in default browser:\n{expected_url}"
in rendered
)
async def test_trace_warns_when_no_messages_sent(self) -> None:
"""Should append a note when the thread has no messages yet."""
@@ -4225,7 +4228,7 @@ class TestTraceCommand:
app_msgs = app.query(AppMessage)
rendered = "\n".join(str(w._content) for w in app_msgs)
assert (
"Opening tracing project 'proj':\n"
"Opening tracing project 'proj' in default browser:\n"
"https://smith.langchain.com/t/test-thread-123"
) in rendered
+231 -3
View File
@@ -1,9 +1,21 @@
"""Unit tests for style-link click handling."""
import os
import webbrowser
from types import SimpleNamespace
from typing import TYPE_CHECKING, cast
from unittest.mock import MagicMock, patch
from deepagents_code.widgets._links import event_targets_link, open_style_link
from deepagents_code._env_vars import SHOW_URL_OPEN_TOAST
from deepagents_code.widgets._links import (
event_targets_link,
open_checked_url_async,
open_style_link,
open_url_async,
)
if TYPE_CHECKING:
from textual.app import App
def _move_event(
@@ -43,15 +55,231 @@ def _event_with_link(url: str) -> SimpleNamespace:
)
def _event_with_meta(meta: dict[str, str]) -> SimpleNamespace:
"""Build a minimal click event whose URL comes from style metadata."""
return SimpleNamespace(
style=SimpleNamespace(link=None, meta=meta),
app=SimpleNamespace(notify=MagicMock()),
stop=MagicMock(),
)
def test_open_style_link_opens_browser_and_stops_event() -> None:
"""Safe links should open and stop event propagation."""
"""Safe links should open, toast confirmation, and stop event propagation."""
event = _event_with_link("https://example.com")
with patch("deepagents_code.widgets._links.webbrowser.open") as mock_open:
with (
patch.dict(os.environ, {SHOW_URL_OPEN_TOAST: "1"}),
patch("deepagents_code.widgets._links.webbrowser.open") as mock_open,
):
mock_open.return_value = True
open_style_link(event) # ty: ignore
mock_open.assert_called_once_with("https://example.com")
event.stop.assert_called_once()
event.app.notify.assert_called_once()
args, kwargs = event.app.notify.call_args
assert args[0] == "Opening URL in default browser: https://example.com"
assert kwargs["severity"] == "information"
assert kwargs["markup"] is False
assert kwargs["timeout"] == 4
def test_open_style_link_stops_event_even_if_toast_fails() -> None:
"""A failing success toast must not turn a successful open into a bubble."""
event = _event_with_link("https://example.com")
event.app.notify.side_effect = TypeError("notify signature mismatch")
with (
patch.dict(os.environ, {SHOW_URL_OPEN_TOAST: "1"}),
patch("deepagents_code.widgets._links.webbrowser.open", return_value=True),
):
open_style_link(event) # ty: ignore
event.app.notify.assert_called_once()
event.stop.assert_called_once()
def test_open_style_link_notifies_from_event_widget_app() -> None:
"""Real Textual click events expose the app through `event.widget.app`."""
notify = MagicMock()
event = SimpleNamespace(
style=SimpleNamespace(link="https://example.com", meta={}),
widget=SimpleNamespace(app=SimpleNamespace(notify=notify)),
stop=MagicMock(),
)
with (
patch.dict(os.environ, {SHOW_URL_OPEN_TOAST: "1"}),
patch("deepagents_code.widgets._links.webbrowser.open", return_value=True),
):
open_style_link(event) # ty: ignore
notify.assert_called_once()
args, kwargs = notify.call_args
assert args[0] == "Opening URL in default browser: https://example.com"
assert kwargs["severity"] == "information"
assert kwargs["markup"] is False
event.stop.assert_called_once()
async def test_open_url_async_can_toast_on_success() -> None:
"""Async link opening can opt into the same success toast."""
notify = MagicMock()
app = cast("App[None]", SimpleNamespace(notify=notify))
with (
patch.dict(os.environ, {SHOW_URL_OPEN_TOAST: "1"}),
patch("deepagents_code.widgets._links.webbrowser.open", return_value=True),
):
opened = await open_url_async(
"https://example.com",
app=app,
notify_on_success=True,
)
assert opened is True
notify.assert_called_once()
args, kwargs = notify.call_args
assert args[0] == "Opening URL in default browser: https://example.com"
assert kwargs["severity"] == "information"
assert kwargs["markup"] is False
async def test_open_checked_url_async_blocks_suspicious_url() -> None:
"""Async checked opening should block suspicious URLs before the browser."""
notify = MagicMock()
app = cast("App[None]", SimpleNamespace(notify=notify))
with patch("deepagents_code.widgets._links.webbrowser.open") as mock_open:
opened = await open_checked_url_async(
"https://example.com/\u200b[admin]",
app=app,
notify_on_success=True,
)
assert opened is False
mock_open.assert_not_called()
notify.assert_called_once()
args, kwargs = notify.call_args
assert "Blocked suspicious URL" in args[0]
assert "https://example.com/[admin]" in args[0]
assert kwargs["severity"] == "warning"
assert kwargs["markup"] is False
async def test_open_url_async_warns_on_failure() -> None:
"""Async link opening warns with the URL when the browser declines."""
notify = MagicMock()
app = cast("App[None]", SimpleNamespace(notify=notify))
with patch("deepagents_code.widgets._links.webbrowser.open", return_value=False):
opened = await open_url_async("https://example.com", app=app)
assert opened is False
notify.assert_called_once()
args, kwargs = notify.call_args
assert "https://example.com" in args[0]
assert kwargs["severity"] == "warning"
assert kwargs["markup"] is False
def test_open_style_link_opens_markdown_link_action() -> None:
"""Markdown `@click=link(...)` metadata should open like Rich link styles."""
event = _event_with_meta({"@click": "link('https://example.com/docs')"})
with (
patch.dict(os.environ, {SHOW_URL_OPEN_TOAST: "1"}),
patch("deepagents_code.widgets._links.webbrowser.open") as mock_open,
):
mock_open.return_value = True
open_style_link(event) # ty: ignore
mock_open.assert_called_once_with("https://example.com/docs")
event.stop.assert_called_once()
event.app.notify.assert_called_once()
def test_open_style_link_env_can_suppress_success_toast() -> None:
"""The env var can disable success toasts while still opening URLs."""
event = _event_with_link("https://example.com")
with (
patch.dict(os.environ, {SHOW_URL_OPEN_TOAST: "0"}),
patch(
"deepagents_code.config_manifest.load_config_toml",
return_value={"ui": {"show_url_open_toast": True}},
),
patch("deepagents_code.widgets._links.webbrowser.open", return_value=True),
):
open_style_link(event) # ty: ignore
event.stop.assert_called_once()
event.app.notify.assert_not_called()
def test_open_style_link_config_can_suppress_success_toast() -> None:
"""The config file can disable success toasts when env is unset."""
event = _event_with_link("https://example.com")
with (
patch.dict(os.environ, {SHOW_URL_OPEN_TOAST: ""}),
patch(
"deepagents_code.config_manifest.load_config_toml",
return_value={"ui": {"show_url_open_toast": False}},
),
patch("deepagents_code.widgets._links.webbrowser.open", return_value=True),
):
open_style_link(event) # ty: ignore
event.stop.assert_called_once()
event.app.notify.assert_not_called()
def test_open_style_link_warns_when_browser_does_not_open() -> None:
"""When the browser backend declines, warn the user and bubble the event."""
event = _event_with_link("https://example.com")
with patch("deepagents_code.widgets._links.webbrowser.open") as mock_open:
mock_open.return_value = False
open_style_link(event) # ty: ignore
mock_open.assert_called_once_with("https://example.com")
event.stop.assert_not_called()
event.app.notify.assert_called_once()
args, kwargs = event.app.notify.call_args
assert "https://example.com" in args[0]
assert kwargs["severity"] == "warning"
assert kwargs["markup"] is False
def test_open_style_link_warns_when_browser_open_raises() -> None:
"""A `webbrowser.Error` should warn the user and bubble the event."""
event = _event_with_link("https://example.com")
with patch(
"deepagents_code.widgets._links.webbrowser.open",
side_effect=webbrowser.Error("no browser backend"),
):
open_style_link(event) # ty: ignore
event.stop.assert_not_called()
event.app.notify.assert_called_once()
args, kwargs = event.app.notify.call_args
assert "https://example.com" in args[0]
assert kwargs["severity"] == "warning"
assert kwargs["markup"] is False
def test_open_style_link_ignores_malformed_markdown_link_action() -> None:
"""Malformed Markdown link metadata should not reach the browser opener."""
event = _event_with_meta({"@click": "link(https://example.com)"})
with patch("deepagents_code.widgets._links.webbrowser.open") as mock_open:
open_style_link(event) # ty: ignore
mock_open.assert_not_called()
event.stop.assert_not_called()
event.app.notify.assert_not_called()
@@ -8,6 +8,7 @@ import pytest
from rich.style import Style
from textual.app import App, ComposeResult
from textual.content import Content
from textual.widgets import Markdown
from deepagents_code import theme
from deepagents_code.input import INPUT_HIGHLIGHT_PATTERN
@@ -356,6 +357,56 @@ class TestAssistantMessageLinkPointer:
assert msg._markdown is not None
assert msg._markdown.styles.pointer == "text"
async def test_markdown_open_links_is_disabled(self) -> None:
"""The app handles Markdown links so it can show URL-opened toasts."""
async with _AssistantMessageApp().run_test() as pilot:
markdown = pilot.app.query_one("#assistant-content", Markdown)
assert markdown._open_links is False
async def test_markdown_link_clicked_uses_checked_toast_helper(self) -> None:
"""Clicked Markdown links should use the checked browser/toast helper."""
async with _AssistantMessageApp().run_test() as pilot:
msg = pilot.app.query_one("#assistant", AssistantMessage)
event = SimpleNamespace(href="https://example.com/docs", stop=MagicMock())
with patch(
"deepagents_code.widgets.messages.open_checked_url_async",
new=AsyncMock(return_value=True),
) as mock_open:
await msg.on_markdown_link_clicked(event) # ty: ignore
event.stop.assert_called_once()
mock_open.assert_awaited_once_with(
"https://example.com/docs",
app=pilot.app,
notify_on_success=True,
)
async def test_markdown_link_clicked_blocks_suspicious_url(self) -> None:
"""Markdown links should apply the same URL safety check as style links."""
async with _AssistantMessageApp().run_test() as pilot:
msg = pilot.app.query_one("#assistant", AssistantMessage)
event = SimpleNamespace(
href="https://example.com/\u200b[admin]",
stop=MagicMock(),
)
with (
patch.object(pilot.app, "notify") as notify,
patch("deepagents_code.widgets._links.webbrowser.open") as mock_open,
):
await msg.on_markdown_link_clicked(event) # ty: ignore
event.stop.assert_called_once()
mock_open.assert_not_called()
notify.assert_called_once()
args, kwargs = notify.call_args
assert "Blocked suspicious URL" in args[0]
assert "https://example.com/[admin]" in args[0]
assert kwargs["severity"] == "warning"
assert kwargs["markup"] is False
def test_mouse_move_before_mount_is_noop(self) -> None:
"""Hovering before mount (no markdown widget yet) must not raise."""
msg = AssistantMessage()