diff --git a/src/components/diff-viewer.tsx b/src/components/diff-viewer.tsx index baba1be9..2aa4b2e7 100644 --- a/src/components/diff-viewer.tsx +++ b/src/components/diff-viewer.tsx @@ -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(() => { + 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(() => { - 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 (
- + + {props.diffText}} + > + {(data) => ( + + )} + +
+ } + > +
+
) -} - +} \ No newline at end of file diff --git a/src/components/tool-call.tsx b/src/components/tool-call.tsx index 38c19f64..cca0f6e4 100644 --- a/src/components/tool-call.tsx +++ b/src/components/tool-call.tsx @@ -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 (
initializeScrollContainer(element)} onScroll={(event) => updateScrollState(toolCallId(), event.currentTarget)} > -
+
{toolbarLabel} +
+ + +
) diff --git a/src/types/message.ts b/src/types/message.ts index ff686fa8..ec442c86 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -2,6 +2,7 @@ export interface RenderCache { text: string html: string theme?: string + mode?: string } export interface MessageDisplayParts {