mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 06:53:14 +00:00
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] <open-swe@users.noreply.github.com> * 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] <open-swe@users.noreply.github.com> * chore: reuse thread_id var, drop redundant danger-color fallback --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
parent
d0db48eacd
commit
8c7b789122
5 changed files with 113 additions and 54 deletions
|
|
@ -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"] = {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
3
ui/.gitignore
vendored
3
ui/.gitignore
vendored
|
|
@ -13,3 +13,6 @@ count.txt
|
|||
dev-dist
|
||||
__unconfig*
|
||||
todos.json
|
||||
.yarn
|
||||
.yarnrc.yml
|
||||
.pnp.*
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<>
|
||||
<Link
|
||||
to="/agents/$threadId"
|
||||
params={{ threadId: thread.id }}
|
||||
onClick={onNavigate}
|
||||
className={cn(
|
||||
"group mb-0.5 flex items-center gap-2 rounded-lg px-2.5 py-1.5 transition-colors",
|
||||
isActive
|
||||
? "bg-[var(--ui-accent-bubble)] text-[var(--ui-text)]"
|
||||
: "text-[var(--ui-text-muted)] hover:bg-[var(--ui-sidebar-hover)]",
|
||||
isDeleting && "opacity-50"
|
||||
)}
|
||||
>
|
||||
{thread.status === "running" ? (
|
||||
<CircleNotchIcon
|
||||
className="size-3 shrink-0 animate-spin text-[var(--ui-accent)]"
|
||||
aria-label="Thread running"
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className={cn(
|
||||
"size-2 shrink-0 rounded-full",
|
||||
showFinishedIndicator
|
||||
? "bg-[var(--ui-accent)]"
|
||||
: "bg-[var(--ui-border)]"
|
||||
)}
|
||||
aria-label={
|
||||
showFinishedIndicator ? "Thread finished" : "Thread viewed"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{source && SourceIcon && (
|
||||
<SourceIcon
|
||||
className="size-3.5 shrink-0 text-[var(--ui-text-dim)]"
|
||||
aria-label={source.label}
|
||||
>
|
||||
<title>{source.label}</title>
|
||||
</SourceIcon>
|
||||
)}
|
||||
<span className="min-w-0 flex-1 truncate text-xs">{thread.title}</span>
|
||||
{badge && (
|
||||
<span className="shrink-0 rounded bg-[var(--ui-panel-2)] px-1.5 py-0.5 text-[10px] text-[var(--ui-text-dim)] group-hover:hidden">
|
||||
{badge}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Delete thread"
|
||||
onClick={onDelete}
|
||||
disabled={isDeleting}
|
||||
className="hidden size-4 shrink-0 items-center justify-center rounded text-[var(--ui-text-dim)] group-hover:flex hover:bg-[var(--ui-panel-2)] hover:text-[var(--ui-text)]"
|
||||
<ContextMenu.Root>
|
||||
<ContextMenu.Trigger
|
||||
render={
|
||||
<Link
|
||||
to="/agents/$threadId"
|
||||
params={{ threadId: thread.id }}
|
||||
onClick={onNavigate}
|
||||
className={cn(
|
||||
"group mb-0.5 flex items-center gap-2 rounded-lg px-2.5 py-1.5 transition-colors",
|
||||
isActive
|
||||
? "bg-[var(--ui-accent-bubble)] text-[var(--ui-text)]"
|
||||
: "text-[var(--ui-text-muted)] hover:bg-[var(--ui-sidebar-hover)]",
|
||||
isDeleting && "opacity-50"
|
||||
)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<XIcon className="size-3" weight="bold" />
|
||||
</button>
|
||||
</Link>
|
||||
{thread.status === "running" ? (
|
||||
<CircleNotchIcon
|
||||
className="size-3 shrink-0 animate-spin text-[var(--ui-accent)]"
|
||||
aria-label="Thread running"
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className={cn(
|
||||
"size-2 shrink-0 rounded-full",
|
||||
showFinishedIndicator
|
||||
? "bg-[var(--ui-accent)]"
|
||||
: "bg-[var(--ui-border)]"
|
||||
)}
|
||||
aria-label={
|
||||
showFinishedIndicator ? "Thread finished" : "Thread viewed"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{source && SourceIcon && (
|
||||
<SourceIcon
|
||||
className="size-3.5 shrink-0 text-[var(--ui-text-dim)]"
|
||||
aria-label={source.label}
|
||||
>
|
||||
<title>{source.label}</title>
|
||||
</SourceIcon>
|
||||
)}
|
||||
<span className="min-w-0 flex-1 truncate text-xs">
|
||||
{thread.title}
|
||||
</span>
|
||||
{badge && (
|
||||
<span className="shrink-0 rounded bg-[var(--ui-panel-2)] px-1.5 py-0.5 text-[10px] text-[var(--ui-text-dim)] group-hover:hidden">
|
||||
{badge}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Delete thread"
|
||||
onClick={onDelete}
|
||||
disabled={isDeleting}
|
||||
className="hidden size-4 shrink-0 items-center justify-center rounded text-[var(--ui-text-dim)] group-hover:flex hover:bg-[var(--ui-panel-2)] hover:text-[var(--ui-text)]"
|
||||
>
|
||||
<XIcon className="size-3" weight="bold" />
|
||||
</button>
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Portal>
|
||||
<ContextMenu.Positioner className="z-50 outline-none">
|
||||
<ContextMenu.Popup className="min-w-[10rem] overflow-hidden rounded-md border border-[var(--ui-border)] bg-popover p-1 text-popover-foreground shadow-md outline-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95">
|
||||
<ContextMenu.Item
|
||||
disabled={!thread.traceUrl}
|
||||
onClick={openTrace}
|
||||
className="flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-xs outline-none select-none data-highlighted:bg-[var(--ui-sidebar-hover)] data-disabled:pointer-events-none data-disabled:opacity-50"
|
||||
>
|
||||
<TreeStructureIcon className="size-3.5" />
|
||||
Open trace
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item
|
||||
onClick={onDelete}
|
||||
disabled={isDeleting}
|
||||
className="flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-xs text-[var(--ui-danger)] outline-none select-none data-highlighted:bg-[var(--ui-sidebar-hover)] data-disabled:pointer-events-none data-disabled:opacity-50"
|
||||
>
|
||||
<TrashIcon className="size-3.5" />
|
||||
Delete thread
|
||||
</ContextMenu.Item>
|
||||
</ContextMenu.Popup>
|
||||
</ContextMenu.Positioner>
|
||||
</ContextMenu.Portal>
|
||||
</ContextMenu.Root>
|
||||
<Dialog.Root open={deleteOpen} onOpenChange={setDeleteOpen}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Backdrop className="fixed inset-0 z-50 bg-black/50 data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0" />
|
||||
|
|
|
|||
|
|
@ -167,6 +167,7 @@ export interface AgentThread {
|
|||
viewedAt?: number | null
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
traceUrl?: string | null
|
||||
messages: Array<Message>
|
||||
pr?: {
|
||||
number: number
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue