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>
This commit is contained in:
parent
cb28fbecc0
commit
403c44f536
6 changed files with 111 additions and 94 deletions
|
|
@ -11,8 +11,7 @@ import { UserIcon, Copy01Icon, CheckmarkCircle02Icon } from "@hugeicons/core-fre
|
|||
import { toast } from "@multica/ui/components/ui/sonner";
|
||||
import { useMessages } from "../hooks/use-messages";
|
||||
import { useGateway } from "../hooks/use-gateway";
|
||||
import { useHub } from "../hooks/use-hub";
|
||||
import { useActiveAgent } from "../hooks/use-active-agent";
|
||||
import { useHubStore } from "../hooks/use-hub-store";
|
||||
import { useDeviceId } from "../hooks/use-device-id";
|
||||
import { useScrollFade } from "../hooks/use-scroll-fade";
|
||||
import { cn } from "@multica/ui/lib/utils";
|
||||
|
|
@ -25,9 +24,9 @@ const STATE_VARIANT: Record<string, "default" | "secondary" | "destructive" | "o
|
|||
}
|
||||
|
||||
export function Chat() {
|
||||
const activeAgentId = useActiveAgent((s) => s.activeAgentId)
|
||||
const activeAgentId = useHubStore((s) => s.activeAgentId)
|
||||
const hub = useHubStore((s) => s.hub)
|
||||
const { messages, addUserMessage, addAssistantMessage } = useMessages()
|
||||
const { hub } = useHub()
|
||||
|
||||
const { state: gwState, send } = useGateway({
|
||||
onMessage: (msg) => {
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ import {
|
|||
import { Button } from "@multica/ui/components/ui/button"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
import { PlusSignIcon, Delete02Icon } from "@hugeicons/core-free-icons"
|
||||
import { useHub } from "../hooks/use-hub"
|
||||
import { useActiveAgent } from "../hooks/use-active-agent"
|
||||
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",
|
||||
|
|
@ -29,9 +29,16 @@ const STATUS_LABEL: Record<string, string> = {
|
|||
}
|
||||
|
||||
export function HubSidebar() {
|
||||
const { status, hub, agents, fetchHub, createAgent, deleteAgent } = useHub()
|
||||
const activeAgentId = useActiveAgent((s) => s.activeAgentId)
|
||||
const setActiveAgentId = useActiveAgent((s) => s.setActiveAgentId)
|
||||
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 (
|
||||
<>
|
||||
|
|
@ -60,13 +67,10 @@ export function HubSidebar() {
|
|||
{status === "connected" && (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Agents</SidebarGroupLabel>
|
||||
<SidebarGroupAction onClick={async () => {
|
||||
const id = await createAgent()
|
||||
if (id) setActiveAgentId(id)
|
||||
}} title="Create agent">
|
||||
<SidebarGroupAction onClick={createAgent} title="Create agent">
|
||||
<HugeiconsIcon icon={PlusSignIcon} strokeWidth={2} className="size-4" />
|
||||
</SidebarGroupAction>
|
||||
<SidebarGroupContent className="flex flex-col gap-1">
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{agents.length === 0 && (
|
||||
<div className="px-2 py-2 text-xs text-muted-foreground/60">
|
||||
|
|
@ -87,7 +91,6 @@ export function HubSidebar() {
|
|||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (activeAgentId === agent.id) setActiveAgentId(null)
|
||||
deleteAgent(agent.id)
|
||||
}}
|
||||
title="Delete agent"
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
import { create } from "zustand"
|
||||
|
||||
interface ActiveAgentState {
|
||||
activeAgentId: string | null
|
||||
setActiveAgentId: (id: string | null) => void
|
||||
}
|
||||
|
||||
export const useActiveAgent = create<ActiveAgentState>()((set) => ({
|
||||
activeAgentId: null,
|
||||
setActiveAgentId: (id: string | null) => set({ activeAgentId: id }),
|
||||
}))
|
||||
19
apps/web/app/hooks/use-hub-init.ts
Normal file
19
apps/web/app/hooks/use-hub-init.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect } from "react"
|
||||
import { useHubStore } from "./use-hub-store"
|
||||
|
||||
export function useHubInit() {
|
||||
const fetchHub = useHubStore((s) => s.fetchHub)
|
||||
const status = useHubStore((s) => s.status)
|
||||
const fetchAgents = useHubStore((s) => s.fetchAgents)
|
||||
|
||||
useEffect(() => { fetchHub() }, [fetchHub])
|
||||
useEffect(() => {
|
||||
if (status === "connected") fetchAgents()
|
||||
}, [status, fetchAgents])
|
||||
useEffect(() => {
|
||||
const id = setInterval(fetchHub, 30_000)
|
||||
return () => clearInterval(id)
|
||||
}, [fetchHub])
|
||||
}
|
||||
75
apps/web/app/hooks/use-hub-store.ts
Normal file
75
apps/web/app/hooks/use-hub-store.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { create } from "zustand"
|
||||
import { CONSOLE_URL } from "../lib/config"
|
||||
|
||||
interface HubInfo {
|
||||
hubId: string
|
||||
url: string
|
||||
connectionState: string
|
||||
agentCount: number
|
||||
}
|
||||
|
||||
interface Agent {
|
||||
id: string
|
||||
closed: boolean
|
||||
}
|
||||
|
||||
type HubStatus = "idle" | "loading" | "connected" | "error"
|
||||
|
||||
interface HubStore {
|
||||
status: HubStatus
|
||||
hub: HubInfo | null
|
||||
agents: Agent[]
|
||||
activeAgentId: string | null
|
||||
|
||||
setActiveAgentId: (id: string | null) => void
|
||||
fetchHub: () => Promise<void>
|
||||
fetchAgents: () => Promise<void>
|
||||
createAgent: () => Promise<void>
|
||||
deleteAgent: (id: string) => Promise<void>
|
||||
}
|
||||
|
||||
export const useHubStore = create<HubStore>()((set, get) => ({
|
||||
status: "idle",
|
||||
hub: null,
|
||||
agents: [],
|
||||
activeAgentId: null,
|
||||
|
||||
setActiveAgentId: (id) => set({ activeAgentId: id }),
|
||||
|
||||
fetchHub: async () => {
|
||||
set({ status: "loading" })
|
||||
try {
|
||||
const res = await fetch(`${CONSOLE_URL}/api/hub`)
|
||||
if (!res.ok) throw new Error(res.statusText)
|
||||
const data: HubInfo = await res.json()
|
||||
set({
|
||||
hub: data,
|
||||
status: data.connectionState === "registered" ? "connected" : "error",
|
||||
})
|
||||
} catch {
|
||||
set({ status: "error", hub: null })
|
||||
}
|
||||
},
|
||||
|
||||
fetchAgents: async () => {
|
||||
try {
|
||||
const res = await fetch(`${CONSOLE_URL}/api/agents`)
|
||||
if (res.ok) set({ agents: await res.json() })
|
||||
} catch { /* silent */ }
|
||||
},
|
||||
|
||||
createAgent: async () => {
|
||||
const res = await fetch(`${CONSOLE_URL}/api/agents`, { method: "POST" })
|
||||
await get().fetchAgents()
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
if (data.id) set({ activeAgentId: data.id })
|
||||
}
|
||||
},
|
||||
|
||||
deleteAgent: async (id) => {
|
||||
if (get().activeAgentId === id) set({ activeAgentId: null })
|
||||
await fetch(`${CONSOLE_URL}/api/agents/${id}`, { method: "DELETE" })
|
||||
await get().fetchAgents()
|
||||
},
|
||||
}))
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
import { useState, useCallback, useEffect } from "react"
|
||||
import { CONSOLE_URL } from "../lib/config"
|
||||
|
||||
interface HubInfo {
|
||||
hubId: string
|
||||
url: string
|
||||
connectionState: string
|
||||
agentCount: number
|
||||
}
|
||||
|
||||
interface Agent {
|
||||
id: string
|
||||
closed: boolean
|
||||
}
|
||||
|
||||
type HubStatus = "idle" | "loading" | "connected" | "error"
|
||||
|
||||
export function useHub() {
|
||||
const [status, setStatus] = useState<HubStatus>("idle")
|
||||
const [hub, setHub] = useState<HubInfo | null>(null)
|
||||
const [agents, setAgents] = useState<Agent[]>([])
|
||||
|
||||
const fetchHub = useCallback(async () => {
|
||||
setStatus("loading")
|
||||
try {
|
||||
const res = await fetch(`${CONSOLE_URL}/api/hub`)
|
||||
if (!res.ok) throw new Error(res.statusText)
|
||||
const data: HubInfo = await res.json()
|
||||
setHub(data)
|
||||
setStatus(data.connectionState === "registered" ? "connected" : "error")
|
||||
} catch {
|
||||
setStatus("error")
|
||||
setHub(null)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const fetchAgents = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch(`${CONSOLE_URL}/api/agents`)
|
||||
if (res.ok) setAgents(await res.json())
|
||||
} catch { /* silent */ }
|
||||
}, [])
|
||||
|
||||
const createAgent = useCallback(async (): Promise<string | null> => {
|
||||
const res = await fetch(`${CONSOLE_URL}/api/agents`, { method: "POST" })
|
||||
await fetchAgents()
|
||||
if (!res.ok) return null
|
||||
const data = await res.json()
|
||||
return data.id ?? null
|
||||
}, [fetchAgents])
|
||||
|
||||
const deleteAgent = useCallback(async (id: string) => {
|
||||
await fetch(`${CONSOLE_URL}/api/agents/${id}`, { method: "DELETE" })
|
||||
await fetchAgents()
|
||||
}, [fetchAgents])
|
||||
|
||||
// Auto-fetch hub on mount, agents when connected, poll every 30s
|
||||
useEffect(() => { fetchHub() }, [fetchHub])
|
||||
useEffect(() => {
|
||||
if (status === "connected") fetchAgents()
|
||||
}, [status, fetchAgents])
|
||||
useEffect(() => {
|
||||
const id = setInterval(fetchHub, 30_000)
|
||||
return () => clearInterval(id)
|
||||
}, [fetchHub])
|
||||
|
||||
return { status, hub, agents, fetchHub, createAgent, deleteAgent }
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue