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;