perf: reduce UI render fanout and scroll jitter
- Cut broad render fanout across the app by replacing shared-store whole-object subscriptions with leaf selectors, memoizing hot chrome boundaries, and isolating disabled global providers from live session/message state. This keeps header controls, composer toolbars, side panels, and other non-hot UI surfaces from repainting on every assistant update or keystroke. - Rework sidebar session ordering so recent, project groups, and worktree groups derive from one ordering source while avoiding streaming-time thrash. The sidebar now uses a stabilized session snapshot, preserves structural identity for unchanged rows, reads live row status/details per session, and applies a one-shot sort bump on idle->busy instead of continuously resorting during activity. - Fix chat/input scroll instability by separating viewport-resize handling from message-growth handling, disabling conflicting native scroll anchoring, and stopping textarea autosize from collapsing on every growth keystroke. This removes the multiline typing jiggle during streaming and reduces unnecessary composer rerenders. - Also gate voice context wiring behind voice-mode enablement and codify the learned render/scroll/order anti-patterns in AGENTS.md so future changes avoid the same classes of regressions.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -51,26 +51,70 @@ const normalizeDirectoryKey = (value: string): string => {
|
||||
return normalized;
|
||||
};
|
||||
|
||||
const MemoSessionSidebar = React.memo(SessionSidebar);
|
||||
const MemoHeader = React.memo(Header);
|
||||
const MemoChatView = React.memo(ChatView);
|
||||
const MemoPlanView = React.memo(PlanView);
|
||||
const MemoGitView = React.memo(GitView);
|
||||
const MemoDiffView = React.memo(DiffView);
|
||||
const MemoTerminalView = React.memo(TerminalView);
|
||||
const MemoFilesView = React.memo(FilesView);
|
||||
const MemoRightSidebarTabs = React.memo(RightSidebarTabs);
|
||||
|
||||
const DesktopLeftSidebar = React.memo(function DesktopLeftSidebar({
|
||||
isSidebarOpen,
|
||||
isMobile,
|
||||
}: {
|
||||
isSidebarOpen: boolean;
|
||||
isMobile: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Sidebar isOpen={isSidebarOpen} isMobile={isMobile} className="border-0">
|
||||
<ErrorBoundary>
|
||||
<MemoSessionSidebar />
|
||||
</ErrorBoundary>
|
||||
</Sidebar>
|
||||
);
|
||||
});
|
||||
|
||||
const DesktopRightPanel = React.memo(function DesktopRightPanel({
|
||||
isRightSidebarOpen,
|
||||
setDesktopRightSidebarActionsHost,
|
||||
}: {
|
||||
isRightSidebarOpen: boolean;
|
||||
setDesktopRightSidebarActionsHost: React.Dispatch<React.SetStateAction<HTMLDivElement | null>>;
|
||||
}) {
|
||||
return (
|
||||
<RightSidebar
|
||||
isOpen={isRightSidebarOpen}
|
||||
className="border-0"
|
||||
onTopActionsHostChange={setDesktopRightSidebarActionsHost}
|
||||
>
|
||||
<ErrorBoundary>
|
||||
<MemoRightSidebarTabs />
|
||||
</ErrorBoundary>
|
||||
</RightSidebar>
|
||||
);
|
||||
});
|
||||
|
||||
export const MainLayout: React.FC = () => {
|
||||
const RIGHT_SIDEBAR_AUTO_CLOSE_WIDTH = 1140;
|
||||
const RIGHT_SIDEBAR_AUTO_OPEN_WIDTH = 1220;
|
||||
const BOTTOM_TERMINAL_AUTO_CLOSE_HEIGHT = 640;
|
||||
const BOTTOM_TERMINAL_AUTO_OPEN_HEIGHT = 700;
|
||||
const {
|
||||
isSidebarOpen,
|
||||
isRightSidebarOpen,
|
||||
isBottomTerminalOpen,
|
||||
setRightSidebarOpen,
|
||||
setBottomTerminalOpen,
|
||||
activeMainTab,
|
||||
setIsMobile,
|
||||
isSessionSwitcherOpen,
|
||||
isSettingsDialogOpen,
|
||||
setSettingsDialogOpen,
|
||||
isMultiRunLauncherOpen,
|
||||
setMultiRunLauncherOpen,
|
||||
multiRunLauncherPrefillPrompt,
|
||||
} = useUIStore();
|
||||
const isSidebarOpen = useUIStore((state) => state.isSidebarOpen);
|
||||
const isRightSidebarOpen = useUIStore((state) => state.isRightSidebarOpen);
|
||||
const isBottomTerminalOpen = useUIStore((state) => state.isBottomTerminalOpen);
|
||||
const setRightSidebarOpen = useUIStore((state) => state.setRightSidebarOpen);
|
||||
const setBottomTerminalOpen = useUIStore((state) => state.setBottomTerminalOpen);
|
||||
const activeMainTab = useUIStore((state) => state.activeMainTab);
|
||||
const setIsMobile = useUIStore((state) => state.setIsMobile);
|
||||
const isSessionSwitcherOpen = useUIStore((state) => state.isSessionSwitcherOpen);
|
||||
const isSettingsDialogOpen = useUIStore((state) => state.isSettingsDialogOpen);
|
||||
const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen);
|
||||
const isMultiRunLauncherOpen = useUIStore((state) => state.isMultiRunLauncherOpen);
|
||||
const setMultiRunLauncherOpen = useUIStore((state) => state.setMultiRunLauncherOpen);
|
||||
const multiRunLauncherPrefillPrompt = useUIStore((state) => state.multiRunLauncherPrefillPrompt);
|
||||
|
||||
const { isMobile } = useDeviceInfo();
|
||||
const isDesktopShellRuntime = React.useMemo(() => isDesktopShell(), []);
|
||||
|
||||
@@ -16,7 +16,8 @@ interface SidebarProps {
|
||||
}
|
||||
|
||||
export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children, className }) => {
|
||||
const { sidebarWidth, setSidebarWidth } = useUIStore();
|
||||
const sidebarWidth = useUIStore((state) => state.sidebarWidth);
|
||||
const setSidebarWidth = useUIStore((state) => state.setSidebarWidth);
|
||||
const isDesktopApp = React.useMemo(() => isDesktopShell(), []);
|
||||
const [isResizing, setIsResizing] = React.useState(false);
|
||||
const startXRef = React.useRef(0);
|
||||
|
||||
Reference in New Issue
Block a user