diff --git a/crates/ui/src/config.rs b/crates/ui/src/config.rs index 03f1d000..351dbb22 100644 --- a/crates/ui/src/config.rs +++ b/crates/ui/src/config.rs @@ -10,6 +10,10 @@ pub struct UiConfig { pub notify_service_url: String, /// URL of the orchestrator service. pub orchestrator_service_url: String, + /// URL of the memory service. + pub memory_service_url: String, + /// URL of the communicate service. + pub communicate_service_url: String, } impl UiConfig { @@ -20,6 +24,8 @@ impl UiConfig { /// - `AGENTD_ASK_SERVICE_URL` — ask service URL (default: `http://localhost:7001`) /// - `AGENTD_NOTIFY_SERVICE_URL` — notify service URL (default: `http://localhost:7004`) /// - `AGENTD_ORCHESTRATOR_SERVICE_URL` — orchestrator service URL (default: `http://localhost:7006`) + /// - `AGENTD_MEMORY_SERVICE_URL` — memory service URL (default: `http://localhost:7008`) + /// - `AGENTD_COMMUNICATE_SERVICE_URL` — communicate service URL (default: `http://localhost:7010`) pub fn from_env() -> Self { Self { port: std::env::var("AGENTD_PORT").ok().and_then(|v| v.parse().ok()).unwrap_or(17009), @@ -30,6 +36,10 @@ impl UiConfig { .unwrap_or_else(|_| "http://localhost:7004".to_string()), orchestrator_service_url: std::env::var("AGENTD_ORCHESTRATOR_SERVICE_URL") .unwrap_or_else(|_| "http://localhost:7006".to_string()), + memory_service_url: std::env::var("AGENTD_MEMORY_SERVICE_URL") + .unwrap_or_else(|_| "http://localhost:7008".to_string()), + communicate_service_url: std::env::var("AGENTD_COMMUNICATE_SERVICE_URL") + .unwrap_or_else(|_| "http://localhost:7010".to_string()), } } } diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 864ad491..396be616 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -22,6 +22,8 @@ //! - `AGENTD_ASK_SERVICE_URL` — Ask service URL (default: `http://localhost:7001`) //! - `AGENTD_NOTIFY_SERVICE_URL` — Notify service URL (default: `http://localhost:7004`) //! - `AGENTD_ORCHESTRATOR_SERVICE_URL` — Orchestrator service URL (default: `http://localhost:7006`) +//! - `AGENTD_MEMORY_SERVICE_URL` — Memory service URL (default: `http://localhost:7008`) +//! - `AGENTD_COMMUNICATE_SERVICE_URL` — Communicate service URL (default: `http://localhost:7010`) //! - `RUST_LOG` — Logging level (default: info) pub mod config; @@ -53,6 +55,8 @@ pub async fn run(config: config::UiConfig) -> Result<()> { ask_url: config.ask_service_url, notify_url: config.notify_service_url, orchestrator_url: config.orchestrator_service_url, + memory_url: config.memory_service_url, + communicate_url: config.communicate_service_url, }; // SPA fallback: serve index.html for any path that doesn't match a file @@ -65,6 +69,8 @@ pub async fn run(config: config::UiConfig) -> Result<()> { .route("/api/ask/{*path}", any(proxy::proxy_ask)) .route("/api/notify/{*path}", any(proxy::proxy_notify)) .route("/api/orchestrator/{*path}", any(proxy::proxy_orchestrator)) + .route("/api/memory/{*path}", any(proxy::proxy_memory)) + .route("/api/communicate/{*path}", any(proxy::proxy_communicate)) .with_state(proxy_state) // Static files with SPA fallback (must be last) .fallback_service(serve_dir) diff --git a/crates/ui/src/proxy.rs b/crates/ui/src/proxy.rs index d3aada24..f5c550a6 100644 --- a/crates/ui/src/proxy.rs +++ b/crates/ui/src/proxy.rs @@ -11,6 +11,8 @@ pub struct ProxyState { pub ask_url: String, pub notify_url: String, pub orchestrator_url: String, + pub memory_url: String, + pub communicate_url: String, } /// Proxy requests under `/api/ask/**` to the ask service. @@ -37,6 +39,22 @@ pub async fn proxy_orchestrator( proxy_request(&state.client, &state.orchestrator_url, "/api/orchestrator", req).await } +/// Proxy requests under `/api/memory/**` to the memory service. +pub async fn proxy_memory( + State(state): State, + req: Request, +) -> Result { + proxy_request(&state.client, &state.memory_url, "/api/memory", req).await +} + +/// Proxy requests under `/api/communicate/**` to the communicate service. +pub async fn proxy_communicate( + State(state): State, + req: Request, +) -> Result { + proxy_request(&state.client, &state.communicate_url, "/api/communicate", req).await +} + /// Forward an inbound request to an upstream service, stripping the prefix. async fn proxy_request( client: &Client, diff --git a/ui/src/components/agents/AgentTerminal.tsx b/ui/src/components/agents/AgentTerminal.tsx index 37afbff0..d71783bb 100644 --- a/ui/src/components/agents/AgentTerminal.tsx +++ b/ui/src/components/agents/AgentTerminal.tsx @@ -46,7 +46,10 @@ import { orchestratorClient } from "@/services/orchestrator"; // --------------------------------------------------------------------------- function agentTerminalUrl(agentId: string): string { - const wsBase = serviceConfig.orchestratorServiceUrl.replace(/^http/, "ws"); + const absBase = serviceConfig.orchestratorServiceUrl.startsWith("/") + ? `${window.location.origin}${serviceConfig.orchestratorServiceUrl}` + : serviceConfig.orchestratorServiceUrl; + const wsBase = absBase.replace(/^http/, "ws"); return `${wsBase}/terminal/${agentId}`; } diff --git a/ui/src/hooks/useAgentStream.ts b/ui/src/hooks/useAgentStream.ts index 9839d4d5..6f5b2d5f 100644 --- a/ui/src/hooks/useAgentStream.ts +++ b/ui/src/hooks/useAgentStream.ts @@ -192,7 +192,10 @@ function capLines(prev: LogLine[], incoming: LogLine[]): LogLine[] { } function agentStreamUrl(agentId: string): string { - const wsBase = serviceConfig.orchestratorServiceUrl.replace(/^http/, "ws"); + const absBase = serviceConfig.orchestratorServiceUrl.startsWith("/") + ? `${window.location.origin}${serviceConfig.orchestratorServiceUrl}` + : serviceConfig.orchestratorServiceUrl; + const wsBase = absBase.replace(/^http/, "ws"); return `${wsBase}/stream/${agentId}`; } diff --git a/ui/src/hooks/useAllAgentsStream.ts b/ui/src/hooks/useAllAgentsStream.ts index 17b26522..22e21d35 100644 --- a/ui/src/hooks/useAllAgentsStream.ts +++ b/ui/src/hooks/useAllAgentsStream.ts @@ -38,7 +38,10 @@ let msgId = 0; // --------------------------------------------------------------------------- function allStreamUrl(): string { - const wsBase = serviceConfig.orchestratorServiceUrl.replace(/^http/, "ws"); + const absBase = serviceConfig.orchestratorServiceUrl.startsWith("/") + ? `${window.location.origin}${serviceConfig.orchestratorServiceUrl}` + : serviceConfig.orchestratorServiceUrl; + const wsBase = absBase.replace(/^http/, "ws"); return `${wsBase}/stream`; } diff --git a/ui/src/hooks/useCommunicateSocket.ts b/ui/src/hooks/useCommunicateSocket.ts index 3194c1d3..d29919ff 100644 --- a/ui/src/hooks/useCommunicateSocket.ts +++ b/ui/src/hooks/useCommunicateSocket.ts @@ -44,7 +44,10 @@ export interface UseCommunicateSocketOptions { // --------------------------------------------------------------------------- function communicateWsUrl(participantId: string, displayName: string): string { - const base = serviceConfig.communicateServiceUrl.replace(/^http/, "ws"); + const absBase = serviceConfig.communicateServiceUrl.startsWith("/") + ? `${window.location.origin}${serviceConfig.communicateServiceUrl}` + : serviceConfig.communicateServiceUrl; + const base = absBase.replace(/^http/, "ws"); const params = new URLSearchParams({ identifier: participantId, kind: "human", diff --git a/ui/src/services/base.ts b/ui/src/services/base.ts index 730e3230..1aca297e 100644 --- a/ui/src/services/base.ts +++ b/ui/src/services/base.ts @@ -191,7 +191,11 @@ export class ApiClient { path: string, params?: Record, ): string { - const url = new URL(`${this.baseUrl}${path}`); + // Support both absolute URLs (http://...) and relative proxy paths (/api/...) + const base = this.baseUrl.startsWith("/") + ? `${window.location.origin}${this.baseUrl}` + : this.baseUrl; + const url = new URL(`${base}${path}`); if (params) { for (const [key, value] of Object.entries(params)) { @@ -240,7 +244,11 @@ export class ApiClient { * http → ws, https → wss */ protected openWebSocket(path: string): WebSocket { - const wsBase = this.baseUrl.replace(/^http/, "ws"); + // Convert http(s) → ws(s), or relative /api/... → ws(s)://host/api/... + const absBase = this.baseUrl.startsWith("/") + ? `${window.location.origin}${this.baseUrl}` + : this.baseUrl; + const wsBase = absBase.replace(/^http/, "ws"); return new WebSocket(`${wsBase}${path}`); } } diff --git a/ui/src/services/config.ts b/ui/src/services/config.ts index b15d1798..52c2f4e9 100644 --- a/ui/src/services/config.ts +++ b/ui/src/services/config.ts @@ -1,19 +1,15 @@ /** - * Service configuration with environment variable defaults + * Service configuration — all services are accessed through the UI proxy server. + * + * The UI server at `/api//**` proxies requests to the appropriate + * backend service, eliminating port mismatch issues when running in production. */ export const serviceConfig = { - askServiceUrl: - import.meta.env.VITE_AGENTD_ASK_SERVICE_URL ?? "http://localhost:17001", - notifyServiceUrl: - import.meta.env.VITE_AGENTD_NOTIFY_SERVICE_URL ?? "http://localhost:17004", - orchestratorServiceUrl: - import.meta.env.VITE_AGENTD_ORCHESTRATOR_SERVICE_URL ?? - "http://localhost:17006", - memoryServiceUrl: - import.meta.env.VITE_AGENTD_MEMORY_SERVICE_URL ?? "http://localhost:17008", - communicateServiceUrl: - import.meta.env.VITE_AGENTD_COMMUNICATE_SERVICE_URL ?? - "http://localhost:17010", + askServiceUrl: "/api/ask", + notifyServiceUrl: "/api/notify", + orchestratorServiceUrl: "/api/orchestrator", + memoryServiceUrl: "/api/memory", + communicateServiceUrl: "/api/communicate", } as const; export type ServiceConfig = typeof serviceConfig; diff --git a/ui/vite.config.ts b/ui/vite.config.ts index 3adbdc98..7d75ae5e 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -13,6 +13,10 @@ export default defineConfig(({ mode }) => { env.VITE_AGENTD_NOTIFY_SERVICE_URL || "http://localhost:17004"; const orchestratorServiceUrl = env.VITE_AGENTD_ORCHESTRATOR_SERVICE_URL || "http://localhost:17006"; + const memoryServiceUrl = + env.VITE_AGENTD_MEMORY_SERVICE_URL || "http://localhost:17008"; + const communicateServiceUrl = + env.VITE_AGENTD_COMMUNICATE_SERVICE_URL || "http://localhost:17010"; return { plugins: [react(), tailwindcss()], @@ -49,6 +53,18 @@ export default defineConfig(({ mode }) => { changeOrigin: true, rewrite: (path) => path.replace(/^\/api\/orchestrator/, ""), }, + "/api/memory": { + target: memoryServiceUrl, + changeOrigin: true, + ws: true, + rewrite: (path) => path.replace(/^\/api\/memory/, ""), + }, + "/api/communicate": { + target: communicateServiceUrl, + changeOrigin: true, + ws: true, + rewrite: (path) => path.replace(/^\/api\/communicate/, ""), + }, }, watch: { ignored: ["design/**/*"],