feat: add Slack Open in Web link (#1392)

* feat: add Slack Open in Web link

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>

* fix: skip web link for Slack reviewer runs

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>

* test: accept include_dashboard_link kwarg in Slack reviewer test double

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Johannes du Plessis <51395795+johannes117@users.noreply.github.com>
Co-authored-by: open-swe[bot] <johannes@langchain.dev>
This commit is contained in:
Mukil Loganathan 2026-06-03 14:50:37 -04:00 • committed by GitHub
parent 1d4f1aed33
commit f4b27c0fee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 77 additions and 11 deletions

View file

@ -12,7 +12,7 @@ import re
import time import time
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
from urllib.parse import urlparse from urllib.parse import quote, urlparse
import httpx import httpx
from langgraph_sdk.client import LangGraphClient 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.""" """Format the initial trace reply with a randomly selected tip."""
tip = random.choice(TRACE_REPLY_TIPS) 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}_" 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.""" """Post a trace URL reply in a Slack thread and return its Slack timestamp."""
trace_url = get_langsmith_trace_url(thread_id) 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( message_ts, _ = await post_slack_thread_reply_with_ts(
channel_id, channel_id,
thread_ts, thread_ts,
_format_trace_reply(trace_url), _format_trace_reply(trace_url, dashboard_url),
unfurl_links=False, unfurl_links=False,
unfurl_media=False, unfurl_media=False,
) )

View file

@ -1176,7 +1176,9 @@ async def process_slack_pr_review_request(
if result.get("success"): if result.get("success"):
thread_id = result.get("thread_id") thread_id = result.get("thread_id")
if isinstance(thread_id, str) and 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) await set_slack_assistant_status(channel_id, thread_ts)
return return

View file

@ -834,11 +834,14 @@ def test_process_slack_pr_review_request_posts_trace_reply(monkeypatch) -> None:
captured["slack_thread_ts"] = slack_thread_ts captured["slack_thread_ts"] = slack_thread_ts
return {"success": True, "thread_id": "reviewer-thread-id", "pr_url": pr_ref.url} 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"] = { captured["trace_reply"] = {
"channel_id": channel_id, "channel_id": channel_id,
"thread_ts": thread_ts, "thread_ts": thread_ts,
"thread_id": thread_id, "thread_id": thread_id,
"include_dashboard_link": include_dashboard_link,
} }
async def fake_set_slack_assistant_status(channel_id: str, thread_ts: str) -> bool: 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", "channel_id": "C123",
"thread_ts": "1700000000.000100", "thread_ts": "1700000000.000100",
"thread_id": "reviewer-thread-id", "thread_id": "reviewer-thread-id",
"include_dashboard_link": False,
} }
assert captured["status_calls"] == [ assert captured["status_calls"] == [
("C123", "1700000000.000100"), ("C123", "1700000000.000100"),

View file

@ -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?" 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, monkeypatch: pytest.MonkeyPatch,
) -> None: ) -> None:
posted: list[dict] = [] 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}) posted.append({"text": text, "unfurl_links": unfurl_links, "unfurl_media": unfurl_media})
return "1.1", None return "1.1", None
monkeypatch.setenv("DASHBOARD_BASE_URL", "https://app.example.com/")
monkeypatch.setattr( monkeypatch.setattr(
slack_utils, "post_slack_thread_reply_with_ts", fake_post_slack_thread_reply_with_ts 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 assert len(posted) == 1
text = posted[0]["text"] text = posted[0]["text"]
assert text.startswith("_Tip: ") and text.endswith("_") head, _, tip_line = text.partition("\n")
assert any(tip in text for tip in TRACE_REPLY_TIPS) assert head == "<https://app.example.com/agents/thread-id|Open in Web>"
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_links"] is False
assert posted[0]["unfurl_media"] 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}) posted.append({"text": text, "unfurl_links": unfurl_links, "unfurl_media": unfurl_media})
return "1.1", None return "1.1", None
monkeypatch.setenv("DASHBOARD_BASE_URL", "https://app.example.com")
monkeypatch.setattr( monkeypatch.setattr(
slack_utils, "post_slack_thread_reply_with_ts", fake_post_slack_thread_reply_with_ts 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 assert len(posted) == 1
text = posted[0]["text"] text = posted[0]["text"]
head, _, tip_line = text.partition("\n") head, _, tip_line = text.partition("\n")
assert head == "<https://smith/x|View trace>" assert (
head
== "<https://smith/x|View trace> • <https://app.example.com/agents/thread-id|Open in Web>"
)
assert tip_line.startswith("_Tip: ") and tip_line.endswith("_") assert tip_line.startswith("_Tip: ") and tip_line.endswith("_")
assert any(tip in tip_line for tip in TRACE_REPLY_TIPS) assert any(tip in tip_line for tip in TRACE_REPLY_TIPS)
assert posted[0]["unfurl_links"] is False assert posted[0]["unfurl_links"] is False
assert posted[0]["unfurl_media"] 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 == "<https://smith/x|View trace>"
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: def test_select_slack_context_messages_detects_username_mention() -> None:
selected, mode = select_slack_context_messages( selected, mode = select_slack_context_messages(
[ [