mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
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:
32
package-lock.json
generated
32
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user