/** * The changes tab's shared bottom preview pane: one selected target — a git * worktree change, a commit patch, or a session file op — rendered through * the unified diff stack (the same one the diff tab uses). Git targets load * on demand (refreshable, with the untracked full-addition fallback); op * targets are pure snapshots (diff / read view / error text). The pane is * resizable by drag (clamped; the height commits to the tab's persisted * meta on release) and by keyboard; git targets can expand into a dedicated * diff tab via the shell. */ import { useEffect, useMemo, useRef, useState, type PointerEvent as ReactPointerEvent } from 'react' import { IconCloseOutline16, IconRefreshOutline16, IconRightUpOutline16, MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives' import type { SessionScope } from '../api.ts' import { api, htmlUrl } from '../api.ts' import { t } from '../locales.ts' import { baseName } from '../paths.ts' import { resolveSidebarPath } from '../produced-files.ts' import { HTML_IFRAME_SANDBOX } from '../html-preview.ts' import type { SidebarDiffRef, SidebarTab } from '../state.ts' import { DiffRows, ReadRows } from '../diff/DiffRows.tsx' import { PdfView } from '../PdfView.tsx' import { DiffFiles } from '../diff/DiffFiles.tsx' import { langOfPath } from '../diff/highlight.ts' import { buildDiffSegments, diffLines, diffStats, displayPath, foldRowsFromContents, parseUnifiedDiff, unifiedSegments, type DiffFile, type DiffRow, type FoldSegment } from '../diff/rows.ts' import { parseReadContent, parseReadLines, type FileOp } from './ops.ts' import { redactText } from '../redact.ts' import { rewriteLocalImageUrls } from '../markdown-images.ts' import { markdownTextProps } from '../markdown-labels.tsx' import { splitMermaidBlocks } from '../mermaid-blocks.ts' import { LazyMermaidMarkdown } from '../mermaid-lazy.tsx' import { createFrameBatcher } from '../frame-batcher.ts' import css from './changes.module.css' import diffCss from '../diff/diff.module.css' /** 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 } | { kind: 'op'; path: string; op: FileOp; prior?: string } /** Diff material for one op snapshot: an edit reconstructs the full file * from the window's known prior content when possible (hunk-style context); * a write with unknown prior content renders all-added. */ function diffOf(op: FileOp, prior: string | undefined): readonly DiffRow[] { if (op.kind === 'read') return [] if (op.kind === 'edit' && op.edit !== undefined) { const { oldString, newString } = op.edit if (prior !== undefined && prior.includes(oldString)) { const newFile = prior.replace(oldString, newString) return diffLines(prior, newFile) } return diffLines(oldString, newString) } if (op.kind === 'write') { const content = op.content ?? '' const old = prior !== undefined && prior !== content ? prior : undefined return diffLines(old ?? '', content) } return [] } /** The render view of one html op target: the route-src iframe. Extracted * (and exported) so the always-sandboxed contract is pinned directly by the * sandbox spec — this surface has NO no-sandbox escape hatch. */ export function HtmlRenderPreview(props: { src: string; title: string }) { return (