- {/* 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(),