From 1f22c3e578efeaef7a2408e1bc35dfc6c29e99ac Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Mon, 2 Feb 2026 14:40:09 +0800 Subject: [PATCH 1/7] feat(hub): register RPC handlers for hub, agent, and gateway operations Add 5 new RPC methods (getHubInfo, listAgents, createAgent, deleteAgent, updateGateway) mirroring the existing Console HTTP API, enabling pure WebSocket communication from the frontend. Co-Authored-By: Claude Opus 4.5 --- docs/rpc.md | 130 +++++++++++++++++++++++++ packages/sdk/src/actions/index.ts | 8 ++ packages/sdk/src/actions/rpc.ts | 44 +++++++++ src/hub/hub.ts | 10 ++ src/hub/rpc/handlers/create-agent.ts | 13 +++ src/hub/rpc/handlers/delete-agent.ts | 19 ++++ src/hub/rpc/handlers/get-hub-info.ts | 17 ++++ src/hub/rpc/handlers/list-agents.ts | 16 +++ src/hub/rpc/handlers/update-gateway.ts | 21 ++++ src/hub/rpc/index.ts | 5 + 10 files changed, 283 insertions(+) create mode 100644 src/hub/rpc/handlers/create-agent.ts create mode 100644 src/hub/rpc/handlers/delete-agent.ts create mode 100644 src/hub/rpc/handlers/get-hub-info.ts create mode 100644 src/hub/rpc/handlers/list-agents.ts create mode 100644 src/hub/rpc/handlers/update-gateway.ts diff --git a/docs/rpc.md b/docs/rpc.md index 535a8078..d3ce36dc 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -207,6 +207,136 @@ const result = await client.request( } ``` +### `getHubInfo` + +Returns Hub status information. No parameters required. + +**Response:** + +```ts +interface GetHubInfoResult { + hubId: string; // Hub device ID + url: string; // Current Gateway URL + connectionState: string; // "disconnected" | "connecting" | "connected" | "registered" + agentCount: number; // Number of active agents +} +``` + +**Example:** + +```ts +const info = await client.request(hubDeviceId, "getHubInfo"); +``` + +--- + +### `listAgents` + +Lists all active agents. No parameters required. + +**Response:** + +```ts +interface ListAgentsResult { + agents: { id: string; closed: boolean }[]; +} +``` + +**Example:** + +```ts +const result = await client.request(hubDeviceId, "listAgents"); +``` + +--- + +### `createAgent` + +Creates a new agent or restores an existing one. + +**Parameters:** + +```ts +interface CreateAgentParams { + id?: string; // optional - reuse existing session ID +} +``` + +**Response:** + +```ts +interface CreateAgentResult { + id: string; // the created/restored agent session ID +} +``` + +**Example:** + +```ts +const result = await client.request(hubDeviceId, "createAgent"); +// or with specific ID: +const result = await client.request(hubDeviceId, "createAgent", { id: "existing-id" }); +``` + +--- + +### `deleteAgent` + +Closes and removes an agent. + +**Parameters:** + +```ts +interface DeleteAgentParams { + id: string; // required - agent ID to delete +} +``` + +**Response:** + +```ts +interface DeleteAgentResult { + ok: boolean; // true if agent was found and deleted +} +``` + +**Example:** + +```ts +const result = await client.request(hubDeviceId, "deleteAgent", { id: "019abc12-..." }); +``` + +--- + +### `updateGateway` + +Reconnects the Hub to a different Gateway URL. + +**Parameters:** + +```ts +interface UpdateGatewayParams { + url: string; // required - new Gateway URL +} +``` + +**Response:** + +```ts +interface UpdateGatewayResult { + url: string; // the new URL + connectionState: string; // connection state after reconnect +} +``` + +**Example:** + +```ts +const result = await client.request(hubDeviceId, "updateGateway", { url: "http://localhost:4000" }); +``` + +--- + ## Adding New RPC Methods 1. Create a handler file in `src/hub/rpc/handlers/`: diff --git a/packages/sdk/src/actions/index.ts b/packages/sdk/src/actions/index.ts index d60637ee..0c673bbd 100644 --- a/packages/sdk/src/actions/index.ts +++ b/packages/sdk/src/actions/index.ts @@ -16,6 +16,14 @@ export { isResponseError, type GetAgentMessagesParams, type GetAgentMessagesResult, + type GetHubInfoResult, + type ListAgentsResult, + type CreateAgentParams, + type CreateAgentResult, + type DeleteAgentParams, + type DeleteAgentResult, + type UpdateGatewayParams, + type UpdateGatewayResult, } from "./rpc.js"; export { StreamAction, type StreamPayload } from "./stream.js"; diff --git a/packages/sdk/src/actions/rpc.ts b/packages/sdk/src/actions/rpc.ts index 0cdc3200..11c0e665 100644 --- a/packages/sdk/src/actions/rpc.ts +++ b/packages/sdk/src/actions/rpc.ts @@ -72,3 +72,47 @@ export interface GetAgentMessagesResult { offset: number; limit: number; } + +/** getHubInfo - no params needed */ +export interface GetHubInfoResult { + hubId: string; + url: string; + connectionState: string; + agentCount: number; +} + +/** listAgents - no params needed */ +export interface ListAgentsResult { + agents: { id: string; closed: boolean }[]; +} + +/** createAgent - request params */ +export interface CreateAgentParams { + id?: string; +} + +/** createAgent - response payload */ +export interface CreateAgentResult { + id: string; +} + +/** deleteAgent - request params */ +export interface DeleteAgentParams { + id: string; +} + +/** deleteAgent - response payload */ +export interface DeleteAgentResult { + ok: boolean; +} + +/** updateGateway - request params */ +export interface UpdateGatewayParams { + url: string; +} + +/** updateGateway - response payload */ +export interface UpdateGatewayResult { + url: string; + connectionState: string; +} diff --git a/src/hub/hub.ts b/src/hub/hub.ts index 5765cef1..c456d8a7 100644 --- a/src/hub/hub.ts +++ b/src/hub/hub.ts @@ -12,6 +12,11 @@ import { getHubId } from "./hub-identity.js"; import { loadAgentRecords, addAgentRecord, removeAgentRecord } from "./agent-store.js"; import { RpcDispatcher, RpcError } from "./rpc/dispatcher.js"; import { createGetAgentMessagesHandler } from "./rpc/handlers/get-agent-messages.js"; +import { createGetHubInfoHandler } from "./rpc/handlers/get-hub-info.js"; +import { createListAgentsHandler } from "./rpc/handlers/list-agents.js"; +import { createCreateAgentHandler } from "./rpc/handlers/create-agent.js"; +import { createDeleteAgentHandler } from "./rpc/handlers/delete-agent.js"; +import { createUpdateGatewayHandler } from "./rpc/handlers/update-gateway.js"; export class Hub { private readonly agents = new Map(); @@ -34,6 +39,11 @@ export class Hub { this.rpc = new RpcDispatcher(); this.rpc.register("getAgentMessages", createGetAgentMessagesHandler()); + this.rpc.register("getHubInfo", createGetHubInfoHandler(this)); + this.rpc.register("listAgents", createListAgentsHandler(this)); + this.rpc.register("createAgent", createCreateAgentHandler(this)); + this.rpc.register("deleteAgent", createDeleteAgentHandler(this)); + this.rpc.register("updateGateway", createUpdateGatewayHandler(this)); this.client = this.createClient(this.url); this.client.connect(); diff --git a/src/hub/rpc/handlers/create-agent.ts b/src/hub/rpc/handlers/create-agent.ts new file mode 100644 index 00000000..c0ab1186 --- /dev/null +++ b/src/hub/rpc/handlers/create-agent.ts @@ -0,0 +1,13 @@ +import type { RpcHandler } from "../dispatcher.js"; + +interface HubLike { + createAgent(id?: string): { sessionId: string }; +} + +export function createCreateAgentHandler(hub: HubLike): RpcHandler { + return (params: unknown) => { + const { id } = (params ?? {}) as { id?: string }; + const agent = hub.createAgent(id); + return { id: agent.sessionId }; + }; +} diff --git a/src/hub/rpc/handlers/delete-agent.ts b/src/hub/rpc/handlers/delete-agent.ts new file mode 100644 index 00000000..72207f24 --- /dev/null +++ b/src/hub/rpc/handlers/delete-agent.ts @@ -0,0 +1,19 @@ +import { RpcError, type RpcHandler } from "../dispatcher.js"; + +interface HubLike { + closeAgent(id: string): boolean; +} + +export function createDeleteAgentHandler(hub: HubLike): RpcHandler { + return (params: unknown) => { + if (!params || typeof params !== "object") { + throw new RpcError("INVALID_PARAMS", "params must be an object"); + } + const { id } = params as { id?: string }; + if (!id) { + throw new RpcError("INVALID_PARAMS", "Missing required param: id"); + } + const ok = hub.closeAgent(id); + return { ok }; + }; +} diff --git a/src/hub/rpc/handlers/get-hub-info.ts b/src/hub/rpc/handlers/get-hub-info.ts new file mode 100644 index 00000000..46da8784 --- /dev/null +++ b/src/hub/rpc/handlers/get-hub-info.ts @@ -0,0 +1,17 @@ +import type { RpcHandler } from "../dispatcher.js"; + +interface HubLike { + hubId: string; + url: string; + connectionState: string; + listAgents(): string[]; +} + +export function createGetHubInfoHandler(hub: HubLike): RpcHandler { + return () => ({ + hubId: hub.hubId, + url: hub.url, + connectionState: hub.connectionState, + agentCount: hub.listAgents().length, + }); +} diff --git a/src/hub/rpc/handlers/list-agents.ts b/src/hub/rpc/handlers/list-agents.ts new file mode 100644 index 00000000..ccd5c3e2 --- /dev/null +++ b/src/hub/rpc/handlers/list-agents.ts @@ -0,0 +1,16 @@ +import type { RpcHandler } from "../dispatcher.js"; + +interface HubLike { + listAgents(): string[]; + getAgent(id: string): { closed: boolean } | undefined; +} + +export function createListAgentsHandler(hub: HubLike): RpcHandler { + return () => { + const agents = hub.listAgents().map((id) => { + const agent = hub.getAgent(id); + return { id, closed: agent?.closed ?? true }; + }); + return { agents }; + }; +} diff --git a/src/hub/rpc/handlers/update-gateway.ts b/src/hub/rpc/handlers/update-gateway.ts new file mode 100644 index 00000000..acc4c9ce --- /dev/null +++ b/src/hub/rpc/handlers/update-gateway.ts @@ -0,0 +1,21 @@ +import { RpcError, type RpcHandler } from "../dispatcher.js"; + +interface HubLike { + url: string; + connectionState: string; + reconnect(url: string): void; +} + +export function createUpdateGatewayHandler(hub: HubLike): RpcHandler { + return (params: unknown) => { + if (!params || typeof params !== "object") { + throw new RpcError("INVALID_PARAMS", "params must be an object"); + } + const { url } = params as { url?: string }; + if (!url) { + throw new RpcError("INVALID_PARAMS", "Missing required param: url"); + } + hub.reconnect(url); + return { url: hub.url, connectionState: hub.connectionState }; + }; +} diff --git a/src/hub/rpc/index.ts b/src/hub/rpc/index.ts index c482999a..7bd88565 100644 --- a/src/hub/rpc/index.ts +++ b/src/hub/rpc/index.ts @@ -1,2 +1,7 @@ export { RpcDispatcher, RpcError, type RpcHandler } from "./dispatcher.js"; export { createGetAgentMessagesHandler } from "./handlers/get-agent-messages.js"; +export { createGetHubInfoHandler } from "./handlers/get-hub-info.js"; +export { createListAgentsHandler } from "./handlers/list-agents.js"; +export { createCreateAgentHandler } from "./handlers/create-agent.js"; +export { createDeleteAgentHandler } from "./handlers/delete-agent.js"; +export { createUpdateGatewayHandler } from "./handlers/update-gateway.js"; From 150c87a4966c524eb8754703d92cb21e03938e3c Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Mon, 2 Feb 2026 15:12:14 +0800 Subject: [PATCH 2/7] refactor(frontend): replace HTTP API with pure WebSocket RPC - gateway store: add hubId state and request() RPC method - hub store: replace consoleApi calls with gateway.request() RPC - hub-init: simplify to react on gwState === "registered" - hub-sidebar: add hub-id input + connect/disconnect flow - chat: use gateway hubId for message routing - layout: remove consoleUrl config (no longer needed) Co-Authored-By: Claude Opus 4.5 --- apps/web/app/layout.tsx | 1 - packages/store/src/gateway.ts | 21 ++++-- packages/store/src/hub-init.ts | 26 +++----- packages/store/src/hub.ts | 34 +++++----- packages/ui/src/components/chat.tsx | 6 +- packages/ui/src/components/hub-sidebar.tsx | 75 ++++++++++++++++------ 6 files changed, 104 insertions(+), 59 deletions(-) diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 716a2e72..8d468eb4 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -12,7 +12,6 @@ import { Toaster } from "@multica/ui/components/ui/sonner"; import { HubSidebar } from "@multica/ui/components/hub-sidebar"; setConfig({ - consoleUrl: process.env.NEXT_PUBLIC_CONSOLE_URL ?? "http://localhost:4000", gatewayUrl: process.env.NEXT_PUBLIC_GATEWAY_URL ?? "http://localhost:3000", }); diff --git a/packages/store/src/gateway.ts b/packages/store/src/gateway.ts index da5dd0d9..ae7baff0 100644 --- a/packages/store/src/gateway.ts +++ b/packages/store/src/gateway.ts @@ -5,26 +5,31 @@ import { useMessagesStore } from "./messages" interface GatewayState { connectionState: ConnectionState + hubId: string | null lastError: SendErrorResponse | null } interface GatewayActions { - connect: (deviceId: string) => void + connect: (deviceId: string, hubId: string) => void disconnect: () => void send: (to: string, action: string, payload: unknown) => void + request: (method: string, params?: unknown) => Promise } export type GatewayStore = GatewayState & GatewayActions let client: GatewayClient | null = null -export const useGatewayStore = create()((set) => ({ +export const useGatewayStore = create()((set, get) => ({ connectionState: "disconnected", + hubId: null, lastError: null, - connect: (deviceId) => { + connect: (deviceId, hubId) => { if (client) return + set({ hubId }) + client = new GatewayClient({ url: getGatewayUrl(), deviceId, @@ -47,11 +52,19 @@ export const useGatewayStore = create()((set) => ({ client.disconnect() client = null } - set({ connectionState: "disconnected" }) + set({ connectionState: "disconnected", hubId: null }) }, send: (to, action, payload) => { if (!client?.isRegistered) return client.send(to, action, payload) }, + + request: (method: string, params?: unknown): Promise => { + const { hubId } = get() + if (!client?.isRegistered || !hubId) { + return Promise.reject(new Error("Not connected")) + } + return client.request(hubId, method, params) + }, })) diff --git a/packages/store/src/hub-init.ts b/packages/store/src/hub-init.ts index bf06c56a..6d7ed702 100644 --- a/packages/store/src/hub-init.ts +++ b/packages/store/src/hub-init.ts @@ -2,29 +2,21 @@ import { useEffect } from "react" import { useHubStore } from "./hub" -import { useDeviceId } from "./device-id" import { useGatewayStore } from "./gateway" export function useHubInit() { + const gwState = useGatewayStore((s) => s.connectionState) const fetchHub = useHubStore((s) => s.fetchHub) - const status = useHubStore((s) => s.status) const fetchAgents = useHubStore((s) => s.fetchAgents) - const deviceId = useDeviceId() - useEffect(() => { fetchHub() }, [fetchHub]) + // Once WS is registered, fetch hub info and agents via RPC useEffect(() => { - if (status === "connected") fetchAgents() - }, [status, fetchAgents]) - useEffect(() => { - const id = setInterval(fetchHub, 30_000) - return () => clearInterval(id) - }, [fetchHub]) - - // Connect gateway when hub is ready and deviceId is available - useEffect(() => { - if (status === "connected" && deviceId) { - useGatewayStore.getState().connect(deviceId) - return () => { useGatewayStore.getState().disconnect() } + if (gwState === "registered") { + fetchHub() + fetchAgents() } - }, [status, deviceId]) + if (gwState === "disconnected") { + useHubStore.setState({ status: "idle", hub: null, agents: [], activeAgentId: null }) + } + }, [gwState, fetchHub, fetchAgents]) } diff --git a/packages/store/src/hub.ts b/packages/store/src/hub.ts index ed5cc41f..b5101eec 100644 --- a/packages/store/src/hub.ts +++ b/packages/store/src/hub.ts @@ -1,13 +1,14 @@ import { create } from "zustand" -import { consoleApi } from "@multica/fetch" import { toast } from "sonner" +import type { + GetHubInfoResult, + ListAgentsResult, + CreateAgentResult, + DeleteAgentResult, +} from "@multica/sdk" +import { useGatewayStore } from "./gateway" -export interface HubInfo { - hubId: string - url: string - connectionState: string - agentCount: number -} +export type HubInfo = GetHubInfoResult export interface Agent { id: string @@ -44,11 +45,9 @@ export const useHubStore = create()((set, get) => ({ fetchHub: async () => { set({ status: "loading" }) try { - const data = await consoleApi.get("/api/hub") - set({ - hub: data, - status: data.connectionState === "registered" ? "connected" : "error", - }) + const { request } = useGatewayStore.getState() + const data = await request("getHubInfo") + set({ hub: data, status: "connected" }) } catch { set({ status: "error", hub: null }) } @@ -56,8 +55,9 @@ export const useHubStore = create()((set, get) => ({ fetchAgents: async () => { try { - const data = await consoleApi.get("/api/agents") - set({ agents: data }) + const { request } = useGatewayStore.getState() + const data = await request("listAgents") + set({ agents: data.agents }) } catch (e) { toast.error("Failed to fetch agents") console.error(e) @@ -66,7 +66,8 @@ export const useHubStore = create()((set, get) => ({ createAgent: async (options?) => { try { - const data = await consoleApi.post<{ id: string }>("/api/agents", options) + const { request } = useGatewayStore.getState() + const data = await request("createAgent", options) await get().fetchAgents() if (data.id) set({ activeAgentId: data.id }) } catch (e) { @@ -78,7 +79,8 @@ export const useHubStore = create()((set, get) => ({ deleteAgent: async (id) => { if (get().activeAgentId === id) set({ activeAgentId: null }) try { - await consoleApi.delete("/api/agents/" + id) + const { request } = useGatewayStore.getState() + await request("deleteAgent", { id }) await get().fetchAgents() } catch (e) { toast.error("Failed to delete agent") diff --git a/packages/ui/src/components/chat.tsx b/packages/ui/src/components/chat.tsx index a510ca30..f7fa8910 100644 --- a/packages/ui/src/components/chat.tsx +++ b/packages/ui/src/components/chat.tsx @@ -29,11 +29,11 @@ export function Chat() { const filtered = useMemo(() => messages.filter(m => m.agentId === activeAgentId), [messages, activeAgentId]) const handleSend = useCallback((text: string) => { - const hub = useHubStore.getState().hub + const { hubId } = useGatewayStore.getState() const agentId = useHubStore.getState().activeAgentId - if (!hub?.hubId || !agentId) return + if (!hubId || !agentId) return useMessagesStore.getState().addUserMessage(text, agentId) - useGatewayStore.getState().send(hub.hubId, "message", { agentId, content: text }) + useGatewayStore.getState().send(hubId, "message", { agentId, content: text }) }, []) const canSend = gwState === "registered" && !!activeAgentId diff --git a/packages/ui/src/components/hub-sidebar.tsx b/packages/ui/src/components/hub-sidebar.tsx index fdd51596..8897ebc7 100644 --- a/packages/ui/src/components/hub-sidebar.tsx +++ b/packages/ui/src/components/hub-sidebar.tsx @@ -1,5 +1,6 @@ "use client" +import { useState } from "react" import { SidebarGroup, SidebarGroupLabel, @@ -9,9 +10,10 @@ import { SidebarMenuItem, } from "@multica/ui/components/ui/sidebar" import { Button } from "@multica/ui/components/ui/button" +import { Input } from "@multica/ui/components/ui/input" import { HugeiconsIcon } from "@hugeicons/react" import { PlusSignIcon, Delete02Icon } from "@hugeicons/core-free-icons" -import { useHubStore } from "@multica/store" +import { useHubStore, useDeviceId, useGatewayStore } from "@multica/store" import { useHubInit } from "@multica/store" import { Skeleton } from "@multica/ui/components/ui/skeleton" @@ -36,38 +38,75 @@ export function HubSidebar() { 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) + const gwState = useGatewayStore((s) => s.connectionState) + const connect = useGatewayStore((s) => s.connect) + const disconnect = useGatewayStore((s) => s.disconnect) + const deviceId = useDeviceId() + + const [hubIdInput, setHubIdInput] = useState("") + const isDisconnected = gwState === "disconnected" + const isConnecting = gwState === "connecting" || gwState === "connected" + + const handleConnect = () => { + const id = hubIdInput.trim() + if (!id || !deviceId) return + connect(deviceId, id) + } + return ( <> Hub -
- - {STATUS_LABEL[status]} -
- {status === "connected" && hub ? ( -
- {hub.hubId} -
- ) : (status === "idle" || status === "loading") ? ( - - ) : null} - {status === "error" && ( -
-
+ ) : ( + <> +
+ + + {isConnecting ? "Connecting..." : STATUS_LABEL[status]} + +
+ {status === "connected" && hub ? ( +
+ {hub.hubId} +
+ ) : (status === "idle" || status === "loading") ? ( + + ) : null} +
+ +
+ )}
- {(status === "idle" || status === "loading") && ( + {isConnecting && ( Agents From 3622f82a7b7398d0856905b875ed0840ef535040 Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:13:40 +0800 Subject: [PATCH 3/7] feat(gateway): add auto-connect, hub discovery via list-devices - Add LIST_DEVICES event and "hub" device type to SDK - Add listDevices() method to GatewayClient - Add handleListDevices handler in Gateway - Change Hub deviceType from "client" to "hub" - Refactor gateway store: auto-connect WS, separate hubId selection - Hub-init: auto-connect on mount, discover hubs on registered - Hub-sidebar: show discovered hubs list with connect/disconnect UI Co-Authored-By: Claude Opus 4.5 --- packages/sdk/src/client.ts | 20 ++++++ packages/sdk/src/index.ts | 1 + packages/sdk/src/types.ts | 8 ++- packages/store/src/gateway.ts | 24 +++++-- packages/store/src/hub-init.ts | 22 +++++- packages/ui/src/components/hub-sidebar.tsx | 78 ++++++++++++++++------ src/gateway/events.gateway.ts | 13 +++- src/hub/hub.ts | 2 +- 8 files changed, 136 insertions(+), 32 deletions(-) diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts index 05849715..f38e6d5d 100644 --- a/packages/sdk/src/client.ts +++ b/packages/sdk/src/client.ts @@ -9,6 +9,8 @@ import type { SendErrorResponse, PingPayload, DeviceType, + DeviceInfo, + ListDevicesResponse, } from "./types.js"; import { GatewayEvents } from "./types.js"; import { @@ -163,6 +165,24 @@ export class GatewayClient { }); } + /** List all devices connected to the Gateway */ + listDevices(): Promise { + return new Promise((resolve, reject) => { + if (!this.socket || !this.isRegistered) { + reject(new Error("Not registered")); + return; + } + + this.socket.emit( + GatewayEvents.LIST_DEVICES, + {}, + (response: ListDevicesResponse) => { + resolve(response.devices); + } + ); + }); + } + /** Send an RPC request and wait for the response */ request( to: string, diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index e1b696f2..7d9643e0 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -11,6 +11,7 @@ export { type ConnectionState, type PingPayload, type PongResponse, + type ListDevicesResponse, } from "./types.js"; // Actions diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index eec02b8d..d0067591 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -4,6 +4,7 @@ export const GatewayEvents = { PING: "ping", PONG: "pong", REGISTERED: "registered", + LIST_DEVICES: "list-devices", // Message routing SEND: "send", @@ -14,7 +15,7 @@ export const GatewayEvents = { // ============ Device Related ============ /** Device type */ -export type DeviceType = "client" | "agent"; +export type DeviceType = "client" | "hub" | "agent"; /** Device information */ export interface DeviceInfo { @@ -54,6 +55,11 @@ export interface SendErrorResponse { code: "DEVICE_NOT_FOUND" | "NOT_REGISTERED" | "INVALID_MESSAGE"; } +/** List devices response */ +export interface ListDevicesResponse { + devices: DeviceInfo[]; +} + // ============ Ping/Pong ============ /** Ping request */ diff --git a/packages/store/src/gateway.ts b/packages/store/src/gateway.ts index ae7baff0..f7cf4958 100644 --- a/packages/store/src/gateway.ts +++ b/packages/store/src/gateway.ts @@ -1,17 +1,20 @@ import { create } from "zustand" -import { GatewayClient, type ConnectionState, type SendErrorResponse } from "@multica/sdk" +import { GatewayClient, type ConnectionState, type DeviceInfo, type SendErrorResponse } from "@multica/sdk" import { getGatewayUrl } from "@multica/fetch" import { useMessagesStore } from "./messages" interface GatewayState { connectionState: ConnectionState hubId: string | null + hubs: DeviceInfo[] lastError: SendErrorResponse | null } interface GatewayActions { - connect: (deviceId: string, hubId: string) => void + connect: (deviceId: string) => void disconnect: () => void + setHubId: (hubId: string) => void + listDevices: () => Promise send: (to: string, action: string, payload: unknown) => void request: (method: string, params?: unknown) => Promise } @@ -23,13 +26,12 @@ let client: GatewayClient | null = null export const useGatewayStore = create()((set, get) => ({ connectionState: "disconnected", hubId: null, + hubs: [], lastError: null, - connect: (deviceId, hubId) => { + connect: (deviceId) => { if (client) return - set({ hubId }) - client = new GatewayClient({ url: getGatewayUrl(), deviceId, @@ -52,7 +54,17 @@ export const useGatewayStore = create()((set, get) => ({ client.disconnect() client = null } - set({ connectionState: "disconnected", hubId: null }) + set({ connectionState: "disconnected", hubId: null, hubs: [] }) + }, + + setHubId: (hubId) => set({ hubId }), + + listDevices: async () => { + if (!client?.isRegistered) return [] + const devices = await client.listDevices() + const hubs = devices.filter((d) => d.deviceType === "hub") + set({ hubs }) + return devices }, send: (to, action, payload) => { diff --git a/packages/store/src/hub-init.ts b/packages/store/src/hub-init.ts index 6d7ed702..ca9de4da 100644 --- a/packages/store/src/hub-init.ts +++ b/packages/store/src/hub-init.ts @@ -2,21 +2,39 @@ import { useEffect } from "react" import { useHubStore } from "./hub" +import { useDeviceId } from "./device-id" import { useGatewayStore } from "./gateway" export function useHubInit() { + const deviceId = useDeviceId() const gwState = useGatewayStore((s) => s.connectionState) + const hubId = useGatewayStore((s) => s.hubId) const fetchHub = useHubStore((s) => s.fetchHub) const fetchAgents = useHubStore((s) => s.fetchAgents) - // Once WS is registered, fetch hub info and agents via RPC + // Auto-connect WS when deviceId is available + useEffect(() => { + if (deviceId) { + useGatewayStore.getState().connect(deviceId) + return () => { useGatewayStore.getState().disconnect() } + } + }, [deviceId]) + + // Once WS is registered, discover available hubs useEffect(() => { if (gwState === "registered") { + useGatewayStore.getState().listDevices() + } + }, [gwState]) + + // Once hubId is set and WS is registered, fetch hub info and agents via RPC + useEffect(() => { + if (gwState === "registered" && hubId) { fetchHub() fetchAgents() } if (gwState === "disconnected") { useHubStore.setState({ status: "idle", hub: null, agents: [], activeAgentId: null }) } - }, [gwState, fetchHub, fetchAgents]) + }, [gwState, hubId, fetchHub, fetchAgents]) } diff --git a/packages/ui/src/components/hub-sidebar.tsx b/packages/ui/src/components/hub-sidebar.tsx index 8897ebc7..a64b232e 100644 --- a/packages/ui/src/components/hub-sidebar.tsx +++ b/packages/ui/src/components/hub-sidebar.tsx @@ -43,18 +43,24 @@ export function HubSidebar() { const setActiveAgentId = useHubStore((s) => s.setActiveAgentId) const gwState = useGatewayStore((s) => s.connectionState) - const connect = useGatewayStore((s) => s.connect) - const disconnect = useGatewayStore((s) => s.disconnect) - const deviceId = useDeviceId() + const hubId = useGatewayStore((s) => s.hubId) + const hubs = useGatewayStore((s) => s.hubs) + const setHubId = useGatewayStore((s) => s.setHubId) + const listDevices = useGatewayStore((s) => s.listDevices) const [hubIdInput, setHubIdInput] = useState("") - const isDisconnected = gwState === "disconnected" - const isConnecting = gwState === "connecting" || gwState === "connected" + const isRegistered = gwState === "registered" + const needsHubSelection = isRegistered && !hubId const handleConnect = () => { const id = hubIdInput.trim() - if (!id || !deviceId) return - connect(deviceId, id) + if (!id) return + setHubId(id) + } + + const handleDisconnect = () => { + useGatewayStore.setState({ hubId: null }) + useHubStore.setState({ status: "idle", hub: null, agents: [], activeAgentId: null }) } return ( @@ -62,31 +68,61 @@ export function HubSidebar() { Hub - {isDisconnected ? ( + {!isRegistered ? ( +
+ + + {gwState === "disconnected" ? "Connecting to Gateway..." : "Registering..."} + +
+ ) : needsHubSelection ? (
+ {hubs.length > 0 && ( +
+ {hubs.map((h) => ( + + ))} +
+ )} setHubIdInput(e.target.value)} - placeholder="Enter Hub ID..." + placeholder="Or enter Hub ID..." className="h-7 text-xs font-mono" onKeyDown={(e) => e.key === "Enter" && handleConnect()} /> - +
+ + +
) : ( <>
- {isConnecting ? "Connecting..." : STATUS_LABEL[status]} + {STATUS_LABEL[status]}
{status === "connected" && hub ? ( @@ -97,7 +133,7 @@ export function HubSidebar() { ) : null}
-
@@ -106,7 +142,7 @@ export function HubSidebar() {
- {isConnecting && ( + {isRegistered && hubId && (status === "idle" || status === "loading") && ( Agents diff --git a/src/gateway/events.gateway.ts b/src/gateway/events.gateway.ts index 0197e0f4..ebeb2191 100644 --- a/src/gateway/events.gateway.ts +++ b/src/gateway/events.gateway.ts @@ -169,6 +169,17 @@ export class EventsGateway this.server.to(targetSocketId).emit(GatewayEvents.RECEIVE, message); } + @SubscribeMessage(GatewayEvents.LIST_DEVICES) + handleListDevices( + @ConnectedSocket() client: Socket + ): { devices: DeviceInfo[] } { + const senderDevice = this.socketToDevice.get(client.id); + if (!senderDevice) { + return { devices: [] }; + } + return { devices: this.getOnlineDevices() }; + } + @SubscribeMessage(GatewayEvents.PING) handlePing( @MessageBody() data: PingPayload, @@ -184,7 +195,7 @@ export class EventsGateway } /** Get online devices of specified type */ - getOnlineDevicesByType(type: "client" | "agent"): DeviceInfo[] { + getOnlineDevicesByType(type: DeviceType): DeviceInfo[] { return this.getOnlineDevices().filter((d) => d.deviceType === type); } diff --git a/src/hub/hub.ts b/src/hub/hub.ts index c456d8a7..984082a2 100644 --- a/src/hub/hub.ts +++ b/src/hub/hub.ts @@ -66,7 +66,7 @@ export class Hub { url, path: this.path, deviceId: this.hubId, - deviceType: "client", + deviceType: "hub", autoReconnect: true, reconnectDelay: 1000, }); From 4d6d57187c2a923f16c9b6297cf33b2b67dd4835 Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:13:59 +0800 Subject: [PATCH 4/7] feat(store): load agent message history via RPC - Add typed AgentMessageItem to SDK with proper content block types - Add fetchAgentMessages action to hub store using getAgentMessages RPC - Extract text from complex content blocks (user string, assistant array) - Auto-load history when selecting an agent with no local messages Co-Authored-By: Claude Opus 4.5 --- packages/sdk/src/actions/index.ts | 1 + packages/sdk/src/actions/rpc.ts | 31 ++++++++++++++++++++- packages/store/src/hub.ts | 46 ++++++++++++++++++++++++++++++- 3 files changed, 76 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/actions/index.ts b/packages/sdk/src/actions/index.ts index 0c673bbd..ab6307fa 100644 --- a/packages/sdk/src/actions/index.ts +++ b/packages/sdk/src/actions/index.ts @@ -14,6 +14,7 @@ export { type ResponseErrorPayload, isResponseSuccess, isResponseError, + type AgentMessageItem, type GetAgentMessagesParams, type GetAgentMessagesResult, type GetHubInfoResult, diff --git a/packages/sdk/src/actions/rpc.ts b/packages/sdk/src/actions/rpc.ts index 11c0e665..a81a15b1 100644 --- a/packages/sdk/src/actions/rpc.ts +++ b/packages/sdk/src/actions/rpc.ts @@ -65,9 +65,38 @@ export interface GetAgentMessagesParams { limit?: number; } +/** Content block types from the agent engine */ +export interface TextContentBlock { + type: "text"; + text: string; +} + +export interface ThinkingContentBlock { + type: "thinking"; + thinking: string; +} + +export interface ToolCallBlock { + type: "tool_use"; + id: string; + name: string; + input: unknown; +} + +export interface ImageContentBlock { + type: "image"; + url: string; +} + +/** Agent message returned by getAgentMessages (mirrors pi-ai Message) */ +export type AgentMessageItem = + | { role: "user"; content: string | (TextContentBlock | ImageContentBlock)[]; timestamp: number } + | { role: "assistant"; content: (TextContentBlock | ThinkingContentBlock | ToolCallBlock)[]; timestamp: number } + | { role: "tool_result"; toolCallId: string; content: (TextContentBlock | ImageContentBlock)[]; isError: boolean; timestamp: number } + /** getAgentMessages - response payload */ export interface GetAgentMessagesResult { - messages: unknown[]; + messages: AgentMessageItem[]; total: number; offset: number; limit: number; diff --git a/packages/store/src/hub.ts b/packages/store/src/hub.ts index b5101eec..2bb0ef70 100644 --- a/packages/store/src/hub.ts +++ b/packages/store/src/hub.ts @@ -1,12 +1,25 @@ import { create } from "zustand" import { toast } from "sonner" +import { v7 as uuidv7 } from "uuid" import type { GetHubInfoResult, ListAgentsResult, CreateAgentResult, DeleteAgentResult, + GetAgentMessagesResult, + AgentMessageItem, } from "@multica/sdk" import { useGatewayStore } from "./gateway" +import { useMessagesStore } from "./messages" + +/** Extract plain text from agent message content (string or content block array) */ +function extractText(content: string | { type: string; text?: string }[]): string { + if (typeof content === "string") return content + return content + .filter((b) => b.type === "text" && b.text) + .map((b) => b.text!) + .join("\n") +} export type HubInfo = GetHubInfoResult @@ -28,6 +41,7 @@ interface HubActions { setActiveAgentId: (id: string | null) => void fetchHub: () => Promise fetchAgents: () => Promise + fetchAgentMessages: (agentId: string) => Promise createAgent: (options?: Record) => Promise deleteAgent: (id: string) => Promise } @@ -40,7 +54,16 @@ export const useHubStore = create()((set, get) => ({ agents: [], activeAgentId: null, - setActiveAgentId: (id) => set({ activeAgentId: id }), + setActiveAgentId: (id) => { + set({ activeAgentId: id }) + if (id) { + // Load history if no messages exist for this agent yet + const existing = useMessagesStore.getState().messages.filter((m) => m.agentId === id) + if (existing.length === 0) { + get().fetchAgentMessages(id) + } + } + }, fetchHub: async () => { set({ status: "loading" }) @@ -64,6 +87,27 @@ export const useHubStore = create()((set, get) => ({ } }, + fetchAgentMessages: async (agentId) => { + try { + const { request } = useGatewayStore.getState() + const data = await request("getAgentMessages", { agentId }) + const msgs = data.messages + .filter((m): m is AgentMessageItem & { role: "user" | "assistant" } => + m.role === "user" || m.role === "assistant" + ) + .map((m) => ({ + id: uuidv7(), + role: m.role, + content: extractText(m.content), + agentId, + })) + .filter((m) => m.content.length > 0) + useMessagesStore.getState().loadMessages(agentId, msgs) + } catch (e) { + console.error("Failed to fetch agent messages:", e) + } + }, + createAgent: async (options?) => { try { const { request } = useGatewayStore.getState() From a1c28b3d043151d7197ee18621cff536e575387c Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:14:18 +0800 Subject: [PATCH 5/7] feat(ui): add auto-scroll to bottom for chat messages - Add useAutoScroll hook using ResizeObserver + MutationObserver - Observes content children for size changes (streaming, images) - Watches for new DOM nodes (new messages, history load) - Respects user scroll position: no force-scroll when reading above - Integrate in Chat component alongside existing useScrollFade Co-Authored-By: Claude Opus 4.5 --- packages/ui/src/components/chat.tsx | 2 + packages/ui/src/hooks/use-auto-scroll.ts | 64 ++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 packages/ui/src/hooks/use-auto-scroll.ts diff --git a/packages/ui/src/components/chat.tsx b/packages/ui/src/components/chat.tsx index f7fa8910..53476365 100644 --- a/packages/ui/src/components/chat.tsx +++ b/packages/ui/src/components/chat.tsx @@ -11,6 +11,7 @@ import { UserIcon, Copy01Icon, CheckmarkCircle02Icon } from "@hugeicons/core-fre import { toast } from "@multica/ui/components/ui/sonner"; import { useHubStore, useDeviceId, useMessagesStore, useGatewayStore } from "@multica/store"; import { useScrollFade } from "@multica/ui/hooks/use-scroll-fade"; +import { useAutoScroll } from "@multica/ui/hooks/use-auto-scroll"; import { Skeleton } from "@multica/ui/components/ui/skeleton"; import { cn } from "@multica/ui/lib/utils"; @@ -54,6 +55,7 @@ export function Chat() { const mainRef = useRef(null) const fadeStyle = useScrollFade(mainRef) + useAutoScroll(mainRef) return (
diff --git a/packages/ui/src/hooks/use-auto-scroll.ts b/packages/ui/src/hooks/use-auto-scroll.ts new file mode 100644 index 00000000..8090631b --- /dev/null +++ b/packages/ui/src/hooks/use-auto-scroll.ts @@ -0,0 +1,64 @@ +import { type RefObject, useEffect, useRef } from "react" + +/** + * Auto-scrolls a scroll container to the bottom when its inner content grows, + * as long as the user hasn't scrolled up to read older content. + * + * Observes child element size changes via ResizeObserver on all children, + * plus MutationObserver for added/removed nodes. Works for new messages, + * history loads, streaming updates, and image loads. + */ +export function useAutoScroll(ref: RefObject) { + const stickRef = useRef(true) + + useEffect(() => { + const el = ref.current + if (!el) return + + const scrollToBottom = () => { + el.scrollTo({ top: el.scrollHeight }) + } + + const onScroll = () => { + const { scrollTop, scrollHeight, clientHeight } = el + stickRef.current = scrollHeight - scrollTop - clientHeight < 50 + } + + const onContentChange = () => { + if (stickRef.current) { + scrollToBottom() + } + } + + // Watch child element resizes (content growth, image loads, streaming) + const ro = new ResizeObserver(onContentChange) + for (const child of el.children) { + ro.observe(child) + } + + // Watch for added/removed child nodes (new messages rendered) + const mo = new MutationObserver((mutations) => { + // Also observe newly added elements + for (const mutation of mutations) { + for (const node of mutation.addedNodes) { + if (node instanceof Element) { + ro.observe(node) + } + } + } + onContentChange() + }) + mo.observe(el, { childList: true, subtree: true }) + + el.addEventListener("scroll", onScroll, { passive: true }) + + // Initial scroll to bottom + scrollToBottom() + + return () => { + el.removeEventListener("scroll", onScroll) + ro.disconnect() + mo.disconnect() + } + }, [ref]) +} From e491949749b47120f96b1f088b3e3c376ffe2a88 Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:23:06 +0800 Subject: [PATCH 6/7] refactor(store): remove @multica/fetch dependency - Move gatewayUrl into gateway store state with setGatewayUrl action - layout.tsx uses useGatewayStore.getState().setGatewayUrl() directly - Remove @multica/fetch from store and web package.json dependencies Co-Authored-By: Claude Opus 4.5 --- apps/web/app/layout.tsx | 9 +++++---- apps/web/package.json | 1 - packages/store/package.json | 1 - packages/store/src/gateway.ts | 10 ++++++++-- pnpm-lock.yaml | 6 ------ 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 8d468eb4..780ad64d 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -1,6 +1,6 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono, Inter, Playfair_Display } from "next/font/google"; -import { setConfig } from "@multica/fetch"; +import { useGatewayStore } from "@multica/store"; import "@multica/ui/globals.css"; import { SidebarProvider, @@ -11,9 +11,10 @@ import { ThemeProvider } from "@multica/ui/components/theme-provider"; import { Toaster } from "@multica/ui/components/ui/sonner"; import { HubSidebar } from "@multica/ui/components/hub-sidebar"; -setConfig({ - gatewayUrl: process.env.NEXT_PUBLIC_GATEWAY_URL ?? "http://localhost:3000", -}); +const gatewayUrl = process.env.NEXT_PUBLIC_GATEWAY_URL; +if (gatewayUrl) { + useGatewayStore.getState().setGatewayUrl(gatewayUrl); +} const inter = Inter({ subsets: ["latin"], variable: "--font-sans" }); diff --git a/apps/web/package.json b/apps/web/package.json index 09bac1ef..954a4008 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -9,7 +9,6 @@ "lint": "eslint" }, "dependencies": { - "@multica/fetch": "workspace:*", "@multica/sdk": "workspace:*", "@multica/store": "workspace:*", "@multica/ui": "workspace:*", diff --git a/packages/store/package.json b/packages/store/package.json index e79cdef1..61a1b936 100644 --- a/packages/store/package.json +++ b/packages/store/package.json @@ -8,7 +8,6 @@ "./*": "./src/*.ts" }, "dependencies": { - "@multica/fetch": "workspace:*", "@multica/sdk": "workspace:*", "react": "catalog:", "sonner": "^2.0.7", diff --git a/packages/store/src/gateway.ts b/packages/store/src/gateway.ts index f7cf4958..fb70f6c0 100644 --- a/packages/store/src/gateway.ts +++ b/packages/store/src/gateway.ts @@ -1,9 +1,11 @@ import { create } from "zustand" import { GatewayClient, type ConnectionState, type DeviceInfo, type SendErrorResponse } from "@multica/sdk" -import { getGatewayUrl } from "@multica/fetch" import { useMessagesStore } from "./messages" +const DEFAULT_GATEWAY_URL = "http://localhost:3000" + interface GatewayState { + gatewayUrl: string connectionState: ConnectionState hubId: string | null hubs: DeviceInfo[] @@ -11,6 +13,7 @@ interface GatewayState { } interface GatewayActions { + setGatewayUrl: (url: string) => void connect: (deviceId: string) => void disconnect: () => void setHubId: (hubId: string) => void @@ -24,16 +27,19 @@ export type GatewayStore = GatewayState & GatewayActions let client: GatewayClient | null = null export const useGatewayStore = create()((set, get) => ({ + gatewayUrl: DEFAULT_GATEWAY_URL, connectionState: "disconnected", hubId: null, hubs: [], lastError: null, + setGatewayUrl: (url) => set({ gatewayUrl: url }), + connect: (deviceId) => { if (client) return client = new GatewayClient({ - url: getGatewayUrl(), + url: get().gatewayUrl, deviceId, deviceType: "client", }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6272ecc3..c576814f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -214,9 +214,6 @@ importers: '@hugeicons/react': specifier: ^1.1.4 version: 1.1.4(react@19.2.3) - '@multica/fetch': - specifier: workspace:* - version: link:../../packages/fetch '@multica/sdk': specifier: workspace:* version: link:../../packages/sdk @@ -285,9 +282,6 @@ importers: packages/store: dependencies: - '@multica/fetch': - specifier: workspace:* - version: link:../fetch '@multica/sdk': specifier: workspace:* version: link:../sdk From a62ac2e07539ff68d293e2a64966397b0ebe5d77 Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:29:27 +0800 Subject: [PATCH 7/7] chore: remove @multica/fetch package No consumers remain after migrating to pure WebSocket RPC. Co-Authored-By: Claude Opus 4.5 --- packages/fetch/package.json | 13 ------------- packages/fetch/src/config.ts | 15 --------------- packages/fetch/src/http-client.ts | 28 ---------------------------- packages/fetch/src/index.ts | 2 -- packages/fetch/tsconfig.json | 14 -------------- pnpm-lock.yaml | 6 ------ 6 files changed, 78 deletions(-) delete mode 100644 packages/fetch/package.json delete mode 100644 packages/fetch/src/config.ts delete mode 100644 packages/fetch/src/http-client.ts delete mode 100644 packages/fetch/src/index.ts delete mode 100644 packages/fetch/tsconfig.json diff --git a/packages/fetch/package.json b/packages/fetch/package.json deleted file mode 100644 index d5bd6971..00000000 --- a/packages/fetch/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "@multica/fetch", - "version": "0.1.0", - "private": true, - "type": "module", - "exports": { - ".": "./src/index.ts", - "./*": "./src/*.ts" - }, - "devDependencies": { - "typescript": "catalog:" - } -} diff --git a/packages/fetch/src/config.ts b/packages/fetch/src/config.ts deleted file mode 100644 index ff2abde9..00000000 --- a/packages/fetch/src/config.ts +++ /dev/null @@ -1,15 +0,0 @@ -let consoleUrl = "http://localhost:4000" -let gatewayUrl = "http://localhost:3000" - -export function setConfig(config: { consoleUrl?: string; gatewayUrl?: string }) { - if (config.consoleUrl) consoleUrl = config.consoleUrl - if (config.gatewayUrl) gatewayUrl = config.gatewayUrl -} - -export function getConsoleUrl(): string { - return consoleUrl -} - -export function getGatewayUrl(): string { - return gatewayUrl -} diff --git a/packages/fetch/src/http-client.ts b/packages/fetch/src/http-client.ts deleted file mode 100644 index e9564d09..00000000 --- a/packages/fetch/src/http-client.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { getConsoleUrl } from "./config" - -export class HttpError extends Error { - constructor( - public status: number, - public statusText: string, - ) { - super(`HTTP ${status}: ${statusText}`) - } -} - -async function request(method: string, path: string, body?: unknown): Promise { - const res = await fetch(`${getConsoleUrl()}${path}`, { - method, - headers: body ? { "Content-Type": "application/json" } : undefined, - body: body ? JSON.stringify(body) : undefined, - }) - if (!res.ok) throw new HttpError(res.status, res.statusText) - return res.json() -} - -/** Console REST API */ -export const consoleApi = { - get: (path: string) => request("GET", path), - post: (path: string, body?: unknown) => request("POST", path, body), - put: (path: string, body: unknown) => request("PUT", path, body), - delete: (path: string) => request("DELETE", path), -} diff --git a/packages/fetch/src/index.ts b/packages/fetch/src/index.ts deleted file mode 100644 index 8500fa04..00000000 --- a/packages/fetch/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { setConfig, getConsoleUrl, getGatewayUrl } from "./config" -export { consoleApi, HttpError } from "./http-client" diff --git a/packages/fetch/tsconfig.json b/packages/fetch/tsconfig.json deleted file mode 100644 index c1103210..00000000 --- a/packages/fetch/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "bundler", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "declaration": true, - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src"] -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c576814f..0fc53daf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -258,12 +258,6 @@ importers: specifier: 'catalog:' version: 5.9.3 - packages/fetch: - devDependencies: - typescript: - specifier: 'catalog:' - version: 5.9.3 - packages/sdk: dependencies: socket.io-client: