Fix Diff view + Caching
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ export interface RenderCache {
|
||||
text: string
|
||||
html: string
|
||||
theme?: string
|
||||
mode?: string
|
||||
}
|
||||
|
||||
export interface MessageDisplayParts {
|
||||
|
||||
Reference in New Issue
Block a user