From db305bba8ba30f56e2157a8b4e10e8de354e2275 Mon Sep 17 00:00:00 2001 From: Teknium Date: Tue, 28 Apr 2026 01:22:55 -0700 Subject: [PATCH] chore(dashboard): address copilot review nits on #16861 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - App.tsx doc comment: replace stale ChatPageHost reference with 'persistent chat host block rendered inline near the bottom of this file' so readers can find the actual code. - App.tsx persistent host: show a small spinner on /chat while plugin manifests are loading instead of a blank content area. Direct /chat deep-links used to paint empty for up to ~2s in the worst case (plugin-registration safety timeout) because both the route sink (null) and the persistent host (!pluginsLoading gate) render nothing during that window. Non-chat routes stay empty as before. - ChatPage.tsx: rename setter to match the 'raw' state — useState now destructures as [mobilePanelOpenRaw, setMobilePanelOpenRaw], and all four call sites (closeMobilePanel, matchMedia listener, open-button onClick, plus destructure) updated accordingly. No behavior change; matches the 'raw vs derived' convention the original comment set up. --- web/src/App.tsx | 53 ++++++++++++++++++++++++++------------ web/src/pages/ChatPage.tsx | 8 +++--- 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index ab6146dd0ca..65b4d800ebe 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -80,11 +80,12 @@ const CHAT_NAV_ITEM: NavItem = { /** * Built-in routes except /chat. Chat is rendered persistently (outside - * ) when embedded — see ChatPageHost below — so the PTY child, - * WebSocket, and xterm instance survive when the user visits another tab - * and comes back. A `display:none` toggle hides the terminal without - * unmounting. Routing still owns the URL so /chat deep-links, browser - * back/forward, and nav highlight keep working. + * ) when embedded — see the persistent chat host block rendered + * inline near the bottom of this file — so the PTY child, WebSocket, + * and xterm instance survive when the user visits another tab and comes + * back. A `display:none` toggle hides the terminal without unmounting. + * Routing still owns the URL so /chat deep-links, browser back/forward, + * and nav highlight keep working. */ const BUILTIN_ROUTES_CORE: Record = { "/": RootRedirect, @@ -578,17 +579,37 @@ export default function App() { or idle-disconnect after N minutes hidden; neither is shipped today. */} - {embeddedChat && !pluginsLoading && !chatOverriddenByPlugin && ( -
- -
+ {embeddedChat && !chatOverriddenByPlugin && ( + pluginsLoading ? ( + // Direct /chat deep-link: plugin manifests haven't resolved + // yet, so we can't tell if a plugin is going to claim this + // route. Show a lightweight placeholder instead of a + // blank page. Typical wait is <50ms; worst case is the + // 2s plugin-registration safety timeout. + isChatRoute ? ( +
+
+ + Loading chat… +
+
+ ) : null + ) : ( +
+ +
+ ) )} diff --git a/web/src/pages/ChatPage.tsx b/web/src/pages/ChatPage.tsx index 900893be7d8..3b7f3b2accc 100644 --- a/web/src/pages/ChatPage.tsx +++ b/web/src/pages/ChatPage.tsx @@ -128,11 +128,11 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) { // /chat re-runs the effect (derived flips back to true) and re-locks. // Keying on the raw state would leak the body.overflow="hidden" across // tabs because the dep wouldn't change on tab switch. - const [mobilePanelOpenRaw, setMobilePanelOpen] = useState(false); + const [mobilePanelOpenRaw, setMobilePanelOpenRaw] = useState(false); const mobilePanelOpen = isActive && mobilePanelOpenRaw; const { setEnd } = usePageHeader(); const { t } = useI18n(); - const closeMobilePanel = useCallback(() => setMobilePanelOpen(false), []); + const closeMobilePanel = useCallback(() => setMobilePanelOpenRaw(false), []); const modelToolsLabel = useMemo( () => `${t.app.modelToolsSheetTitle} ${t.app.modelToolsSheetSubtitle}`, [t.app.modelToolsSheetSubtitle, t.app.modelToolsSheetTitle], @@ -174,7 +174,7 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) { useEffect(() => { const mql = window.matchMedia("(min-width: 1024px)"); const onChange = (e: MediaQueryListEvent) => { - if (e.matches) setMobilePanelOpen(false); + if (e.matches) setMobilePanelOpenRaw(false); }; mql.addEventListener("change", onChange); return () => mql.removeEventListener("change", onChange); @@ -194,7 +194,7 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) { setEnd(