mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 09:07:46 +02:00
Integrate SidebarSection for agents sidebar
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
63a5ff4674
commit
0891c1c72f
3 files changed
+297
-86
No files matched your search
@@ -145,6 +145,34 @@ async function openAgentMenu(label = "Open actions for Alpha") {
|
||||
await flushReact();
|
||||
}
|
||||
|
||||
async function openAgentsSectionMenu() {
|
||||
const trigger = document.body.querySelector('button[aria-label="Agents section actions"]');
|
||||
expect(trigger).not.toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
trigger?.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, button: 0 }));
|
||||
trigger?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
await flushReact();
|
||||
}
|
||||
|
||||
async function chooseSortMode(label: string) {
|
||||
const item = Array.from(document.body.querySelectorAll('[data-slot="dropdown-menu-radio-item"]'))
|
||||
.find((element) => element.textContent?.includes(label));
|
||||
expect(item).toBeTruthy();
|
||||
|
||||
await act(async () => {
|
||||
item?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
await flushReact();
|
||||
}
|
||||
|
||||
function agentLinkLabels(container: HTMLElement) {
|
||||
return Array.from(container.querySelectorAll('a[href^="/agents/"]'))
|
||||
.map((anchor) => anchor.textContent?.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
describe("SidebarAgents", () => {
|
||||
let container: HTMLDivElement;
|
||||
let root: ReturnType<typeof createRoot> | null;
|
||||
@@ -165,6 +193,7 @@ describe("SidebarAgents", () => {
|
||||
user: { id: "user-1" },
|
||||
});
|
||||
mockHeartbeatsApi.liveRunsForCompany.mockResolvedValue([]);
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -177,10 +206,11 @@ describe("SidebarAgents", () => {
|
||||
queryClient.clear();
|
||||
container.remove();
|
||||
document.body.innerHTML = "";
|
||||
localStorage.clear();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("shows edit and pause actions for an active sidebar agent", async () => {
|
||||
async function renderSidebarAgents() {
|
||||
const currentRoot = createRoot(container);
|
||||
root = currentRoot;
|
||||
|
||||
@@ -192,6 +222,92 @@ describe("SidebarAgents", () => {
|
||||
);
|
||||
});
|
||||
await flushReact();
|
||||
}
|
||||
|
||||
it("keeps top mode in stored org-aware order", async () => {
|
||||
localStorage.setItem("paperclip.agentOrder:company-1:user-1", JSON.stringify(["agent-b", "agent-a", "agent-c"]));
|
||||
mockAgentsApi.list.mockResolvedValue([
|
||||
makeAgent({ id: "agent-a", name: "Alpha", urlKey: "alpha" }),
|
||||
makeAgent({ id: "agent-b", name: "Bravo", urlKey: "bravo" }),
|
||||
makeAgent({ id: "agent-c", name: "Charlie", urlKey: "charlie" }),
|
||||
]);
|
||||
|
||||
await renderSidebarAgents();
|
||||
|
||||
expect(agentLinkLabels(container)).toEqual(["Bravo", "Alpha", "Charlie"]);
|
||||
});
|
||||
|
||||
it("moves agent creation into the section menu and links browse agents", async () => {
|
||||
await renderSidebarAgents();
|
||||
|
||||
expect(container.querySelector('button[aria-label="New agent"]')).toBeNull();
|
||||
await openAgentsSectionMenu();
|
||||
|
||||
const newAgentItem = Array.from(document.body.querySelectorAll('[data-slot="dropdown-menu-item"]'))
|
||||
.find((element) => element.textContent?.includes("New agent"));
|
||||
expect(newAgentItem).toBeTruthy();
|
||||
const browseLink = Array.from(document.body.querySelectorAll("a"))
|
||||
.find((element) => element.textContent?.includes("Browse agents"));
|
||||
expect(browseLink?.getAttribute("href")).toBe("/agents/all");
|
||||
|
||||
await act(async () => {
|
||||
newAgentItem?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
expect(mockOpenNewAgent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("sorts alphabetically and persists the selected mode per company and user", async () => {
|
||||
mockAgentsApi.list.mockResolvedValue([
|
||||
makeAgent({ id: "agent-c", name: "Charlie", urlKey: "charlie" }),
|
||||
makeAgent({ id: "agent-a", name: "Alpha", urlKey: "alpha" }),
|
||||
makeAgent({ id: "agent-b", name: "Bravo", urlKey: "bravo" }),
|
||||
]);
|
||||
|
||||
await renderSidebarAgents();
|
||||
await openAgentsSectionMenu();
|
||||
await chooseSortMode("Alphabetical");
|
||||
|
||||
expect(agentLinkLabels(container)).toEqual(["Alpha", "Bravo", "Charlie"]);
|
||||
expect(localStorage.getItem("paperclip.agentSortMode:company-1:user-1")).toBe("alphabetical");
|
||||
});
|
||||
|
||||
it("sorts recent agents by heartbeat, updated time, and created time descending", async () => {
|
||||
mockAgentsApi.list.mockResolvedValue([
|
||||
makeAgent({
|
||||
id: "agent-a",
|
||||
name: "Alpha",
|
||||
urlKey: "alpha",
|
||||
lastHeartbeatAt: null,
|
||||
updatedAt: new Date("2026-01-20T00:00:00Z"),
|
||||
createdAt: new Date("2026-01-01T00:00:00Z"),
|
||||
}),
|
||||
makeAgent({
|
||||
id: "agent-b",
|
||||
name: "Bravo",
|
||||
urlKey: "bravo",
|
||||
lastHeartbeatAt: new Date("2026-01-10T00:00:00Z"),
|
||||
updatedAt: new Date("2026-01-02T00:00:00Z"),
|
||||
createdAt: new Date("2026-01-02T00:00:00Z"),
|
||||
}),
|
||||
makeAgent({
|
||||
id: "agent-c",
|
||||
name: "Charlie",
|
||||
urlKey: "charlie",
|
||||
lastHeartbeatAt: null,
|
||||
updatedAt: new Date("2026-01-20T00:00:00Z"),
|
||||
createdAt: new Date("2026-01-03T00:00:00Z"),
|
||||
}),
|
||||
]);
|
||||
|
||||
await renderSidebarAgents();
|
||||
await openAgentsSectionMenu();
|
||||
await chooseSortMode("Recent");
|
||||
|
||||
expect(agentLinkLabels(container)).toEqual(["Bravo", "Charlie", "Alpha"]);
|
||||
});
|
||||
|
||||
it("shows edit and pause actions for an active sidebar agent", async () => {
|
||||
await renderSidebarAgents();
|
||||
await openAgentMenu();
|
||||
|
||||
const editLink = Array.from(document.body.querySelectorAll("a"))
|
||||
@@ -216,17 +332,8 @@ describe("SidebarAgents", () => {
|
||||
mockAgentsApi.list.mockResolvedValue([
|
||||
makeAgent({ status: "paused", pauseReason: "manual", pausedAt: new Date("2026-01-02T00:00:00Z") }),
|
||||
]);
|
||||
const currentRoot = createRoot(container);
|
||||
root = currentRoot;
|
||||
|
||||
await act(async () => {
|
||||
currentRoot.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SidebarAgents />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
});
|
||||
await flushReact();
|
||||
await renderSidebarAgents();
|
||||
await openAgentMenu();
|
||||
|
||||
const resumeItem = Array.from(document.body.querySelectorAll('[data-slot="dropdown-menu-item"]'))
|
||||
@@ -248,17 +355,8 @@ describe("SidebarAgents", () => {
|
||||
makeAgent({ id: "agent-2", name: "Beta", urlKey: "beta" }),
|
||||
]);
|
||||
mockAgentsApi.pause.mockImplementation(() => new Promise(() => {}));
|
||||
const currentRoot = createRoot(container);
|
||||
root = currentRoot;
|
||||
|
||||
await act(async () => {
|
||||
currentRoot.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SidebarAgents />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
});
|
||||
await flushReact();
|
||||
await renderSidebarAgents();
|
||||
await openAgentMenu();
|
||||
|
||||
const pauseItem = Array.from(document.body.querySelectorAll('[data-slot="dropdown-menu-item"]'))
|
||||
@@ -287,17 +385,8 @@ describe("SidebarAgents", () => {
|
||||
pausedAt: new Date("2026-01-02T00:00:00Z"),
|
||||
}),
|
||||
]);
|
||||
const currentRoot = createRoot(container);
|
||||
root = currentRoot;
|
||||
|
||||
await act(async () => {
|
||||
currentRoot.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SidebarAgents />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
});
|
||||
await flushReact();
|
||||
await renderSidebarAgents();
|
||||
await openAgentMenu();
|
||||
|
||||
const budgetPausedItem = Array.from(
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Link, NavLink, useLocation } from "@/lib/router";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
ChevronRight,
|
||||
MoreHorizontal,
|
||||
PauseCircle,
|
||||
Pencil,
|
||||
PlayCircle,
|
||||
Plus,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useDialogActions } from "../context/DialogContext";
|
||||
@@ -20,14 +20,17 @@ import { SIDEBAR_SCROLL_RESET_STATE } from "../lib/navigation-scroll";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { cn, agentRouteRef, agentUrl } from "../lib/utils";
|
||||
import { useAgentOrder } from "../hooks/useAgentOrder";
|
||||
import {
|
||||
AGENT_SORT_MODE_UPDATED_EVENT,
|
||||
getAgentSortModeStorageKey,
|
||||
readAgentSortMode,
|
||||
type AgentSidebarSortMode,
|
||||
writeAgentSortMode,
|
||||
} from "../lib/agent-order";
|
||||
import { AgentIcon } from "./AgentIconPicker";
|
||||
import { BudgetSidebarMarker } from "./BudgetSidebarMarker";
|
||||
import { SidebarSection, type SidebarSectionRadioChoice } from "./SidebarSection";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -37,6 +40,46 @@ import {
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import type { Agent } from "@paperclipai/shared";
|
||||
|
||||
const AGENT_SORT_CHOICES: SidebarSectionRadioChoice[] = [
|
||||
{ value: "top", label: "Top" },
|
||||
{ value: "alphabetical", label: "Alphabetical" },
|
||||
{ value: "recent", label: "Recent" },
|
||||
];
|
||||
|
||||
type AgentSortModeUpdatedDetail = {
|
||||
storageKey: string;
|
||||
sortMode: AgentSidebarSortMode;
|
||||
};
|
||||
|
||||
function agentTimestamp(agent: Agent, field: "lastHeartbeatAt" | "updatedAt" | "createdAt"): number {
|
||||
const raw = agent[field];
|
||||
if (!raw) return 0;
|
||||
const time = new Date(raw).getTime();
|
||||
return Number.isFinite(time) ? time : 0;
|
||||
}
|
||||
|
||||
function sortAgents(agents: Agent[], sortMode: AgentSidebarSortMode): Agent[] {
|
||||
if (sortMode === "top") return agents;
|
||||
const sorted = [...agents];
|
||||
if (sortMode === "alphabetical") {
|
||||
sorted.sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: "base" }));
|
||||
return sorted;
|
||||
}
|
||||
sorted.sort((left, right) => {
|
||||
const heartbeatDiff = agentTimestamp(right, "lastHeartbeatAt") - agentTimestamp(left, "lastHeartbeatAt");
|
||||
if (heartbeatDiff !== 0) return heartbeatDiff;
|
||||
|
||||
const updatedDiff = agentTimestamp(right, "updatedAt") - agentTimestamp(left, "updatedAt");
|
||||
if (updatedDiff !== 0) return updatedDiff;
|
||||
|
||||
const createdDiff = agentTimestamp(right, "createdAt") - agentTimestamp(left, "createdAt");
|
||||
return createdDiff !== 0
|
||||
? createdDiff
|
||||
: left.name.localeCompare(right.name, undefined, { sensitivity: "base" });
|
||||
});
|
||||
return sorted;
|
||||
}
|
||||
|
||||
function SidebarAgentItem({
|
||||
activeAgentId,
|
||||
activeTab,
|
||||
@@ -195,16 +238,69 @@ export function SidebarAgents() {
|
||||
return filtered;
|
||||
}, [agents]);
|
||||
const currentUserId = session?.user?.id ?? session?.session?.userId ?? null;
|
||||
const sortModeStorageKey = useMemo(() => {
|
||||
if (!selectedCompanyId) return null;
|
||||
return getAgentSortModeStorageKey(selectedCompanyId, currentUserId);
|
||||
}, [currentUserId, selectedCompanyId]);
|
||||
const [sortMode, setSortMode] = useState<AgentSidebarSortMode>(() => {
|
||||
if (!sortModeStorageKey) return "top";
|
||||
return readAgentSortMode(sortModeStorageKey);
|
||||
});
|
||||
const { orderedAgents } = useAgentOrder({
|
||||
agents: visibleAgents,
|
||||
companyId: selectedCompanyId,
|
||||
userId: currentUserId,
|
||||
});
|
||||
const sortedAgents = useMemo(
|
||||
() => sortAgents(orderedAgents, sortMode),
|
||||
[orderedAgents, sortMode],
|
||||
);
|
||||
|
||||
const agentMatch = location.pathname.match(/^\/(?:[^/]+\/)?agents\/([^/]+)(?:\/([^/]+))?/);
|
||||
const activeAgentId = agentMatch?.[1] ?? null;
|
||||
const activeTab = agentMatch?.[2] ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!sortModeStorageKey) {
|
||||
setSortMode("top");
|
||||
return;
|
||||
}
|
||||
setSortMode(readAgentSortMode(sortModeStorageKey));
|
||||
}, [sortModeStorageKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!sortModeStorageKey) return;
|
||||
|
||||
const onStorage = (event: StorageEvent) => {
|
||||
if (event.key !== sortModeStorageKey) return;
|
||||
setSortMode(readAgentSortMode(sortModeStorageKey));
|
||||
};
|
||||
const onCustomEvent = (event: Event) => {
|
||||
const detail = (event as CustomEvent<AgentSortModeUpdatedDetail>).detail;
|
||||
if (!detail || detail.storageKey !== sortModeStorageKey) return;
|
||||
setSortMode(detail.sortMode);
|
||||
};
|
||||
|
||||
window.addEventListener("storage", onStorage);
|
||||
window.addEventListener(AGENT_SORT_MODE_UPDATED_EVENT, onCustomEvent);
|
||||
return () => {
|
||||
window.removeEventListener("storage", onStorage);
|
||||
window.removeEventListener(AGENT_SORT_MODE_UPDATED_EVENT, onCustomEvent);
|
||||
};
|
||||
}, [sortModeStorageKey]);
|
||||
|
||||
const persistSortMode = useCallback(
|
||||
(value: string) => {
|
||||
const nextSortMode: AgentSidebarSortMode =
|
||||
value === "alphabetical" || value === "recent" ? value : "top";
|
||||
setSortMode(nextSortMode);
|
||||
if (sortModeStorageKey) {
|
||||
writeAgentSortMode(sortModeStorageKey, nextSortMode);
|
||||
}
|
||||
},
|
||||
[sortModeStorageKey],
|
||||
);
|
||||
|
||||
const pauseResumeAgent = useMutation({
|
||||
mutationFn: ({ agent, action }: { agent: Agent; action: "pause" | "resume" }) =>
|
||||
action === "pause"
|
||||
@@ -252,53 +348,38 @@ export function SidebarAgents() {
|
||||
});
|
||||
|
||||
return (
|
||||
<Collapsible open={open} onOpenChange={setOpen}>
|
||||
<div className="group">
|
||||
<div className="flex items-center px-3 py-1.5">
|
||||
<CollapsibleTrigger className="flex items-center gap-1 flex-1 min-w-0">
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
"h-3 w-3 text-muted-foreground/60 transition-transform opacity-0 group-hover:opacity-100",
|
||||
open && "rotate-90"
|
||||
)}
|
||||
/>
|
||||
<span className="text-[10px] font-medium uppercase tracking-widest font-mono text-muted-foreground/60">
|
||||
Agents
|
||||
</span>
|
||||
</CollapsibleTrigger>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openNewAgent();
|
||||
}}
|
||||
className="flex items-center justify-center h-4 w-4 rounded text-muted-foreground/60 hover:text-foreground hover:bg-accent/50 transition-colors"
|
||||
aria-label="New agent"
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CollapsibleContent>
|
||||
<div className="flex flex-col gap-0.5 mt-0.5">
|
||||
{orderedAgents.map((agent: Agent) => {
|
||||
const runCount = liveCountByAgent.get(agent.id) ?? 0;
|
||||
return (
|
||||
<SidebarAgentItem
|
||||
key={agent.id}
|
||||
activeAgentId={activeAgentId}
|
||||
activeTab={activeTab}
|
||||
agent={agent}
|
||||
disabled={pendingAgentIds.has(agent.id)}
|
||||
isMobile={isMobile}
|
||||
onPauseResume={(targetAgent, action) => pauseResumeAgent.mutate({ agent: targetAgent, action })}
|
||||
runCount={runCount}
|
||||
setSidebarOpen={setSidebarOpen}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
<SidebarSection
|
||||
label="Agents"
|
||||
collapsible={{ open, onOpenChange: setOpen }}
|
||||
menu={{
|
||||
ariaLabel: "Agents section actions",
|
||||
actions: [
|
||||
{ type: "item", label: "New agent", icon: Plus, onSelect: openNewAgent },
|
||||
{ type: "item", label: "Browse agents", icon: Users, href: "/agents/all" },
|
||||
{ type: "separator" },
|
||||
],
|
||||
radioLabel: "Agent sort",
|
||||
radioChoices: AGENT_SORT_CHOICES,
|
||||
radioValue: sortMode,
|
||||
onRadioValueChange: persistSortMode,
|
||||
}}
|
||||
>
|
||||
{sortedAgents.map((agent: Agent) => {
|
||||
const runCount = liveCountByAgent.get(agent.id) ?? 0;
|
||||
return (
|
||||
<SidebarAgentItem
|
||||
key={agent.id}
|
||||
activeAgentId={activeAgentId}
|
||||
activeTab={activeTab}
|
||||
agent={agent}
|
||||
disabled={pendingAgentIds.has(agent.id)}
|
||||
isMobile={isMobile}
|
||||
onPauseResume={(targetAgent, action) => pauseResumeAgent.mutate({ agent: targetAgent, action })}
|
||||
runCount={runCount}
|
||||
setSidebarOpen={setSidebarOpen}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</SidebarSection>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +1,32 @@
|
||||
import type { Agent } from "@paperclipai/shared";
|
||||
|
||||
export const AGENT_ORDER_UPDATED_EVENT = "paperclip:agent-order-updated";
|
||||
export const AGENT_SORT_MODE_UPDATED_EVENT = "paperclip:agent-sort-mode-updated";
|
||||
const AGENT_ORDER_STORAGE_PREFIX = "paperclip.agentOrder";
|
||||
const AGENT_SORT_MODE_STORAGE_PREFIX = "paperclip.agentSortMode";
|
||||
const ANONYMOUS_USER_ID = "anonymous";
|
||||
|
||||
export type AgentSidebarSortMode = "top" | "alphabetical" | "recent";
|
||||
|
||||
type AgentOrderUpdatedDetail = {
|
||||
storageKey: string;
|
||||
orderedIds: string[];
|
||||
};
|
||||
|
||||
type AgentSortModeUpdatedDetail = {
|
||||
storageKey: string;
|
||||
sortMode: AgentSidebarSortMode;
|
||||
};
|
||||
|
||||
function normalizeIdList(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value.filter((item): item is string => typeof item === "string" && item.length > 0);
|
||||
}
|
||||
|
||||
function normalizeSortMode(value: unknown): AgentSidebarSortMode {
|
||||
return value === "alphabetical" || value === "recent" || value === "top" ? value : "top";
|
||||
}
|
||||
|
||||
function resolveUserId(userId: string | null | undefined): string {
|
||||
if (!userId) return ANONYMOUS_USER_ID;
|
||||
const trimmed = userId.trim();
|
||||
@@ -24,6 +37,10 @@ export function getAgentOrderStorageKey(companyId: string, userId: string | null
|
||||
return `${AGENT_ORDER_STORAGE_PREFIX}:${companyId}:${resolveUserId(userId)}`;
|
||||
}
|
||||
|
||||
export function getAgentSortModeStorageKey(companyId: string, userId: string | null | undefined): string {
|
||||
return `${AGENT_SORT_MODE_STORAGE_PREFIX}:${companyId}:${resolveUserId(userId)}`;
|
||||
}
|
||||
|
||||
export function readAgentOrder(storageKey: string): string[] {
|
||||
try {
|
||||
const raw = localStorage.getItem(storageKey);
|
||||
@@ -34,6 +51,14 @@ export function readAgentOrder(storageKey: string): string[] {
|
||||
}
|
||||
}
|
||||
|
||||
export function readAgentSortMode(storageKey: string): AgentSidebarSortMode {
|
||||
try {
|
||||
return normalizeSortMode(localStorage.getItem(storageKey));
|
||||
} catch {
|
||||
return "top";
|
||||
}
|
||||
}
|
||||
|
||||
export function writeAgentOrder(storageKey: string, orderedIds: string[]) {
|
||||
const normalized = normalizeIdList(orderedIds);
|
||||
try {
|
||||
@@ -50,6 +75,22 @@ export function writeAgentOrder(storageKey: string, orderedIds: string[]) {
|
||||
}
|
||||
}
|
||||
|
||||
export function writeAgentSortMode(storageKey: string, sortMode: AgentSidebarSortMode) {
|
||||
const normalized = normalizeSortMode(sortMode);
|
||||
try {
|
||||
localStorage.setItem(storageKey, normalized);
|
||||
} catch {
|
||||
// Ignore storage write failures in restricted browser contexts.
|
||||
}
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent<AgentSortModeUpdatedDetail>(AGENT_SORT_MODE_UPDATED_EVENT, {
|
||||
detail: { storageKey, sortMode: normalized },
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function sortAgentsByDefaultSidebarOrder(agents: Agent[]): Agent[] {
|
||||
if (agents.length === 0) return [];
|
||||
|
||||
|
||||
Reference in new issue
Block a user