From 5a4c291dc0564cb19930d136160f9d8ebc95e649 Mon Sep 17 00:00:00 2001 From: Jovines <1246634075@qq.com> Date: Tue, 27 Jan 2026 15:15:00 +0800 Subject: [PATCH] feat(ui): add markdown preview toggle for FilesView (#224) --- .../ui/src/components/views/FilesView.tsx | 75 ++++++++++++++++++- .../components/views/PreviewToggleButton.tsx | 51 +++++++++++++ 2 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 packages/ui/src/components/views/PreviewToggleButton.tsx 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)) && (