diff --git a/package-lock.json b/package-lock.json index 98d865bc..93e0bf7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 29837798..0f90738a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 16fdb06b..237d6a26 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -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} diff --git a/src/renderer/components/FileExplorerPanel.tsx b/src/renderer/components/FileExplorerPanel.tsx index 5e52a6e7..b46a423b 100644 --- a/src/renderer/components/FileExplorerPanel.tsx +++ b/src/renderer/components/FileExplorerPanel.tsx @@ -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>; 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 ( -
0 ? "ml-3 border-l pl-2" : ""} style={{ borderColor: theme.colors.border }}> - + ); + }, [session.fullPath, session.changedFiles, session.fileExplorerExpanded, session.id, previewFile?.path, activeFocus, activeRightTab, selectedFileIndex, theme, toggleFolder, setSessions, setSelectedFileIndex, setActiveFocus, handleFileClick]); return (
@@ -316,10 +391,21 @@ export function FileExplorerPanel(props: FileExplorerPanelProps) {
+
- {/* File tree content */} + {/* File tree content - virtualized */} {session.fileTreeError ? (
@@ -339,8 +425,33 @@ export function FileExplorerPanel(props: FileExplorerPanelProps) { {(!session.fileTree || session.fileTree.length === 0) && (
Loading files...
)} - {filteredFileTree && renderTree(filteredFileTree)} - {fileTreeFilter && filteredFileTree && filteredFileTree.length === 0 && ( + {flattenedTree.length > 0 && ( +
+
+ {virtualizer.getVirtualItems().map((virtualRow) => { + const item = flattenedTree[virtualRow.index]; + return ( + + ); + })} +
+
+ )} + {fileTreeFilter && flattenedTree.length === 0 && (
No files match your search
)} diff --git a/src/renderer/components/RightPanel.tsx b/src/renderer/components/RightPanel.tsx index 71aaea10..f3a3f6ed 100644 --- a/src/renderer/components/RightPanel.tsx +++ b/src/renderer/components/RightPanel.tsx @@ -55,6 +55,8 @@ interface RightPanelProps { setSessions: React.Dispatch>; 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(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(function setSessions={setSessions} onAutoRefreshChange={onAutoRefreshChange} onShowFlash={onShowFlash} + showHiddenFiles={showHiddenFiles} + setShowHiddenFiles={setShowHiddenFiles} />
)} diff --git a/src/renderer/components/TerminalOutput.tsx b/src/renderer/components/TerminalOutput.tsx index bcab37f6..846d927b 100644 --- a/src/renderer/components/TerminalOutput.tsx +++ b/src/renderer/components/TerminalOutput.tsx @@ -279,9 +279,27 @@ const LogItemComponent = memo(({ return (
-
- {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 ( + <> +
{year}-{month}-{day}
+
{time}
+ + ); + })()}
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) => { 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,