Fix Diff view + Caching

This commit is contained in:
Shantur Rathore
2025-11-11 11:09:06 +00:00
parent d277d50ed7
commit 34dec16e22
3 changed files with 135 additions and 39 deletions

View File

@@ -1,56 +1,102 @@
import { createMemo } from "solid-js"
import type { TextPart } from "../types/message"
import { Markdown } from "./markdown"
import { createMemo, Show, onMount, createEffect } from "solid-js"
import { DiffView, DiffModeEnum } from "@git-diff-view/solid"
import { getLanguageFromPath } from "../lib/markdown"
import { normalizeDiffText } from "../lib/diff-utils"
import { getToolRenderCache, setToolRenderCache } from "../lib/tool-render-cache"
type ThemeKey = "light" | "dark"
import { setToolRenderCache } from "../lib/tool-render-cache"
import type { DiffViewMode } from "../stores/preferences"
interface ToolCallDiffViewerProps {
diffText: string
filePath?: string
theme: ThemeKey
renderCacheKey?: string
theme: "light" | "dark"
mode: DiffViewMode
onRendered?: () => void
cachedHtml?: string
cacheKey?: string
}
function formatDiffMarkdown(diffText: string, filePath?: string): string {
const body = normalizeDiffText(diffText) || diffText
const trimmed = body.trimStart()
const alreadyFenced = trimmed.startsWith("```")
const fenced = alreadyFenced ? body : `\`\`\`diff\n${body}\n\`\`\``
if (!filePath) {
return fenced
}
return `### ${filePath}\n\n${fenced}`
type DiffData = {
oldFile?: { fileName?: string | null; fileLang?: string | null; content?: string | null }
newFile?: { fileName?: string | null; fileLang?: string | null; content?: string | null }
hunks: string[]
}
export function ToolCallDiffViewer(props: ToolCallDiffViewerProps) {
const diffMarkdown = createMemo(() => {
return formatDiffMarkdown(props.diffText, props.filePath)
const diffData = createMemo<DiffData | null>(() => {
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 as any, // Cast to any to avoid type issues with DiffHighlighterLang
},
newFile: {
fileName,
fileLang: language as any, // Cast to any to avoid type issues with DiffHighlighterLang
},
hunks: [normalized],
}
})
const diffPart = createMemo<TextPart>(() => {
const part: TextPart = { type: "text", text: diffMarkdown() }
if (props.renderCacheKey) {
const cached = getToolRenderCache(props.renderCacheKey)
if (cached) {
part.renderCache = cached
let diffContainerRef: HTMLDivElement | undefined
const captureAndCacheHtml = () => {
if (diffContainerRef && props.cacheKey && !props.cachedHtml) {
// Extract the rendered HTML from DiffView container
const renderedHtml = diffContainerRef.innerHTML
if (renderedHtml) {
setToolRenderCache(props.cacheKey, {
text: props.diffText,
html: renderedHtml,
theme: props.theme,
mode: props.mode,
})
}
}
return part
})
const handleRendered = () => {
if (!props.renderCacheKey) return
setToolRenderCache(props.renderCacheKey, diffPart().renderCache)
props.onRendered?.()
}
// Also capture HTML when diff data changes
createEffect(() => {
const data = diffData()
if (data && !props.cachedHtml) {
// Delay to allow DiffView to re-render with new data
setTimeout(captureAndCacheHtml, 100)
}
})
return (
<div class="tool-call-diff-viewer">
<Markdown part={diffPart()} isDark={props.theme === "dark"} onRendered={handleRendered} />
<Show
when={props.cachedHtml}
fallback={
<div ref={diffContainerRef}>
<Show
when={diffData()}
fallback={<pre class="tool-call-diff-fallback">{props.diffText}</pre>}
>
{(data) => (
<DiffView
data={data()}
diffViewMode={props.mode === "split" ? DiffModeEnum.Split : DiffModeEnum.Unified}
diffViewTheme={props.theme}
diffViewHighlight
diffViewWrap={false}
diffViewFontSize={13}
/>
)}
</Show>
</div>
}
>
<div innerHTML={props.cachedHtml} />
</Show>
</div>
)
}
}

View File

@@ -6,6 +6,7 @@ import { useTheme } from "../lib/theme"
import { getLanguageFromPath } from "../lib/markdown"
import { isRenderableDiffText } from "../lib/diff-utils"
import { getToolRenderCache, setToolRenderCache } from "../lib/tool-render-cache"
import { preferences, setDiffViewMode, type DiffViewMode } from "../stores/preferences"
import type { TextPart } from "../types/message"
@@ -385,6 +386,33 @@ export default function ToolCall(props: ToolCallProps) {
const relativePath = payload.filePath ? getRelativePath(payload.filePath) : ""
const toolbarLabel = relativePath ? `Diff · ${relativePath}` : "Diff"
const cacheKey = makeRenderCacheKey(toolCallId(), props.messageId, props.messageVersion, props.partVersion)
const diffMode = () => (preferences().diffViewMode || "split") as DiffViewMode
const themeKey = isDark() ? "dark" : "light"
// Check if we have valid cache
let cachedHtml: string | undefined
if (cacheKey) {
const cached = getToolRenderCache(cacheKey)
const currentMode = diffMode()
if (cached &&
cached.text === payload.diffText &&
cached.theme === themeKey &&
cached.mode === currentMode) {
cachedHtml = cached.html
}
}
const handleModeChange = (mode: DiffViewMode) => {
setDiffViewMode(mode)
}
const handleDiffRendered = () => {
if (cacheKey && !cachedHtml) {
// Cache will be updated by the diff viewer component itself
// We'll capture HTML from the rendered component
}
handleScrollRendered()
}
return (
<div
@@ -392,14 +420,35 @@ export default function ToolCall(props: ToolCallProps) {
ref={(element) => initializeScrollContainer(element)}
onScroll={(event) => updateScrollState(toolCallId(), event.currentTarget)}
>
<div class="tool-call-diff-toolbar">
<div class="tool-call-diff-toolbar" role="group" aria-label="Diff view mode">
<span class="tool-call-diff-toolbar-label">{toolbarLabel}</span>
<div class="tool-call-diff-toggle">
<button
type="button"
class={`tool-call-diff-mode-button${diffMode() === "split" ? " active" : ""}`}
aria-pressed={diffMode() === "split"}
onClick={() => handleModeChange("split")}
>
Split
</button>
<button
type="button"
class={`tool-call-diff-mode-button${diffMode() === "unified" ? " active" : ""}`}
aria-pressed={diffMode() === "unified"}
onClick={() => handleModeChange("unified")}
>
Unified
</button>
</div>
</div>
<ToolCallDiffViewer
diffText={payload.diffText}
filePath={payload.filePath}
theme={isDark() ? "dark" : "light"}
renderCacheKey={cacheKey}
theme={themeKey}
mode={diffMode()}
cachedHtml={cachedHtml}
cacheKey={cacheKey}
onRendered={handleDiffRendered}
/>
</div>
)

View File

@@ -2,6 +2,7 @@ export interface RenderCache {
text: string
html: string
theme?: string
mode?: string
}
export interface MessageDisplayParts {