Reduce workspace render churn

This commit is contained in:
Mohamed Boudra
2026-06-12 21:53:22 +07:00
parent 889d058658
commit c4d544baf6
11 changed files with 252 additions and 99 deletions
@@ -50,7 +50,6 @@ interface SidebarAnimationContextValue {
animateToClose: () => void;
startMobilePanelTransition: (mobileView: "agent" | "agent-list" | "file-explorer") => void;
settleMobilePanel: (mobileView: "agent" | "agent-list" | "file-explorer") => void;
settledGeneration: number;
isGesturing: SharedValue<boolean>;
mobileVisualPanel: SharedValue<number>;
mobilePanelState: SharedValue<number>;
@@ -61,6 +60,10 @@ interface SidebarAnimationContextValue {
const SidebarAnimationContext = createContext<SidebarAnimationContextValue | null>(null);
// Separate context so that settle-driven re-renders only reach MobileSidebar,
// not every other useSidebarAnimation() consumer.
const SidebarSettledGenerationContext = createContext<number>(0);
function getMobileVisualPanel(mobileView: "agent" | "agent-list" | "file-explorer"): number {
if (mobileView === "agent-list") {
return MOBILE_VISUAL_PANEL_AGENT_LIST;
@@ -357,7 +360,6 @@ export function SidebarAnimationProvider({ children }: { children: ReactNode })
animateToClose,
startMobilePanelTransition,
settleMobilePanel,
settledGeneration,
isGesturing,
mobileVisualPanel,
mobilePanelState,
@@ -373,7 +375,6 @@ export function SidebarAnimationProvider({ children }: { children: ReactNode })
animateToClose,
startMobilePanelTransition,
settleMobilePanel,
settledGeneration,
isGesturing,
mobileVisualPanel,
mobilePanelState,
@@ -384,7 +385,11 @@ export function SidebarAnimationProvider({ children }: { children: ReactNode })
);
return (
<SidebarAnimationContext.Provider value={value}>{children}</SidebarAnimationContext.Provider>
<SidebarAnimationContext.Provider value={value}>
<SidebarSettledGenerationContext.Provider value={settledGeneration}>
{children}
</SidebarSettledGenerationContext.Provider>
</SidebarAnimationContext.Provider>
);
}
@@ -395,3 +400,7 @@ export function useSidebarAnimation() {
}
return context;
}
export function useSidebarSettledGeneration() {
return useContext(SidebarSettledGenerationContext);
}
+20 -14
View File
@@ -2,6 +2,7 @@ import {
createContext,
useContext,
useEffect,
useMemo,
useRef,
useSyncExternalStore,
type ReactNode,
@@ -57,22 +58,27 @@ export function useVoice() {
export function useVoiceOptional(): VoiceContextValue | null {
const runtime = useContext(VoiceRuntimeContext);
const snapshot = useSyncExternalStore(
runtime ? runtime.subscribe.bind(runtime) : noopSubscribe,
runtime ? runtime.getSnapshot.bind(runtime) : getEmptySnapshot,
runtime ? runtime.getSnapshot.bind(runtime) : getEmptySnapshot,
runtime ? runtime.subscribe : noopSubscribe,
runtime ? runtime.getSnapshot : getEmptySnapshot,
runtime ? runtime.getSnapshot : getEmptySnapshot,
);
if (!runtime) {
return null;
}
return {
...snapshot,
startVoice: runtime.startVoice.bind(runtime),
stopVoice: runtime.stopVoice.bind(runtime),
isVoiceModeForAgent: runtime.isVoiceModeForAgent.bind(runtime),
toggleMute: runtime.toggleMute.bind(runtime),
};
// Methods on the runtime object literal close over factory-local state; they
// don't use `this`, so no binding is needed. Memoising on [snapshot, runtime]
// keeps the returned object reference stable across re-renders that don't
// change either, preventing downstream memo/useMemo misses.
return useMemo(() => {
if (!runtime) {
return null;
}
return {
...snapshot,
startVoice: runtime.startVoice,
stopVoice: runtime.stopVoice,
isVoiceModeForAgent: runtime.isVoiceModeForAgent,
toggleMute: runtime.toggleMute,
};
}, [snapshot, runtime]);
}
export function useVoiceTelemetry() {