feat: syntax highlighting in diff preview + fix Allow All button style
- DiffViewer: add syntaxHighlight prop — maps diff lines to highlighted HTML via highlightLines(), passed to DiffLineRow - ToolApprovalDiffPreview: enable syntaxHighlight for file diffs - Allow All button: proper border style matching Deny button sizing
This commit is contained in:
parent
d92ab08416
commit
1f9f0b6390
3 changed files with 47 additions and 11 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CODE_BG,
|
CODE_BG,
|
||||||
|
|
@ -19,6 +19,7 @@ import {
|
||||||
TAG_TEXT,
|
TAG_TEXT,
|
||||||
} from '@renderer/constants/cssVariables';
|
} from '@renderer/constants/cssVariables';
|
||||||
import { getBaseName } from '@renderer/utils/pathUtils';
|
import { getBaseName } from '@renderer/utils/pathUtils';
|
||||||
|
import { highlightLines } from '@renderer/utils/syntaxHighlighter';
|
||||||
import { formatTokens } from '@shared/utils/tokenFormatting';
|
import { formatTokens } from '@shared/utils/tokenFormatting';
|
||||||
import { diffLines as semanticDiffLines } from 'diff';
|
import { diffLines as semanticDiffLines } from 'diff';
|
||||||
import { Pencil } from 'lucide-react';
|
import { Pencil } from 'lucide-react';
|
||||||
|
|
@ -33,6 +34,7 @@ interface DiffViewerProps {
|
||||||
newString: string; // The new text
|
newString: string; // The new text
|
||||||
maxHeight?: string; // CSS max-height class (default: "max-h-96")
|
maxHeight?: string; // CSS max-height class (default: "max-h-96")
|
||||||
tokenCount?: number; // Optional token count to display in header
|
tokenCount?: number; // Optional token count to display in header
|
||||||
|
syntaxHighlight?: boolean; // Enable syntax highlighting via highlight.js
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DiffLine {
|
interface DiffLine {
|
||||||
|
|
@ -259,9 +261,10 @@ function inferLanguage(fileName: string): string {
|
||||||
|
|
||||||
interface DiffLineRowProps {
|
interface DiffLineRowProps {
|
||||||
line: DiffLine;
|
line: DiffLine;
|
||||||
|
highlightedHtml?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DiffLineRow: React.FC<DiffLineRowProps> = ({ line }): React.JSX.Element => {
|
const DiffLineRow: React.FC<DiffLineRowProps> = ({ line, highlightedHtml }): React.JSX.Element => {
|
||||||
// Theme-aware styles using CSS variables
|
// Theme-aware styles using CSS variables
|
||||||
const getStyles = (
|
const getStyles = (
|
||||||
type: DiffLine['type']
|
type: DiffLine['type']
|
||||||
|
|
@ -312,10 +315,18 @@ const DiffLineRow: React.FC<DiffLineRowProps> = ({ line }): React.JSX.Element =>
|
||||||
<span className="w-6 shrink-0 select-none" style={{ color: style.text }}>
|
<span className="w-6 shrink-0 select-none" style={{ color: style.text }}>
|
||||||
{style.prefix}
|
{style.prefix}
|
||||||
</span>
|
</span>
|
||||||
{/* Content */}
|
{/* Content — optionally syntax-highlighted via hljs (HTML-escaped, safe) */}
|
||||||
<span className="flex-1 whitespace-pre" style={{ color: style.text }}>
|
{highlightedHtml !== undefined ? (
|
||||||
{line.content ?? ' '}
|
<span
|
||||||
</span>
|
className="flex-1 whitespace-pre"
|
||||||
|
style={{ color: style.text }}
|
||||||
|
dangerouslySetInnerHTML={{ __html: highlightedHtml || ' ' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="flex-1 whitespace-pre" style={{ color: style.text }}>
|
||||||
|
{line.content ?? ' '}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -330,6 +341,7 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
|
||||||
newString,
|
newString,
|
||||||
maxHeight = 'max-h-96',
|
maxHeight = 'max-h-96',
|
||||||
tokenCount,
|
tokenCount,
|
||||||
|
syntaxHighlight = false,
|
||||||
}): React.JSX.Element => {
|
}): React.JSX.Element => {
|
||||||
// Compute diff
|
// Compute diff
|
||||||
const oldLines = oldString.split(/\r?\n/);
|
const oldLines = oldString.split(/\r?\n/);
|
||||||
|
|
@ -337,6 +349,29 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
|
||||||
const diffLines = generateDiff(oldLines, newLines);
|
const diffLines = generateDiff(oldLines, newLines);
|
||||||
const stats = computeStats(diffLines);
|
const stats = computeStats(diffLines);
|
||||||
|
|
||||||
|
// Syntax highlighting: build a map from content line → highlighted HTML
|
||||||
|
const highlightMap = useMemo(() => {
|
||||||
|
if (!syntaxHighlight) return null;
|
||||||
|
const oldHtml = highlightLines(oldString, fileName);
|
||||||
|
const newHtml = highlightLines(newString, fileName);
|
||||||
|
// Map each diff line to its highlighted HTML by tracking old/new line indices
|
||||||
|
const map = new Map<number, string>();
|
||||||
|
let oldIdx = 0;
|
||||||
|
let newIdx = 0;
|
||||||
|
for (let i = 0; i < diffLines.length; i++) {
|
||||||
|
const line = diffLines[i];
|
||||||
|
if (line.type === 'removed') {
|
||||||
|
map.set(i, oldHtml[oldIdx++] ?? '');
|
||||||
|
} else if (line.type === 'added') {
|
||||||
|
map.set(i, newHtml[newIdx++] ?? '');
|
||||||
|
} else {
|
||||||
|
map.set(i, oldHtml[oldIdx++] ?? '');
|
||||||
|
newIdx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}, [syntaxHighlight, oldString, newString, fileName, diffLines]);
|
||||||
|
|
||||||
// Infer language from file extension
|
// Infer language from file extension
|
||||||
const detectedLanguage = inferLanguage(fileName);
|
const detectedLanguage = inferLanguage(fileName);
|
||||||
|
|
||||||
|
|
@ -396,7 +431,7 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
|
||||||
<div className={`overflow-auto font-mono text-xs ${maxHeight}`}>
|
<div className={`overflow-auto font-mono text-xs ${maxHeight}`}>
|
||||||
<div className="inline-block min-w-full">
|
<div className="inline-block min-w-full">
|
||||||
{diffLines.map((line, index) => (
|
{diffLines.map((line, index) => (
|
||||||
<DiffLineRow key={index} line={line} />
|
<DiffLineRow key={index} line={line} highlightedHtml={highlightMap?.get(index)} />
|
||||||
))}
|
))}
|
||||||
{diffLines.length === 0 && (
|
{diffLines.length === 0 && (
|
||||||
<div className="px-3 py-2 italic" style={{ color: COLOR_TEXT_MUTED }}>
|
<div className="px-3 py-2 italic" style={{ color: COLOR_TEXT_MUTED }}>
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,7 @@ export const ToolApprovalDiffPreview: React.FC<ToolApprovalDiffPreviewProps> = (
|
||||||
oldString={diff.oldString}
|
oldString={diff.oldString}
|
||||||
newString={diff.newString}
|
newString={diff.newString}
|
||||||
maxHeight="max-h-[300px]"
|
maxHeight="max-h-[300px]"
|
||||||
|
syntaxHighlight
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -288,21 +288,21 @@ export const ToolApprovalSheet: React.FC = () => {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => void updateToolApprovalSettings({ autoAllowAll: true })}
|
onClick={() => void updateToolApprovalSettings({ autoAllowAll: true })}
|
||||||
className="rounded-md px-2.5 py-1.5 text-[10px] transition-colors"
|
className="rounded-md border px-3.5 py-1.5 text-xs font-medium transition-colors"
|
||||||
style={{
|
style={{
|
||||||
color: 'var(--color-text-muted)',
|
color: 'var(--color-text-muted)',
|
||||||
backgroundColor: 'var(--color-surface-raised)',
|
borderColor: 'var(--color-border-emphasis)',
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
Object.assign(e.currentTarget.style, {
|
Object.assign(e.currentTarget.style, {
|
||||||
color: 'var(--color-text-secondary)',
|
color: 'var(--color-text-secondary)',
|
||||||
backgroundColor: 'var(--color-border-emphasis)',
|
backgroundColor: 'var(--color-surface-raised)',
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
Object.assign(e.currentTarget.style, {
|
Object.assign(e.currentTarget.style, {
|
||||||
color: 'var(--color-text-muted)',
|
color: 'var(--color-text-muted)',
|
||||||
backgroundColor: 'var(--color-surface-raised)',
|
backgroundColor: 'transparent',
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue