diff --git a/Makefile b/Makefile index 95c69fe..745d9b9 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ BASE_PATH ?= /magic-base-path-placeholder $(CHAT_SOURCES_STAMP): $(CHAT_SOURCES) @echo "Chat sources changed. Running build steps..." - cd chat && BASE_PATH=${BASE_PATH} bun run build + cd chat && NEXT_PUBLIC_BASE_PATH="${BASE_PATH}" bun run build rm -rf lib/httpapi/chat && mkdir -p lib/httpapi/chat && touch lib/httpapi/chat/marker cp -r chat/out/. lib/httpapi/chat/ touch $@ diff --git a/chat/next.config.ts b/chat/next.config.ts index baf9e51..358d829 100644 --- a/chat/next.config.ts +++ b/chat/next.config.ts @@ -1,5 +1,8 @@ import type { NextConfig } from "next"; -const basePath = process.env.BASE_PATH ?? "/chat"; +let basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "/chat"; +if (basePath.endsWith("/")) { + basePath = basePath.slice(0, -1); +} const nextConfig: NextConfig = { // Enable static exports diff --git a/chat/src/components/chat-provider.tsx b/chat/src/components/chat-provider.tsx index db9754e..d57c3c8 100644 --- a/chat/src/components/chat-provider.tsx +++ b/chat/src/components/chat-provider.tsx @@ -51,14 +51,20 @@ interface ChatContextValue { const ChatContext = createContext(undefined); -export function ChatProvider({ children }: PropsWithChildren) { - const [messages, setMessages] = useState<(Message | DraftMessage)[]>([]); - const [loading, setLoading] = useState(false); - const [serverStatus, setServerStatus] = useState("unknown"); - const eventSourceRef = useRef(null); +const useAgentAPIUrl = (): string => { const searchParams = useSearchParams(); - // NOTE(cian): We use '../../' here to construct the agent API URL relative - // to the current window location. Let's say the app is hosted on a subpath + const paramsUrl = searchParams.get("url"); + if (paramsUrl) { + return paramsUrl; + } + const basePath = process.env.NEXT_PUBLIC_BASE_PATH; + if (!basePath) { + throw new Error( + "agentAPIUrl is not set. Please set the url query parameter to the URL of the AgentAPI or the NEXT_PUBLIC_BASE_PATH environment variable." + ); + } + // NOTE(cian): We use '../' here to construct the agent API URL relative + // to the chat's location. Let's say the app is hosted on a subpath // `/@admin/workspace.agent/apps/ccw/`. When you visit this URL you get // redirected to `/@admin/workspace.agent/apps/ccw/chat/embed`. This serves // this React application, but it needs to know where the agent API is hosted. @@ -67,8 +73,25 @@ export function ChatProvider({ children }: PropsWithChildren) { // `window.location.origin` but this assumes that the application owns the // entire origin. // See: https://github.com/coder/coder/issues/18779#issuecomment-3133290494 for more context. - const defaultAgentAPIURL = new URL("../../", window.location.href).toString(); - const agentAPIUrl = searchParams.get("url") || defaultAgentAPIURL; + let chatURL: string = new URL(basePath, window.location.origin).toString(); + // NOTE: trailing slashes and relative URLs are tricky. + // https://developer.mozilla.org/en-US/docs/Web/API/URL_API/Resolving_relative_references#current_directory_relative + if (!chatURL.endsWith("/")) { + chatURL += "/"; + } + const agentAPIURL = new URL("..", chatURL).toString(); + if (agentAPIURL.endsWith("/")) { + return agentAPIURL.slice(0, -1); + } + return agentAPIURL; +}; + +export function ChatProvider({ children }: PropsWithChildren) { + const [messages, setMessages] = useState<(Message | DraftMessage)[]>([]); + const [loading, setLoading] = useState(false); + const [serverStatus, setServerStatus] = useState("unknown"); + const eventSourceRef = useRef(null); + const agentAPIUrl = useAgentAPIUrl(); // Set up SSE connection to the events endpoint useEffect(() => { diff --git a/lib/httpapi/server.go b/lib/httpapi/server.go index a76cf68..3d8c710 100644 --- a/lib/httpapi/server.go +++ b/lib/httpapi/server.go @@ -7,6 +7,7 @@ import ( "log/slog" "net/http" "net/url" + "strings" "sync" "time" @@ -97,7 +98,7 @@ func NewServer(ctx context.Context, agentType mf.AgentType, process *termexec.Pr agentio: process, agentType: agentType, emitter: emitter, - chatBasePath: chatBasePath, + chatBasePath: strings.TrimSuffix(chatBasePath, "/"), } // Register API routes