Merge remote-tracking branch 'origin/main' into feat/desktop-api-client

This commit is contained in:
yushen 2026-02-13 17:57:19 +08:00
commit 5ee08e9368
21 changed files with 975 additions and 125 deletions

View file

@ -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([
</OnboardingGuard>
),
},
{ path: 'chat', element: <ChatPage /> },
{ path: 'chat', element: null },
{ path: 'agent/profile', element: <ProfilePage /> },
{ path: 'agent/skills', element: <SkillsPage /> },
{ path: 'agent/tools', element: <ToolsPage /> },

View file

@ -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<string | undefined>(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 })

View file

@ -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() {
<SidebarInset className="overflow-hidden">
<MainHeader />
<main className="flex-1 overflow-hidden min-h-1">
<Outlet />
<div className={cn('h-full', isOnChat && 'hidden')}>
<Outlet />
</div>
{chatMounted && (
<div className={cn('h-full flex flex-col overflow-hidden', !isOnChat && 'hidden')}>
<LocalChat initialPrompt={initialPrompt} />
</div>
)}
</main>
</SidebarInset>

View file

@ -23,14 +23,20 @@ export default function LoginPage() {
if (isLoading) {
return (
<div className="flex h-screen items-center justify-center bg-background">
<div
className="flex h-screen items-center justify-center bg-background"
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
>
<Loading className="size-6" />
</div>
)
}
return (
<div className="flex h-screen flex-col items-center justify-center bg-background p-8 animate-in fade-in duration-300">
<div
className="flex h-screen flex-col items-center justify-center bg-background p-8 animate-in fade-in duration-300"
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
>
<div className="w-full max-w-sm flex flex-col items-center text-center space-y-6">
{/* Brand */}
<div className="flex items-center gap-2">
@ -44,7 +50,12 @@ export default function LoginPage() {
</p>
{/* Sign In */}
<Button onClick={startLogin} size="lg" className="px-8">
<Button
onClick={startLogin}
size="lg"
className="px-8"
style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}
>
Sign In to Continue
</Button>