feat(dashboard): add chat interface via API server proxy - #9903
Conversation
Adds a Chat tab to the dashboard that proxies to the gateway's existing API server (/v1/chat/completions) with SSE streaming. Backend (web_server.py): - POST /api/chat/send — proxies to API server with streaming, falls back to non-streaming request when stream returns empty content (surfaces agent errors instead of hanging) - GET /api/chat/status — health check for API server availability Frontend (ChatPage.tsx): - SSE stream parsing (OpenAI-compatible chunks) - Markdown rendering for assistant responses - Session continuity via X-Hermes-Session-Id header - Auto-scroll, auto-resize textarea, Enter to send - Unavailable state with setup instructions when gateway not running Requires the gateway running with api_server platform enabled. i18n: en + zh translations for chat UI strings.
|
Field note from building a similar thing externally, in case it's useful for this design. I built a per-user auth bridge in front of api_server for a third-party integration (a control-room UI, not the built-in dashboard), and initially planned to reuse the dashboard's own OIDC/PKCE session the way this issue proposes — a dashboard route gated by the real auth middleware, forwarding to api_server with the key attached server-side. Ran into gated_auth_middleware being a no-op whenever the dashboard is bound to loopback (should_require_auth() treats loopback as "local-only, trusted operator" by design). Confirmed live: completed a real OIDC login against a self-hosted IdP, landed back on the dashboard signed in — but GET /api/auth/me still returned 401 immediately after, since request.state.session never gets populated on that code path when auth_required is False. For any integration that's also running on the same loopback interface as the dashboard (which seems like the common self-hosted case), reusing the dashboard's session this way only actually works once the dashboard itself is bound non-loopback — a real network-exposure decision, not something you'd want to force just to let a local integration authenticate. Worked around it by having the integration act as its own independent OIDC/PKCE relying party — its own registered IdP client, its own login/callback routes, no dependency on the dashboard's session or gated_auth_middleware at all. Sidesteps the loopback behavior entirely since it never goes through that code path. Live-tested end to end (real login, real session, real proxied request with the key held server-side only, confirmed the key never reaches client code). Might be worth a note in this design (or wherever the dashboard-chat-proxy pattern gets documented) that for integrations that aren't the dashboard's own same-origin UI, an independent-client-per-integration pattern avoids the loopback gate entirely, rather than assuming the dashboard's own auth_required/session state is always the right thing to piggyback on. |
Summary
Adds a Chat tab to the dashboard. Proxies to the gateway's existing API server (
/v1/chat/completions) — no new agent lifecycle code, no new dependencies.Backend — 2 endpoints, 110 lines
POST /api/chat/send— SSE streaming proxy tolocalhost:8642/v1/chat/completionsX-Hermes-Session-Idfor session continuityAPI_SERVER_KEYto the API server when setfinish_reasonwithout content, falls back to a non-streaming request to surface the actual error messageAPI_SERVER_PORTenv varGET /api/chat/status— health check, hits the API server's/healthendpointBoth are protected by the existing auth middleware (#9800).
Frontend — ChatPage.tsx, 212 lines
choices[0].delta.contentchunks)Markdowncomponenthermes gateway startinstructionRequires
The gateway must be running with the
api_serverplatform enabled:Then
hermes gateway start. The Chat tab detects this automatically and shows setup instructions if not configured.Files changed
hermes_cli/web_server.pyweb/src/pages/ChatPage.tsxweb/src/App.tsxweb/src/lib/api.tscheckChatStatus()methodweb/src/i18n/types.tsweb/src/i18n/en.tsweb/src/i18n/zh.ts