feat: add 'Open files in preview mode' setting (#988)

* feat: add 'Open files in preview mode' setting

- Add defaultFileViewerPreview setting to persist user preference for file viewer default mode
- Add checkbox in Settings → Sessions → Session Defaults to toggle the setting
- Files now open in preview mode by default when setting is enabled
- Respects per-file-type localStorage persistence for markdown/HTML/JSON files
- Setting persists across sessions via /api/config/settings endpoint

* fix: address PR review feedback

- FilesView: respect HTML localStorage preference in file-change effect,
  falling back to global setting only when nothing stored
- DefaultsSettings: read defaultFileViewerPreview directly from config store
  instead of redundant local state + separate fetch
- DefaultsSettings: extract duplicated toggle logic into
  handleToggleFileViewerPreview callback

* fix: honor default preview setting for markdown files

Previously mdViewMode only read localStorage on mount (deps: []), so
when settingsDefaultFileViewerPreview was enabled and no MD_VIEWER_MODE_KEY
was stored, markdown files silently opened in edit mode — ignoring the
setting for the very file type users most want to preview.

Fold md init into the per-file-change effect, mirroring the html handling:
localStorage preference wins, falling back to the setting-derived default.
The saveMdViewMode callback stays untouched so user-initiated toggles
still persist.

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
Dave Otero
2026-04-23 16:14:14 +03:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent bf110527a5
commit 4f60fce1e1
5 changed files with 67 additions and 25 deletions
@@ -30,6 +30,8 @@ export const DefaultsSettings: React.FC = () => {
const setSettingsDefaultModel = useConfigStore((state) => state.setSettingsDefaultModel);
const setSettingsDefaultVariant = useConfigStore((state) => state.setSettingsDefaultVariant);
const setSettingsDefaultAgent = useConfigStore((state) => state.setSettingsDefaultAgent);
const setSettingsDefaultFileViewerPreview = useConfigStore((state) => state.setSettingsDefaultFileViewerPreview);
const settingsDefaultFileViewerPreview = useConfigStore((state) => state.settingsDefaultFileViewerPreview);
const showDeletionDialog = useUIStore((state) => state.showDeletionDialog);
const setShowDeletionDialog = useUIStore((state) => state.setShowDeletionDialog);
const providers = useConfigStore((state) => state.providers);
@@ -187,6 +189,12 @@ export const DefaultsSettings: React.FC = () => {
[setAgent, setSettingsDefaultAgent]
);
const handleToggleFileViewerPreview = React.useCallback(() => {
const next = !settingsDefaultFileViewerPreview;
setSettingsDefaultFileViewerPreview(next);
updateDesktopSettings({ defaultFileViewerPreview: next }).catch(console.warn);
}, [settingsDefaultFileViewerPreview, setSettingsDefaultFileViewerPreview]);
const availableVariants = React.useMemo(() => {
if (!parsedModel.providerId || !parsedModel.modelId) return [];
const provider = providers.find((p) => p.id === parsedModel.providerId);
@@ -300,6 +308,23 @@ export const DefaultsSettings: React.FC = () => {
<span className="typography-ui-label text-foreground">Show Deletion Dialog</span>
</div>
<div
className="group flex cursor-pointer items-center gap-2 py-1"
role="button"
tabIndex={0}
aria-pressed={settingsDefaultFileViewerPreview}
onClick={handleToggleFileViewerPreview}
onKeyDown={(event) => {
if (event.key === ' ' || event.key === 'Enter') {
event.preventDefault();
handleToggleFileViewerPreview();
}
}}
>
<Checkbox checked={settingsDefaultFileViewerPreview} onChange={setSettingsDefaultFileViewerPreview} ariaLabel="Open files in preview mode" />
<span className="typography-ui-label text-foreground">Open files in preview mode</span>
</div>
</section>
</div>
);
+24 -25
View File
@@ -71,6 +71,7 @@ import { useThemeSystem } from '@/contexts/useThemeSystem';
import { useUIStore } from '@/stores/useUIStore';
import { useFilesViewTabsStore } from '@/stores/useFilesViewTabsStore';
import { useGitStatus } from '@/stores/useGitStore';
import { useConfigStore } from '@/stores/useConfigStore';
import { buildCodeMirrorCommentWidgets, normalizeLineRange, useInlineCommentController } from '@/components/comments';
import { opencodeClient } from '@/lib/opencode/client';
import { useDirectoryShowHidden } from '@/lib/directoryShowHidden';
@@ -729,6 +730,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
const pendingFileFocusPath = useUIStore((state) => state.pendingFileFocusPath);
const setPendingFileFocusPath = useUIStore((state) => state.setPendingFileFocusPath);
const shortcutOverrides = useUIStore((state) => state.shortcutOverrides);
const settingsDefaultFileViewerPreview = useConfigStore((state) => state.settingsDefaultFileViewerPreview);
// Global mouseup to end drag selection
React.useEffect(() => {
@@ -1864,25 +1866,37 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
}
}, [canEdit, textViewMode]);
React.useEffect(() => {
setTextViewMode('edit');
setHtmlViewMode('edit');
}, [selectedFile?.path]);
const MD_VIEWER_MODE_KEY = 'openchamber:files:md-viewer-mode';
const HTML_VIEWER_MODE_KEY = 'openchamber:files:html-viewer-mode';
React.useEffect(() => {
const defaultMode = settingsDefaultFileViewerPreview ? 'view' : 'edit';
setTextViewMode(defaultMode);
// Respect per-type localStorage preference when available,
// falling back to the setting-derived default when nothing is stored.
let mdDefault: 'preview' | 'edit' = settingsDefaultFileViewerPreview ? 'preview' : 'edit';
try {
const stored = localStorage.getItem(MD_VIEWER_MODE_KEY);
if (stored === 'preview') {
setMdViewMode('preview');
} else if (stored === 'edit') {
setMdViewMode('edit');
if (stored === 'preview' || stored === 'edit') {
mdDefault = stored;
}
} catch {
// Ignore localStorage errors
}
}, []);
setMdViewMode(mdDefault);
let htmlDefault: 'preview' | 'edit' = settingsDefaultFileViewerPreview ? 'preview' : 'edit';
try {
const stored = localStorage.getItem(HTML_VIEWER_MODE_KEY);
if (stored === 'preview' || stored === 'edit') {
htmlDefault = stored;
}
} catch {
// Ignore localStorage errors
}
setHtmlViewMode(htmlDefault);
}, [selectedFile?.path, settingsDefaultFileViewerPreview]);
const saveMdViewMode = React.useCallback((mode: 'preview' | 'edit') => {
setMdViewMode(mode);
@@ -1912,21 +1926,6 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
}
}, []);
const HTML_VIEWER_MODE_KEY = 'openchamber:files:html-viewer-mode';
React.useEffect(() => {
try {
const stored = localStorage.getItem(HTML_VIEWER_MODE_KEY);
if (stored === 'preview') {
setHtmlViewMode('preview');
} else if (stored === 'edit') {
setHtmlViewMode('edit');
}
} catch {
// Ignore localStorage errors
}
}, []);
const saveJsonViewMode = React.useCallback((mode: 'tree' | 'text') => {
setJsonViewMode(mode);
try {