From 298299d88e168a0fae595b3c66e82acc86e15100 Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Fri, 13 Feb 2026 17:04:15 +0800 Subject: [PATCH] fix(desktop): persist chat state across tab navigation Keep LocalChat mounted at the Layout level with CSS visibility toggle instead of unmounting on route change, preserving messages, streaming state, and IPC subscriptions when switching sidebar tabs. Co-Authored-By: Claude Opus 4.6 --- apps/desktop/src/renderer/src/App.tsx | 3 +-- .../renderer/src/components/local-chat.tsx | 7 +++--- .../desktop/src/renderer/src/pages/layout.tsx | 23 ++++++++++++++++++- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/renderer/src/App.tsx b/apps/desktop/src/renderer/src/App.tsx index 68a23c621..3372a97ea 100644 --- a/apps/desktop/src/renderer/src/App.tsx +++ b/apps/desktop/src/renderer/src/App.tsx @@ -5,7 +5,6 @@ import { TooltipProvider } from '@multica/ui/components/ui/tooltip' import { Toaster } from './components/toaster' import Layout from './pages/layout' import HomePage from './pages/home' -import ChatPage from './pages/chat' import ProfilePage from './pages/agent/profile' import SkillsPage from './pages/agent/skills' import ToolsPage from './pages/agent/tools' @@ -73,7 +72,7 @@ const router = createHashRouter([ ), }, - { path: 'chat', element: }, + { path: 'chat', element: null }, { path: 'agent/profile', element: }, { path: 'agent/skills', element: }, { path: 'agent/tools', element: }, diff --git a/apps/desktop/src/renderer/src/components/local-chat.tsx b/apps/desktop/src/renderer/src/components/local-chat.tsx index 39c6c0b38..0f842a059 100644 --- a/apps/desktop/src/renderer/src/components/local-chat.tsx +++ b/apps/desktop/src/renderer/src/components/local-chat.tsx @@ -73,12 +73,13 @@ export function LocalChat({ initialPrompt }: LocalChatProps) { const currentMeta = current ? providers.find((p) => p.id === current.provider) : null // Auto-send initial prompt after a short delay - const hasSentInitialPrompt = useRef(false) + const lastPromptRef = useRef(undefined) useEffect(() => { - if (!agentId || !initialPrompt || hasSentInitialPrompt.current) return + if (!agentId || !initialPrompt) return + if (initialPrompt === lastPromptRef.current) return const timer = setTimeout(() => { - hasSentInitialPrompt.current = true + lastPromptRef.current = initialPrompt sendMessage(initialPrompt) // Remove prompt from URL to prevent re-sending on back navigation navigate('/chat', { replace: true }) diff --git a/apps/desktop/src/renderer/src/pages/layout.tsx b/apps/desktop/src/renderer/src/pages/layout.tsx index 1651e447a..476f61c73 100644 --- a/apps/desktop/src/renderer/src/pages/layout.tsx +++ b/apps/desktop/src/renderer/src/pages/layout.tsx @@ -1,3 +1,4 @@ +import { useState, useEffect } from 'react' import { Outlet, NavLink, useLocation, useNavigate } from 'react-router-dom' import { Button } from '@multica/ui/components/ui/button' import { MulticaIcon } from '@multica/ui/components/multica-icon' @@ -43,6 +44,7 @@ import { } from '@multica/ui/components/ui/sidebar' import { cn } from '@multica/ui/lib/utils' import { ModeToggle } from '../components/mode-toggle' +import { LocalChat } from '../components/local-chat' import { DeviceConfirmDialog } from '../components/device-confirm-dialog' import { UpdateNotification } from '../components/update-notification' import { useAuthStore } from '../stores/auth' @@ -151,8 +153,20 @@ export default function Layout() { const location = useLocation() const navigate = useNavigate() const isAgentActive = location.pathname.startsWith('/agent') + const isOnChat = location.pathname === '/chat' const { user, clearAuth } = useAuthStore() + // Lazy mount: only mount Chat on first visit, then keep it mounted forever + const [chatMounted, setChatMounted] = useState(false) + useEffect(() => { + if (isOnChat && !chatMounted) setChatMounted(true) + }, [isOnChat, chatMounted]) + + // Extract initialPrompt from URL search params when navigating to /chat?prompt=... + const initialPrompt = isOnChat + ? new URLSearchParams(location.search).get('prompt') ?? undefined + : undefined + const handleLogout = async () => { await clearAuth() navigate('/login') @@ -285,7 +299,14 @@ export default function Layout() {
- +
+ +
+ {chatMounted && ( +
+ +
+ )}