perf: optimize app startup and background lifecycles

This commit is contained in:
inman committed 2026-08-19 00:56:15 +08:00
1 parent 31332f6d76
commit 927e13349b
121 files changed
+4359 -1400

No files matched your search

+58 -45
View File
@@ -24,11 +24,13 @@ import { Button } from '@/components/ui/button';
import { DISCLOSURE_EXIT_MS } from '@/components/ui/disclosure';
import {
deriveAgentBrowserDiagnostics,
closeAgentBrowser,
getAgentBrowserState,
navigateAgentBrowser,
openAgentBrowser,
presentAgentBrowser,
readAgentBrowserEvents,
setAgentBrowserDiagnostics,
} from '@/lib/agent-browser';
import { subscribeHostEvent } from '@/lib/host-events';
import { cn } from '@/lib/utils';
@@ -260,6 +262,10 @@ export function AgentBrowserPanel({ projectId, projectPath }: AgentBrowserPanelP
}, [isOpen]);
useEffect(() => {
if (!isOpen) {
setOccluded(false);
return undefined;
}
const updateOcclusion = () => setOccluded(hasModalOcclusion());
updateOcclusion();
const observer = new MutationObserver(updateOcclusion);
@@ -270,7 +276,7 @@ export function AgentBrowserPanel({ projectId, projectPath }: AgentBrowserPanelP
subtree: true,
});
return () => observer.disconnect();
}, []);
}, [isOpen]);
const applySnapshot = useCallback((next: AgentBrowserSnapshot) => {
setSnapshot(next);
@@ -336,31 +342,11 @@ export function AgentBrowserPanel({ projectId, projectPath }: AgentBrowserPanelP
};
}, [applySnapshot, projectPath]);
useEffect(() => {
if (!projectPath || (!isOpen && !browserActive)) return undefined;
let cancelled = false;
const refresh = async () => {
try {
const next = await getAgentBrowserState(projectPath);
if (!cancelled) {
applySnapshot(next);
setError(null);
}
} catch (cause) {
if (!cancelled) {
setError(cause instanceof Error ? cause.message : '无法读取开发浏览器状态');
}
}
};
void refresh();
const interval = window.setInterval(() => void refresh(), 2500);
return () => {
cancelled = true;
window.clearInterval(interval);
};
}, [applySnapshot, browserActive, isOpen, projectPath]);
useEffect(() => () => {
if (projectPath) {
void closeAgentBrowser(projectPath).catch(() => undefined);
}
}, [projectPath]);
useEffect(() => {
eventCursorRef.current = 0;
@@ -368,43 +354,67 @@ export function AgentBrowserPanel({ projectId, projectPath }: AgentBrowserPanelP
}, [snapshot?.browserId, snapshot?.projectPath]);
useEffect(() => {
if (!isOpen || !projectPath || snapshot?.state !== 'attached') {
if (!projectPath || !browserActive) return undefined;
void setAgentBrowserDiagnostics({ projectPath, enabled: diagnosticsOpen })
.then((next) => applySnapshot(next))
.catch(() => undefined);
if (!diagnosticsOpen) {
eventCursorRef.current = 0;
setEvents([]);
}
return undefined;
}, [applySnapshot, browserActive, diagnosticsOpen, projectPath]);
useEffect(() => {
if (!isOpen || !diagnosticsOpen || !projectPath || snapshot?.state !== 'attached') {
return undefined;
}
let cancelled = false;
let inFlight = false;
let pollTimer: number | null = null;
const poll = async () => {
if (inFlight) return;
inFlight = true;
let retryDelay = 250;
try {
const page = await readAgentBrowserEvents({
projectPath,
after: eventCursorRef.current,
methods: EVENT_METHODS,
limit: 200,
waitMs: 500,
waitMs: 5_000,
});
if (cancelled) return;
eventCursorRef.current = page.nextCursor;
setEvents((current) => {
const base = page.gap ? [] : current;
return [...base, ...page.events].slice(-1000);
return [...base, ...page.events].slice(-500);
});
// A real request is held by Main for up to waitMs. Test doubles and
// transient failures can return immediately; back off those empty
// responses so a fast server cannot turn the long-poll into a hot
// microtask loop.
retryDelay = page.events.length > 0 || page.hasMore ? 0 : 250;
} catch {
// State polling presents attachment failures without turning the panel noisy.
} finally {
inFlight = false;
if (!cancelled) {
pollTimer = window.setTimeout(() => {
pollTimer = null;
void poll();
}, retryDelay);
}
}
};
void poll();
const interval = window.setInterval(() => void poll(), 750);
return () => {
cancelled = true;
window.clearInterval(interval);
if (pollTimer !== null) window.clearTimeout(pollTimer);
};
}, [isOpen, projectPath, snapshot?.state, snapshot?.generation]);
}, [diagnosticsOpen, isOpen, projectPath, snapshot?.state, snapshot?.generation]);
useEffect(() => {
if (!isOpen || occluded || !projectPath || !browserActive) return undefined;
@@ -509,6 +519,10 @@ export function AgentBrowserPanel({ projectId, projectPath }: AgentBrowserPanelP
const closePanel = useCallback(() => {
setIsOpen(false);
setSnapshot(null);
if (projectPath) {
void closeAgentBrowser(projectPath).catch(() => undefined);
}
setPanelWidth(DEFAULT_PANEL_WIDTH);
setDiagnosticsOpen(false);
setDiagnosticsHeight(DEFAULT_DIAGNOSTICS_HEIGHT);
@@ -521,7 +535,7 @@ export function AgentBrowserPanel({ projectId, projectPath }: AgentBrowserPanelP
setPanelMounted(false);
panelCloseTimerRef.current = null;
}, DISCLOSURE_EXIT_MS);
}, []);
}, [projectPath]);
const handleResizePointerDown = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
if (event.button !== 0) return;
@@ -648,7 +662,7 @@ export function AgentBrowserPanel({ projectId, projectPath }: AgentBrowserPanelP
<button
type="button"
className={cn(
'no-drag relative flex h-10 w-10 items-center justify-center border-0 bg-transparent p-0 text-muted-foreground outline-none transition-[color,transform] duration-300 ease-spring hover:bg-transparent hover:text-foreground focus-visible:ring-2 focus-visible:ring-brand/30 active:scale-[0.96]',
'no-drag relative flex h-10 w-10 items-center justify-center border-0 bg-transparent p-0 text-muted-foreground outline-none transition-colors duration-100 hover:bg-transparent hover:text-foreground focus-visible:ring-2 focus-visible:ring-brand/30',
isOpen || browserActive
? 'text-brand hover:text-brand'
: 'text-muted-foreground hover:text-foreground',
@@ -691,7 +705,7 @@ export function AgentBrowserPanel({ projectId, projectPath }: AgentBrowserPanelP
<>
<div
aria-hidden="true"
className="shrink-0 overflow-hidden transition-[width] [transition-duration:180ms] ease-out motion-reduce:transition-none"
className="shrink-0 overflow-hidden"
data-testid="agent-browser-panel-spacer"
style={{
width: isOpen ? `${panelWidth}px` : '0px',
@@ -735,7 +749,7 @@ export function AgentBrowserPanel({ projectId, projectPath }: AgentBrowserPanelP
<span
aria-hidden
className={cn(
'absolute inset-y-0 left-1/2 w-px -translate-x-1/2 bg-border/70 transition-[background-color,width] duration-300 ease-spring group-hover:w-0.5 group-hover:bg-brand/70',
'absolute inset-y-0 left-1/2 w-px -translate-x-1/2 bg-border/70 transition-colors duration-100 group-hover:bg-brand/70',
isResizing && 'w-0.5 bg-brand',
)}
/>
@@ -758,7 +772,7 @@ export function AgentBrowserPanel({ projectId, projectPath }: AgentBrowserPanelP
type="button"
variant="ghost"
size="icon"
className="h-7 w-7 rounded-md text-muted-foreground active:scale-[0.96]"
className="h-7 w-7 rounded-md text-muted-foreground"
aria-label="后退"
disabled={!browserActive || !snapshot?.canGoBack || busy}
onClick={() => handleNavigate('back')}
@@ -769,7 +783,7 @@ export function AgentBrowserPanel({ projectId, projectPath }: AgentBrowserPanelP
type="button"
variant="ghost"
size="icon"
className="h-7 w-7 rounded-md text-muted-foreground active:scale-[0.96]"
className="h-7 w-7 rounded-md text-muted-foreground"
aria-label="前进"
disabled={!browserActive || !snapshot?.canGoForward || busy}
onClick={() => handleNavigate('forward')}
@@ -780,7 +794,7 @@ export function AgentBrowserPanel({ projectId, projectPath }: AgentBrowserPanelP
type="button"
variant="ghost"
size="icon"
className="h-7 w-7 rounded-md text-muted-foreground active:scale-[0.96]"
className="h-7 w-7 rounded-md text-muted-foreground"
aria-label="刷新网页"
disabled={!browserActive || busy}
onClick={() => handleNavigate('reload')}
@@ -793,7 +807,7 @@ export function AgentBrowserPanel({ projectId, projectPath }: AgentBrowserPanelP
value={address}
onChange={(event) => setAddress(event.target.value)}
aria-label="网页地址"
className="h-7 min-w-0 flex-1 rounded-lg border border-border/90 bg-surface-input px-2.5 text-[11px] font-medium text-foreground outline-none transition-[border-color,box-shadow] duration-150 placeholder:text-muted-foreground focus:border-brand/40 focus:ring-2 focus:ring-brand/15"
className="h-7 min-w-0 flex-1 rounded-lg border border-border/90 bg-surface-input px-2.5 text-[11px] font-medium text-foreground outline-none transition-colors duration-100 placeholder:text-muted-foreground focus:border-brand/40 focus:ring-2 focus:ring-brand/15"
placeholder="输入 URL"
spellCheck={false}
/>
@@ -827,8 +841,7 @@ export function AgentBrowserPanel({ projectId, projectPath }: AgentBrowserPanelP
<div
className={cn(
'relative flex shrink-0 flex-col overflow-hidden border-t border-border/80 bg-background transition-[height,box-shadow] duration-200 ease-spring motion-reduce:transition-none',
diagnosticsOpen && 'shadow-[0_-10px_24px_hsl(220_20%_16%_/_0.06)]',
'relative flex shrink-0 flex-col overflow-hidden border-t border-border/80 bg-background',
isDiagnosticsResizing && 'select-none transition-none',
)}
style={{
@@ -876,7 +889,7 @@ export function AgentBrowserPanel({ projectId, projectPath }: AgentBrowserPanelP
aria-selected={activeTab === tab}
aria-expanded={activeTab === tab && diagnosticsOpen}
className={cn(
'relative flex h-10 items-center gap-1.5 border-b-2 px-2.5 text-[11px] font-medium transition-[border-color,color,transform] duration-150 active:scale-[0.98]',
'relative flex h-10 items-center gap-1.5 border-b-2 px-2.5 text-[11px] font-medium transition-colors duration-100',
activeTab === tab
? 'border-brand text-foreground'
: 'border-transparent text-muted-foreground hover:text-foreground',
@@ -895,7 +908,7 @@ export function AgentBrowserPanel({ projectId, projectPath }: AgentBrowserPanelP
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 rounded-md text-muted-foreground hover:text-foreground active:scale-[0.96]"
className="h-8 w-8 rounded-md text-muted-foreground hover:text-foreground"
aria-label="收起调试面板"
title="收起调试面板"
onClick={() => setDiagnosticsOpen(false)}
@@ -907,7 +920,7 @@ export function AgentBrowserPanel({ projectId, projectPath }: AgentBrowserPanelP
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 rounded-md text-muted-foreground hover:text-foreground active:scale-[0.96]"
className="h-8 w-8 rounded-md text-muted-foreground hover:text-foreground"
aria-label="展开调试面板"
title="展开调试面板"
onClick={() => setDiagnosticsOpen(true)}