From 13f062d093a5f0d4da35df679f2b3795d8ee9d0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eren=20=C3=87akar?= Date: Thu, 4 Jun 2026 23:02:06 +0300 Subject: [PATCH] feat: hide markdown front matter by default with per-file toggle (#4468) (#4636) * feat: hide markdown front matter by default with per-file toggle (#4468) Front matter in the markdown preview was always visible above the rendered body, which clutters reading for files where the metadata is incidental. Default the card to hidden and let users opt in per file via a small switch in the section header. * Add a per-file markdownFrontmatterVisible flag in EditorSlice, mirroring the editorViewMode pattern (absent entry = hidden; storing only explicit true values keeps the record minimal). * Clean the entry up on closeFile, preview-tab replacement, and worktree removal; reset on runtime switch. * Render the existing front-matter card unchanged when the toggle is on, hide only the
 body when it is off, and keep the
  section header + SettingsSwitch visible so the affordance stays
  discoverable.
* Add 4 regression cases in editor.test.ts mirroring the
  editorViewMode tests.

* fix: persist markdown front matter visibility

Co-authored-by: Orca 

---------

Co-authored-by: Jinwoo-H 
Co-authored-by: Orca 
---
 .../src/components/editor/EditorContent.tsx   |   6 +-
 .../src/components/editor/EditorPanel.tsx     |  31 +++++
 .../components/editor/EditorPanelHeader.tsx   |  57 +++-----
 .../editor/EditorPanelMarkdownActionsMenu.tsx |  75 +++++++++++
 .../components/editor/EditorPanelShell.tsx    |  10 ++
 .../src/components/editor/MarkdownPreview.tsx |  18 ++-
 .../workspace-session-browser-history.test.ts |   1 +
 .../workspace-session-editor-drafts.test.ts   |   1 +
 .../lib/workspace-session-liveness.test.ts    |   1 +
 .../src/lib/workspace-session-patch.test.ts   |  39 +++++-
 .../src/lib/workspace-session-patch.ts        |   2 +
 .../workspace-session-relevant-fields.test.ts |   1 +
 .../src/lib/workspace-session.test.ts         |  17 +++
 src/renderer/src/lib/workspace-session.ts     |  18 ++-
 src/renderer/src/store/slices/editor.test.ts  | 123 ++++++++++++++++++
 src/renderer/src/store/slices/editor.ts       |  96 ++++++++++++++
 .../src/store/slices/settings.test.ts         |   2 +
 src/renderer/src/store/slices/settings.ts     |   1 +
 .../slices/store-session-cascades.test.ts     |  39 +++++-
 .../src/store/slices/worktrees.test.ts        |  61 +++++++++
 src/renderer/src/store/slices/worktrees.ts    |  23 +++-
 src/shared/constants.ts                       |   1 +
 src/shared/types.ts                           |   2 +
 src/shared/workspace-session-schema.ts        |   1 +
 24 files changed, 572 insertions(+), 54 deletions(-)
 create mode 100644 src/renderer/src/components/editor/EditorPanelMarkdownActionsMenu.tsx

diff --git a/src/renderer/src/components/editor/EditorContent.tsx b/src/renderer/src/components/editor/EditorContent.tsx
index 45def20fc..bad4adb0b 100644
--- a/src/renderer/src/components/editor/EditorContent.tsx
+++ b/src/renderer/src/components/editor/EditorContent.tsx
@@ -108,6 +108,7 @@ export function EditorContent({
   isChangesMode,
   sideBySide,
   showMarkdownTableOfContents = false,
+  showMarkdownFrontmatter = false,
   onCloseMarkdownTableOfContents = noopCloseMarkdownTableOfContents,
   markdownAnnotationsEnabled = true,
   pendingEditorReveal,
@@ -134,6 +135,7 @@ export function EditorContent({
   isChangesMode: boolean
   sideBySide: boolean
   showMarkdownTableOfContents?: boolean
+  showMarkdownFrontmatter?: boolean
   onCloseMarkdownTableOfContents?: () => void
   markdownAnnotationsEnabled?: boolean
   pendingEditorReveal: PendingEditorReveal | null
@@ -397,7 +399,9 @@ export function EditorContent({
                 // (inside the editor shell) so formatting controls remain at
                 // the top of the pane — the banner is read-only context, not
                 // a header above the toolbar.
-                headerSlot={fm ?  : null}
+                headerSlot={
+                  fm && showMarkdownFrontmatter ?  : null
+                }
               />
             
           
diff --git a/src/renderer/src/components/editor/EditorPanel.tsx b/src/renderer/src/components/editor/EditorPanel.tsx
index 90ac0ba8c..0990cc3b7 100644
--- a/src/renderer/src/components/editor/EditorPanel.tsx
+++ b/src/renderer/src/components/editor/EditorPanel.tsx
@@ -18,6 +18,7 @@ import { useEditorCmdSaveRequest } from './useEditorCmdSaveRequest'
 import { useEditorPanelContentState } from './useEditorPanelContentState'
 import { useMarkdownPreviewShortcut } from './useMarkdownPreviewShortcut'
 import { useUntitledFileRename } from './useUntitledFileRename'
+import { extractFrontMatter } from './markdown-frontmatter'
 
 function EditorPanelInner({
   activeFileId: activeFileIdProp,
@@ -43,6 +44,8 @@ function EditorPanelInner({
   const setEditorViewMode = useAppStore((s) => s.setEditorViewMode)
   const openFile = useAppStore((s) => s.openFile)
   const openMarkdownPreview = useAppStore((s) => s.openMarkdownPreview)
+  const markdownFrontmatterVisible = useAppStore((s) => s.markdownFrontmatterVisible)
+  const setMarkdownFrontmatterVisible = useAppStore((s) => s.setMarkdownFrontmatterVisible)
   const closeFile = useAppStore((s) => s.closeFile)
   const clearUntitled = useAppStore((s) => s.clearUntitled)
   const editorDrafts = useAppStore((s) => s.editorDrafts)
@@ -304,6 +307,26 @@ function EditorPanelInner({
     )?.activeRuntimeEnvironmentId?.trim() ||
     (renameDialogFile ? getConnectionId(renameDialogFile.worktreeId) : null)
   )
+  const markdownFrontmatterSourceFileId =
+    activeFile.mode === 'markdown-preview'
+      ? (activeFile.markdownPreviewSourceFileId ?? activeFile.filePath)
+      : activeFile.id
+  let activeMarkdownContent: string | null = null
+  if (activeFile.mode === 'markdown-preview') {
+    activeMarkdownContent =
+      editorDrafts[markdownFrontmatterSourceFileId] ?? fileContents[activeFile.id]?.content ?? null
+  } else if (activeFile.mode === 'edit') {
+    activeMarkdownContent =
+      editorDrafts[activeFile.id] ?? fileContents[activeFile.id]?.content ?? null
+  }
+  const canShowMarkdownFrontmatterToggle = Boolean(
+    model.isMarkdown &&
+    (activeFile.mode === 'markdown-preview' || model.mdViewMode !== 'source') &&
+    activeMarkdownContent &&
+    extractFrontMatter(activeMarkdownContent)
+  )
+  const isMarkdownFrontmatterVisible =
+    markdownFrontmatterVisible[markdownFrontmatterSourceFileId] ?? false
 
   return (
      setSideBySide((prev) => !prev)}
       onEditorToggleChange={handleEditorToggleChange}
       onToggleMarkdownTableOfContents={() => setShowMarkdownTableOfContents((shown) => !shown)}
+      onToggleMarkdownFrontmatter={() =>
+        setMarkdownFrontmatterVisible(
+          markdownFrontmatterSourceFileId,
+          !isMarkdownFrontmatterVisible
+        )
+      }
       onExportMarkdownToPdf={() => void exportActiveMarkdownToPdf()}
       onContentChange={handleContentChange}
       onContentChangeForFile={handleContentChangeForFile}
diff --git a/src/renderer/src/components/editor/EditorPanelHeader.tsx b/src/renderer/src/components/editor/EditorPanelHeader.tsx
index 1df03df78..f0ce094c1 100644
--- a/src/renderer/src/components/editor/EditorPanelHeader.tsx
+++ b/src/renderer/src/components/editor/EditorPanelHeader.tsx
@@ -1,15 +1,5 @@
 import { useEffect, useMemo, useRef, useState } from 'react'
-import {
-  Columns2,
-  Copy,
-  Eye,
-  ExternalLink,
-  FileText,
-  ListTree,
-  MoreHorizontal,
-  Pencil,
-  Rows2
-} from 'lucide-react'
+import { Columns2, Copy, Eye, ExternalLink, FileText, ListTree, Pencil, Rows2 } from 'lucide-react'
 import { useAppStore } from '@/store'
 import type { MarkdownViewMode, OpenFile } from '@/store/slices/editor'
 import {
@@ -33,6 +23,7 @@ import type { EditorHeaderOpenFileState } from './editor-header'
 import { getEditorHeaderCopyState } from './editor-header'
 import { DiffNotesSendMenu } from './DiffNotesSendMenu'
 import { useEditorHeaderFileRename } from './editor-header-file-rename'
+import { EditorPanelMarkdownActionsMenu } from './EditorPanelMarkdownActionsMenu'
 
 const isMac = navigator.userAgent.includes('Mac')
 const isLinux = navigator.userAgent.includes('Linux')
@@ -62,6 +53,8 @@ type EditorPanelHeaderProps = {
   canShowMarkdownTableOfContents: boolean
   isMarkdownTableOfContentsDisabled: boolean
   showMarkdownTableOfContents: boolean
+  canShowMarkdownFrontmatterToggle: boolean
+  markdownFrontmatterVisible: boolean
   sideBySide: boolean
   openFileState: EditorHeaderOpenFileState
   onCopyPath: () => void
@@ -72,6 +65,7 @@ type EditorPanelHeaderProps = {
   onToggleSideBySide: () => void
   onEditorToggleChange: (next: EditorToggleValue) => void
   onToggleMarkdownTableOfContents: () => void
+  onToggleMarkdownFrontmatter: () => void
   onExportMarkdownToPdf: () => void
 }
 
@@ -93,6 +87,8 @@ export function EditorPanelHeader({
   canShowMarkdownTableOfContents,
   isMarkdownTableOfContentsDisabled,
   showMarkdownTableOfContents,
+  canShowMarkdownFrontmatterToggle,
+  markdownFrontmatterVisible,
   sideBySide,
   openFileState,
   onCopyPath,
@@ -103,6 +99,7 @@ export function EditorPanelHeader({
   onToggleSideBySide,
   onEditorToggleChange,
   onToggleMarkdownTableOfContents,
+  onToggleMarkdownFrontmatter,
   onExportMarkdownToPdf
 }: EditorPanelHeaderProps): React.JSX.Element {
   const [pathMenuOpen, setPathMenuOpen] = useState(false)
@@ -363,35 +360,15 @@ export function EditorPanelHeader({
           
         
       )}
-      {hasViewModeToggle && isMarkdown && (
-        
-          
-            
-          
-          
-            
-              Export as PDF
-            
-          
-        
-      )}
+      
     
   )
 }
diff --git a/src/renderer/src/components/editor/EditorPanelMarkdownActionsMenu.tsx b/src/renderer/src/components/editor/EditorPanelMarkdownActionsMenu.tsx
new file mode 100644
index 000000000..4818aca18
--- /dev/null
+++ b/src/renderer/src/components/editor/EditorPanelMarkdownActionsMenu.tsx
@@ -0,0 +1,75 @@
+import type React from 'react'
+import { MoreHorizontal } from 'lucide-react'
+import type { MarkdownViewMode } from '@/store/slices/editor'
+import {
+  DropdownMenu,
+  DropdownMenuContent,
+  DropdownMenuItem,
+  DropdownMenuSeparator,
+  DropdownMenuTrigger
+} from '@/components/ui/dropdown-menu'
+
+type EditorPanelMarkdownActionsMenuProps = {
+  isMarkdown: boolean
+  hasViewModeToggle: boolean
+  mdViewMode: MarkdownViewMode
+  canShowMarkdownFrontmatterToggle: boolean
+  markdownFrontmatterVisible: boolean
+  onToggleMarkdownFrontmatter: () => void
+  onExportMarkdownToPdf: () => void
+}
+
+export function EditorPanelMarkdownActionsMenu({
+  isMarkdown,
+  hasViewModeToggle,
+  mdViewMode,
+  canShowMarkdownFrontmatterToggle,
+  markdownFrontmatterVisible,
+  onToggleMarkdownFrontmatter,
+  onExportMarkdownToPdf
+}: EditorPanelMarkdownActionsMenuProps): React.JSX.Element | null {
+  if (!isMarkdown || (!hasViewModeToggle && !canShowMarkdownFrontmatterToggle)) {
+    return null
+  }
+
+  return (
+    
+      
+        
+      
+      
+        {canShowMarkdownFrontmatterToggle ? (
+          <>
+             {
+                event.preventDefault()
+                onToggleMarkdownFrontmatter()
+              }}
+            >
+              {markdownFrontmatterVisible ? 'Hide front matter' : 'Show front matter'}
+            
+            {hasViewModeToggle ?  : null}
+          
+        ) : null}
+        {hasViewModeToggle ? (
+          
+            Export as PDF
+          
+        ) : null}
+      
+    
+  )
+}
diff --git a/src/renderer/src/components/editor/EditorPanelShell.tsx b/src/renderer/src/components/editor/EditorPanelShell.tsx
index f1089dcb5..58516d6f9 100644
--- a/src/renderer/src/components/editor/EditorPanelShell.tsx
+++ b/src/renderer/src/components/editor/EditorPanelShell.tsx
@@ -19,6 +19,8 @@ type EditorPanelShellProps = {
   model: EditorPanelRenderModel
   copiedPathVisible: boolean
   showMarkdownTableOfContents: boolean
+  canShowMarkdownFrontmatterToggle: boolean
+  markdownFrontmatterVisible: boolean
   sideBySide: boolean
   openFiles: OpenFile[]
   fileContents: Record
@@ -36,6 +38,7 @@ type EditorPanelShellProps = {
   onToggleSideBySide: () => void
   onEditorToggleChange: (next: EditorToggleValue) => void
   onToggleMarkdownTableOfContents: () => void
+  onToggleMarkdownFrontmatter: () => void
   onExportMarkdownToPdf: () => void
   onContentChange: (content: string) => void
   onContentChangeForFile: (file: OpenFile, content: string) => void
@@ -56,6 +59,8 @@ export function EditorPanelShell({
   model,
   copiedPathVisible,
   showMarkdownTableOfContents,
+  canShowMarkdownFrontmatterToggle,
+  markdownFrontmatterVisible,
   sideBySide,
   openFiles,
   fileContents,
@@ -73,6 +78,7 @@ export function EditorPanelShell({
   onToggleSideBySide,
   onEditorToggleChange,
   onToggleMarkdownTableOfContents,
+  onToggleMarkdownFrontmatter,
   onExportMarkdownToPdf,
   onContentChange,
   onContentChangeForFile,
@@ -106,6 +112,8 @@ export function EditorPanelShell({
           canShowMarkdownTableOfContents={model.canShowMarkdownTableOfContents}
           isMarkdownTableOfContentsDisabled={model.isMarkdownTableOfContentsDisabled}
           showMarkdownTableOfContents={showMarkdownTableOfContents}
+          canShowMarkdownFrontmatterToggle={canShowMarkdownFrontmatterToggle}
+          markdownFrontmatterVisible={markdownFrontmatterVisible}
           sideBySide={sideBySide}
           openFileState={model.openFileState}
           onCopyPath={onCopyPath}
@@ -116,6 +124,7 @@ export function EditorPanelShell({
           onToggleSideBySide={onToggleSideBySide}
           onEditorToggleChange={onEditorToggleChange}
           onToggleMarkdownTableOfContents={onToggleMarkdownTableOfContents}
+          onToggleMarkdownFrontmatter={onToggleMarkdownFrontmatter}
           onExportMarkdownToPdf={onExportMarkdownToPdf}
         />
       )}
@@ -144,6 +153,7 @@ export function EditorPanelShell({
           handleSaveForFile={onSaveForFile}
           reloadFileContent={onReloadFileContent}
           showMarkdownTableOfContents={showMarkdownTableOfContents}
+          showMarkdownFrontmatter={markdownFrontmatterVisible}
           onCloseMarkdownTableOfContents={onCloseMarkdownTableOfContents}
           markdownAnnotationsEnabled={markdownAnnotationsEnabled}
         />
diff --git a/src/renderer/src/components/editor/MarkdownPreview.tsx b/src/renderer/src/components/editor/MarkdownPreview.tsx
index d2c9b24e2..caef7bc28 100644
--- a/src/renderer/src/components/editor/MarkdownPreview.tsx
+++ b/src/renderer/src/components/editor/MarkdownPreview.tsx
@@ -466,6 +466,7 @@ export default function MarkdownPreview({
   const activateMarkdownLink = useAppStore((s) => s.activateMarkdownLink)
   const openMarkdownPreview = useAppStore((s) => s.openMarkdownPreview)
   const setMarkdownViewMode = useAppStore((s) => s.setMarkdownViewMode)
+  const frontmatterVisibleByFile = useAppStore((s) => s.markdownFrontmatterVisible)
   const setPendingEditorReveal = useAppStore((s) => s.setPendingEditorReveal)
   const addDiffComment = useAppStore((s) => s.addDiffComment)
   const deleteDiffComment = useAppStore((s) => s.deleteDiffComment)
@@ -559,6 +560,13 @@ export default function MarkdownPreview({
       .replace(/\r?\n(?:---|\+\+\+)\r?\n?$/, '')
       .trim()
   }, [frontMatter])
+  // Why: front matter is hidden by default (#4468) and controlled from the
+  // markdown preview actions menu, keeping metadata out of the reading surface
+  // unless the user explicitly asks for it.
+  const toggleableSourceFileId: string | null = sourceFileId ?? null
+  const frontmatterVisible = toggleableSourceFileId
+    ? (frontmatterVisibleByFile[toggleableSourceFileId] ?? false)
+    : true
   const [activeAnnotationBlockKey, setActiveAnnotationBlockKey] = useState(null)
   const [reviewNotesCopied, setReviewNotesCopied] = useState(false)
   const [copiedReviewNoteId, setCopiedReviewNoteId] = useState(null)
@@ -1737,10 +1745,10 @@ export default function MarkdownPreview({
           
         ) : null}
         
- {/* Why: remarkFrontmatter silently strips front-matter from rendered - output. We extract it ourselves and render it as a styled code block so - the user can see the metadata in preview mode. */} - {frontMatter && ( + {/* Why: remarkFrontmatter strips front matter from normal markdown + output. When the user opts in from the preview actions menu, render the + raw metadata as a compact read-only block above the document body. */} + {frontMatter && frontmatterVisible ? (
Front Matter @@ -1749,7 +1757,7 @@ export default function MarkdownPreview({ {frontMatterInner}
- )} + ) : null} { const patch = buildWorkspaceSessionPatch(createSnapshot(), ['openFiles']) expect(Object.keys(patch).sort()).toEqual( - ['activeFileIdByWorktree', 'activeTabTypeByWorktree', 'openFilesByWorktree'].sort() + [ + 'activeFileIdByWorktree', + 'activeTabTypeByWorktree', + 'markdownFrontmatterVisible', + 'openFilesByWorktree' + ].sort() ) expect(patch.openFilesByWorktree).toEqual({ 'wt-1': [ @@ -125,7 +133,12 @@ describe('buildWorkspaceSessionPatch', () => { ) expect(Object.keys(patch).sort()).toEqual( - ['activeFileIdByWorktree', 'activeTabTypeByWorktree', 'openFilesByWorktree'].sort() + [ + 'activeFileIdByWorktree', + 'activeTabTypeByWorktree', + 'markdownFrontmatterVisible', + 'openFilesByWorktree' + ].sort() ) expect(patch.openFilesByWorktree?.['wt-1'][0]).toEqual( expect.objectContaining({ @@ -135,6 +148,28 @@ describe('buildWorkspaceSessionPatch', () => { ) }) + it('derives editor session keys when markdown front-matter visibility changes', () => { + const patch = buildWorkspaceSessionPatch( + createSnapshot({ + markdownFrontmatterVisible: { + '/tmp/demo.ts': true, + '/tmp/demo.diff': true + } + }), + ['markdownFrontmatterVisible'] + ) + + expect(Object.keys(patch).sort()).toEqual( + [ + 'activeFileIdByWorktree', + 'activeTabTypeByWorktree', + 'markdownFrontmatterVisible', + 'openFilesByWorktree' + ].sort() + ) + expect(patch.markdownFrontmatterVisible).toEqual({ '/tmp/demo.ts': true }) + }) + it('sanitizes terminal tabs and prunes local buffers when tab topology changes', () => { const localWorktreeId = 'repo-1::/local/worktree' const patch = buildWorkspaceSessionPatch( diff --git a/src/renderer/src/lib/workspace-session-patch.ts b/src/renderer/src/lib/workspace-session-patch.ts index 667a92d64..19f0edad7 100644 --- a/src/renderer/src/lib/workspace-session-patch.ts +++ b/src/renderer/src/lib/workspace-session-patch.ts @@ -79,6 +79,7 @@ export function buildWorkspaceSessionPatch( hasAnyChangedField(changed, [ 'openFiles', 'editorDrafts', + 'markdownFrontmatterVisible', 'activeFileIdByWorktree', 'activeTabTypeByWorktree' ] as const) @@ -88,6 +89,7 @@ export function buildWorkspaceSessionPatch( buildEditorSessionData( snapshot.openFiles, snapshot.editorDrafts, + snapshot.markdownFrontmatterVisible, snapshot.activeFileIdByWorktree, snapshot.activeTabTypeByWorktree ) diff --git a/src/renderer/src/lib/workspace-session-relevant-fields.test.ts b/src/renderer/src/lib/workspace-session-relevant-fields.test.ts index c15b09c91..df9794e4d 100644 --- a/src/renderer/src/lib/workspace-session-relevant-fields.test.ts +++ b/src/renderer/src/lib/workspace-session-relevant-fields.test.ts @@ -14,6 +14,7 @@ describe('SESSION_RELEVANT_FIELDS', () => { activeTabIdByWorktree: true, openFiles: true, editorDrafts: true, + markdownFrontmatterVisible: true, activeFileIdByWorktree: true, activeTabTypeByWorktree: true, browserTabsByWorktree: true, diff --git a/src/renderer/src/lib/workspace-session.test.ts b/src/renderer/src/lib/workspace-session.test.ts index 03710b498..50218f3d6 100644 --- a/src/renderer/src/lib/workspace-session.test.ts +++ b/src/renderer/src/lib/workspace-session.test.ts @@ -21,8 +21,10 @@ function createSnapshot(overrides: Partial = {}): AppState { }, activeTabIdByWorktree: { 'wt-1': 'tab-1', 'wt-2': 'tab-2' }, editorDrafts: {}, + markdownFrontmatterVisible: {}, openFiles: [ { + id: '/tmp/demo.ts', filePath: '/tmp/demo.ts', relativePath: 'demo.ts', worktreeId: 'wt-1', @@ -34,6 +36,7 @@ function createSnapshot(overrides: Partial = {}): AppState { originalContent: '' }, { + id: '/tmp/demo.diff', filePath: '/tmp/demo.diff', relativePath: 'demo.diff', worktreeId: 'wt-1', @@ -173,6 +176,20 @@ describe('buildWorkspaceSessionPayload', () => { expect(payload.browserTabsByWorktree?.['wt-1'][0].loading).toBe(false) }) + it('persists front-matter visibility only for restored editor files', () => { + const payload = buildWorkspaceSessionPayload( + createSnapshot({ + markdownFrontmatterVisible: { + '/tmp/demo.ts': true, + '/tmp/demo.diff': true, + '/tmp/closed.md': true + } + }) + ) + + expect(payload.markdownFrontmatterVisible).toEqual({ '/tmp/demo.ts': true }) + }) + it('drops local terminal scrollback buffers from session payloads', () => { const localWorktreeId = 'repo-1::/local/worktree' const payload = buildWorkspaceSessionPayload( diff --git a/src/renderer/src/lib/workspace-session.ts b/src/renderer/src/lib/workspace-session.ts index c03a02461..4ac7dbe11 100644 --- a/src/renderer/src/lib/workspace-session.ts +++ b/src/renderer/src/lib/workspace-session.ts @@ -38,6 +38,7 @@ export type WorkspaceSessionSnapshot = Pick< | 'activeTabIdByWorktree' | 'openFiles' | 'editorDrafts' + | 'markdownFrontmatterVisible' | 'activeFileIdByWorktree' | 'activeTabTypeByWorktree' | 'browserTabsByWorktree' @@ -72,6 +73,7 @@ export const SESSION_RELEVANT_FIELDS = [ 'activeTabIdByWorktree', 'openFiles', 'editorDrafts', + 'markdownFrontmatterVisible', 'activeFileIdByWorktree', 'activeTabTypeByWorktree', 'browserTabsByWorktree', @@ -102,11 +104,15 @@ void _exhaustive export function buildEditorSessionData( openFiles: OpenFile[], editorDrafts: Record, + markdownFrontmatterVisible: Record, activeFileIdByWorktree: Record, activeTabTypeByWorktree: Record ): Pick< WorkspaceSessionState, - 'openFilesByWorktree' | 'activeFileIdByWorktree' | 'activeTabTypeByWorktree' + | 'openFilesByWorktree' + | 'activeFileIdByWorktree' + | 'activeTabTypeByWorktree' + | 'markdownFrontmatterVisible' > { const editFiles = openFiles.filter((f) => f.mode === 'edit') const byWorktree: Record = {} @@ -160,11 +166,18 @@ export function buildEditorSessionData( string, WorkspaceVisibleTabType > + const allEditFileIds = new Set(Object.values(editFileIdsByWorktree).flatMap((ids) => [...ids])) + const persistedMarkdownFrontmatterVisible = Object.fromEntries( + Object.keys(markdownFrontmatterVisible ?? {}) + .filter((fileId) => allEditFileIds.has(fileId)) + .map((fileId) => [fileId, true]) + ) return { openFilesByWorktree: byWorktree, activeFileIdByWorktree: persistedActiveFileIdByWorktree, - activeTabTypeByWorktree: persistedActiveTabTypeByWorktree + activeTabTypeByWorktree: persistedActiveTabTypeByWorktree, + markdownFrontmatterVisible: persistedMarkdownFrontmatterVisible } } @@ -335,6 +348,7 @@ export function buildWorkspaceSessionPayload( ...buildEditorSessionData( snapshot.openFiles, snapshot.editorDrafts, + snapshot.markdownFrontmatterVisible, snapshot.activeFileIdByWorktree, snapshot.activeTabTypeByWorktree ), diff --git a/src/renderer/src/store/slices/editor.test.ts b/src/renderer/src/store/slices/editor.test.ts index d44d9e3b2..da594a2b2 100644 --- a/src/renderer/src/store/slices/editor.test.ts +++ b/src/renderer/src/store/slices/editor.test.ts @@ -895,6 +895,129 @@ describe('createEditorSlice editor view mode', () => { }) }) +describe('createEditorSlice markdown frontmatter visibility (#4468)', () => { + it('stores visible=true as an explicit entry keyed by fileId', () => { + const store = createEditorStore() + + store.getState().setMarkdownFrontmatterVisible('/repo/notes.md', true) + + expect(store.getState().markdownFrontmatterVisible).toEqual({ '/repo/notes.md': true }) + }) + + it('deletes the entry when visibility resets to hidden', () => { + const store = createEditorStore() + store.getState().setMarkdownFrontmatterVisible('/repo/notes.md', true) + + store.getState().setMarkdownFrontmatterVisible('/repo/notes.md', false) + + expect(store.getState().markdownFrontmatterVisible).toEqual({}) + }) + + it('is a no-op when hiding a file that was never shown', () => { + const store = createEditorStore() + const before = store.getState().markdownFrontmatterVisible + + store.getState().setMarkdownFrontmatterVisible('/repo/notes.md', false) + + expect(store.getState().markdownFrontmatterVisible).toBe(before) + }) + + it('drops the visibility flag when the file is closed', () => { + const store = createEditorStore() + store.getState().openFile({ + filePath: '/repo/notes.md', + relativePath: 'notes.md', + worktreeId: 'wt-1', + language: 'markdown', + mode: 'edit' + }) + store.getState().setMarkdownFrontmatterVisible('/repo/notes.md', true) + + store.getState().closeFile('/repo/notes.md') + + expect(store.getState().markdownFrontmatterVisible).toEqual({}) + }) + + it('keeps the visibility flag while a preview tab still references the source file', () => { + const store = createEditorStore() + store.getState().openFile({ + filePath: '/repo/notes.md', + relativePath: 'notes.md', + worktreeId: 'wt-1', + language: 'markdown', + mode: 'edit' + }) + store.getState().openMarkdownPreview({ + filePath: '/repo/notes.md', + relativePath: 'notes.md', + worktreeId: 'wt-1', + language: 'markdown' + }) + store.getState().setMarkdownFrontmatterVisible('/repo/notes.md', true) + + store.getState().closeFile('/repo/notes.md') + + expect(store.getState().markdownFrontmatterVisible).toEqual({ '/repo/notes.md': true }) + + store.getState().closeFile('markdown-preview::/repo/notes.md') + + expect(store.getState().markdownFrontmatterVisible).toEqual({}) + }) + + it('keeps the visibility flag when replacing an edit preview referenced by a markdown preview', () => { + const store = createEditorStore() + store.getState().openFile( + { + filePath: '/repo/notes.md', + relativePath: 'notes.md', + worktreeId: 'wt-1', + language: 'markdown', + mode: 'edit' + }, + { preview: true } + ) + store.getState().openMarkdownPreview( + { + filePath: '/repo/notes.md', + relativePath: 'notes.md', + worktreeId: 'wt-1', + language: 'markdown' + }, + { sourceFileId: '/repo/notes.md' } + ) + store.getState().setMarkdownFrontmatterVisible('/repo/notes.md', true) + + store.getState().openFile( + { + filePath: '/repo/guide.md', + relativePath: 'guide.md', + worktreeId: 'wt-1', + language: 'markdown', + mode: 'edit' + }, + { preview: true } + ) + + expect(store.getState().markdownFrontmatterVisible).toEqual({ '/repo/notes.md': true }) + }) + + it('drops the visibility flag when all files are closed', () => { + const store = createEditorStore() + store.getState().openFile({ + filePath: '/repo/notes.md', + relativePath: 'notes.md', + worktreeId: 'wt-1', + language: 'markdown', + mode: 'edit' + }) + store.getState().setMarkdownFrontmatterVisible('/repo/notes.md', true) + + store.getState().closeAllFiles() + + expect(store.getState().markdownFrontmatterVisible).toEqual({}) + }) +}) + describe('createEditorSlice openMarkdownPreview', () => { it('opens markdown preview as a separate read-only tab', () => { const store = createEditorStore() diff --git a/src/renderer/src/store/slices/editor.ts b/src/renderer/src/store/slices/editor.ts index 86c5d81fb..a14e8b223 100644 --- a/src/renderer/src/store/slices/editor.ts +++ b/src/renderer/src/store/slices/editor.ts @@ -287,6 +287,12 @@ export type EditorSlice = { editorViewMode: Record setEditorViewMode: (fileId: string, mode: EditorViewMode) => void + // Per-file opt-in to render front matter in the markdown preview (#4468). + // Default is hidden; absent entry means hidden. Storing only the explicit + // true values keeps the record minimal and the default implicit. + markdownFrontmatterVisible: Record + setMarkdownFrontmatterVisible: (fileId: string, visible: boolean) => void + // Right sidebar rightSidebarOpen: boolean rightSidebarWidth: number @@ -1215,6 +1221,26 @@ export const createEditorSlice: StateCreator = (s return { editorViewMode: { ...s.editorViewMode, [fileId]: mode } } }), + // Markdown preview front-matter visibility (#4468). Default is hidden; the + // preview only renders the front-matter card when the user opts in per file. + markdownFrontmatterVisible: {}, + setMarkdownFrontmatterVisible: (fileId, visible) => + set((s) => { + // Why: default is hidden. Writing `false` explicitly when no entry exists + // would grow the record unnecessarily; delete instead so the shape stays + // minimal and hydration round-trips cleanly — same trade-off as + // setEditorViewMode above. + if (!visible) { + if (!(fileId in s.markdownFrontmatterVisible)) { + return s + } + const next = { ...s.markdownFrontmatterVisible } + delete next[fileId] + return { markdownFrontmatterVisible: next } + } + return { markdownFrontmatterVisible: { ...s.markdownFrontmatterVisible, [fileId]: true } } + }), + // Right sidebar rightSidebarOpen: false, rightSidebarWidth: 280, @@ -1425,6 +1451,27 @@ export const createEditorSlice: StateCreator = (s ([fileId]) => fileId !== replacedPreview.id ) ) + const frontmatterVisibilityKeys = new Set([replacedPreview.id]) + if (replacedPreview.markdownPreviewSourceFileId) { + frontmatterVisibilityKeys.add(replacedPreview.markdownPreviewSourceFileId) + } + const frontmatterKeysToRemove = [...frontmatterVisibilityKeys].filter( + (key) => + key in s.markdownFrontmatterVisible && + !s.openFiles.some( + (file, index) => + index !== existingPreviewIdx && + (file.id === key || file.markdownPreviewSourceFileId === key) + ) + ) + const nextMarkdownFrontmatterVisible = + replacedPreview.id === id || frontmatterKeysToRemove.length === 0 + ? s.markdownFrontmatterVisible + : Object.fromEntries( + Object.entries(s.markdownFrontmatterVisible).filter( + ([fileId]) => !frontmatterKeysToRemove.includes(fileId) + ) + ) // Why: editorCursorLine entries accumulate per file; clean up the // evicted preview's entry so it does not leak across tab replacements. const nextEditorCursorLine = @@ -1474,6 +1521,7 @@ export const createEditorSlice: StateCreator = (s editorCursorLine: nextEditorCursorLine, markdownViewMode: nextMarkdownViewMode, editorViewMode: nextEditorViewMode, + markdownFrontmatterVisible: nextMarkdownFrontmatterVisible, recentlyClosedEditorTabsByWorktree: nextRecentlyClosed, ...previewTabBarUpdate, ...activeResult @@ -1692,6 +1740,25 @@ export const createEditorSlice: StateCreator = (s delete newMarkdownViewMode[fileId] const newEditorViewMode = { ...s.editorViewMode } delete newEditorViewMode[fileId] + const frontmatterVisibilityKeys = new Set([fileId]) + if (closedFile?.markdownPreviewSourceFileId) { + frontmatterVisibilityKeys.add(closedFile.markdownPreviewSourceFileId) + } + const keysToRemove = [...frontmatterVisibilityKeys].filter( + (key) => + key in s.markdownFrontmatterVisible && + !newFiles.some((file) => file.id === key || file.markdownPreviewSourceFileId === key) + ) + const newMarkdownFrontmatterVisible = + keysToRemove.length > 0 + ? (() => { + const next = { ...s.markdownFrontmatterVisible } + for (const key of keysToRemove) { + delete next[key] + } + return next + })() + : s.markdownFrontmatterVisible // Why: editorCursorLine entries are keyed by fileId and accumulate on // every cursor move. Without cleanup they grow without bound across a // long session as files are opened and closed. @@ -1817,6 +1884,7 @@ export const createEditorSlice: StateCreator = (s activeTabTypeByWorktree: newActiveTabTypeByWorktree, markdownViewMode: newMarkdownViewMode, editorViewMode: newEditorViewMode, + markdownFrontmatterVisible: newMarkdownFrontmatterVisible, tabBarOrderByWorktree: nextTabBarOrderByWorktree, pendingEditorReveal: null, recentlyClosedEditorTabsByWorktree: nextRecentlyClosed @@ -1902,6 +1970,7 @@ export const createEditorSlice: StateCreator = (s activeTabType: 'terminal', markdownViewMode: {}, editorViewMode: {}, + markdownFrontmatterVisible: {}, pendingEditorReveal: null } } @@ -1917,6 +1986,11 @@ export const createEditorSlice: StateCreator = (s const newEditorViewMode = Object.fromEntries( Object.entries(s.editorViewMode).filter(([fileId]) => remainingFileIds.has(fileId)) ) + const newMarkdownFrontmatterVisible = Object.fromEntries( + Object.entries(s.markdownFrontmatterVisible).filter(([fileId]) => + remainingFileIds.has(fileId) + ) + ) const newEditorCursorLine = Object.fromEntries( Object.entries(s.editorCursorLine).filter(([fileId]) => remainingFileIds.has(fileId)) ) @@ -1982,6 +2056,7 @@ export const createEditorSlice: StateCreator = (s activeTabType: browserTabsForWorktree.length > 0 ? 'browser' : 'terminal', markdownViewMode: newMarkdownViewMode, editorViewMode: newEditorViewMode, + markdownFrontmatterVisible: newMarkdownFrontmatterVisible, activeFileIdByWorktree: newActiveFileIdByWorktree, activeTabTypeByWorktree: newActiveTabTypeByWorktree, tabBarOrderByWorktree: nextTabBarOrderByWorktree, @@ -3440,6 +3515,7 @@ export const createEditorSlice: StateCreator = (s const openFilesByWorktree = session.openFilesByWorktree ?? {} const persistedActiveFileIdByWorktree = session.activeFileIdByWorktree ?? {} const persistedActiveTabTypeByWorktree = session.activeTabTypeByWorktree ?? {} + const persistedMarkdownFrontmatterVisible = session.markdownFrontmatterVisible ?? {} // Why: worktrees may have been deleted between sessions. Filter out // files for worktrees that no longer exist, mirroring the validation @@ -3576,10 +3652,30 @@ export const createEditorSlice: StateCreator = (s // browser or terminal instead of showing an empty editor surface. const nextActiveTabType = nextActiveFileId || activeTabType !== 'editor' ? activeTabType : 'terminal' + const openFileIds = new Set(openFiles.map((file) => file.id)) + const visibleFrontmatterEntries = new Map() + for (const [persistedFileId, visible] of Object.entries( + persistedMarkdownFrontmatterVisible + )) { + if (!visible) { + continue + } + if (openFileIds.has(persistedFileId)) { + visibleFrontmatterEntries.set(persistedFileId, true) + } + for (const migrations of Object.values(editorFileIdMigrationsByWorktree)) { + const migratedFileId = migrations.get(persistedFileId) + if (migratedFileId && openFileIds.has(migratedFileId)) { + visibleFrontmatterEntries.set(migratedFileId, true) + } + } + } + const markdownFrontmatterVisible = Object.fromEntries(visibleFrontmatterEntries) return { openFiles, editorDrafts, + markdownFrontmatterVisible, activeFileId: nextActiveFileId, activeFileIdByWorktree: filteredActiveFileIdByWorktree, activeTabType: nextActiveTabType, diff --git a/src/renderer/src/store/slices/settings.test.ts b/src/renderer/src/store/slices/settings.test.ts index 41040c4c8..c2261859b 100644 --- a/src/renderer/src/store/slices/settings.test.ts +++ b/src/renderer/src/store/slices/settings.test.ts @@ -213,6 +213,7 @@ describe('createSettingsSlice runtime switching', () => { editorDrafts: { '/env-1/repo/stale.md': 'stale' }, markdownViewMode: { '/env-1/repo/stale.md': 'rich' }, editorViewMode: { '/env-1/repo/stale.md': 'changes' }, + markdownFrontmatterVisible: { '/env-1/repo/stale.md': true }, editorCursorLine: { '/env-1/repo/stale.md': 4 }, showDotfilesByWorktree: { 'repo-env-1::/env-1/repo': false }, gitIgnoredPathsByWorktree: { 'repo-env-1::/env-1/repo': ['dist/'] }, @@ -271,6 +272,7 @@ describe('createSettingsSlice runtime switching', () => { expect(store.getState().editorDrafts).toEqual({}) expect(store.getState().markdownViewMode).toEqual({}) expect(store.getState().editorViewMode).toEqual({}) + expect(store.getState().markdownFrontmatterVisible).toEqual({}) expect(store.getState().editorCursorLine).toEqual({}) expect(store.getState().showDotfilesByWorktree).toEqual({}) expect(store.getState().gitIgnoredPathsByWorktree).toEqual({}) diff --git a/src/renderer/src/store/slices/settings.ts b/src/renderer/src/store/slices/settings.ts index b54544fca..3fbd429ca 100644 --- a/src/renderer/src/store/slices/settings.ts +++ b/src/renderer/src/store/slices/settings.ts @@ -98,6 +98,7 @@ function runtimeScopedStateReset(): Partial { editorDrafts: {}, markdownViewMode: {}, editorViewMode: {}, + markdownFrontmatterVisible: {}, editorCursorLine: {}, gitIgnoredPathsByWorktree: {}, activeFileId: null, diff --git a/src/renderer/src/store/slices/store-session-cascades.test.ts b/src/renderer/src/store/slices/store-session-cascades.test.ts index b4ffc2434..753b756c7 100644 --- a/src/renderer/src/store/slices/store-session-cascades.test.ts +++ b/src/renderer/src/store/slices/store-session-cascades.test.ts @@ -1796,7 +1796,8 @@ describe('hydrateEditorSession', () => { ] }, activeFileIdByWorktree: { [wt]: '/path/wt1/src/index.ts' }, - activeTabTypeByWorktree: { [wt]: 'editor' } + activeTabTypeByWorktree: { [wt]: 'editor' }, + markdownFrontmatterVisible: { '/path/wt1/README.md': true } }) const s = store.getState() @@ -1805,6 +1806,7 @@ describe('hydrateEditorSession', () => { expect(s.openFiles[0].mode).toBe('edit') expect(s.openFiles[0].isDirty).toBe(false) expect(s.openFiles[1].isPreview).toBe(true) + expect(s.markdownFrontmatterVisible).toEqual({ '/path/wt1/README.md': true }) expect(s.activeFileId).toBe('/path/wt1/src/index.ts') expect(s.activeTabType).toBe('editor') }) @@ -1851,9 +1853,44 @@ describe('hydrateEditorSession', () => { }) ]) expect(s.editorDrafts).toEqual({ [fileId]: '' }) + expect(s.markdownFrontmatterVisible).toEqual({}) expect(s.activeFileIdByWorktree[FLOATING_TERMINAL_WORKTREE_ID]).toBe(fileId) }) + it('migrates hydrated front-matter visibility to owner-qualified editor file ids', () => { + const store = createTestStore() + const filePath = '/orca/userData/floating-workspace/note.md' + const fileId = ownedEditorFileId(filePath, FLOATING_TERMINAL_WORKTREE_ID, null) + + store.setState({ activeWorktreeId: FLOATING_TERMINAL_WORKTREE_ID }) + + store.getState().hydrateEditorSession({ + activeRepoId: null, + activeWorktreeId: FLOATING_TERMINAL_WORKTREE_ID, + activeTabId: null, + tabsByWorktree: {}, + terminalLayoutsByTabId: {}, + openFilesByWorktree: { + [FLOATING_TERMINAL_WORKTREE_ID]: [ + { + filePath, + relativePath: 'note.md', + worktreeId: FLOATING_TERMINAL_WORKTREE_ID, + language: 'markdown', + runtimeEnvironmentId: null + } + ] + }, + activeFileIdByWorktree: { + [FLOATING_TERMINAL_WORKTREE_ID]: filePath + }, + activeTabTypeByWorktree: { [FLOATING_TERMINAL_WORKTREE_ID]: 'editor' }, + markdownFrontmatterVisible: { [filePath]: true } + }) + + expect(store.getState().markdownFrontmatterVisible).toEqual({ [fileId]: true }) + }) + it('falls back to the floating workspace file id when duplicate paths are owner-qualified', () => { const store = createTestStore() const wt = 'repo1::/path/wt1' diff --git a/src/renderer/src/store/slices/worktrees.test.ts b/src/renderer/src/store/slices/worktrees.test.ts index bf1ee533a..39e3ec1e4 100644 --- a/src/renderer/src/store/slices/worktrees.test.ts +++ b/src/renderer/src/store/slices/worktrees.test.ts @@ -1573,6 +1573,64 @@ describe('removeWorktree state cleanup', () => { expect(store.getState().editorViewMode).toEqual({ 'file-2': 'changes' }) }) + it('cleans up markdownFrontmatterVisible for files in the removed worktree', async () => { + const store = createTestStore() + const wt = makeWorktree({ id: 'repo1::/path/wt1', repoId: 'repo1', path: '/path/wt1' }) + + store.setState({ + worktreesByRepo: { repo1: [wt] }, + openFiles: [ + { + id: 'file-1', + worktreeId: 'repo1::/path/wt1', + filePath: '/path/wt1/readme.md', + relativePath: 'readme.md', + language: 'markdown', + isDirty: false, + isPreview: false, + mode: 'edit' as const + } + ], + markdownFrontmatterVisible: { + 'file-1': true, + 'file-2': true + } + } as unknown as Partial) + + await store.getState().removeWorktree('repo1::/path/wt1') + + expect(store.getState().markdownFrontmatterVisible).toEqual({ 'file-2': true }) + }) + + it('cleans up markdownFrontmatterVisible for preview-only source files in the removed worktree', async () => { + const store = createTestStore() + const wt = makeWorktree({ id: 'repo1::/path/wt1', repoId: 'repo1', path: '/path/wt1' }) + + store.setState({ + worktreesByRepo: { repo1: [wt] }, + openFiles: [ + { + id: 'markdown-preview::file-1', + worktreeId: 'repo1::/path/wt1', + filePath: '/path/wt1/readme.md', + relativePath: 'readme.md', + language: 'markdown', + isDirty: false, + markdownPreviewSourceFileId: 'file-1', + mode: 'markdown-preview' as const + } + ], + markdownFrontmatterVisible: { + 'file-1': true, + 'file-2': true + } + } as unknown as Partial) + + await store.getState().removeWorktree('repo1::/path/wt1') + + expect(store.getState().markdownFrontmatterVisible).toEqual({ 'file-2': true }) + }) + it('records the sidebar scroll anchor in the same tick it removes the worktree', async () => { const store = createTestStore() const wt = makeWorktree({ id: 'repo1::/path/wt1', repoId: 'repo1', path: '/path/wt1' }) @@ -2985,11 +3043,13 @@ describe('purgeWorktreeTerminalState direct (design §4.4)', () => { relativePath: 'a.ts', language: 'typescript', isDirty: false, + markdownPreviewSourceFileId: 'source-1', isPreview: false, mode: 'edit' as const } ], editorDrafts: { 'file-1': 'draft', 'file-99': 'other' }, + markdownFrontmatterVisible: { 'source-1': true, 'file-99': true }, gitIgnoredPathsByWorktree: { 'repoA::/a/wt1': ['dist/'], 'repoA::/a/wt2': ['coverage/'] @@ -3022,6 +3082,7 @@ describe('purgeWorktreeTerminalState direct (design §4.4)', () => { expect(s.runtimePaneTitlesByTabId).toEqual({ 'tab-3': 'bash' }) expect(s.openFiles).toEqual([]) expect(s.editorDrafts).toEqual({ 'file-99': 'other' }) + expect(s.markdownFrontmatterVisible).toEqual({ 'file-99': true }) expect(s.gitIgnoredPathsByWorktree).toEqual({ 'repoA::/a/wt2': ['coverage/'] }) expect(s.rightSidebarTabByWorktree).toEqual({ 'repoA::/a/wt2': 'checks' }) expect(s.activeWorktreeId).toBeNull() diff --git a/src/renderer/src/store/slices/worktrees.ts b/src/renderer/src/store/slices/worktrees.ts index 0af740d4c..e91d34fc6 100644 --- a/src/renderer/src/store/slices/worktrees.ts +++ b/src/renderer/src/store/slices/worktrees.ts @@ -568,6 +568,9 @@ function buildWorktreePurgeState(s: AppState, worktreeIds: string[]): Partial delete nextGitBranchCompareRequestKeyByWorktree[worktreeId] // Why: clean up per-file editor state for files belonging to the removed // worktree so stale drafts and view modes never accumulate in memory. - const removedFileIds = new Set( - s.openFiles.filter((f) => f.worktreeId === worktreeId).map((f) => f.id) - ) + const removedFileIds = new Set() + for (const file of s.openFiles) { + if (file.worktreeId !== worktreeId) { + continue + } + removedFileIds.add(file.id) + if (file.markdownPreviewSourceFileId) { + removedFileIds.add(file.markdownPreviewSourceFileId) + } + } const nextEditorDrafts = removedFileIds.size > 0 ? { ...s.editorDrafts } : s.editorDrafts const nextMarkdownViewMode = removedFileIds.size > 0 ? { ...s.markdownViewMode } : s.markdownViewMode const nextEditorViewMode = removedFileIds.size > 0 ? { ...s.editorViewMode } : s.editorViewMode + const nextMarkdownFrontmatterVisible = + removedFileIds.size > 0 + ? { ...s.markdownFrontmatterVisible } + : s.markdownFrontmatterVisible if (removedFileIds.size > 0) { for (const fileId of removedFileIds) { delete nextEditorDrafts[fileId] delete nextMarkdownViewMode[fileId] delete nextEditorViewMode[fileId] + delete nextMarkdownFrontmatterVisible[fileId] } } const nextExpandedDirs = { ...s.expandedDirs } @@ -1379,6 +1395,7 @@ export const createWorktreeSlice: StateCreator editorDrafts: nextEditorDrafts, markdownViewMode: nextMarkdownViewMode, editorViewMode: nextEditorViewMode, + markdownFrontmatterVisible: nextMarkdownFrontmatterVisible, showDotfilesByWorktree: nextShowDotfilesByWorktree, expandedDirs: nextExpandedDirs, gitStatusByWorktree: nextGitStatusByWorktree, diff --git a/src/shared/constants.ts b/src/shared/constants.ts index c1124ee0d..ff917e246 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -431,6 +431,7 @@ export function getDefaultWorkspaceSession(): WorkspaceSessionState { tabsByWorktree: {}, terminalLayoutsByTabId: {}, openFilesByWorktree: {}, + markdownFrontmatterVisible: {}, browserTabsByWorktree: {}, browserPagesByWorkspace: {}, activeBrowserTabIdByWorktree: {}, diff --git a/src/shared/types.ts b/src/shared/types.ts index 12013616c..b7db93a61 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -684,6 +684,8 @@ export type WorkspaceSessionState = { openFilesByWorktree?: Record /** Per-worktree active editor file ID (filePath) at shutdown. */ activeFileIdByWorktree?: Record + /** Per-file markdown preview front-matter visibility. Absent entry means hidden. */ + markdownFrontmatterVisible?: Record /** Persisted browser workspaces, keyed by worktree ID. */ browserTabsByWorktree?: Record /** Persisted browser pages, keyed by workspace ID. */ diff --git a/src/shared/workspace-session-schema.ts b/src/shared/workspace-session-schema.ts index 5ab61590b..c8b1f42c1 100644 --- a/src/shared/workspace-session-schema.ts +++ b/src/shared/workspace-session-schema.ts @@ -218,6 +218,7 @@ export const workspaceSessionStateSchema: z.ZodType = z.o activeWorktreeIdsOnShutdown: z.array(z.string()).optional(), openFilesByWorktree: z.record(z.string(), z.array(persistedOpenFileSchema)).optional(), activeFileIdByWorktree: z.record(z.string(), z.string().nullable()).optional(), + markdownFrontmatterVisible: z.record(z.string(), z.boolean()).optional(), browserTabsByWorktree: z.record(z.string(), z.array(browserWorkspaceSchema)).optional(), browserPagesByWorkspace: z.record(z.string(), z.array(browserPageSchema)).optional(), activeBrowserTabIdByWorktree: z.record(z.string(), z.string().nullable()).optional(),