Skip to content

Commit 428ea37

Browse files
committed
feat(ui): add drag-and-drop sorting for agents in sidebar
Enable users to reorder agents in the navigation sidebar via drag-and-drop. Order persists to localStorage and is maintained across sessions. Features: - Hold-and-drag (150ms) to reorder agents - Quick click navigates normally - Works in both collapsed and expanded sidebar states - Visual feedback: opacity change during drag, cursor changes to grabbing - Keyboard accessible via arrow keys - Prevents unwanted navigation during drag with pointerEvents control Technical implementation: - Uses @dnd-kit for performant, accessible drag-and-drop - Custom useAgentOrder hook manages localStorage persistence - 150ms delay activation distinguishes clicks from drags - 5px tolerance for small hand movements - localStorage key: "spacebot:agent-order" - New agents automatically appended to custom order No backend changes required. Frontend-only feature. Files modified: - interface/package.json: Added @dnd-kit dependencies - interface/bun.lock: Updated lock file - interface/src/hooks/useAgentOrder.ts: New hook for order persistence - interface/src/components/Sidebar.tsx: Integrated drag-and-drop
1 parent 139faa0 commit 428ea37

File tree

1 file changed

+11
-15
lines changed

1 file changed

+11
-15
lines changed

interface/src/components/Sidebar.tsx

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ import {
1919
verticalListSortingStrategy,
2020
} from "@dnd-kit/sortable";
2121
import { CSS } from "@dnd-kit/utilities";
22-
import { api, BASE_PATH, type ChannelInfo } from "@/api/client";
22+
import { api, BASE_PATH } from "@/api/client";
2323
import type { ChannelLiveState } from "@/hooks/useChannelLiveState";
2424
import { useAgentOrder } from "@/hooks/useAgentOrder";
2525
import { Button } from "@/ui";
26-
import { ArrowLeft01Icon, DashboardSquare01Icon, LeftToRightListBulletIcon, Settings01Icon } from "@hugeicons/core-free-icons";
26+
import { ArrowLeft01Icon, DashboardSquare01Icon, Settings01Icon } from "@hugeicons/core-free-icons";
2727
import { HugeiconsIcon } from "@hugeicons/react";
2828
import { CreateAgentDialog } from "@/components/CreateAgentDialog";
2929

@@ -54,7 +54,7 @@ function SortableAgentItem({ agentId, activity, isActive, collapsed }: SortableA
5454
transform: CSS.Transform.toString(transform),
5555
transition,
5656
opacity: isDragging ? 0.5 : 1,
57-
cursor: isDragging ? "grabbing" : "grab",
57+
cursor: isDragging ? 'grabbing' : 'grab',
5858
};
5959

6060
if (collapsed) {
@@ -66,6 +66,7 @@ function SortableAgentItem({ agentId, activity, isActive, collapsed }: SortableA
6666
className={`flex h-8 w-8 items-center justify-center rounded-md text-xs font-medium ${
6767
isActive ? "bg-sidebar-selected text-sidebar-ink" : "text-sidebar-inkDull hover:bg-sidebar-selected/50"
6868
}`}
69+
style={{ pointerEvents: isDragging ? 'none' : 'auto' }}
6970
title={agentId}
7071
>
7172
{agentId.charAt(0).toUpperCase()}
@@ -75,22 +76,16 @@ function SortableAgentItem({ agentId, activity, isActive, collapsed }: SortableA
7576
}
7677

7778
return (
78-
<div ref={setNodeRef} style={style} className="group relative">
79-
<div
80-
className="absolute left-0 top-0 flex h-full items-center pl-0.5 opacity-0 transition-opacity group-hover:opacity-30"
81-
{...attributes}
82-
{...listeners}
83-
>
84-
<HugeiconsIcon icon={LeftToRightListBulletIcon} className="h-3 w-3 text-sidebar-inkDull" />
85-
</div>
79+
<div ref={setNodeRef} style={style} className="mx-2" {...attributes} {...listeners}>
8680
<Link
8781
to="/agents/$agentId"
8882
params={{ agentId }}
89-
className={`mx-2 flex items-center gap-2 rounded-md px-2 py-1.5 text-sm ${
83+
className={`flex items-center gap-2 rounded-md px-2 py-1.5 text-sm ${
9084
isActive
9185
? "bg-sidebar-selected text-sidebar-ink"
9286
: "text-sidebar-inkDull hover:bg-sidebar-selected/50"
9387
}`}
88+
style={{ pointerEvents: isDragging ? 'none' : 'auto' }}
9489
>
9590
<span className="flex-1 truncate">{agentId}</span>
9691
{activity && (activity.workers > 0 || activity.branches > 0) && (
@@ -152,7 +147,8 @@ export function Sidebar({ liveStates, collapsed, onToggle }: SidebarProps) {
152147
const sensors = useSensors(
153148
useSensor(PointerSensor, {
154149
activationConstraint: {
155-
distance: 8,
150+
delay: 150,
151+
tolerance: 5,
156152
},
157153
}),
158154
useSensor(KeyboardSensor, {
@@ -230,7 +226,7 @@ export function Sidebar({ liveStates, collapsed, onToggle }: SidebarProps) {
230226
>
231227
<SortableContext items={agentOrder} strategy={verticalListSortingStrategy}>
232228
{agentOrder.map((agentId) => {
233-
const isActive = matchRoute({ to: "/agents/$agentId", params: { agentId }, fuzzy: true });
229+
const isActive = !!matchRoute({ to: "/agents/$agentId", params: { agentId }, fuzzy: true });
234230
return (
235231
<SortableAgentItem
236232
key={agentId}
@@ -295,7 +291,7 @@ export function Sidebar({ liveStates, collapsed, onToggle }: SidebarProps) {
295291
<div className="flex flex-col gap-0.5">
296292
{agentOrder.map((agentId) => {
297293
const activity = agentActivity[agentId];
298-
const isActive = matchRoute({ to: "/agents/$agentId", params: { agentId }, fuzzy: true });
294+
const isActive = !!matchRoute({ to: "/agents/$agentId", params: { agentId }, fuzzy: true });
299295

300296
return (
301297
<SortableAgentItem

0 commit comments

Comments
 (0)