From f4b27c0fee0b5bd1aafccef698b90b031a91bc3a Mon Sep 17 00:00:00 2001 From: Mukil Loganathan Date: Wed, 3 Jun 2026 14:50:37 -0400 Subject: [PATCH] feat: add Slack Open in Web link (#1392) * feat: add Slack Open in Web link Co-authored-by: open-swe[bot] * fix: skip web link for Slack reviewer runs Co-authored-by: open-swe[bot] * test: accept include_dashboard_link kwarg in Slack reviewer test double --------- Co-authored-by: open-swe[bot] Co-authored-by: Johannes du Plessis <51395795+johannes117@users.noreply.github.com> Co-authored-by: open-swe[bot] --- agent/utils/slack.py | 28 ++++++++++++++--- agent/webapp.py | 4 ++- tests/test_github_issue_webhook.py | 6 +++- tests/test_slack_context.py | 50 +++++++++++++++++++++++++++--- 4 files changed, 77 insertions(+), 11 deletions(-) diff --git a/agent/utils/slack.py b/agent/utils/slack.py index 71fb612f..3db5de8c 100644 --- a/agent/utils/slack.py +++ b/agent/utils/slack.py @@ -12,7 +12,7 @@ import re import time from dataclasses import dataclass from typing import Any -from urllib.parse import urlparse +from urllib.parse import quote, urlparse import httpx from langgraph_sdk.client import LangGraphClient @@ -715,20 +715,38 @@ TRACE_REPLY_TIPS: tuple[str, ...] = ( ) -def _format_trace_reply(trace_url: str | None) -> str: +def _get_dashboard_thread_url(thread_id: str) -> str | None: + """Build the dashboard thread URL for a given thread ID.""" + base_url = ( + os.environ.get("DASHBOARD_BASE_URL", "https://openswe.vercel.app").strip().rstrip("/") + ) + if not base_url: + return None + return f"{base_url}/agents/{quote(thread_id, safe='')}" + + +def _format_trace_reply(trace_url: str | None, dashboard_url: str | None) -> str: """Format the initial trace reply with a randomly selected tip.""" tip = random.choice(TRACE_REPLY_TIPS) - head = f"<{trace_url}|View trace>\n" if trace_url else "" + links = [] + if trace_url: + links.append(f"<{trace_url}|View trace>") + if dashboard_url: + links.append(f"<{dashboard_url}|Open in Web>") + head = f"{' • '.join(links)}\n" if links else "" return f"{head}_Tip: {tip}_" -async def post_slack_trace_reply(channel_id: str, thread_ts: str, thread_id: str) -> str | None: +async def post_slack_trace_reply( + channel_id: str, thread_ts: str, thread_id: str, *, include_dashboard_link: bool = True +) -> str | None: """Post a trace URL reply in a Slack thread and return its Slack timestamp.""" trace_url = get_langsmith_trace_url(thread_id) + dashboard_url = _get_dashboard_thread_url(thread_id) if include_dashboard_link else None message_ts, _ = await post_slack_thread_reply_with_ts( channel_id, thread_ts, - _format_trace_reply(trace_url), + _format_trace_reply(trace_url, dashboard_url), unfurl_links=False, unfurl_media=False, ) diff --git a/agent/webapp.py b/agent/webapp.py index 0f6825a8..174a36e8 100644 --- a/agent/webapp.py +++ b/agent/webapp.py @@ -1176,7 +1176,9 @@ async def process_slack_pr_review_request( if result.get("success"): thread_id = result.get("thread_id") if isinstance(thread_id, str) and thread_id: - await post_slack_trace_reply(channel_id, thread_ts, thread_id) + await post_slack_trace_reply( + channel_id, thread_ts, thread_id, include_dashboard_link=False + ) await set_slack_assistant_status(channel_id, thread_ts) return diff --git a/tests/test_github_issue_webhook.py b/tests/test_github_issue_webhook.py index 0906a190..acc61193 100644 --- a/tests/test_github_issue_webhook.py +++ b/tests/test_github_issue_webhook.py @@ -834,11 +834,14 @@ def test_process_slack_pr_review_request_posts_trace_reply(monkeypatch) -> None: captured["slack_thread_ts"] = slack_thread_ts return {"success": True, "thread_id": "reviewer-thread-id", "pr_url": pr_ref.url} - async def fake_post_slack_trace_reply(channel_id: str, thread_ts: str, thread_id: str) -> None: + async def fake_post_slack_trace_reply( + channel_id: str, thread_ts: str, thread_id: str, *, include_dashboard_link: bool = True + ) -> None: captured["trace_reply"] = { "channel_id": channel_id, "thread_ts": thread_ts, "thread_id": thread_id, + "include_dashboard_link": include_dashboard_link, } async def fake_set_slack_assistant_status(channel_id: str, thread_ts: str) -> bool: @@ -869,6 +872,7 @@ def test_process_slack_pr_review_request_posts_trace_reply(monkeypatch) -> None: "channel_id": "C123", "thread_ts": "1700000000.000100", "thread_id": "reviewer-thread-id", + "include_dashboard_link": False, } assert captured["status_calls"] == [ ("C123", "1700000000.000100"), diff --git a/tests/test_slack_context.py b/tests/test_slack_context.py index 1e34b18f..9a65bafb 100644 --- a/tests/test_slack_context.py +++ b/tests/test_slack_context.py @@ -221,7 +221,7 @@ def test_format_slack_messages_for_prompt_replaces_bot_id_mention_in_text() -> N assert formatted == "@alice(U123): @open-swe status update?" -def test_post_slack_trace_reply_emits_tip_only_when_no_trace_url( +def test_post_slack_trace_reply_includes_web_link_without_trace_url( monkeypatch: pytest.MonkeyPatch, ) -> None: posted: list[dict] = [] @@ -237,6 +237,7 @@ def test_post_slack_trace_reply_emits_tip_only_when_no_trace_url( posted.append({"text": text, "unfurl_links": unfurl_links, "unfurl_media": unfurl_media}) return "1.1", None + monkeypatch.setenv("DASHBOARD_BASE_URL", "https://app.example.com/") monkeypatch.setattr( slack_utils, "post_slack_thread_reply_with_ts", fake_post_slack_thread_reply_with_ts ) @@ -246,8 +247,10 @@ def test_post_slack_trace_reply_emits_tip_only_when_no_trace_url( assert len(posted) == 1 text = posted[0]["text"] - assert text.startswith("_Tip: ") and text.endswith("_") - assert any(tip in text for tip in TRACE_REPLY_TIPS) + head, _, tip_line = text.partition("\n") + assert head == "" + assert tip_line.startswith("_Tip: ") and tip_line.endswith("_") + assert any(tip in tip_line for tip in TRACE_REPLY_TIPS) assert posted[0]["unfurl_links"] is False assert posted[0]["unfurl_media"] is False @@ -268,6 +271,7 @@ def test_post_slack_trace_reply_includes_trace_link_and_tip( posted.append({"text": text, "unfurl_links": unfurl_links, "unfurl_media": unfurl_media}) return "1.1", None + monkeypatch.setenv("DASHBOARD_BASE_URL", "https://app.example.com") monkeypatch.setattr( slack_utils, "post_slack_thread_reply_with_ts", fake_post_slack_thread_reply_with_ts ) @@ -278,13 +282,51 @@ def test_post_slack_trace_reply_includes_trace_link_and_tip( assert len(posted) == 1 text = posted[0]["text"] head, _, tip_line = text.partition("\n") - assert head == "" + assert ( + head + == " • " + ) assert tip_line.startswith("_Tip: ") and tip_line.endswith("_") assert any(tip in tip_line for tip in TRACE_REPLY_TIPS) assert posted[0]["unfurl_links"] is False assert posted[0]["unfurl_media"] is False +def test_post_slack_trace_reply_can_skip_web_link( + monkeypatch: pytest.MonkeyPatch, +) -> None: + posted: list[dict] = [] + + async def fake_post_slack_thread_reply_with_ts( + channel_id: str, + thread_ts: str, + text: str, + *, + unfurl_links: bool = True, + unfurl_media: bool = True, + ) -> tuple[str | None, str | None]: + posted.append({"text": text, "unfurl_links": unfurl_links, "unfurl_media": unfurl_media}) + return "1.1", None + + monkeypatch.setenv("DASHBOARD_BASE_URL", "https://app.example.com") + monkeypatch.setattr( + slack_utils, "post_slack_thread_reply_with_ts", fake_post_slack_thread_reply_with_ts + ) + monkeypatch.setattr(slack_utils, "get_langsmith_trace_url", lambda thread_id: "https://smith/x") + + asyncio.run( + post_slack_trace_reply("C123", "1.0", "reviewer-thread-id", include_dashboard_link=False) + ) + + assert len(posted) == 1 + head, _, tip_line = posted[0]["text"].partition("\n") + assert head == "" + assert "Open in Web" not in posted[0]["text"] + assert tip_line.startswith("_Tip: ") and tip_line.endswith("_") + assert posted[0]["unfurl_links"] is False + assert posted[0]["unfurl_media"] is False + + def test_select_slack_context_messages_detects_username_mention() -> None: selected, mode = select_slack_context_messages( [