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
@@ -43,8 +43,6 @@ type Props = {
collapsedProjects: Set<string>;
hideDirectoryControls: boolean;
projectRepoStatus: Map<string, boolean | null>;
hoveredProjectId: string | null;
setHoveredProjectId: (id: string | null) => void;
isDesktopShellRuntime: boolean;
stuckProjectHeaders: Set<string>;
mobileVariant: boolean;
@@ -144,7 +142,6 @@ export function SidebarProjectsList(props: Props): React.ReactNode {
const projectDescription = formatPathForDisplay(project.normalizedPath, props.homeDirectory);
const isCollapsed = props.collapsedProjects.has(projectKey);
const isActiveProject = projectKey === props.activeProjectId;
const isHovered = props.hoveredProjectId === projectKey;
const isRepo = props.projectRepoStatus.get(projectKey);
const orderedGroups = props.getOrderedGroups(projectKey, section.groups);
const rootGroup = orderedGroups.find((group) => group.isMain) ?? null;
@@ -164,14 +161,12 @@ export function SidebarProjectsList(props: Props): React.ReactNode {
projectIconBackground={project.iconBackground}
isCollapsed={isCollapsed}
isActiveProject={isActiveProject}
isHovered={isHovered}
isRepo={Boolean(isRepo)}
isDesktopShell={props.isDesktopShellRuntime}
isStuck={props.stuckProjectHeaders.has(projectKey)}
hideDirectoryControls={props.hideDirectoryControls}
mobileVariant={props.mobileVariant}
onToggle={() => props.toggleProject(projectKey)}
onHoverChange={(hovered) => props.setHoveredProjectId(hovered ? projectKey : null)}
onNewSession={() => {
if (projectKey !== props.activeProjectId) props.setActiveProjectIdOnly(projectKey);
props.setActiveMainTab('chat');