diff --git a/src/App.tsx b/src/App.tsx index 285d25d4..00e501a2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,6 @@ import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; import { I18nextProvider } from 'react-i18next'; + import { ThemeProvider } from './contexts/ThemeContext'; import { AuthProvider, ProtectedRoute } from './components/auth'; import { TaskMasterProvider } from './contexts/TaskMasterContext'; @@ -9,12 +10,26 @@ import { PluginsProvider } from './contexts/PluginsContext'; import AppContent from './components/app/AppContent'; import i18n from './i18n/config.js'; +const DEPLOYMENT_ASSET_DIRECTORIES = new Set(['assets', 'static', 'icons', 'images']); + /** * Detect the router basename from explicit runtime config or deployment hints. + * + * CloudCLI can be served from a path prefix by a reverse proxy, for example: + * /ai/manifest.json + * /ai/assets/index-abc123.js + * /ai/icons/icon-192x192.png + * + * React Router needs that prefix as its basename, but the packaged app should + * also keep working when served directly from the domain root. The direct-root + * case is easy to misread because asset URLs such as /icons/icon-192x192.png + * contain a directory even though there is no application basename. */ function detectRouterBasename() { const explicitBasename = typeof window !== 'undefined' ? window.__ROUTER_BASENAME__ || '' : ''; if (explicitBasename) { + // Keep the deployment escape hatch authoritative. A trailing slash is + // harmless for humans but React Router expects a normalized basename. return explicitBasename.replace(/\/+$/, ''); } @@ -58,9 +73,18 @@ function detectRouterBasename() { const match = candidate.kind === 'manifest' ? manifestMatch : iconMatch; if (match?.[1]) { const segments = match[1].split('/').filter(Boolean); - while (segments.length > 1 && ['assets', 'static', 'icons', 'images'].includes(segments[segments.length - 1])) { + + // Strip directories that describe where static files live, not where + // the app is mounted. This must also run for a single segment: + // /icons/icon-192x192.png -> '' + // /ai/icons/icon-192x192.png -> '/ai' + // The previous implementation only stripped while more than one + // segment remained, which incorrectly turned root deployments into a + // Router basename of /icons and caused a blank page after login. + while (segments.length > 0 && DEPLOYMENT_ASSET_DIRECTORIES.has(segments[segments.length - 1])) { segments.pop(); } + normalized = segments.length > 0 ? `/${segments.join('/')}` : ''; } }