import { createMemo, Show, createEffect } from "solid-js" import { DiffView, DiffModeEnum } from "@git-diff-view/solid" import "@git-diff-view/solid/styles/diff-view-pure.css" import { disableCache } from "@git-diff-view/core" import type { DiffHighlighterLang } from "@git-diff-view/core" import { ErrorBoundary } from "solid-js" import { getLanguageFromPath } from "../lib/text-render-utils" import { normalizeDiffText } from "../lib/diff-utils" import { setCacheEntry } from "../lib/global-cache" import type { CacheEntryParams } from "../lib/global-cache" import type { DiffViewMode } from "../stores/preferences" import { getLogger } from "../lib/logger" const log = getLogger("session") disableCache() interface ToolCallDiffViewerProps { diffText: string filePath?: string theme: "light" | "dark" mode: DiffViewMode wrap?: boolean onRendered?: () => void cachedHtml?: string cacheEntryParams?: CacheEntryParams } type DiffData = { oldFile?: { fileName?: string | null; fileLang?: string | null; content?: string | null } newFile?: { fileName?: string | null; fileLang?: string | null; content?: string | null } hunks: string[] } function measureTextWidth(container: HTMLElement, text: string, source: HTMLElement) { const computed = window.getComputedStyle(source) const probe = document.createElement("span") probe.textContent = text || "" probe.style.position = "absolute" probe.style.visibility = "hidden" probe.style.pointerEvents = "none" probe.style.display = "inline-block" probe.style.width = "auto" probe.style.maxWidth = "none" probe.style.whiteSpace = "nowrap" probe.style.fontFamily = computed.fontFamily probe.style.fontSize = computed.fontSize probe.style.fontWeight = computed.fontWeight probe.style.fontStyle = computed.fontStyle probe.style.letterSpacing = computed.letterSpacing probe.style.fontVariant = computed.fontVariant probe.style.textTransform = computed.textTransform probe.style.lineHeight = computed.lineHeight container.appendChild(probe) const width = Math.ceil(probe.getBoundingClientRect().width) probe.remove() return width } function computeCompactWidth( container: HTMLElement, entries: Array<{ text: string; source: HTMLElement }>, maxWidthPx = 40, ) { const measuredLabelWidthPx = entries.reduce((max, entry) => { return Math.max(max, measureTextWidth(container, entry.text, entry.source)) }, 0) const fallbackTextLength = entries.reduce((max, entry) => Math.max(max, entry.text.length), 1) const fallbackWidthPx = Math.round(fallbackTextLength * 7 + 4) return Math.max(2, Math.min(maxWidthPx, measuredLabelWidthPx > 0 ? measuredLabelWidthPx + 2 : fallbackWidthPx)) } function applyCompactUnifiedGutter(container: HTMLElement, wrap: boolean) { const tableWrapper = container.querySelector(".unified-diff-table-wrapper") const table = container.querySelector(".unified-diff-table") const numberCol = container.querySelector(".unified-diff-table-num-col") const gutterRows = container.querySelectorAll(".diff-line-num") const hunkGutters = container.querySelectorAll(".diff-line-hunk-action, .diff-line-widget-wrapper, .diff-line-extend-wrapper") const entries: Array<{ gutter: HTMLElement; label: HTMLElement; text: string }> = [] if (table) { if (wrap) { table.classList.add("table-fixed") table.style.tableLayout = "fixed" table.style.width = "100%" table.style.minWidth = "100%" } else { table.classList.remove("table-fixed") table.style.tableLayout = "auto" table.style.width = "max-content" table.style.minWidth = "100%" } } gutterRows.forEach((gutter) => { const oldSpan = gutter.querySelector("[data-line-old-num]") const newSpan = gutter.querySelector("[data-line-new-num]") const spacer = gutter.querySelector(".shrink-0") const flexWrapper = gutter.querySelector(":scope > .flex") const currentLabel = gutter.querySelector(":scope > .tool-call-diff-compact-line-number") const oldText = oldSpan?.textContent?.trim() ?? "" const newText = newSpan?.textContent?.trim() ?? "" const hasUsableNew = newText.length > 0 && newText !== "0" const hasUsableOld = oldText.length > 0 && oldText !== "0" const visibleText = hasUsableNew ? newText : hasUsableOld ? oldText : newText || oldText if (flexWrapper) flexWrapper.style.display = "none" if (spacer) spacer.style.display = "none" if (oldSpan) { oldSpan.style.display = "none"; oldSpan.style.width = "auto" } if (newSpan) { newSpan.style.display = "none"; newSpan.style.width = "auto" } gutter.style.paddingLeft = "1px" gutter.style.paddingRight = "1px" gutter.style.textAlign = "left" const label = currentLabel ?? document.createElement("span") label.className = "tool-call-diff-compact-line-number" label.textContent = visibleText label.setAttribute("aria-hidden", visibleText ? "false" : "true") if (!currentLabel) gutter.appendChild(label) entries.push({ gutter, label, text: visibleText }) }) const gutterWidthPx = computeCompactWidth(container, entries.map((entry) => ({ text: entry.text, source: entry.label }))) const gutterWidth = `${gutterWidthPx}px` const compactAsideWidth = `${Math.max(8, gutterWidthPx - 10)}px` if (tableWrapper) { tableWrapper.style.setProperty("--diff-aside-width", compactAsideWidth) tableWrapper.style.setProperty("--diff-aside-width--", compactAsideWidth) } if (numberCol) { numberCol.style.width = gutterWidth } entries.forEach(({ gutter, label }) => { gutter.style.width = gutterWidth gutter.style.minWidth = gutterWidth gutter.style.maxWidth = gutterWidth label.style.width = "auto" label.style.maxWidth = "none" }) hunkGutters.forEach((gutter) => { gutter.style.width = gutterWidth gutter.style.minWidth = gutterWidth gutter.style.maxWidth = gutterWidth gutter.style.paddingLeft = "0" gutter.style.paddingRight = "0" }) } function applyCompactSplitGutter(container: HTMLElement) { const oldWrapper = container.querySelector(".old-diff-table-wrapper") const newWrapper = container.querySelector(".new-diff-table-wrapper") const numberCells = Array.from(container.querySelectorAll(".diff-line-old-num, .diff-line-new-num")) const hunkActions = Array.from(container.querySelectorAll(".diff-line-hunk-action, .diff-line-widget-wrapper, .diff-line-extend-wrapper")) const numberSpans = numberCells .map((cell) => ({ cell, span: cell.querySelector("[data-line-num]") })) .filter((entry): entry is { cell: HTMLElement; span: HTMLElement } => Boolean(entry.span)) const gutterWidthPx = computeCompactWidth( container, numberSpans.map(({ span }) => ({ text: span.textContent?.trim() ?? "", source: span })), 64, ) const gutterWidth = `${gutterWidthPx}px` ;[oldWrapper, newWrapper].forEach((wrapper) => { if (wrapper) { wrapper.style.setProperty("--diff-aside-width", gutterWidth) } }) numberCells.forEach((cell) => { cell.style.width = gutterWidth cell.style.minWidth = gutterWidth cell.style.maxWidth = gutterWidth cell.style.paddingLeft = "2px" cell.style.paddingRight = "2px" cell.style.textAlign = "left" cell.style.whiteSpace = "nowrap" cell.style.overflowWrap = "normal" cell.style.wordBreak = "normal" }) numberSpans.forEach(({ span }) => { span.style.whiteSpace = "nowrap" span.style.overflowWrap = "normal" span.style.wordBreak = "normal" }) hunkActions.forEach((cell) => { cell.style.width = gutterWidth cell.style.minWidth = gutterWidth cell.style.maxWidth = gutterWidth cell.style.paddingLeft = "0" cell.style.paddingRight = "0" }) } function applyCompactDiffLayout(container: HTMLElement, mode: DiffViewMode, wrap = false) { if (mode === "unified") { applyCompactUnifiedGutter(container, wrap) return } if (mode === "split") { applyCompactSplitGutter(container) } } export function ToolCallDiffViewer(props: ToolCallDiffViewerProps) { const diffData = createMemo(() => { const normalized = normalizeDiffText(props.diffText) if (!normalized) { return null } const language = getLanguageFromPath(props.filePath) || "text" const fileName = props.filePath || "diff" return { oldFile: { fileName, fileLang: (language || "text") as DiffHighlighterLang | null, }, newFile: { fileName, fileLang: (language || "text") as DiffHighlighterLang | null, }, hunks: [normalized], } }) let diffContainerRef: HTMLDivElement | undefined let lastCapturedKey: string | undefined const contextKey = createMemo(() => { const data = diffData() if (!data) return "" return `${props.theme}|${props.mode}|${props.wrap ? "wrap" : "nowrap"}|${props.diffText}` }) createEffect(() => { const cachedHtml = props.cachedHtml if (cachedHtml) { if (diffContainerRef) { applyCompactDiffLayout(diffContainerRef, props.mode, Boolean(props.wrap)) } // When we are given cached HTML, we rely on the caller's cache // and simply notify once rendered. props.onRendered?.() return } const key = contextKey() if (!key) return if (!diffContainerRef) return if (lastCapturedKey === key) return requestAnimationFrame(() => { if (!diffContainerRef) return applyCompactDiffLayout(diffContainerRef, props.mode, Boolean(props.wrap)) const markup = diffContainerRef.innerHTML if (!markup) return lastCapturedKey = key if (props.cacheEntryParams) { setCacheEntry(props.cacheEntryParams, { text: props.diffText, html: markup, theme: props.theme, mode: props.mode, wrap: props.wrap, }) } props.onRendered?.() }) }) return (
{props.diffText}} > {(data) => ( { log.warn("Failed to render diff view", error) return
{props.diffText}
}}>
)}
} >
) }