multica/apps/web/app/components/hub-sidebar.tsx
Naiyuan Qing 403c44f536 refactor(web): unify hub + agent state into single Zustand store
Replace useState-based useHub hook and separate useActiveAgent store
with a single useHubStore (Zustand). This fixes the bug where
HubSidebar and Chat held independent state copies, causing stale data
and duplicate 30s polling. Agent create/delete logic now lives in the
store with automatic activeAgentId management.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 23:15:11 +08:00

110 lines
4.1 KiB
TypeScript

"use client"
import {
SidebarGroup,
SidebarGroupLabel,
SidebarGroupAction,
SidebarGroupContent,
SidebarMenu,
SidebarMenuItem,
} from "@multica/ui/components/ui/sidebar"
import { Button } from "@multica/ui/components/ui/button"
import { HugeiconsIcon } from "@hugeicons/react"
import { PlusSignIcon, Delete02Icon } from "@hugeicons/core-free-icons"
import { useHubStore } from "../hooks/use-hub-store"
import { useHubInit } from "../hooks/use-hub-init"
const STATUS_DOT: Record<string, string> = {
connected: "bg-green-500/60",
loading: "bg-yellow-500/50 animate-pulse",
error: "bg-red-500/60",
idle: "bg-muted-foreground/50",
}
const STATUS_LABEL: Record<string, string> = {
connected: "Connected",
loading: "Connecting...",
error: "Disconnected",
idle: "Idle",
}
export function HubSidebar() {
useHubInit()
const status = useHubStore((s) => s.status)
const hub = useHubStore((s) => s.hub)
const agents = useHubStore((s) => s.agents)
const activeAgentId = useHubStore((s) => s.activeAgentId)
const fetchHub = useHubStore((s) => s.fetchHub)
const createAgent = useHubStore((s) => s.createAgent)
const deleteAgent = useHubStore((s) => s.deleteAgent)
const setActiveAgentId = useHubStore((s) => s.setActiveAgentId)
return (
<>
<SidebarGroup>
<SidebarGroupLabel>Hub</SidebarGroupLabel>
<SidebarGroupContent>
<div className="flex items-center gap-2 px-2 py-1 text-sm">
<span className={`size-2 rounded-full shrink-0 ${STATUS_DOT[status]}`} />
<span className="text-muted-foreground/70 text-xs">{STATUS_LABEL[status]}</span>
</div>
{status === "connected" && hub && (
<div className="px-2 text-xs text-muted-foreground/50 font-mono truncate">
{hub.hubId}
</div>
)}
{status === "error" && (
<div className="px-2 pt-1">
<Button variant="outline" size="sm" onClick={fetchHub} className="w-full text-xs">
Retry
</Button>
</div>
)}
</SidebarGroupContent>
</SidebarGroup>
{status === "connected" && (
<SidebarGroup>
<SidebarGroupLabel>Agents</SidebarGroupLabel>
<SidebarGroupAction onClick={createAgent} title="Create agent">
<HugeiconsIcon icon={PlusSignIcon} strokeWidth={2} className="size-4" />
</SidebarGroupAction>
<SidebarGroupContent>
<SidebarMenu>
{agents.length === 0 && (
<div className="px-2 py-2 text-xs text-muted-foreground/60">
No agents
</div>
)}
{agents.map(agent => (
<SidebarMenuItem key={agent.id} className="group/agent-item">
<div
role="button"
onClick={() => setActiveAgentId(agent.id)}
data-active={agent.id === activeAgentId || undefined}
className="flex items-center w-full h-8 px-2 rounded-md cursor-pointer hover:bg-sidebar-accent hover:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground data-active:font-medium"
>
<span className="flex-1 min-w-0 truncate font-mono text-xs">
{agent.id}
</span>
<button
onClick={(e) => {
e.stopPropagation()
deleteAgent(agent.id)
}}
title="Delete agent"
className="shrink-0 size-5 flex items-center justify-center rounded-md opacity-0 group-hover/agent-item:opacity-100 hover:bg-sidebar-accent-foreground/10 text-muted-foreground transition-opacity cursor-pointer"
>
<HugeiconsIcon icon={Delete02Icon} strokeWidth={1.5} className="size-3.5" />
</button>
</div>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
)}
</>
)
}