diff --git a/src/client/changes/DiffPane.tsx b/src/client/changes/DiffPane.tsx
index 4dff973..c1df107 100644
--- a/src/client/changes/DiffPane.tsx
+++ b/src/client/changes/DiffPane.tsx
@@ -32,10 +32,14 @@ import { createFrameBatcher } from '../frame-batcher.ts'
import css from './changes.module.css'
import diffCss from '../diff/diff.module.css'
-/** The drag handle height clamp (px) and keyboard-resize step. */
+/** Drag handle height clamp (px) and keyboard-resize step. */
const HEIGHT_MIN = 140
const HEIGHT_STEP = 24
+/** The redaction preference, persisted under the repo's sidebar storage
+ * prefix (see state.ts's `dsh-sidebar:v1`). */
+const REDACTION_KEY = 'dsh-sidebar:v1:redaction'
+
/** What the pane is showing right now. */
export type ChangesPreview =
| { kind: 'git'; ref: SidebarDiffRef }
@@ -80,6 +84,43 @@ export function HtmlRenderPreview(props: { src: string; title: string }) {
)
}
+/** One header pill toggle — the redaction / reading / render toggles share
+ * the shape (on-state styling + aria-pressed). */
+function PaneToggle(props: { on: boolean; label: string; title?: string; onClick: () => void }) {
+ return (
+
+ )
+}
+
+/** The reading-mode body of one markdown op target: the shared MarkdownText
+ * pass (local image destinations already rewritten to the media route by
+ * the caller). Mermaid fences render through the same chunk-resident
+ * renderer the editor preview uses (one MarkdownText pass with the fences
+ * lifted out); the plain path stays byte-for-byte for documents without
+ * any. */
+function MdReadingView(props: { text: string }) {
+ const codeLabels = { copyLabel: t('copy'), copiedLabel: t('copied') }
+ const hasMermaid = splitMermaidBlocks(props.text).some((block) => block.kind === 'mermaid')
+ return (
+