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
+13 -1
View File
@@ -132,7 +132,19 @@ export function useBrowserVoice(): UseBrowserVoiceReturn {
const sendMessage = useSessionUIStore((s) => s.sendMessage);
const setPendingInputText = useInputStore((s) => s.setPendingInputText);
const createSession = useSessionUIStore((s) => s.createSession);
const { currentProviderId, currentModelId, currentAgentName, voiceModeEnabled, voiceProvider, speechRate, speechPitch, speechVolume, sayVoice, browserVoice, openaiVoice, summarizeVoiceConversation, summarizeCharacterThreshold } = useConfigStore();
const currentProviderId = useConfigStore((state) => state.currentProviderId);
const currentModelId = useConfigStore((state) => state.currentModelId);
const currentAgentName = useConfigStore((state) => state.currentAgentName);
const voiceModeEnabled = useConfigStore((state) => state.voiceModeEnabled);
const voiceProvider = useConfigStore((state) => state.voiceProvider);
const speechRate = useConfigStore((state) => state.speechRate);
const speechPitch = useConfigStore((state) => state.speechPitch);
const speechVolume = useConfigStore((state) => state.speechVolume);
const sayVoice = useConfigStore((state) => state.sayVoice);
const browserVoice = useConfigStore((state) => state.browserVoice);
const openaiVoice = useConfigStore((state) => state.openaiVoice);
const summarizeVoiceConversation = useConfigStore((state) => state.summarizeVoiceConversation);
const summarizeCharacterThreshold = useConfigStore((state) => state.summarizeCharacterThreshold);
const shouldCheckOpenAIAvailability = voiceModeEnabled && voiceProvider === 'openai';
const shouldCheckSayAvailability = voiceModeEnabled && voiceProvider === 'say';