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:
@@ -457,7 +457,48 @@ export const useChatScrollManager = ({
|
||||
const container = scrollRef.current;
|
||||
if (!container || typeof ResizeObserver === 'undefined') return;
|
||||
|
||||
let lastScrollHeight = container.scrollHeight;
|
||||
let lastClientHeight = container.clientHeight;
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
const nextScrollHeight = container.scrollHeight;
|
||||
const nextClientHeight = container.clientHeight;
|
||||
const scrollHeightChanged = nextScrollHeight !== lastScrollHeight;
|
||||
const clientHeightChanged = nextClientHeight !== lastClientHeight;
|
||||
|
||||
if (clientHeightChanged) {
|
||||
const previousDistanceFromBottom = Math.max(
|
||||
0,
|
||||
lastScrollHeight - lastScrollTopRef.current - lastClientHeight,
|
||||
);
|
||||
|
||||
if (isPinnedRef.current) {
|
||||
const targetScrollTop = Math.max(
|
||||
0,
|
||||
nextScrollHeight - nextClientHeight - previousDistanceFromBottom,
|
||||
);
|
||||
|
||||
if (Math.abs(container.scrollTop - targetScrollTop) > 0.5) {
|
||||
markProgrammaticScroll();
|
||||
container.scrollTop = targetScrollTop;
|
||||
lastScrollTopRef.current = targetScrollTop;
|
||||
}
|
||||
|
||||
lastScrollHeight = nextScrollHeight;
|
||||
lastClientHeight = nextClientHeight;
|
||||
updateScrollButtonVisibility();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
lastScrollHeight = nextScrollHeight;
|
||||
lastClientHeight = nextClientHeight;
|
||||
|
||||
if (clientHeightChanged && !scrollHeightChanged) {
|
||||
updateScrollButtonVisibility();
|
||||
return;
|
||||
}
|
||||
|
||||
schedulePinnedStateAndIndicators();
|
||||
});
|
||||
|
||||
@@ -474,7 +515,7 @@ export const useChatScrollManager = ({
|
||||
observer.disconnect();
|
||||
childObserver.disconnect();
|
||||
};
|
||||
}, [schedulePinnedStateAndIndicators]);
|
||||
}, [schedulePinnedStateAndIndicators, updateScrollButtonVisibility]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
|
||||
Reference in New Issue
Block a user