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:
Bohdan Triapitsyn
2026-04-04 02:19:55 +03:00
parent c5497e9823
commit 8587db89b1
42 changed files with 1713 additions and 1047 deletions
@@ -55,10 +55,13 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
allowedProviderIds,
placeholder
}) => {
const { providers, modelsMetadata } = useConfigStore();
const providers = useConfigStore((state) => state.providers);
const modelsMetadata = useConfigStore((state) => state.modelsMetadata);
const isMobile = useUIStore(state => state.isMobile);
const hiddenModels = useUIStore(state => state.hiddenModels);
const { toggleFavoriteModel, isFavoriteModel, addRecentModel } = useUIStore();
const toggleFavoriteModel = useUIStore((state) => state.toggleFavoriteModel);
const isFavoriteModel = useUIStore((state) => state.isFavoriteModel);
const addRecentModel = useUIStore((state) => state.addRecentModel);
const { favoriteModelsList, recentModelsList } = useModelLists();
const { isMobile: deviceIsMobile } = useDeviceInfo();
const isActuallyMobile = isMobile || deviceIsMobile;
@@ -45,12 +45,10 @@ const keyboardEventToCombo = (event: React.KeyboardEvent<HTMLInputElement>): Sho
};
export const KeyboardShortcutsSettings: React.FC = () => {
const {
shortcutOverrides,
setShortcutOverride,
clearShortcutOverride,
resetAllShortcutOverrides,
} = useUIStore();
const shortcutOverrides = useUIStore((state) => state.shortcutOverrides);
const setShortcutOverride = useUIStore((state) => state.setShortcutOverride);
const clearShortcutOverride = useUIStore((state) => state.clearShortcutOverride);
const resetAllShortcutOverrides = useUIStore((state) => state.resetAllShortcutOverrides);
const actions = React.useMemo(() => getCustomizableShortcutActions(), []);
@@ -54,36 +54,34 @@ export const VoiceSettings: React.FC = () => {
language,
setLanguage,
} = useBrowserVoice();
const {
voiceProvider,
setVoiceProvider,
speechRate,
setSpeechRate,
speechPitch,
setSpeechPitch,
speechVolume,
setSpeechVolume,
sayVoice,
setSayVoice,
browserVoice,
setBrowserVoice,
openaiVoice,
setOpenaiVoice,
openaiApiKey,
setOpenaiApiKey,
showMessageTTSButtons,
setShowMessageTTSButtons,
voiceModeEnabled,
setVoiceModeEnabled,
summarizeMessageTTS,
setSummarizeMessageTTS,
summarizeVoiceConversation,
setSummarizeVoiceConversation,
summarizeCharacterThreshold,
setSummarizeCharacterThreshold,
summarizeMaxLength,
setSummarizeMaxLength,
} = useConfigStore();
const voiceProvider = useConfigStore((state) => state.voiceProvider);
const setVoiceProvider = useConfigStore((state) => state.setVoiceProvider);
const speechRate = useConfigStore((state) => state.speechRate);
const setSpeechRate = useConfigStore((state) => state.setSpeechRate);
const speechPitch = useConfigStore((state) => state.speechPitch);
const setSpeechPitch = useConfigStore((state) => state.setSpeechPitch);
const speechVolume = useConfigStore((state) => state.speechVolume);
const setSpeechVolume = useConfigStore((state) => state.setSpeechVolume);
const sayVoice = useConfigStore((state) => state.sayVoice);
const setSayVoice = useConfigStore((state) => state.setSayVoice);
const browserVoice = useConfigStore((state) => state.browserVoice);
const setBrowserVoice = useConfigStore((state) => state.setBrowserVoice);
const openaiVoice = useConfigStore((state) => state.openaiVoice);
const setOpenaiVoice = useConfigStore((state) => state.setOpenaiVoice);
const openaiApiKey = useConfigStore((state) => state.openaiApiKey);
const setOpenaiApiKey = useConfigStore((state) => state.setOpenaiApiKey);
const showMessageTTSButtons = useConfigStore((state) => state.showMessageTTSButtons);
const setShowMessageTTSButtons = useConfigStore((state) => state.setShowMessageTTSButtons);
const voiceModeEnabled = useConfigStore((state) => state.voiceModeEnabled);
const setVoiceModeEnabled = useConfigStore((state) => state.setVoiceModeEnabled);
const summarizeMessageTTS = useConfigStore((state) => state.summarizeMessageTTS);
const setSummarizeMessageTTS = useConfigStore((state) => state.setSummarizeMessageTTS);
const summarizeVoiceConversation = useConfigStore((state) => state.summarizeVoiceConversation);
const setSummarizeVoiceConversation = useConfigStore((state) => state.setSummarizeVoiceConversation);
const summarizeCharacterThreshold = useConfigStore((state) => state.summarizeCharacterThreshold);
const setSummarizeCharacterThreshold = useConfigStore((state) => state.setSummarizeCharacterThreshold);
const summarizeMaxLength = useConfigStore((state) => state.summarizeMaxLength);
const setSummarizeMaxLength = useConfigStore((state) => state.setSummarizeMaxLength);
const [isSayAvailable, setIsSayAvailable] = useState(false);
const [sayVoices, setSayVoices] = useState<Array<{ name: string; locale: string }>>([]);