OAuth enabled but no valid token found. Starting authentication...

Found expired OAuth token, attempting refresh...
Token refresh successful
## CHANGES

- Upgraded to version 0.8.1 with performance boost! 🚀
- Added @tanstack/react-virtual for blazing fast file trees 🌳
- Implemented virtualized rendering for massive file lists 
- Added show/hide dotfiles toggle with eye icon 👁️
- Enhanced date display showing full dates for older logs 📅
- Optimized file tree with flattened structure for speed 🏎️
- Added depth-based indent guides for better hierarchy 📏
- Improved memory usage with virtualized row rendering 💾
- Added showHiddenFiles setting that persists between sessions 💾
- Fixed performance issues when browsing large directories 🎯
This commit is contained in:
Pedram Amini
2025-12-13 14:37:30 -06:00
parent 755a14f88e
commit 2dd52a9f7b
7 changed files with 246 additions and 66 deletions

32
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "maestro",
"version": "0.8.0",
"version": "0.8.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "maestro",
"version": "0.8.0",
"version": "0.8.1",
"hasInstallScript": true,
"license": "AGPL 3.0",
"dependencies": {
@@ -16,6 +16,7 @@
"@fastify/rate-limit": "^9.1.0",
"@fastify/static": "^7.0.4",
"@fastify/websocket": "^9.0.0",
"@tanstack/react-virtual": "^3.13.13",
"@types/dompurify": "^3.0.5",
"adm-zip": "^0.5.16",
"ansi-to-html": "^0.7.2",
@@ -2294,6 +2295,33 @@
"node": ">=10"
}
},
"node_modules/@tanstack/react-virtual": {
"version": "3.13.13",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.13.tgz",
"integrity": "sha512-4o6oPMDvQv+9gMi8rE6gWmsOjtUZUYIJHv7EB+GblyYdi8U6OqLl8rhHWIUZSL1dUU2dPwTdTgybCKf9EjIrQg==",
"license": "MIT",
"dependencies": {
"@tanstack/virtual-core": "3.13.13"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.13.13",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.13.tgz",
"integrity": "sha512-uQFoSdKKf5S8k51W5t7b2qpfkyIbdHMzAn+AMQvHPxKUPeo1SsGaA4JRISQT87jm28b7z8OEqPcg1IOZagQHcA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@testing-library/dom": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",

View File

@@ -131,6 +131,7 @@
"@fastify/rate-limit": "^9.1.0",
"@fastify/static": "^7.0.4",
"@fastify/websocket": "^9.0.0",
"@tanstack/react-virtual": "^3.13.13",
"@types/dompurify": "^3.0.5",
"adm-zip": "^0.5.16",
"ansi-to-html": "^0.7.2",

View File

@@ -165,6 +165,7 @@ export default function MaestroConsole() {
leftSidebarWidth, setLeftSidebarWidth,
rightPanelWidth, setRightPanelWidth,
markdownEditMode, setMarkdownEditMode,
showHiddenFiles, setShowHiddenFiles,
terminalWidth, setTerminalWidth,
logLevel, setLogLevel,
logViewerSelectedLevels, setLogViewerSelectedLevels,
@@ -7368,6 +7369,8 @@ export default function MaestroConsole() {
setSessions={setSessions}
onAutoRefreshChange={handleAutoRefreshChange}
onShowFlash={showSuccessFlash}
showHiddenFiles={showHiddenFiles}
setShowHiddenFiles={setShowHiddenFiles}
autoRunDocumentList={autoRunDocumentList}
autoRunDocumentTree={autoRunDocumentTree}
autoRunContent={autoRunContent}

View File

@@ -1,7 +1,8 @@
import React, { useEffect, useRef, useState, useCallback } from 'react';
import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react';
import { createPortal } from 'react-dom';
import { ChevronRight, ChevronDown, ChevronUp, Folder, RefreshCw, Check } from 'lucide-react';
import type { Session, Theme, FileChangeType } from '../types';
import { useVirtualizer } from '@tanstack/react-virtual';
import { ChevronRight, ChevronDown, ChevronUp, Folder, RefreshCw, Check, Eye, EyeOff } from 'lucide-react';
import type { Session, Theme } from '../types';
import type { FileTreeChanges } from '../utils/fileExplorer';
import { getFileIcon } from '../utils/theme';
import { useLayerStack } from '../contexts/LayerStackContext';
@@ -21,6 +22,14 @@ interface FileNode {
children?: FileNode[];
}
// Flattened node for virtualization
interface FlattenedNode {
node: FileNode;
path: string;
depth: number;
globalIndex: number;
}
interface FileExplorerPanelProps {
session: Session;
theme: Theme;
@@ -46,6 +55,8 @@ interface FileExplorerPanelProps {
setSessions: React.Dispatch<React.SetStateAction<Session[]>>;
onAutoRefreshChange?: (interval: number) => void;
onShowFlash?: (message: string) => void;
showHiddenFiles: boolean;
setShowHiddenFiles: (value: boolean) => void;
}
export function FileExplorerPanel(props: FileExplorerPanelProps) {
@@ -53,7 +64,8 @@ export function FileExplorerPanel(props: FileExplorerPanelProps) {
session, theme, fileTreeFilter, setFileTreeFilter, fileTreeFilterOpen, setFileTreeFilterOpen,
filteredFileTree, selectedFileIndex, setSelectedFileIndex, activeFocus, activeRightTab,
previewFile, setActiveFocus, fileTreeContainerRef, fileTreeFilterInputRef, toggleFolder, handleFileClick, expandAllFolders,
collapseAllFolders, updateSessionWorkingDirectory, refreshFileTree, setSessions, onAutoRefreshChange, onShowFlash
collapseAllFolders, updateSessionWorkingDirectory, refreshFileTree, setSessions, onAutoRefreshChange, onShowFlash,
showHiddenFiles, setShowHiddenFiles
} = props;
const { registerLayer, unregisterLayer, updateLayerHandler } = useLayerStack();
@@ -193,65 +205,128 @@ export function FileExplorerPanel(props: FileExplorerPanelProps) {
}
}, [fileTreeFilterOpen, setFileTreeFilterOpen, setFileTreeFilter, updateLayerHandler]);
const renderTree = (nodes: FileNode[], currentPath = '', depth = 0, globalIndex = { value: 0 }) => {
const expandedSet = new Set(session.fileExplorerExpanded || []);
return nodes.map((node, idx) => {
const fullPath = currentPath ? `${currentPath}/${node.name}` : node.name;
const absolutePath = `${session.fullPath}/${fullPath}`;
const change = session.changedFiles?.find(f => f.path.includes(node.name));
const isFolder = node.type === 'folder';
const isExpanded = expandedSet.has(fullPath);
const isSelected = previewFile?.path === absolutePath;
const currentIndex = globalIndex.value;
const isKeyboardSelected = activeFocus === 'right' && activeRightTab === 'files' && currentIndex === selectedFileIndex;
globalIndex.value++;
// Filter hidden files from the tree based on showHiddenFiles setting
const filterHiddenFiles = useCallback((nodes: FileNode[]): FileNode[] => {
if (showHiddenFiles) return nodes;
return nodes
.filter(node => !node.name.startsWith('.'))
.map(node => ({
...node,
children: node.children ? filterHiddenFiles(node.children) : undefined
}));
}, [showHiddenFiles]);
return (
<div key={idx} className={depth > 0 ? "ml-3 border-l pl-2" : ""} style={{ borderColor: theme.colors.border }}>
<div
data-file-index={currentIndex}
className={`flex items-center gap-2 py-1 text-xs cursor-pointer hover:bg-white/5 px-2 rounded transition-colors border-l-2 select-none min-w-0 ${isSelected ? 'bg-white/10' : ''}`}
// Apply hidden file filtering to the already-filtered tree
const displayTree = useMemo(() => {
return filterHiddenFiles(filteredFileTree);
}, [filteredFileTree, filterHiddenFiles]);
// Flatten tree for virtualization - only includes visible nodes (respects expanded state)
const flattenedTree = useMemo(() => {
const expandedSet = new Set(session.fileExplorerExpanded || []);
const result: FlattenedNode[] = [];
let globalIndex = 0;
const flatten = (nodes: FileNode[], currentPath = '', depth = 0) => {
for (const node of nodes) {
const fullPath = currentPath ? `${currentPath}/${node.name}` : node.name;
result.push({ node, path: fullPath, depth, globalIndex });
globalIndex++;
// Only include children if folder is expanded
if (node.type === 'folder' && expandedSet.has(fullPath) && node.children) {
flatten(node.children, fullPath, depth + 1);
}
}
};
flatten(displayTree);
return result;
}, [displayTree, session.fileExplorerExpanded]);
// Virtualization setup
const parentRef = useRef<HTMLDivElement>(null);
const ROW_HEIGHT = 28; // Height of each tree row in pixels
const virtualizer = useVirtualizer({
count: flattenedTree.length,
getScrollElement: () => parentRef.current,
estimateSize: () => ROW_HEIGHT,
overscan: 10, // Render 10 extra items above/below viewport for smooth scrolling
});
// Memoized row renderer
const TreeRow = useCallback(({ item, virtualRow }: { item: FlattenedNode; virtualRow: { index: number; start: number; size: number } }) => {
const { node, path: fullPath, depth, globalIndex } = item;
const absolutePath = `${session.fullPath}/${fullPath}`;
const change = session.changedFiles?.find(f => f.path.includes(node.name));
const isFolder = node.type === 'folder';
const expandedSet = new Set(session.fileExplorerExpanded || []);
const isExpanded = expandedSet.has(fullPath);
const isSelected = previewFile?.path === absolutePath;
const isKeyboardSelected = activeFocus === 'right' && activeRightTab === 'files' && globalIndex === selectedFileIndex;
// Generate indent guides for each depth level
const indentGuides = [];
for (let i = 0; i < depth; i++) {
indentGuides.push(
<div
key={i}
className="absolute top-0 bottom-0 w-px"
style={{
left: `${12 + i * 16}px`,
backgroundColor: theme.colors.border,
}}
/>
);
}
return (
<div
data-file-index={globalIndex}
className={`absolute top-0 left-0 w-full flex items-center gap-2 py-1 text-xs cursor-pointer hover:bg-white/5 px-2 rounded transition-colors border-l-2 select-none min-w-0 ${isSelected ? 'bg-white/10' : ''}`}
style={{
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`,
paddingLeft: `${8 + depth * 16}px`,
color: change ? theme.colors.textMain : theme.colors.textDim,
borderLeftColor: isKeyboardSelected ? theme.colors.accent : 'transparent',
backgroundColor: isKeyboardSelected ? theme.colors.bgActivity : (isSelected ? 'rgba(255,255,255,0.1)' : 'transparent')
}}
onClick={() => {
if (isFolder) {
toggleFolder(fullPath, session.id, setSessions);
} else {
setSelectedFileIndex(globalIndex);
setActiveFocus('right');
}
}}
onDoubleClick={() => {
if (!isFolder) {
handleFileClick(node, fullPath, session);
}
}}
>
{indentGuides}
{isFolder && (
isExpanded ? <ChevronDown className="w-3 h-3 flex-shrink-0" /> : <ChevronRight className="w-3 h-3 flex-shrink-0" />
)}
<span className="flex-shrink-0">{isFolder ? <Folder className="w-3.5 h-3.5" style={{ color: theme.colors.accent }} /> : getFileIcon(change?.type, theme)}</span>
<span className={`truncate min-w-0 flex-1 ${change ? 'font-medium' : ''}`} title={node.name}>{node.name}</span>
{change && (
<span
className="flex-shrink-0 text-[9px] px-1 rounded uppercase"
style={{
color: change ? theme.colors.textMain : theme.colors.textDim,
borderLeftColor: isKeyboardSelected ? theme.colors.accent : 'transparent',
backgroundColor: isKeyboardSelected ? theme.colors.bgActivity : (isSelected ? 'rgba(255,255,255,0.1)' : 'transparent')
}}
onClick={() => {
if (isFolder) {
toggleFolder(fullPath, session.id, setSessions);
} else {
setSelectedFileIndex(currentIndex);
setActiveFocus('right');
}
}}
onDoubleClick={() => {
if (!isFolder) {
handleFileClick(node, fullPath, session);
}
backgroundColor: change.type === 'added' ? theme.colors.success + '20' : change.type === 'deleted' ? theme.colors.error + '20' : theme.colors.warning + '20',
color: change.type === 'added' ? theme.colors.success : change.type === 'deleted' ? theme.colors.error : theme.colors.warning
}}
>
{isFolder && (
isExpanded ? <ChevronDown className="w-3 h-3 flex-shrink-0" /> : <ChevronRight className="w-3 h-3 flex-shrink-0" />
)}
<span className="flex-shrink-0">{isFolder ? <Folder className="w-3.5 h-3.5" style={{ color: theme.colors.accent }} /> : getFileIcon(change?.type, theme)}</span>
<span className={`truncate min-w-0 flex-1 ${change ? 'font-medium' : ''}`} title={node.name}>{node.name}</span>
{change && (
<span
className="flex-shrink-0 text-[9px] px-1 rounded uppercase"
style={{
backgroundColor: change.type === 'added' ? theme.colors.success + '20' : change.type === 'deleted' ? theme.colors.error + '20' : theme.colors.warning + '20',
color: change.type === 'added' ? theme.colors.success : change.type === 'deleted' ? theme.colors.error : theme.colors.warning
}}
>
{change.type}
</span>
)}
</div>
{isFolder && isExpanded && node.children && renderTree(node.children, fullPath, depth + 1, globalIndex)}
</div>
);
});
};
{change.type}
</span>
)}
</div>
);
}, [session.fullPath, session.changedFiles, session.fileExplorerExpanded, session.id, previewFile?.path, activeFocus, activeRightTab, selectedFileIndex, theme, toggleFolder, setSessions, setSelectedFileIndex, setActiveFocus, handleFileClick]);
return (
<div className="space-y-2 relative">
@@ -316,10 +391,21 @@ export function FileExplorerPanel(props: FileExplorerPanelProps) {
<ChevronUp className="w-3.5 h-3.5" />
</div>
</button>
<button
onClick={() => setShowHiddenFiles(!showHiddenFiles)}
className="p-1 rounded hover:bg-white/10 transition-colors"
title={showHiddenFiles ? "Hide dotfiles" : "Show dotfiles"}
style={{
color: showHiddenFiles ? theme.colors.accent : theme.colors.textDim,
backgroundColor: showHiddenFiles ? `${theme.colors.accent}20` : 'transparent'
}}
>
{showHiddenFiles ? <Eye className="w-3.5 h-3.5" /> : <EyeOff className="w-3.5 h-3.5" />}
</button>
</div>
</div>
{/* File tree content */}
{/* File tree content - virtualized */}
{session.fileTreeError ? (
<div className="flex flex-col items-center justify-center gap-3 py-8">
<div className="text-xs text-center" style={{ color: theme.colors.error }}>
@@ -339,8 +425,33 @@ export function FileExplorerPanel(props: FileExplorerPanelProps) {
{(!session.fileTree || session.fileTree.length === 0) && (
<div className="text-xs opacity-50 italic">Loading files...</div>
)}
{filteredFileTree && renderTree(filteredFileTree)}
{fileTreeFilter && filteredFileTree && filteredFileTree.length === 0 && (
{flattenedTree.length > 0 && (
<div
ref={parentRef}
className="flex-1 overflow-auto"
style={{ height: 'calc(100vh - 200px)' }}
>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualRow) => {
const item = flattenedTree[virtualRow.index];
return (
<TreeRow
key={item.path}
item={item}
virtualRow={virtualRow}
/>
);
})}
</div>
</div>
)}
{fileTreeFilter && flattenedTree.length === 0 && (
<div className="text-xs opacity-50 italic text-center py-4">No files match your search</div>
)}
</>

View File

@@ -55,6 +55,8 @@ interface RightPanelProps {
setSessions: React.Dispatch<React.SetStateAction<Session[]>>;
onAutoRefreshChange?: (interval: number) => void;
onShowFlash?: (message: string) => void;
showHiddenFiles: boolean;
setShowHiddenFiles: (value: boolean) => void;
// Auto Run handlers
autoRunDocumentList: string[]; // List of document filenames (without .md)
@@ -94,6 +96,7 @@ export const RightPanel = forwardRef<RightPanelHandle, RightPanelProps>(function
filteredFileTree, selectedFileIndex, setSelectedFileIndex, previewFile, fileTreeContainerRef,
fileTreeFilterInputRef, toggleFolder, handleFileClick, expandAllFolders, collapseAllFolders,
updateSessionWorkingDirectory, refreshFileTree, setSessions, onAutoRefreshChange, onShowFlash,
showHiddenFiles, setShowHiddenFiles,
autoRunDocumentList, autoRunDocumentTree, autoRunContent, autoRunContentVersion, autoRunIsLoadingDocuments,
onAutoRunContentChange, onAutoRunModeChange, onAutoRunStateChange,
onAutoRunSelectDocument, onAutoRunCreateDocument, onAutoRunRefresh, onAutoRunOpenSetup,
@@ -286,6 +289,8 @@ export const RightPanel = forwardRef<RightPanelHandle, RightPanelProps>(function
setSessions={setSessions}
onAutoRefreshChange={onAutoRefreshChange}
onShowFlash={onShowFlash}
showHiddenFiles={showHiddenFiles}
setShowHiddenFiles={setShowHiddenFiles}
/>
</div>
)}

View File

@@ -279,9 +279,27 @@ const LogItemComponent = memo(({
return (
<div ref={logItemRef} className={`flex gap-4 group ${isUserMessage ? 'flex-row-reverse' : ''} px-6 py-2`} data-log-index={index}>
<div className={`w-12 shrink-0 text-[10px] pt-2 ${isUserMessage ? 'text-right' : 'text-left'}`}
<div className={`w-16 shrink-0 text-[10px] pt-2 ${isUserMessage ? 'text-right' : 'text-left'}`}
style={{ fontFamily, color: theme.colors.textDim, opacity: 0.6 }}>
{new Date(log.timestamp).toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})}
{(() => {
const logDate = new Date(log.timestamp);
const today = new Date();
const isToday = logDate.toDateString() === today.toDateString();
const time = logDate.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'});
if (isToday) {
return time;
}
// Format: YYYY-MM-DD on first line, time on second
const year = logDate.getFullYear();
const month = String(logDate.getMonth() + 1).padStart(2, '0');
const day = String(logDate.getDate()).padStart(2, '0');
return (
<>
<div>{year}-{month}-{day}</div>
<div>{time}</div>
</>
);
})()}
</div>
<div className={`flex-1 min-w-0 p-4 pb-10 ${isUserMessage && log.readOnly ? 'pt-8' : ''} rounded-xl border ${isUserMessage ? 'rounded-tr-none' : 'rounded-tl-none'} relative overflow-hidden`}
style={{

View File

@@ -126,6 +126,8 @@ export interface UseSettingsReturn {
setLeftSidebarWidth: (value: number) => void;
setRightPanelWidth: (value: number) => void;
setMarkdownEditMode: (value: boolean) => void;
showHiddenFiles: boolean;
setShowHiddenFiles: (value: boolean) => void;
// Terminal settings
terminalWidth: number;
@@ -246,6 +248,7 @@ export function useSettings(): UseSettingsReturn {
const [leftSidebarWidth, setLeftSidebarWidthState] = useState(256);
const [rightPanelWidth, setRightPanelWidthState] = useState(384);
const [markdownEditMode, setMarkdownEditModeState] = useState(false);
const [showHiddenFiles, setShowHiddenFilesState] = useState(true); // Default: show hidden files
// Terminal Config
const [terminalWidth, setTerminalWidthState] = useState(100);
@@ -378,6 +381,11 @@ export function useSettings(): UseSettingsReturn {
window.maestro.settings.set('markdownEditMode', value);
}, []);
const setShowHiddenFiles = useCallback((value: boolean) => {
setShowHiddenFilesState(value);
window.maestro.settings.set('showHiddenFiles', value);
}, []);
const setShortcuts = useCallback((value: Record<string, Shortcut>) => {
setShortcutsState(value);
window.maestro.settings.set('shortcuts', value);
@@ -833,6 +841,7 @@ export function useSettings(): UseSettingsReturn {
const savedLeftSidebarWidth = await window.maestro.settings.get('leftSidebarWidth');
const savedRightPanelWidth = await window.maestro.settings.get('rightPanelWidth');
const savedMarkdownEditMode = await window.maestro.settings.get('markdownEditMode');
const savedShowHiddenFiles = await window.maestro.settings.get('showHiddenFiles');
const savedShortcuts = await window.maestro.settings.get('shortcuts');
const savedActiveThemeId = await window.maestro.settings.get('activeThemeId');
const savedTerminalWidth = await window.maestro.settings.get('terminalWidth');
@@ -871,6 +880,7 @@ export function useSettings(): UseSettingsReturn {
if (savedLeftSidebarWidth !== undefined) setLeftSidebarWidthState(Math.max(256, Math.min(600, savedLeftSidebarWidth)));
if (savedRightPanelWidth !== undefined) setRightPanelWidthState(savedRightPanelWidth);
if (savedMarkdownEditMode !== undefined) setMarkdownEditModeState(savedMarkdownEditMode);
if (savedShowHiddenFiles !== undefined) setShowHiddenFilesState(savedShowHiddenFiles);
if (savedActiveThemeId !== undefined) setActiveThemeIdState(savedActiveThemeId);
if (savedTerminalWidth !== undefined) setTerminalWidthState(savedTerminalWidth);
if (savedLogLevel !== undefined) setLogLevelState(savedLogLevel);
@@ -1027,6 +1037,8 @@ export function useSettings(): UseSettingsReturn {
setLeftSidebarWidth,
setRightPanelWidth,
setMarkdownEditMode,
showHiddenFiles,
setShowHiddenFiles,
terminalWidth,
setTerminalWidth,
logLevel,
@@ -1100,6 +1112,7 @@ export function useSettings(): UseSettingsReturn {
leftSidebarWidth,
rightPanelWidth,
markdownEditMode,
showHiddenFiles,
terminalWidth,
logLevel,
maxLogBuffer,
@@ -1136,6 +1149,7 @@ export function useSettings(): UseSettingsReturn {
setLeftSidebarWidth,
setRightPanelWidth,
setMarkdownEditMode,
setShowHiddenFiles,
setTerminalWidth,
setLogLevel,
setMaxLogBuffer,