perf: optimize app startup and background lifecycles
This commit is contained in:
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
BarChart3,
|
||||
Bot,
|
||||
Cpu,
|
||||
ChevronRight,
|
||||
Crown,
|
||||
@@ -19,6 +20,13 @@ import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { DisclosureContent } from '@/components/ui/disclosure';
|
||||
import { getAgentAvatarSrc } from '@/lib/agent-avatars';
|
||||
import { getAiHardwareOverview, type AiHardwareOverview } from '@/lib/ai-hardware';
|
||||
import {
|
||||
AI_HARDWARE_NAVIGATION_EVENT,
|
||||
AI_HARDWARE_OVERVIEW_EVENT,
|
||||
announceAiHardwareNavigation,
|
||||
readAiHardwareAgentId,
|
||||
} from '@/lib/ai-hardware-navigation';
|
||||
import { getAiModuleForPath } from '@/lib/ai-modules';
|
||||
import { invokeIpc } from '@/lib/api-client';
|
||||
import { getAuthUserDisplayName } from '@/lib/auth-user-display';
|
||||
@@ -159,6 +167,9 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
const [creatingProject, setCreatingProject] = useState(false);
|
||||
const [projectEntryError, setProjectEntryError] = useState<ProjectEntryError | null>(null);
|
||||
const [removeProjectCandidate, setRemoveProjectCandidate] = useState<OpencodeProject | null>(null);
|
||||
const [robotOverview, setRobotOverview] = useState<AiHardwareOverview | null>(null);
|
||||
const [robotOverviewLoading, setRobotOverviewLoading] = useState(false);
|
||||
const [robotSelectedAgentId, setRobotSelectedAgentId] = useState<string | null>(() => readAiHardwareAgentId());
|
||||
const accountButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const accountMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
const tokenUsageRequestIdRef = useRef(0);
|
||||
@@ -168,6 +179,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
const isProgrammingModule = activeModule === 'programming';
|
||||
const isPaintingModule = activeModule === 'painting';
|
||||
const isLearningModule = activeModule === 'learning';
|
||||
const isRobotModule = activeModule === 'robot';
|
||||
const isPromptMuseum = location.pathname === '/image-prompts' || location.pathname.startsWith('/image-prompts/');
|
||||
const projectConfigPath = '/project-config';
|
||||
const visibleProjects = opencodeProjects;
|
||||
@@ -182,6 +194,16 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
const tokenUsageExhausted = isTokenUsageExhausted(tokenUsageState);
|
||||
const fiveHourRefreshLabel = getRefreshTimeLabel(tokenUsageState.usage?.five_hour_refresh_at);
|
||||
const weeklyRefreshLabel = getRefreshTimeLabel(tokenUsageState.usage?.weekly_refresh_at);
|
||||
const activeRobotAgentId = isRobotModule ? robotSelectedAgentId : null;
|
||||
|
||||
const openRobotRoute = useCallback((agentId: string | null = null, openCreate = false) => {
|
||||
const params = new URLSearchParams();
|
||||
if (agentId) params.set('agent', agentId);
|
||||
if (openCreate) params.set('create', '1');
|
||||
const search = params.toString();
|
||||
navigate({ pathname: '/ai-hardware', search: search ? `?${search}` : '' });
|
||||
announceAiHardwareNavigation({ agentId, openCreate });
|
||||
}, [navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isProgrammingModule) return;
|
||||
@@ -200,6 +222,54 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
void refreshProviderSnapshot().catch(() => undefined);
|
||||
}, [isProgrammingModule, refreshProviderSnapshot]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isRobotModule) {
|
||||
setRobotOverview(null);
|
||||
setRobotOverviewLoading(false);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let active = true;
|
||||
const handleOverview = (event: Event) => {
|
||||
const next = (event as CustomEvent<AiHardwareOverview>).detail;
|
||||
if (!next || !Array.isArray(next.agents) || !Array.isArray(next.devices)) return;
|
||||
setRobotOverview(next);
|
||||
setRobotOverviewLoading(false);
|
||||
};
|
||||
window.addEventListener(AI_HARDWARE_OVERVIEW_EVENT, handleOverview);
|
||||
setRobotOverviewLoading(true);
|
||||
void getAiHardwareOverview()
|
||||
.then((next) => {
|
||||
if (!active) return;
|
||||
setRobotOverview(next);
|
||||
setRobotOverviewLoading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!active) return;
|
||||
setRobotOverviewLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
window.removeEventListener(AI_HARDWARE_OVERVIEW_EVENT, handleOverview);
|
||||
};
|
||||
}, [isRobotModule]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isRobotModule) {
|
||||
setRobotSelectedAgentId(null);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
setRobotSelectedAgentId(readAiHardwareAgentId(location.search));
|
||||
const handleNavigation = (event: Event) => {
|
||||
const detail = (event as CustomEvent<{ agentId?: string | null; openCreate?: boolean }>).detail;
|
||||
if (!detail?.openCreate && detail?.agentId !== undefined) setRobotSelectedAgentId(detail.agentId);
|
||||
};
|
||||
window.addEventListener(AI_HARDWARE_NAVIGATION_EVENT, handleNavigation);
|
||||
return () => window.removeEventListener(AI_HARDWARE_NAVIGATION_EVENT, handleNavigation);
|
||||
}, [isRobotModule, location.search]);
|
||||
|
||||
useEffect(() => {
|
||||
if (profileRequired || profileSyncError) setProfileDialogOpen(true);
|
||||
}, [profileRequired, profileSyncError]);
|
||||
@@ -455,7 +525,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
onPointerEnter={() => onSidebarPeekChange?.(true, 'sidebar')}
|
||||
onPointerLeave={() => onSidebarPeekChange?.(false, 'sidebar')}
|
||||
className={cn(
|
||||
'sidebar-glass-surface flex min-h-0 shrink-0 flex-col overflow-hidden border-r border-border/80 text-foreground transition-[width,opacity,box-shadow] duration-300 ease-spring motion-reduce:transition-none',
|
||||
'sidebar-glass-surface flex min-h-0 shrink-0 flex-col overflow-hidden border-r border-border/80 text-foreground',
|
||||
sidebarCollapsed
|
||||
? 'pointer-events-none w-0 border-r-0 opacity-0'
|
||||
: workspaceLayout
|
||||
@@ -489,7 +559,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
type="button"
|
||||
data-testid="sidebar-create-project"
|
||||
className={cn(
|
||||
'motion-press mb-1 flex min-h-9 w-full items-center gap-2 rounded-lg border-0 bg-transparent px-2 py-1.5 text-left text-sm font-medium text-foreground shadow-none transition-colors hover:bg-surface-subtle active:scale-[0.985]',
|
||||
'motion-press mb-1 flex min-h-9 w-full items-center gap-2 rounded-lg border-0 bg-transparent px-2 py-1.5 text-left text-sm font-medium text-foreground shadow-none transition-colors duration-100 hover:bg-surface-subtle',
|
||||
sidebarCollapsed ? 'justify-center px-0' : '',
|
||||
)}
|
||||
aria-label="新建项目"
|
||||
@@ -536,7 +606,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
aria-current={active ? 'page' : undefined}
|
||||
onClick={() => void openProject(project)}
|
||||
className={cn(
|
||||
'motion-press group/project flex w-full items-center gap-2 rounded-xl px-3 py-2 text-left transition-[background-color,color,box-shadow,transform] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/35 focus-visible:ring-offset-1 active:scale-[0.985]',
|
||||
'motion-press group/project flex w-full items-center gap-2 rounded-xl px-3 py-2 text-left transition-colors duration-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/35 focus-visible:ring-offset-1',
|
||||
active
|
||||
? 'bg-brand-selected text-foreground shadow-soft ring-1 ring-brand/25'
|
||||
: 'bg-transparent text-foreground hover:bg-surface-tertiary',
|
||||
@@ -574,8 +644,8 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
<button
|
||||
type="button"
|
||||
aria-label="机器人工作台"
|
||||
aria-current="page"
|
||||
onClick={() => navigate('/ai-hardware')}
|
||||
aria-current={location.pathname === '/ai-hardware' ? 'page' : undefined}
|
||||
onClick={() => openRobotRoute()}
|
||||
className={cn(
|
||||
'flex min-h-10 w-full items-center gap-2 rounded-lg bg-brand-soft px-2 py-2 text-left text-sm font-semibold text-foreground',
|
||||
sidebarCollapsed && 'justify-center px-0',
|
||||
@@ -585,9 +655,55 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
{!sidebarCollapsed ? <span>机器人工作台</span> : null}
|
||||
</button>
|
||||
{!sidebarCollapsed ? (
|
||||
<p className="mt-3 px-2 text-xs font-medium leading-5 text-muted-foreground">
|
||||
管理机器人智能体、设备绑定与配置。
|
||||
</p>
|
||||
<div className="mt-4 border-t border-border/60 pt-4">
|
||||
<div className="flex items-center justify-between gap-2 px-2">
|
||||
<span className="text-xs font-semibold tracking-wide text-muted-foreground">智能体</span>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="sidebar-robot-create-agent"
|
||||
aria-label="创建智能体"
|
||||
title="创建智能体"
|
||||
onClick={() => openRobotRoute(null, true)}
|
||||
className="motion-press flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors duration-100 hover:bg-surface-subtle hover:text-brand focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/35"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div data-testid="sidebar-robot-agents" className="mt-2 space-y-1">
|
||||
{robotOverviewLoading ? (
|
||||
<p role="status" className="px-2 py-2 text-xs text-muted-foreground">正在读取智能体…</p>
|
||||
) : robotOverview ? (
|
||||
robotOverview.agents.length > 0 ? robotOverview.agents.map((agent) => {
|
||||
const active = activeRobotAgentId === agent.id;
|
||||
const deviceCount = robotOverview.devices.filter((device) => device.agent_id === agent.id).length;
|
||||
return (
|
||||
<button
|
||||
key={agent.id}
|
||||
type="button"
|
||||
data-testid={`sidebar-robot-agent-${agent.id}`}
|
||||
aria-label={`进入智能体 ${agent.name}`}
|
||||
aria-current={active ? 'page' : undefined}
|
||||
onClick={() => openRobotRoute(agent.id)}
|
||||
className={cn(
|
||||
'motion-press flex min-h-10 w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left transition-colors duration-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/35',
|
||||
active ? 'bg-brand-selected text-foreground' : 'text-foreground hover:bg-surface-tertiary',
|
||||
)}
|
||||
>
|
||||
<Bot className={cn('h-4 w-4 shrink-0', active ? 'text-brand' : 'text-muted-foreground')} />
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate text-sm font-medium">{agent.name}</span>
|
||||
<span className="block text-[11px] tabular-nums text-muted-foreground">{deviceCount} 台设备</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}) : (
|
||||
<p className="px-2 py-2 text-xs leading-5 text-muted-foreground">还没有智能体,点击右上角 + 创建。</p>
|
||||
)
|
||||
) : (
|
||||
<p className="px-2 py-2 text-xs leading-5 text-muted-foreground">暂时无法加载智能体列表。</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
@@ -639,7 +755,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
<label
|
||||
key={value}
|
||||
className={cn(
|
||||
'min-h-28 cursor-pointer rounded-xl bg-white p-3 shadow-soft transition-[box-shadow,transform] active:scale-[0.96]',
|
||||
'min-h-28 cursor-pointer rounded-xl bg-white p-3 shadow-soft',
|
||||
newProjectType === value
|
||||
? 'ring-2 ring-brand/35 shadow-float'
|
||||
: 'ring-1 ring-foreground/10 hover:shadow-float',
|
||||
@@ -796,7 +912,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
? createPortal(
|
||||
<div
|
||||
data-testid="project-config-error-overlay"
|
||||
className="fixed inset-0 z-[1000] flex min-h-screen items-center justify-center bg-foreground/15 p-4 backdrop-blur-md sm:p-6"
|
||||
className="fixed inset-0 z-[1000] flex min-h-screen items-center justify-center bg-foreground/15 p-4 sm:p-6"
|
||||
>
|
||||
<div
|
||||
role="alertdialog"
|
||||
@@ -886,7 +1002,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate">{accountName}</span>
|
||||
</span>
|
||||
<ChevronRight className={cn('h-3.5 w-3.5 shrink-0 transition-transform duration-200 ease-out', accountMenuOpen && 'rotate-90')} />
|
||||
<ChevronRight className={cn('h-3.5 w-3.5 shrink-0', accountMenuOpen && 'rotate-90')} />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
@@ -928,12 +1044,12 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
data-testid="sidebar-account-usage-menuitem"
|
||||
aria-expanded={usageDrawerOpen}
|
||||
aria-controls="sidebar-account-usage-drawer"
|
||||
className="flex min-h-8 w-full items-center gap-2 rounded-md px-1 py-1 text-left text-sm font-medium transition-colors hover:bg-surface-subtle active:scale-[0.985]"
|
||||
className="flex min-h-8 w-full items-center gap-2 rounded-md px-1 py-1 text-left text-sm font-medium transition-colors duration-100 hover:bg-surface-subtle"
|
||||
onClick={() => setUsageDrawerOpen((open) => !open)}
|
||||
>
|
||||
<BarChart3 className="h-3.5 w-3.5 shrink-0" />
|
||||
<span className="min-w-0 flex-1 truncate">剩余用量</span>
|
||||
<ChevronRight className={cn('h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform duration-200 ease-out', usageDrawerOpen && 'rotate-90')} />
|
||||
<ChevronRight className={cn('h-3.5 w-3.5 shrink-0 text-muted-foreground', usageDrawerOpen && 'rotate-90')} />
|
||||
</button>
|
||||
<DisclosureContent
|
||||
open={usageDrawerOpen}
|
||||
@@ -974,7 +1090,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
<button
|
||||
type="button"
|
||||
data-testid="sidebar-account-upgrade-button"
|
||||
className="mt-1.5 flex min-h-8 w-full items-center justify-center gap-1.5 rounded-md border border-foreground/15 bg-accent-soft px-2 py-1.5 text-xs font-semibold text-foreground shadow-soft transition-transform hover:bg-background active:scale-[0.985]"
|
||||
className="mt-1.5 flex min-h-8 w-full items-center justify-center gap-1.5 rounded-md border border-foreground/15 bg-accent-soft px-2 py-1.5 text-xs font-semibold text-foreground shadow-soft transition-colors duration-100 hover:bg-background"
|
||||
onClick={openSubscriptionUpgrade}
|
||||
>
|
||||
<Crown className="h-3.5 w-3.5" />
|
||||
@@ -987,7 +1103,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className="flex min-h-8 w-full items-center gap-2 rounded-md px-1 py-1 text-left text-sm font-medium transition-colors hover:bg-surface-subtle active:scale-[0.985]"
|
||||
className="flex min-h-8 w-full items-center gap-2 rounded-md px-1 py-1 text-left text-sm font-medium transition-colors duration-100 hover:bg-surface-subtle"
|
||||
onClick={openProfileDialog}
|
||||
>
|
||||
<UserCircle2 className="h-3.5 w-3.5 shrink-0" />
|
||||
@@ -997,7 +1113,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
type="button"
|
||||
role="menuitem"
|
||||
data-testid="sidebar-nav-settings"
|
||||
className="flex min-h-8 w-full items-center gap-2 rounded-md px-1 py-1 text-left text-sm font-medium transition-colors hover:bg-surface-subtle active:scale-[0.985]"
|
||||
className="flex min-h-8 w-full items-center gap-2 rounded-md px-1 py-1 text-left text-sm font-medium transition-colors duration-100 hover:bg-surface-subtle"
|
||||
onClick={() => navigateFromAccountMenu('/settings')}
|
||||
>
|
||||
<SettingsIcon className="h-3.5 w-3.5 shrink-0" />
|
||||
@@ -1009,7 +1125,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className="flex min-h-8 w-full items-center gap-2 rounded-md px-1 py-1 text-left text-sm font-medium text-accent-strong transition-colors hover:bg-accent-soft hover:text-foreground active:scale-[0.985]"
|
||||
className="flex min-h-8 w-full items-center gap-2 rounded-md px-1 py-1 text-left text-sm font-medium text-accent-strong transition-colors duration-100 hover:bg-accent-soft hover:text-foreground"
|
||||
onClick={() => void handleAccountLogout()}
|
||||
>
|
||||
<LogOut className="h-3.5 w-3.5 shrink-0" />
|
||||
|
||||
Reference in New Issue
Block a user