mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
## 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:
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user