chore(dashboard): address copilot review nits on #16861

- 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.
This commit is contained in:
Teknium 2026-04-28 01:22:55 -07:00 committed by Teknium
parent d293e0051e
commit db305bba8b
2 changed files with 41 additions and 20 deletions

View file

@ -80,11 +80,12 @@ const CHAT_NAV_ITEM: NavItem = {
/**
* Built-in routes except /chat. Chat is rendered persistently (outside
* <Routes>) 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.
* <Routes>) 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<string, ComponentType> = {
"/": RootRedirect,
@ -578,17 +579,37 @@ export default function App() {
or idle-disconnect after N minutes hidden; neither is
shipped today.
*/}
{embeddedChat && !pluginsLoading && !chatOverriddenByPlugin && (
<div
data-chat-active={isChatRoute ? "true" : "false"}
className={cn(
"min-h-0 min-w-0",
isChatRoute ? "flex flex-1 flex-col" : "hidden",
)}
aria-hidden={!isChatRoute}
>
<ChatPage isActive={isChatRoute} />
</div>
{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 ? (
<div
className="flex min-h-0 min-w-0 flex-1 items-center justify-center"
aria-busy="true"
aria-live="polite"
>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" aria-hidden />
<span>Loading chat</span>
</div>
</div>
) : null
) : (
<div
data-chat-active={isChatRoute ? "true" : "false"}
className={cn(
"min-h-0 min-w-0",
isChatRoute ? "flex flex-1 flex-col" : "hidden",
)}
aria-hidden={!isChatRoute}
>
<ChatPage isActive={isChatRoute} />
</div>
)
)}
</div>
<PluginSlot name="post-main" />

View file

@ -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(
<button
type="button"
onClick={() => setMobilePanelOpen(true)}
onClick={() => setMobilePanelOpenRaw(true)}
className={cn(
"inline-flex items-center gap-1.5 rounded border border-current/20",
"px-2 py-1 text-[0.65rem] font-medium tracking-wide normal-case",