From 48cc601fd4d4edb776d5eea88d41598b3bec4a92 Mon Sep 17 00:00:00 2001 From: Pedram Amini Date: Sat, 27 Dec 2025 15:53:28 -0600 Subject: [PATCH] ## CHANGES MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Improved session restore: AI tabs now default `saveToHistory` for older sessions ๐Ÿงท - Preserved tab preferences when reopening last tab: inherit history/thinking flags ๐Ÿง  - Stabilized input keydown handler via refs + `useCallback` wrapper ๐Ÿš€ - Stabilized paste handler to avoid rerenders during frequent clipboard events ๐Ÿ“‹ - Stabilized drag-and-drop handler for smoother image dropping interactions ๐Ÿช‚ - Memoized File Explorer panel to cut unnecessary parent-driven rerenders ๐Ÿ—‚๏ธ - Memoized Right Panel (with forwardRef) to keep UI snappy ๐Ÿงฑ - Memoized Session List to reduce sidebar churn on state updates ๐Ÿ“š - Memoized Tab Bar to prevent tab UI redrawing constantly ๐Ÿงญ --- src/renderer/App.tsx | 28 +++++++++++++++++-- src/renderer/components/FileExplorerPanel.tsx | 8 ++++-- src/renderer/components/RightPanel.tsx | 7 +++-- src/renderer/components/SessionList.tsx | 8 ++++-- src/renderer/components/TabBar.tsx | 9 ++++-- src/renderer/utils/tabHelpers.ts | 6 +++- 6 files changed, 53 insertions(+), 13 deletions(-) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 9eec5552..1edae1c0 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -685,10 +685,13 @@ function MaestroConsoleInner() { } // Reset all tab states to idle - processes don't survive app restart + // Also ensure saveToHistory defaults to true for old sessions that predate this feature const resetAiTabs = correctedSession.aiTabs.map(tab => ({ ...tab, state: 'idle' as const, thinkingStartTime: undefined, + // Default saveToHistory to true for tabs from old sessions (backwards compatibility) + saveToHistory: tab.saveToHistory ?? true, })); // Session restored - no superfluous messages added to AI Terminal or Command Terminal @@ -2326,6 +2329,10 @@ function MaestroConsoleInner() { const fileTreeContainerRef = useRef(null); const fileTreeFilterInputRef = useRef(null); const fileTreeKeyboardNavRef = useRef(false); // Track if selection change came from keyboard + // PERFORMANCE: Refs for input handlers to avoid recreating functions on every render + const handleInputKeyDownRef = useRef<((e: React.KeyboardEvent) => void) | null>(null); + const handlePasteRef = useRef<((e: React.ClipboardEvent) => void) | null>(null); + const handleDropRef = useRef<((e: React.DragEvent) => void) | null>(null); const rightPanelRef = useRef(null); const mainPanelRef = useRef(null); @@ -6836,7 +6843,8 @@ function MaestroConsoleInner() { } }; - const handleInputKeyDown = (e: React.KeyboardEvent) => { + // PERFORMANCE: Assign to ref during render, create stable wrapper below + handleInputKeyDownRef.current = (e: React.KeyboardEvent) => { // Cmd+F opens output search from input field - handle first, before any modal logic if (e.key === 'f' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); @@ -7020,6 +7028,10 @@ function MaestroConsoleInner() { // We just need to prevent default here } }; + // PERFORMANCE: Stable callback wrapper - prevents InputArea re-render on every keystroke + const handleInputKeyDown = useCallback((e: React.KeyboardEvent) => { + handleInputKeyDownRef.current?.(e); + }, []); // Image Handlers const showImageAttachBlockedNotice = useCallback(() => { @@ -7028,7 +7040,8 @@ function MaestroConsoleInner() { setTimeout(() => setSuccessFlashNotification(null), 4000); }, [setSuccessFlashNotification]); - const handlePaste = (e: React.ClipboardEvent) => { + // PERFORMANCE: Assign to ref during render, create stable wrapper below + handlePasteRef.current = (e: React.ClipboardEvent) => { // Allow image pasting in group chat or direct AI mode const isGroupChatActive = !!activeGroupChatId; const isDirectAIMode = activeSession && activeSession.inputMode === 'ai'; @@ -7079,8 +7092,13 @@ function MaestroConsoleInner() { } } }; + // PERFORMANCE: Stable callback wrapper + const handlePaste = useCallback((e: React.ClipboardEvent) => { + handlePasteRef.current?.(e); + }, []); - const handleDrop = (e: React.DragEvent) => { + // PERFORMANCE: Assign to ref during render, create stable wrapper below + handleDropRef.current = (e: React.DragEvent) => { e.preventDefault(); dragCounterRef.current = 0; setIsDraggingImage(false); @@ -7130,6 +7148,10 @@ function MaestroConsoleInner() { } } }; + // PERFORMANCE: Stable callback wrapper + const handleDrop = useCallback((e: React.DragEvent) => { + handleDropRef.current?.(e); + }, []); // --- FILE TREE MANAGEMENT --- // Extracted hook for file tree operations (refresh, git state, filtering) diff --git a/src/renderer/components/FileExplorerPanel.tsx b/src/renderer/components/FileExplorerPanel.tsx index 807a4310..9681ac58 100644 --- a/src/renderer/components/FileExplorerPanel.tsx +++ b/src/renderer/components/FileExplorerPanel.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react'; +import React, { useEffect, useRef, useState, useCallback, useMemo, memo } from 'react'; import { createPortal } from 'react-dom'; import { useVirtualizer } from '@tanstack/react-virtual'; import { ChevronRight, ChevronDown, ChevronUp, Folder, RefreshCw, Check, Eye, EyeOff } from 'lucide-react'; @@ -54,7 +54,8 @@ interface FileExplorerPanelProps { setShowHiddenFiles: (value: boolean) => void; } -export function FileExplorerPanel(props: FileExplorerPanelProps) { +// PERFORMANCE: Memoize to prevent re-renders when parent state changes but props are the same +function FileExplorerPanelInner(props: FileExplorerPanelProps) { const { session, theme, fileTreeFilter, setFileTreeFilter, fileTreeFilterOpen, setFileTreeFilterOpen, filteredFileTree, selectedFileIndex, setSelectedFileIndex, activeFocus, activeRightTab, @@ -548,3 +549,6 @@ export function FileExplorerPanel(props: FileExplorerPanelProps) { ); } + +// PERFORMANCE: Export memoized version to prevent unnecessary re-renders +export const FileExplorerPanel = memo(FileExplorerPanelInner); diff --git a/src/renderer/components/RightPanel.tsx b/src/renderer/components/RightPanel.tsx index 7f7d0397..a71fc8a7 100644 --- a/src/renderer/components/RightPanel.tsx +++ b/src/renderer/components/RightPanel.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useEffect, useImperativeHandle, forwardRef, useState, useCallback } from 'react'; +import React, { useRef, useEffect, useImperativeHandle, forwardRef, useState, useCallback, memo } from 'react'; import { PanelRightClose, PanelRightOpen, Loader2, GitBranch } from 'lucide-react'; import type { Session, Theme, RightPanelTab, Shortcut, BatchRunState, FocusArea } from '../types'; import type { FileTreeChanges } from '../utils/fileExplorer'; @@ -99,7 +99,8 @@ interface RightPanelProps { onFileClick?: (path: string) => void; } -export const RightPanel = forwardRef(function RightPanel(props, ref) { +// PERFORMANCE: Wrap with memo to prevent re-renders when props haven't changed +export const RightPanel = memo(forwardRef(function RightPanel(props, ref) { const { session, theme, shortcuts, rightPanelOpen, setRightPanelOpen, rightPanelWidth, setRightPanelWidthState, activeRightTab, setActiveRightTab, activeFocus, setActiveFocus, @@ -560,4 +561,4 @@ export const RightPanel = forwardRef(function )} ); -}); +})); diff --git a/src/renderer/components/SessionList.tsx b/src/renderer/components/SessionList.tsx index 6b23101f..cb384d6f 100644 --- a/src/renderer/components/SessionList.tsx +++ b/src/renderer/components/SessionList.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef, memo } from 'react'; import { Wand2, Plus, Settings, ChevronRight, ChevronDown, ChevronUp, X, Keyboard, Radio, Copy, ExternalLink, PanelLeftClose, PanelLeftOpen, Folder, Info, GitBranch, Bot, Clock, @@ -751,7 +751,8 @@ interface SessionListProps { allGroupChatParticipantStates?: Map>; } -export function SessionList(props: SessionListProps) { +// PERFORMANCE: Memoize to prevent re-renders when parent state changes but props are the same +function SessionListInner(props: SessionListProps) { const { theme, sessions, groups, sortedSessions, activeSessionId, leftSidebarOpen, leftSidebarWidthState, activeFocus, selectedSidebarIndex, editingGroupId, @@ -2167,3 +2168,6 @@ export function SessionList(props: SessionListProps) { ); } + +// PERFORMANCE: Export memoized version to prevent unnecessary re-renders +export const SessionList = memo(SessionListInner); diff --git a/src/renderer/components/TabBar.tsx b/src/renderer/components/TabBar.tsx index 1b1a6c51..6e774d1e 100644 --- a/src/renderer/components/TabBar.tsx +++ b/src/renderer/components/TabBar.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef, useCallback, useEffect } from 'react'; +import React, { useState, useRef, useCallback, useEffect, memo } from 'react'; import { createPortal } from 'react-dom'; import { X, Plus, Star, Copy, Edit2, Mail, Pencil, Search, GitMerge, ArrowRightCircle, Minimize2 } from 'lucide-react'; import type { AITab, Theme } from '../types'; @@ -628,8 +628,10 @@ function Tab({ * TabBar component for displaying AI session tabs. * Shows tabs for each Claude Code conversation within a Maestro session. * Appears only in AI mode (hidden in terminal mode). + * + * PERFORMANCE: Memoized to prevent re-renders when parent state changes but props are the same. */ -export function TabBar({ +function TabBarInner({ tabs, activeTabId, theme, @@ -931,3 +933,6 @@ export function TabBar({ ); } + +// PERFORMANCE: Export memoized version to prevent unnecessary re-renders +export const TabBar = memo(TabBarInner); diff --git a/src/renderer/utils/tabHelpers.ts b/src/renderer/utils/tabHelpers.ts index c632213f..349454e0 100644 --- a/src/renderer/utils/tabHelpers.ts +++ b/src/renderer/utils/tabHelpers.ts @@ -229,7 +229,11 @@ export function closeTab(session: Session, tabId: string, showUnreadOnly = false inputValue: '', stagedImages: [], createdAt: Date.now(), - state: 'idle' + state: 'idle', + // Inherit saveToHistory and showThinking from the closed tab + // to preserve user's preferences when closing the last tab + saveToHistory: tabToClose.saveToHistory ?? true, + showThinking: tabToClose.showThinking ?? false }; updatedTabs = [freshTab]; newActiveTabId = freshTab.id;