From f6ccf08ee6f53a9d02893ace22fb33f76ace92d9 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sun, 28 Jun 2026 21:27:06 -0500 Subject: [PATCH] refactor(web): centralize dashboard websocket URL calls Keep dashboard pages and components on the dashboard API helper instead of calling the raw shared URL primitive directly. The shared helper remains the single low-level implementation; web/src/lib/api.ts is the dashboard-specific facade for auth, base path, and ticket minting. --- web/src/components/ChatSidebar.tsx | 16 +++--------- web/src/pages/ChatPage.tsx | 41 ++++++++---------------------- 2 files changed, 14 insertions(+), 43 deletions(-) diff --git a/web/src/components/ChatSidebar.tsx b/web/src/components/ChatSidebar.tsx index d924c6d2ee4..0c3286f528e 100644 --- a/web/src/components/ChatSidebar.tsx +++ b/web/src/components/ChatSidebar.tsx @@ -26,13 +26,12 @@ import { Button } from "@nous-research/ui/ui/components/button"; import { Badge } from "@nous-research/ui/ui/components/badge"; import { Card } from "@nous-research/ui/ui/components/card"; -import { buildHermesWebSocketUrl } from "@hermes/shared"; import { ModelPickerDialog } from "@/components/ModelPickerDialog"; import { ModelReloadConfirm } from "@/components/ModelReloadConfirm"; import { ReasoningPicker } from "@/components/ReasoningPicker"; import { GatewayClient, type ConnectionState } from "@/lib/gatewayClient"; -import { api, HERMES_BASE_PATH, buildWsAuthParam } from "@/lib/api"; +import { api, buildWsUrl } from "@/lib/api"; import { titleFromSessionInfoPayload } from "@/lib/chat-title"; import { cn } from "@/lib/utils"; @@ -231,18 +230,11 @@ export function ChatSidebar({ let unmounting = false; let ws: WebSocket | null = null; void (async () => { - const authParam = await buildWsAuthParam(); - if (!authParam[1] || unmounting) { + const url = await buildWsUrl("/api/events", { channel }); + if (unmounting) { return; } - ws = new WebSocket( - buildHermesWebSocketUrl({ - authParam, - basePath: HERMES_BASE_PATH, - params: { channel }, - path: "/api/events", - }), - ); + ws = new WebSocket(url); // `unmounting` suppresses the banner during cleanup — `ws.close()` // from the effect's return fires a close event with code 1005 that diff --git a/web/src/pages/ChatPage.tsx b/web/src/pages/ChatPage.tsx index 202fedbf8e7..a3297cb7cca 100644 --- a/web/src/pages/ChatPage.tsx +++ b/web/src/pages/ChatPage.tsx @@ -24,8 +24,6 @@ import { Terminal } from "@xterm/xterm"; import "@xterm/xterm/css/xterm.css"; import { Button } from "@nous-research/ui/ui/components/button"; import { Typography } from "@nous-research/ui/ui/components/typography/index"; -import { buildHermesWebSocketUrl, type WebSocketAuthParam } from "@hermes/shared"; -import { HERMES_BASE_PATH, buildWsAuthParam } from "@/lib/api"; import { cn } from "@/lib/utils"; import { Copy, PanelRight, RotateCcw, X } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; @@ -42,31 +40,6 @@ import { PluginSlot } from "@/plugins"; import { useTheme } from "@/themes"; import { useProfileScope } from "@/contexts/useProfileScope"; -function buildWsUrl( - authParam: WebSocketAuthParam, - resume: string | null, - channel: string, - profile: string, - fresh: boolean, -): string { - // ``authParam`` is ``["token", ]`` in loopback mode and - // ``["ticket", ]`` in gated mode. The server-side helper - // ``_ws_auth_ok`` picks whichever shape matches the current gate state. - const params: Record = { channel }; - if (resume) params.resume = resume; - if (fresh) params.fresh = "1"; - // Profile-scoped chat: the PTY child gets HERMES_HOME pointed at the - // selected profile, so the conversation runs with that profile's model, - // skills, memory, and sessions (see web_server._resolve_chat_argv). - if (profile) params.profile = profile; - return buildHermesWebSocketUrl({ - authParam, - basePath: HERMES_BASE_PATH, - params, - path: "/api/pty", - }); -} - // Channel id ties this chat tab's PTY child (publisher) to its sidebar // (subscriber). Generated once per mount so a tab refresh starts a fresh // channel — the previous PTY child terminates with the old WS, and its @@ -140,7 +113,7 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) { // Lazy-init: the missing-token check happens at construction so the effect // body doesn't have to setState (React 19's set-state-in-effect rule). // In gated (OAuth) mode the server intentionally omits the session token — - // the SPA authenticates the WS via a single-use ticket (buildWsAuthParam), + // the dashboard API layer authenticates the WS via a single-use ticket, // so a missing token there is expected, not an error. const [banner, setBanner] = useState(() => typeof window !== "undefined" && @@ -393,7 +366,7 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) { const token = window.__HERMES_SESSION_TOKEN__; const gated = !!window.__HERMES_AUTH_REQUIRED__; // Banner already initialised above; just bail before wiring xterm/WS. - // In gated mode the token is absent by design — buildWsAuthParam() mints + // In gated mode the token is absent by design — api.buildWsUrl() mints // a WS ticket instead, so don't bail; let the effect reach that path. if (!token && !gated) { return; @@ -696,9 +669,15 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) { }, delayMs); }; void (async () => { - const authParam = await buildWsAuthParam(); if (unmounting) return; - const url = buildWsUrl(authParam, resumeParam, channel, scopedProfile, forceFresh); + const params: Record = { channel }; + if (resumeParam) params.resume = resumeParam; + if (forceFresh) params.fresh = "1"; + // Profile-scoped chat: the PTY child gets HERMES_HOME pointed at the + // selected profile, so the conversation runs with that profile's model, + // skills, memory, and sessions (see web_server._resolve_chat_argv). + if (scopedProfile) params.profile = scopedProfile; + const url = await api.buildWsUrl("/api/pty", params); const ws = new WebSocket(url); ws.binaryType = "arraybuffer"; wsRef.current = ws;