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 <pre> 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 <help@stably.ai>

---------

Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Eren Çakar 2026-06-04 23:02:06 +03:00 committed by GitHub
parent c6293c8050
commit 13f062d093
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 572 additions and 54 deletions

View File

@ -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 ? <FrontMatterBanner raw={fm.raw} /> : null}
headerSlot={
fm && showMarkdownFrontmatter ? <FrontMatterBanner raw={fm.raw} /> : null
}
/>
</RichMarkdownErrorBoundary>
</div>

View File

@ -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 (
<EditorPanelShell
@ -313,6 +336,8 @@ function EditorPanelInner({
model={model}
copiedPathVisible={copiedPathToast?.fileId === activeFile.id}
showMarkdownTableOfContents={showMarkdownTableOfContents}
canShowMarkdownFrontmatterToggle={canShowMarkdownFrontmatterToggle}
markdownFrontmatterVisible={isMarkdownFrontmatterVisible}
sideBySide={sideBySide}
openFiles={openFiles}
fileContents={fileContents}
@ -330,6 +355,12 @@ function EditorPanelInner({
onToggleSideBySide={() => setSideBySide((prev) => !prev)}
onEditorToggleChange={handleEditorToggleChange}
onToggleMarkdownTableOfContents={() => setShowMarkdownTableOfContents((shown) => !shown)}
onToggleMarkdownFrontmatter={() =>
setMarkdownFrontmatterVisible(
markdownFrontmatterSourceFileId,
!isMarkdownFrontmatterVisible
)
}
onExportMarkdownToPdf={() => void exportActiveMarkdownToPdf()}
onContentChange={handleContentChange}
onContentChangeForFile={handleContentChangeForFile}

View File

@ -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({
</Tooltip>
</TooltipProvider>
)}
{hasViewModeToggle && isMarkdown && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="p-1 rounded hover:bg-accent text-muted-foreground hover:text-foreground transition-colors flex-shrink-0"
aria-label="More actions"
title="More actions"
>
<MoreHorizontal size={14} />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" sideOffset={4}>
<DropdownMenuItem
// Why: the item is disabled (not hidden) only in source/Monaco
// mode, which has no document DOM to export. We intentionally
// don't poll the DOM (canExportActiveMarkdown) at render time:
// the Radix content renders in a Portal and the lookup can
// race with the active surface's paint, producing a stuck
// disabled state. exportActiveMarkdownToPdf is a safe no-op
// when no subtree is found.
disabled={mdViewMode === 'source'}
onSelect={onExportMarkdownToPdf}
>
Export as PDF
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
<EditorPanelMarkdownActionsMenu
isMarkdown={isMarkdown}
hasViewModeToggle={hasViewModeToggle}
mdViewMode={mdViewMode}
canShowMarkdownFrontmatterToggle={canShowMarkdownFrontmatterToggle}
markdownFrontmatterVisible={markdownFrontmatterVisible}
onToggleMarkdownFrontmatter={onToggleMarkdownFrontmatter}
onExportMarkdownToPdf={onExportMarkdownToPdf}
/>
</div>
)
}

View File

@ -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 (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="p-1 rounded hover:bg-accent text-muted-foreground hover:text-foreground transition-colors flex-shrink-0"
aria-label="More actions"
title="More actions"
>
<MoreHorizontal size={14} />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" sideOffset={4}>
{canShowMarkdownFrontmatterToggle ? (
<>
<DropdownMenuItem
onSelect={(event) => {
event.preventDefault()
onToggleMarkdownFrontmatter()
}}
>
{markdownFrontmatterVisible ? 'Hide front matter' : 'Show front matter'}
</DropdownMenuItem>
{hasViewModeToggle ? <DropdownMenuSeparator /> : null}
</>
) : null}
{hasViewModeToggle ? (
<DropdownMenuItem
// Why: source/Monaco mode has no document DOM. Avoid polling the
// portal-mounted menu; exportActiveMarkdownToPdf is a safe no-op
// when no rendered markdown subtree is found.
disabled={mdViewMode === 'source'}
onSelect={onExportMarkdownToPdf}
>
Export as PDF
</DropdownMenuItem>
) : null}
</DropdownMenuContent>
</DropdownMenu>
)
}

View File

@ -19,6 +19,8 @@ type EditorPanelShellProps = {
model: EditorPanelRenderModel
copiedPathVisible: boolean
showMarkdownTableOfContents: boolean
canShowMarkdownFrontmatterToggle: boolean
markdownFrontmatterVisible: boolean
sideBySide: boolean
openFiles: OpenFile[]
fileContents: Record<string, FileContent>
@ -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}
/>

View File

@ -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<string | null>(null)
const [reviewNotesCopied, setReviewNotesCopied] = useState(false)
const [copiedReviewNoteId, setCopiedReviewNoteId] = useState<string | null>(null)
@ -1737,10 +1745,10 @@ export default function MarkdownPreview({
</div>
) : null}
<div ref={bodyRef} className="markdown-body">
{/* 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 ? (
<div className="mb-4 rounded border border-border/60 bg-muted/40 px-3 py-2">
<div className="mb-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
Front Matter
@ -1749,7 +1757,7 @@ export default function MarkdownPreview({
{frontMatterInner}
</pre>
</div>
)}
) : null}
<Markdown
components={components}
// Why: react-markdown filters file:// after rehype-sanitize; preview

View File

@ -14,6 +14,7 @@ function createSnapshot(browserUrlHistory: BrowserHistoryEntry[]): WorkspaceSess
activeTabIdByWorktree: {},
openFiles: [],
editorDrafts: {},
markdownFrontmatterVisible: {},
activeFileIdByWorktree: {},
activeTabTypeByWorktree: {},
browserTabsByWorktree: {},

View File

@ -15,6 +15,7 @@ function createSnapshot(
activeTabIdByWorktree: {},
openFiles: [],
editorDrafts: {},
markdownFrontmatterVisible: {},
activeFileIdByWorktree: {},
activeTabTypeByWorktree: {},
browserTabsByWorktree: {},

View File

@ -14,6 +14,7 @@ function createSnapshot(
activeTabIdByWorktree: {},
openFiles: [],
editorDrafts: {},
markdownFrontmatterVisible: {},
activeFileIdByWorktree: {},
activeTabTypeByWorktree: {},
browserTabsByWorktree: {},

View File

@ -23,8 +23,10 @@ function createSnapshot(
},
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',
@ -36,6 +38,7 @@ function createSnapshot(
originalContent: ''
},
{
id: '/tmp/demo.diff',
filePath: '/tmp/demo.diff',
relativePath: 'demo.diff',
worktreeId: 'wt-1',
@ -90,7 +93,12 @@ describe('buildWorkspaceSessionPatch', () => {
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(

View File

@ -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
)

View File

@ -14,6 +14,7 @@ describe('SESSION_RELEVANT_FIELDS', () => {
activeTabIdByWorktree: true,
openFiles: true,
editorDrafts: true,
markdownFrontmatterVisible: true,
activeFileIdByWorktree: true,
activeTabTypeByWorktree: true,
browserTabsByWorktree: true,

View File

@ -21,8 +21,10 @@ function createSnapshot(overrides: Partial<AppState> = {}): 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> = {}): 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(

View File

@ -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<string, string>,
markdownFrontmatterVisible: Record<string, boolean>,
activeFileIdByWorktree: Record<string, string | null>,
activeTabTypeByWorktree: Record<string, WorkspaceVisibleTabType>
): Pick<
WorkspaceSessionState,
'openFilesByWorktree' | 'activeFileIdByWorktree' | 'activeTabTypeByWorktree'
| 'openFilesByWorktree'
| 'activeFileIdByWorktree'
| 'activeTabTypeByWorktree'
| 'markdownFrontmatterVisible'
> {
const editFiles = openFiles.filter((f) => f.mode === 'edit')
const byWorktree: Record<string, PersistedOpenFile[]> = {}
@ -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
),

View File

@ -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()

View File

@ -287,6 +287,12 @@ export type EditorSlice = {
editorViewMode: Record<string, EditorViewMode>
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<string, boolean>
setMarkdownFrontmatterVisible: (fileId: string, visible: boolean) => void
// Right sidebar
rightSidebarOpen: boolean
rightSidebarWidth: number
@ -1215,6 +1221,26 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (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<AppState, [], [], EditorSlice> = (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<AppState, [], [], EditorSlice> = (s
editorCursorLine: nextEditorCursorLine,
markdownViewMode: nextMarkdownViewMode,
editorViewMode: nextEditorViewMode,
markdownFrontmatterVisible: nextMarkdownFrontmatterVisible,
recentlyClosedEditorTabsByWorktree: nextRecentlyClosed,
...previewTabBarUpdate,
...activeResult
@ -1692,6 +1740,25 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (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<AppState, [], [], EditorSlice> = (s
activeTabTypeByWorktree: newActiveTabTypeByWorktree,
markdownViewMode: newMarkdownViewMode,
editorViewMode: newEditorViewMode,
markdownFrontmatterVisible: newMarkdownFrontmatterVisible,
tabBarOrderByWorktree: nextTabBarOrderByWorktree,
pendingEditorReveal: null,
recentlyClosedEditorTabsByWorktree: nextRecentlyClosed
@ -1902,6 +1970,7 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
activeTabType: 'terminal',
markdownViewMode: {},
editorViewMode: {},
markdownFrontmatterVisible: {},
pendingEditorReveal: null
}
}
@ -1917,6 +1986,11 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (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<AppState, [], [], EditorSlice> = (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<AppState, [], [], EditorSlice> = (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<AppState, [], [], EditorSlice> = (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<string, boolean>()
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,

View File

@ -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({})

View File

@ -98,6 +98,7 @@ function runtimeScopedStateReset(): Partial<AppState> {
editorDrafts: {},
markdownViewMode: {},
editorViewMode: {},
markdownFrontmatterVisible: {},
editorCursorLine: {},
gitIgnoredPathsByWorktree: {},
activeFileId: null,

View File

@ -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'

View File

@ -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<AppState>)
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<AppState>)
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()

View File

@ -568,6 +568,9 @@ function buildWorktreePurgeState(s: AppState, worktreeIds: string[]): Partial<Ap
for (const file of s.openFiles) {
if (worktreeIdSet.has(file.worktreeId)) {
removedFileIds.add(file.id)
if (file.markdownPreviewSourceFileId) {
removedFileIds.add(file.markdownPreviewSourceFileId)
}
}
}
@ -673,6 +676,7 @@ function buildWorktreePurgeState(s: AppState, worktreeIds: string[]): Partial<Ap
// Per-file editor state for removed files
editorDrafts: omitByFileId(s.editorDrafts),
markdownViewMode: omitByFileId(s.markdownViewMode),
markdownFrontmatterVisible: omitByFileId(s.markdownFrontmatterVisible),
// Top-level actives
openFiles: nextOpenFiles,
everActivatedWorktreeIds: nextEverActivatedWorktreeIds,
@ -1300,19 +1304,31 @@ export const createWorktreeSlice: StateCreator<AppState, [], [], WorktreeSlice>
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<string>()
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<AppState, [], [], WorktreeSlice>
editorDrafts: nextEditorDrafts,
markdownViewMode: nextMarkdownViewMode,
editorViewMode: nextEditorViewMode,
markdownFrontmatterVisible: nextMarkdownFrontmatterVisible,
showDotfilesByWorktree: nextShowDotfilesByWorktree,
expandedDirs: nextExpandedDirs,
gitStatusByWorktree: nextGitStatusByWorktree,

View File

@ -431,6 +431,7 @@ export function getDefaultWorkspaceSession(): WorkspaceSessionState {
tabsByWorktree: {},
terminalLayoutsByTabId: {},
openFilesByWorktree: {},
markdownFrontmatterVisible: {},
browserTabsByWorktree: {},
browserPagesByWorkspace: {},
activeBrowserTabIdByWorktree: {},

View File

@ -684,6 +684,8 @@ export type WorkspaceSessionState = {
openFilesByWorktree?: Record<string, PersistedOpenFile[]>
/** Per-worktree active editor file ID (filePath) at shutdown. */
activeFileIdByWorktree?: Record<string, string | null>
/** Per-file markdown preview front-matter visibility. Absent entry means hidden. */
markdownFrontmatterVisible?: Record<string, boolean>
/** Persisted browser workspaces, keyed by worktree ID. */
browserTabsByWorktree?: Record<string, BrowserWorkspace[]>
/** Persisted browser pages, keyed by workspace ID. */

View File

@ -218,6 +218,7 @@ export const workspaceSessionStateSchema: z.ZodType<WorkspaceSessionState> = 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(),