Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions crates/ui/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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),
Expand All @@ -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()),
}
}
}
6 changes: 6 additions & 0 deletions crates/ui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
18 changes: 18 additions & 0 deletions crates/ui/src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<ProxyState>,
req: Request<Body>,
) -> Result<Response, StatusCode> {
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<ProxyState>,
req: Request<Body>,
) -> Result<Response, StatusCode> {
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,
Expand Down
5 changes: 4 additions & 1 deletion ui/src/components/agents/AgentTerminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
}

Expand Down
5 changes: 4 additions & 1 deletion ui/src/hooks/useAgentStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
}

Expand Down
5 changes: 4 additions & 1 deletion ui/src/hooks/useAllAgentsStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
}

Expand Down
5 changes: 4 additions & 1 deletion ui/src/hooks/useCommunicateSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 10 additions & 2 deletions ui/src/services/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@
if (err instanceof DOMException && err.name === "AbortError") {
throw new ApiError(408, `Request timed out after ${this.timeoutMs}ms`);
}
throw new ApiError(0, err instanceof Error ? err.message : String(err));

Check failure on line 135 in ui/src/services/base.ts

View workflow job for this annotation

GitHub Actions / Frontend Tests

src/test/hooks/useAgentDetail.test.ts > useAgentDetail > denyRequest removes approval from local state

ApiError: [MSW] Cannot bypass a request when using the "error" strategy for the "onUnhandledRequest" option. ❯ OrchestratorClient.executeRequest src/services/base.ts:135:10 ❯ OrchestratorClient.request src/services/base.ts:87:12 ❯ Object.denyRequest src/hooks/useAgentDetail.ts:191:3 ❯ src/test/hooks/useAgentDetail.test.ts:160:4 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { status: +0, body: undefined }

Check failure on line 135 in ui/src/services/base.ts

View workflow job for this annotation

GitHub Actions / Frontend Tests

src/test/hooks/useAgentDetail.test.ts > useAgentDetail > updateModel updates local agent state

ApiError: [MSW] Cannot bypass a request when using the "error" strategy for the "onUnhandledRequest" option. ❯ OrchestratorClient.executeRequest src/services/base.ts:135:10 ❯ OrchestratorClient.request src/services/base.ts:87:12 ❯ Object.updateModel src/hooks/useAgentDetail.ts:166:20 ❯ src/test/hooks/useAgentDetail.test.ts:109:4 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { status: +0, body: undefined }
}
}

Expand Down Expand Up @@ -191,7 +191,11 @@
path: string,
params?: Record<string, string | number | boolean | undefined>,
): 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)) {
Expand Down Expand Up @@ -240,7 +244,11 @@
* 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}`);
}
}
22 changes: 9 additions & 13 deletions ui/src/services/config.ts
Original file line number Diff line number Diff line change
@@ -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/<service>/**` 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;
16 changes: 16 additions & 0 deletions ui/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()],
Expand Down Expand Up @@ -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/**/*"],
Expand Down
Loading