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