## CHANGES

- 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 🧭
This commit is contained in:
Pedram Amini
2025-12-27 15:53:28 -06:00
parent 3cc33c63e3
commit 48cc601fd4
6 changed files with 53 additions and 13 deletions

View File

@@ -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<HTMLDivElement>(null);
const fileTreeFilterInputRef = useRef<HTMLInputElement>(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<RightPanelHandle>(null);
const mainPanelRef = useRef<MainPanelHandle>(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)

View File

@@ -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) {
</div>
);
}
// PERFORMANCE: Export memoized version to prevent unnecessary re-renders
export const FileExplorerPanel = memo(FileExplorerPanelInner);

View File

@@ -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<RightPanelHandle, RightPanelProps>(function RightPanel(props, ref) {
// PERFORMANCE: Wrap with memo to prevent re-renders when props haven't changed
export const RightPanel = memo(forwardRef<RightPanelHandle, RightPanelProps>(function RightPanel(props, ref) {
const {
session, theme, shortcuts, rightPanelOpen, setRightPanelOpen, rightPanelWidth,
setRightPanelWidthState, activeRightTab, setActiveRightTab, activeFocus, setActiveFocus,
@@ -560,4 +561,4 @@ export const RightPanel = forwardRef<RightPanelHandle, RightPanelProps>(function
)}
</div>
);
});
}));

View File

@@ -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<string, Map<string, 'idle' | 'working'>>;
}
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) {
</div>
);
}
// PERFORMANCE: Export memoized version to prevent unnecessary re-renders
export const SessionList = memo(SessionListInner);

View File

@@ -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({
</div>
);
}
// PERFORMANCE: Export memoized version to prevent unnecessary re-renders
export const TabBar = memo(TabBarInner);

View File

@@ -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;