mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-01 08:33:14 +00:00
* feat: surface all of a user's agent threads in the Agents UI Previously the Agents UI only listed and allowed opening threads with source=dashboard. Threads triggered from GitHub, Slack, or Linear were hidden and could not be opened. - Persist owner-identifying metadata (source, github_login, triggering_user_email, source_context) onto the thread for each webhook-triggered main-agent run via upsert_agent_thread_owner_metadata, resolving a github_login from the triggering email where possible. - Relax dashboard thread listing/ownership to surface github/slack/linear threads owned by the logged-in user (matched by github_login or email), and preserve the original source + reply-routing context when continuing such threads from the UI. - Add a source field to the thread summary and render a source icon (GitHub/Slack/Linear) on thread items in the sidebar and run cards. * Normalize triggering email when persisting thread owner metadata Mixed-case Slack/Linear emails were stored verbatim, but the dashboard searches with a lowercased value, so those threads never surfaced for their owner. Normalize at write time to match _thread_owner_email.
204 lines
6.5 KiB
TypeScript
204 lines
6.5 KiB
TypeScript
import { Link } from "@tanstack/react-router";
|
|
import {
|
|
ChartLineUpIcon,
|
|
ChatCircleIcon,
|
|
GithubLogoIcon,
|
|
KanbanIcon,
|
|
PlusIcon,
|
|
SlackLogoIcon,
|
|
XIcon,
|
|
} from "@phosphor-icons/react";
|
|
import type { Icon } from "@phosphor-icons/react";
|
|
|
|
import type { SessionUser } from "@/lib/api";
|
|
import type { AgentSource, AgentThread } from "@/lib/agents/types";
|
|
import { SidebarUserMenu } from "@/components/SidebarUserMenu";
|
|
import {
|
|
SidebarCollapseButton,
|
|
SidebarFrame,
|
|
useSidebarLayout,
|
|
} from "@/components/sidebar-layout";
|
|
import { groupThreads } from "@/lib/agents/api";
|
|
import { useAgentThreads, useDeleteAgentThread } from "@/lib/agents/queries";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
const SOURCE_META: Record<AgentSource, { icon: Icon; label: string }> = {
|
|
dashboard: { icon: ChatCircleIcon, label: "Started from the dashboard" },
|
|
github: { icon: GithubLogoIcon, label: "Triggered from GitHub" },
|
|
slack: { icon: SlackLogoIcon, label: "Triggered from Slack" },
|
|
linear: { icon: KanbanIcon, label: "Triggered from Linear" },
|
|
};
|
|
|
|
interface AgentsSidebarProps {
|
|
user: SessionUser;
|
|
activeThreadId?: string;
|
|
}
|
|
|
|
const NAV = [{ to: "/my-settings", label: "Dashboard", icon: ChartLineUpIcon }] as const;
|
|
|
|
export function AgentsSidebar({ user, activeThreadId }: AgentsSidebarProps) {
|
|
const threadsQuery = useAgentThreads();
|
|
const threads = threadsQuery.data ?? [];
|
|
const groups = groupThreads(threads);
|
|
const layout = useSidebarLayout();
|
|
|
|
return (
|
|
<SidebarFrame {...layout} className="border-r border-[var(--ui-border)] bg-[var(--ui-sidebar)]">
|
|
<div className="flex items-center justify-between px-4 pt-5 pb-4">
|
|
<Link
|
|
to="/my-settings"
|
|
className="flex items-center gap-2 font-heading text-sm font-medium tracking-tight text-[var(--ui-text)]"
|
|
>
|
|
<img src="/logo-mark.png" alt="" className="size-5" />
|
|
open-swe
|
|
</Link>
|
|
<SidebarCollapseButton onToggle={layout.toggle} />
|
|
</div>
|
|
|
|
<div className="px-2 pb-1">
|
|
<Link
|
|
to="/agents"
|
|
className="flex w-full items-center gap-2.5 rounded-md px-2.5 py-1.5 text-xs font-medium text-[var(--ui-text)] transition-colors hover:bg-[var(--ui-sidebar-hover)]"
|
|
>
|
|
<PlusIcon className="size-4" />
|
|
New Agent
|
|
</Link>
|
|
</div>
|
|
|
|
<nav className="flex flex-col gap-0.5 px-2 pb-4">
|
|
{NAV.map((item) => {
|
|
const Icon = item.icon;
|
|
return (
|
|
<Link
|
|
key={item.to}
|
|
to={item.to}
|
|
className="flex items-center gap-2.5 rounded-md px-2.5 py-1.5 text-xs text-[var(--ui-text-muted)] transition-colors hover:bg-[var(--ui-sidebar-hover)] hover:text-[var(--ui-text)]"
|
|
>
|
|
<Icon className="size-4" />
|
|
{item.label}
|
|
</Link>
|
|
);
|
|
})}
|
|
</nav>
|
|
|
|
<div className="min-h-0 flex-1 overflow-y-auto px-2 pb-2">
|
|
<ThreadGroup label="Today" threads={groups.today} activeThreadId={activeThreadId} />
|
|
<ThreadGroup label="Last 30 days" threads={groups.last30} activeThreadId={activeThreadId} />
|
|
<ThreadGroup label="Older" threads={groups.older} activeThreadId={activeThreadId} />
|
|
</div>
|
|
|
|
<div className="p-2">
|
|
<SidebarUserMenu user={user} showSettingsLink />
|
|
</div>
|
|
</SidebarFrame>
|
|
);
|
|
}
|
|
|
|
function ThreadGroup({
|
|
label,
|
|
threads,
|
|
activeThreadId,
|
|
}: {
|
|
label: string;
|
|
threads: AgentThread[];
|
|
activeThreadId?: string;
|
|
}) {
|
|
if (threads.length === 0) return null;
|
|
|
|
return (
|
|
<div className="mb-3">
|
|
<div className="px-2 py-1 text-[10px] font-semibold tracking-wide text-[var(--ui-text-dim)] uppercase">
|
|
{label}
|
|
</div>
|
|
{threads.map((thread) => (
|
|
<ThreadRow key={thread.id} thread={thread} isActive={thread.id === activeThreadId} />
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ThreadRow({ thread, isActive }: { thread: AgentThread; isActive: boolean }) {
|
|
const deleteThread = useDeleteAgentThread();
|
|
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;
|
|
if (!window.confirm(`Delete "${thread.title}"? This cannot be undone.`)) return;
|
|
deleteThread.mutate(thread.id);
|
|
};
|
|
|
|
const source = thread.source && thread.source !== "dashboard" ? SOURCE_META[thread.source] : null;
|
|
const SourceIcon = source?.icon;
|
|
|
|
return (
|
|
<Link
|
|
to="/agents/$threadId"
|
|
params={{ threadId: thread.id }}
|
|
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",
|
|
)}
|
|
>
|
|
<span
|
|
className={cn(
|
|
"size-2 shrink-0 rounded-full",
|
|
thread.status === "running"
|
|
? "animate-pulse bg-[var(--ui-accent)]"
|
|
: thread.status === "finished"
|
|
? "bg-[var(--ui-accent)]"
|
|
: "bg-[var(--ui-border)]",
|
|
)}
|
|
/>
|
|
{source && SourceIcon && (
|
|
<SourceIcon
|
|
className="size-3.5 shrink-0 text-[var(--ui-text-dim)]"
|
|
weight="fill"
|
|
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)] hover:bg-[var(--ui-panel-2)] hover:text-[var(--ui-text)] group-hover:flex"
|
|
>
|
|
<XIcon className="size-3" weight="bold" />
|
|
</button>
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
export function AgentsShell({
|
|
user,
|
|
activeThreadId,
|
|
children,
|
|
}: {
|
|
user: SessionUser;
|
|
activeThreadId?: string;
|
|
children: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<div className="agents-ui flex h-svh overflow-hidden bg-[var(--ui-bg)]">
|
|
<AgentsSidebar user={user} activeThreadId={activeThreadId} />
|
|
<div className="flex min-w-0 flex-1">{children}</div>
|
|
</div>
|
|
);
|
|
}
|