diff --git a/packages/ui/src/components/views/FilesView.tsx b/packages/ui/src/components/views/FilesView.tsx index 979972d4..d7c51df9 100644 --- a/packages/ui/src/components/views/FilesView.tsx +++ b/packages/ui/src/components/views/FilesView.tsx @@ -38,6 +38,8 @@ import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; import { CodeMirrorEditor } from '@/components/ui/CodeMirrorEditor'; +import { PreviewToggleButton } from './PreviewToggleButton'; +import { SimpleMarkdownRenderer } from '@/components/chat/MarkdownRenderer'; import { languageByExtension } from '@/lib/codemirror/languageByExtension'; import { createFlexokiCodeMirrorTheme } from '@/lib/codemirror/flexokiTheme'; import { generateSyntaxTheme } from '@/lib/theme/syntaxThemeGenerator'; @@ -65,6 +67,7 @@ import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { opencodeClient } from '@/lib/opencode/client'; import { useDirectoryShowHidden } from '@/lib/directoryShowHidden'; import { useFilesViewShowGitignored } from '@/lib/filesViewShowGitignored'; +import { ErrorBoundary } from '@/components/ui/ErrorBoundary'; type FileNode = { name: string; @@ -230,7 +233,7 @@ const DOCUMENT_EXTENSIONS = new Set([ const getFileIcon = (extension?: string): React.ReactNode => { const ext = extension?.toLowerCase(); - + if (ext && CODE_EXTENSIONS.has(ext)) { return ; } @@ -246,6 +249,12 @@ const getFileIcon = (extension?: string): React.ReactNode => { return ; }; +const isMarkdownFile = (path: string): boolean => { + if (!path) return false; + const ext = path.toLowerCase().split('.').pop(); + return ext === 'md' || ext === 'markdown'; +}; + export const FilesView: React.FC = () => { const { files, runtime } = useRuntimeAPIs(); const { currentTheme } = useThemeSystem(); @@ -297,6 +306,9 @@ export const FilesView: React.FC = () => { const [copiedContent, setCopiedContent] = React.useState(false); const [copiedPath, setCopiedPath] = React.useState(false); + // Markdown view mode (global, not per-file) + const [mdViewMode, setMdViewMode] = React.useState<'preview' | 'edit'>('edit'); + const canCreateFile = Boolean(files.writeFile); const canCreateFolder = Boolean(files.createDirectory); const canRename = Boolean(files.rename); @@ -554,6 +566,38 @@ export const FilesView: React.FC = () => { void refreshRoot(); }, [currentDirectory, refreshRoot, showGitignored]); + const MD_VIEWER_MODE_KEY = 'openchamber:files:md-viewer-mode'; + + // Load markdown view mode preference from localStorage on mount + React.useEffect(() => { + try { + const stored = localStorage.getItem(MD_VIEWER_MODE_KEY); + if (stored) { + const parsed = JSON.parse(stored); + if (parsed === 'preview' || parsed === 'edit') { + setMdViewMode(parsed); + } + } + } catch { + // Ignore localStorage errors + } + }, []); + + // Save markdown view mode preference to localStorage + const saveMdViewMode = React.useCallback((mode: 'preview' | 'edit') => { + setMdViewMode(mode); + try { + localStorage.setItem(MD_VIEWER_MODE_KEY, JSON.stringify(mode)); + } catch { + // Ignore localStorage errors + } + }, []); + + // Get the view mode for a markdown file (from state, default to 'edit') + const getMdViewMode = React.useCallback((): 'preview' | 'edit' => { + return mdViewMode; + }, [mdViewMode]); + const handleDialogSubmit = React.useCallback(async (e?: React.FormEvent) => { e?.preventDefault(); if (!dialogData || !activeDialog) return; @@ -1408,10 +1452,17 @@ export const FilesView: React.FC = () => { )} - {(canCopy || canCopyPath) && (canEdit || (selectedFile && !isSelectedImage)) && ( + {(canCopy || canCopyPath || (selectedFile && isMarkdownFile(selectedFile.path))) && (canEdit || (selectedFile && !isSelectedImage)) && ( )} + {selectedFile && isMarkdownFile(selectedFile.path) && ( + saveMdViewMode(getMdViewMode() === 'preview' ? 'edit' : 'preview')} + /> + )} + {canCopy && ( { className="max-w-full max-h-[70vh] object-contain rounded-md border border-border/30 bg-primary/10" /> + ) : selectedFile && isMarkdownFile(selectedFile.path) && getMdViewMode() === 'preview' ? ( + + {fileContent.length > 500 * 1024 && ( + + ⚠️ This file is large ({Math.round(fileContent.length / 1024)}KB). Preview may be limited. + + )} + + Preview unavailable + + Switch to edit mode to fix the issue. + + + } + > + + + ) : ( void; +}; + +/** + * PreviewToggleButton - A toggle button for switching between preview and edit modes. + * + * Displays an eye icon when in preview mode (indicating the content is visible/read-only) + * and a slashed eye icon when in edit mode (indicating the content can be edited). + */ +export const PreviewToggleButton: React.FC = ({ + currentMode, + onToggle, +}) => { + const isPreview = currentMode === 'preview'; + const ariaLabel = isPreview ? 'Switch to edit mode' : 'Switch to preview mode'; + const tooltipText = isPreview ? 'Edit' : 'Preview'; + + return ( + + + + {isPreview ? ( + + ) : ( + + )} + + + + {tooltipText} + + + ); +};