From 8c7b7891221e3ec8abd8c1cd1df145b71f7008c8 Mon Sep 17 00:00:00 2001 From: Johannes du Plessis Date: Wed, 10 Jun 2026 15:40:43 -0700 Subject: [PATCH] feat: add right-click open trace context menu for threads (#1490) * feat: add right-click open trace context menu for threads Add a context menu to sidebar thread rows exposing an Open trace action that links to the LangSmith thread trace, surfaced via a new traceUrl field on the thread summary. Co-authored-by: open-swe[bot] * chore: drop stray yarn artifacts from PR Remove accidentally committed ui/.yarnrc.yml and ui/.yarn/install-state.gz that broke immutable installs against the v1 lockfile, and gitignore yarn artifacts to prevent recurrence. Co-authored-by: open-swe[bot] * chore: reuse thread_id var, drop redundant danger-color fallback --------- Co-authored-by: open-swe[bot] --- agent/dashboard/thread_api.py | 7 +- tests/test_dashboard_repo_optional.py | 12 ++ ui/.gitignore | 3 + ui/src/components/agents/AgentsSidebar.tsx | 144 +++++++++++++-------- ui/src/lib/agents/types.ts | 1 + 5 files changed, 113 insertions(+), 54 deletions(-) diff --git a/agent/dashboard/thread_api.py b/agent/dashboard/thread_api.py index 6e5cdfbb..21106b9b 100644 --- a/agent/dashboard/thread_api.py +++ b/agent/dashboard/thread_api.py @@ -17,6 +17,7 @@ from langchain_core.messages.content import create_image_block from langgraph_sdk.errors import InternalServerError from pydantic import BaseModel, ConfigDict, Field +from ..utils.langsmith import get_langsmith_trace_url from ..utils.thread_ops import is_thread_active, langgraph_client, queue_message_for_thread from .agent_overrides import normalize_profile_overrides from .message_adapter import state_messages_to_ui @@ -272,8 +273,11 @@ def _thread_summary( pr_title = metadata.get("pr_title") pr_state = metadata.get("pr_state") + thread_id = thread.get("thread_id") or thread.get("id") + trace_url = get_langsmith_trace_url(thread_id) if isinstance(thread_id, str) else None + summary: dict[str, Any] = { - "id": thread.get("thread_id") or thread.get("id"), + "id": thread_id, "title": title, "repo": name, "repoFullName": full_name, @@ -290,6 +294,7 @@ def _thread_summary( ), "createdAt": int(created_at) if isinstance(created_at, (int, float)) else _now_ms(), "updatedAt": int(updated_at) if isinstance(updated_at, (int, float)) else _now_ms(), + "traceUrl": trace_url, } if isinstance(pr_number, int) and isinstance(pr_url, str): summary["pr"] = { diff --git a/tests/test_dashboard_repo_optional.py b/tests/test_dashboard_repo_optional.py index 025b3a3b..cb09d7d8 100644 --- a/tests/test_dashboard_repo_optional.py +++ b/tests/test_dashboard_repo_optional.py @@ -42,6 +42,18 @@ def test_thread_summary_keeps_repo_when_present() -> None: assert summary["repoFullName"] == "octo/repo" +def test_thread_summary_includes_trace_url(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr( + thread_api, + "get_langsmith_trace_url", + lambda thread_id: f"https://smith.example/t/{thread_id}", + ) + summary = thread_api._thread_summary( + {"thread_id": "t3", "metadata": {"source": "dashboard", "title": "traced run"}} + ) + assert summary["traceUrl"] == "https://smith.example/t/t3" + + class _FakeThreadsClient: async def create( self, *, thread_id: str, metadata: dict[str, Any], if_exists: str diff --git a/ui/.gitignore b/ui/.gitignore index fffa2feb..ca3beef8 100644 --- a/ui/.gitignore +++ b/ui/.gitignore @@ -13,3 +13,6 @@ count.txt dev-dist __unconfig* todos.json +.yarn +.yarnrc.yml +.pnp.* diff --git a/ui/src/components/agents/AgentsSidebar.tsx b/ui/src/components/agents/AgentsSidebar.tsx index f46acdaa..340dadc7 100644 --- a/ui/src/components/agents/AgentsSidebar.tsx +++ b/ui/src/components/agents/AgentsSidebar.tsx @@ -1,3 +1,4 @@ +import { ContextMenu } from "@base-ui/react/context-menu" import { Dialog } from "@base-ui/react/dialog" import { Link } from "@tanstack/react-router" import { @@ -9,6 +10,8 @@ import { CircleNotchIcon, LightningIcon, PlusIcon, + TrashIcon, + TreeStructureIcon, XIcon, } from "@phosphor-icons/react" import { IoLogoGithub, IoLogoSlack } from "react-icons/io5" @@ -226,62 +229,97 @@ function ThreadRow({ const SourceIcon = source?.icon const showFinishedIndicator = thread.status === "finished" && !thread.viewed + const openTrace = () => { + if (!thread.traceUrl) return + window.open(thread.traceUrl, "_blank", "noopener,noreferrer") + } + return ( <> - - {thread.status === "running" ? ( - - ) : ( - - )} - {source && SourceIcon && ( - - {source.label} - - )} - {thread.title} - {badge && ( - - {badge} - - )} - - + {thread.status === "running" ? ( + + ) : ( + + )} + {source && SourceIcon && ( + + {source.label} + + )} + + {thread.title} + + {badge && ( + + {badge} + + )} + + + + + + + + Open trace + + + + Delete thread + + + + + diff --git a/ui/src/lib/agents/types.ts b/ui/src/lib/agents/types.ts index 3333bb40..da2b0f43 100644 --- a/ui/src/lib/agents/types.ts +++ b/ui/src/lib/agents/types.ts @@ -167,6 +167,7 @@ export interface AgentThread { viewedAt?: number | null createdAt: number updatedAt: number + traceUrl?: string | null messages: Array pr?: { number: number