import { ContextMenu } from "@base-ui/react/context-menu" import { Dialog } from "@base-ui/react/dialog" import { Link } from "@tanstack/react-router" import { ArrowCounterClockwiseIcon, CalendarBlankIcon, CaretDownIcon, CaretRightIcon, ChartLineUpIcon, ChatCircleIcon, CheckCircleIcon, CircleNotchIcon, GitMergeIcon, GitPullRequestIcon, LightningIcon, PlusIcon, TrashIcon, TreeStructureIcon, XIcon, } from "@phosphor-icons/react" import { IoLogoGithub, IoLogoSlack } from "react-icons/io5" import { SiLinear } from "react-icons/si" import { useState } from "react" import type { ComponentType, SVGProps } from "react" import type { SessionUser } from "@/lib/api" import type { AgentSource, AgentThread } from "@/lib/agents/types" import type { SidebarLayout } from "@/components/sidebar-layout" import { SidebarUserMenu } from "@/components/SidebarUserMenu" import { Button } from "@/components/ui/button" import { SidebarCollapseButton, SidebarFrame, SidebarLayoutProvider, useSidebarLayout, } from "@/components/sidebar-layout" import { groupThreads } from "@/lib/agents/api" import { useDeleteAgentThread, useResolveAgentThread, useSeedAgentThreadDetails, useSidebarThreads, } from "@/lib/agents/queries" import { useRunCompletionNotifier } from "@/lib/agents/useRunCompletionNotifier" import { cn } from "@/lib/utils" const RESOLVED_SIDEBAR_LIMIT = 20 type SourceIcon = ComponentType> const SOURCE_META: Record = { dashboard: { icon: ChatCircleIcon, label: "Started from the dashboard" }, github: { icon: IoLogoGithub, label: "Triggered from GitHub" }, slack: { icon: IoLogoSlack, label: "Triggered from Slack" }, linear: { icon: SiLinear, label: "Triggered from Linear" }, schedule: { icon: CalendarBlankIcon, label: "Triggered from a schedule" }, } type PrState = NonNullable["state"] const PR_STATE_META: Record< PrState, { icon: SourceIcon; label: string; className: string } > = { draft: { icon: GitPullRequestIcon, label: "Draft pull request", className: "text-[var(--ui-text-dim)]", }, open: { icon: GitPullRequestIcon, label: "Open pull request", className: "text-[var(--ui-success)]", }, merged: { icon: GitMergeIcon, label: "Merged pull request", className: "text-[var(--ui-accent)]", }, closed: { icon: GitPullRequestIcon, label: "Closed pull request", className: "text-[var(--ui-danger)]", }, } interface AgentsSidebarProps { user: SessionUser activeThreadId?: string layout: SidebarLayout } const NAV = [ { to: "/agents/automations", label: "Automations", icon: LightningIcon }, { to: "/my-settings", label: "Dashboard", icon: ChartLineUpIcon }, { to: "/agents/reviews", label: "Reviews", icon: GitPullRequestIcon }, ] as const export function AgentsSidebar({ user, activeThreadId, layout, }: AgentsSidebarProps) { const sidebar = useSidebarThreads(RESOLVED_SIDEBAR_LIMIT) const activeThreads = sidebar.data?.active.items ?? [] const resolvedThreads = sidebar.data?.resolved.items ?? [] const resolvedHasMore = sidebar.data?.resolved.hasMore ?? false const visibleThreads = [...activeThreads, ...resolvedThreads] useSeedAgentThreadDetails(visibleThreads, activeThreadId) useRunCompletionNotifier(visibleThreads, activeThreadId) const groups = groupThreads(activeThreads) return (
open-swe
New Agent
) } function ThreadGroup({ label, threads, activeThreadId, onNavigate, defaultCollapsed = false, }: { label: string threads: Array activeThreadId?: string onNavigate?: () => void defaultCollapsed?: boolean }) { const [collapsed, setCollapsed] = useState(defaultCollapsed) if (threads.length === 0) return null const ToggleIcon = collapsed ? CaretRightIcon : CaretDownIcon return (
{!collapsed && threads.map((thread) => ( ))}
) } function ResolvedThreadGroup({ threads, hasMore, activeThreadId, onNavigate, }: { threads: Array hasMore: boolean activeThreadId?: string onNavigate?: () => void }) { const [collapsed, setCollapsed] = useState(true) if (threads.length === 0) return null const ToggleIcon = collapsed ? CaretRightIcon : CaretDownIcon const visible = threads.slice(0, RESOLVED_SIDEBAR_LIMIT) return (
{!collapsed && ( <> {visible.map((thread) => ( ))} {hasMore && ( Show all )} )}
) } function ThreadRow({ thread, isActive, onNavigate, }: { thread: AgentThread isActive: boolean onNavigate?: () => void }) { const deleteThread = useDeleteAgentThread() const resolveThread = useResolveAgentThread() const [deleteOpen, setDeleteOpen] = useState(false) const isReadOnly = thread.isOwner === false const badge = thread.diffStats && thread.diffStats.additions > 0 ? `+${thread.diffStats.additions}` : null const isDeleting = deleteThread.isPending && deleteThread.variables === thread.id const onDelete = (e: React.MouseEvent) => { e.preventDefault() e.stopPropagation() if (isDeleting) return setDeleteOpen(true) } const onConfirmDelete = () => { if (isDeleting) return deleteThread.mutate(thread.id, { onSuccess: () => setDeleteOpen(false), }) } const isResolved = thread.resolved === true const onToggleResolved = (e?: React.MouseEvent) => { e?.preventDefault() e?.stopPropagation() if (resolveThread.isPending) return resolveThread.mutate({ threadId: thread.id, resolved: !isResolved }) } const source = thread.source && thread.source !== "dashboard" ? SOURCE_META[thread.source] : null const SourceIcon = source?.icon const prMeta = thread.pr ? PR_STATE_META[thread.pr.state] : null const PrIcon = prMeta?.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} {prMeta && PrIcon && ( {prMeta.label} )} {badge && ( {badge} )} {!isReadOnly && ( )} {!isReadOnly && ( )} Open trace {!isReadOnly && ( onToggleResolved()} disabled={resolveThread.isPending} 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" > {isResolved ? ( ) : ( )} {isResolved ? "Unresolve thread" : "Resolve thread"} )} {!isReadOnly && ( Delete thread )}
Delete thread Delete "{thread.title}"? This cannot be undone.
) } export function AgentsShell({ user, activeThreadId, children, }: { user: SessionUser activeThreadId?: string children: React.ReactNode }) { const layout = useSidebarLayout() return (
{children}
) }