/** * 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 * through the shared {@link useGitDiffTarget} hook (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 { IconCloseOutlineRegular, IconRefreshOutlineRegular, IconRightUpOutlineRegular, MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives' import type { SessionScope } from '../api.ts' import { htmlUrl } from '../api.ts' import { t } from '../locales.ts' import { baseName } from '../paths.ts' import { resolveSidebarPath } from '../paths.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, parseUnifiedDiff, unifiedSegments, type DiffRow } from '../diff/rows.ts' import { useGitDiffTarget } from '../diff/use-git-diff.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 { Chip, IconButton, Notice, StatusBadge } from '../ui/index.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 /** * Clamp one pane height to the pane's own bounds: never below the content * minimum, never above 70% of the window. Both the drag handler and the * RESTORE path (a height persisted under a taller window) run through this, * so a saved value can never open the pane taller than the viewport allows. */ export function clampPaneHeight(value: number): number { return Math.min(Math.max(value, HEIGHT_MIN), Math.round(window.innerHeight * 0.7)) } /** 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 (