Fix Diff view + Caching
This commit is contained in:
@@ -1,56 +1,102 @@
|
|||||||
import { createMemo } from "solid-js"
|
import { createMemo, Show, onMount, createEffect } from "solid-js"
|
||||||
import type { TextPart } from "../types/message"
|
import { DiffView, DiffModeEnum } from "@git-diff-view/solid"
|
||||||
import { Markdown } from "./markdown"
|
import { getLanguageFromPath } from "../lib/markdown"
|
||||||
import { normalizeDiffText } from "../lib/diff-utils"
|
import { normalizeDiffText } from "../lib/diff-utils"
|
||||||
import { getToolRenderCache, setToolRenderCache } from "../lib/tool-render-cache"
|
import { setToolRenderCache } from "../lib/tool-render-cache"
|
||||||
|
import type { DiffViewMode } from "../stores/preferences"
|
||||||
type ThemeKey = "light" | "dark"
|
|
||||||
|
|
||||||
interface ToolCallDiffViewerProps {
|
interface ToolCallDiffViewerProps {
|
||||||
diffText: string
|
diffText: string
|
||||||
filePath?: string
|
filePath?: string
|
||||||
theme: ThemeKey
|
theme: "light" | "dark"
|
||||||
renderCacheKey?: string
|
mode: DiffViewMode
|
||||||
|
onRendered?: () => void
|
||||||
|
cachedHtml?: string
|
||||||
|
cacheKey?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DiffData = {
|
||||||
function formatDiffMarkdown(diffText: string, filePath?: string): string {
|
oldFile?: { fileName?: string | null; fileLang?: string | null; content?: string | null }
|
||||||
const body = normalizeDiffText(diffText) || diffText
|
newFile?: { fileName?: string | null; fileLang?: string | null; content?: string | null }
|
||||||
const trimmed = body.trimStart()
|
hunks: string[]
|
||||||
const alreadyFenced = trimmed.startsWith("```")
|
|
||||||
const fenced = alreadyFenced ? body : `\`\`\`diff\n${body}\n\`\`\``
|
|
||||||
|
|
||||||
if (!filePath) {
|
|
||||||
return fenced
|
|
||||||
}
|
|
||||||
return `### ${filePath}\n\n${fenced}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolCallDiffViewer(props: ToolCallDiffViewerProps) {
|
export function ToolCallDiffViewer(props: ToolCallDiffViewerProps) {
|
||||||
const diffMarkdown = createMemo(() => {
|
const diffData = createMemo<DiffData | null>(() => {
|
||||||
return formatDiffMarkdown(props.diffText, props.filePath)
|
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>(() => {
|
let diffContainerRef: HTMLDivElement | undefined
|
||||||
const part: TextPart = { type: "text", text: diffMarkdown() }
|
|
||||||
if (props.renderCacheKey) {
|
const captureAndCacheHtml = () => {
|
||||||
const cached = getToolRenderCache(props.renderCacheKey)
|
if (diffContainerRef && props.cacheKey && !props.cachedHtml) {
|
||||||
if (cached) {
|
// Extract the rendered HTML from DiffView container
|
||||||
part.renderCache = cached
|
const renderedHtml = diffContainerRef.innerHTML
|
||||||
|
if (renderedHtml) {
|
||||||
|
setToolRenderCache(props.cacheKey, {
|
||||||
|
text: props.diffText,
|
||||||
|
html: renderedHtml,
|
||||||
|
theme: props.theme,
|
||||||
|
mode: props.mode,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return part
|
props.onRendered?.()
|
||||||
})
|
|
||||||
|
|
||||||
const handleRendered = () => {
|
|
||||||
if (!props.renderCacheKey) return
|
|
||||||
setToolRenderCache(props.renderCacheKey, diffPart().renderCache)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 (
|
return (
|
||||||
<div class="tool-call-diff-viewer">
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6,6 +6,7 @@ import { useTheme } from "../lib/theme"
|
|||||||
import { getLanguageFromPath } from "../lib/markdown"
|
import { getLanguageFromPath } from "../lib/markdown"
|
||||||
import { isRenderableDiffText } from "../lib/diff-utils"
|
import { isRenderableDiffText } from "../lib/diff-utils"
|
||||||
import { getToolRenderCache, setToolRenderCache } from "../lib/tool-render-cache"
|
import { getToolRenderCache, setToolRenderCache } from "../lib/tool-render-cache"
|
||||||
|
import { preferences, setDiffViewMode, type DiffViewMode } from "../stores/preferences"
|
||||||
import type { TextPart } from "../types/message"
|
import type { TextPart } from "../types/message"
|
||||||
|
|
||||||
|
|
||||||
@@ -385,6 +386,33 @@ export default function ToolCall(props: ToolCallProps) {
|
|||||||
const relativePath = payload.filePath ? getRelativePath(payload.filePath) : ""
|
const relativePath = payload.filePath ? getRelativePath(payload.filePath) : ""
|
||||||
const toolbarLabel = relativePath ? `Diff · ${relativePath}` : "Diff"
|
const toolbarLabel = relativePath ? `Diff · ${relativePath}` : "Diff"
|
||||||
const cacheKey = makeRenderCacheKey(toolCallId(), props.messageId, props.messageVersion, props.partVersion)
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -392,14 +420,35 @@ export default function ToolCall(props: ToolCallProps) {
|
|||||||
ref={(element) => initializeScrollContainer(element)}
|
ref={(element) => initializeScrollContainer(element)}
|
||||||
onScroll={(event) => updateScrollState(toolCallId(), event.currentTarget)}
|
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>
|
<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>
|
</div>
|
||||||
<ToolCallDiffViewer
|
<ToolCallDiffViewer
|
||||||
diffText={payload.diffText}
|
diffText={payload.diffText}
|
||||||
filePath={payload.filePath}
|
filePath={payload.filePath}
|
||||||
theme={isDark() ? "dark" : "light"}
|
theme={themeKey}
|
||||||
renderCacheKey={cacheKey}
|
mode={diffMode()}
|
||||||
|
cachedHtml={cachedHtml}
|
||||||
|
cacheKey={cacheKey}
|
||||||
|
onRendered={handleDiffRendered}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export interface RenderCache {
|
|||||||
text: string
|
text: string
|
||||||
html: string
|
html: string
|
||||||
theme?: string
|
theme?: string
|
||||||
|
mode?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageDisplayParts {
|
export interface MessageDisplayParts {
|
||||||
|
|||||||
Reference in New Issue
Block a user