- Introduced a new function to notify task owners on comments from other members, improving communication and responsiveness. - Updated existing comment handling functions to include logic for notifying owners based on comment type and author. - Added tests to validate the new notification behavior, ensuring that owners are correctly alerted for relevant comments while avoiding unnecessary notifications for self-comments. - Refactored task management logic to streamline comment processing and notification handling.
155 lines
4.8 KiB
TypeScript
155 lines
4.8 KiB
TypeScript
import { useMemo } from 'react';
|
|
|
|
import { highlightLines } from '@renderer/utils/syntaxHighlighter';
|
|
import { diffLines } from 'diff';
|
|
|
|
import type { FileChangeSummary, SnippetDiff } from '@shared/types/review';
|
|
|
|
// =============================================================================
|
|
// Types
|
|
// =============================================================================
|
|
|
|
interface ReviewDiffContentProps {
|
|
file: FileChangeSummary;
|
|
}
|
|
|
|
interface DiffLine {
|
|
type: 'added' | 'removed' | 'unchanged';
|
|
html: string;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Helpers
|
|
// =============================================================================
|
|
|
|
/** Build highlighted diff lines by mapping diff parts onto pre-highlighted old/new lines. */
|
|
function buildHighlightedDiffLines(snippet: SnippetDiff, fileName: string): DiffLine[] {
|
|
const isFullNew = snippet.type === 'write-new' || snippet.type === 'write-update';
|
|
const oldCode = isFullNew ? '' : snippet.oldString;
|
|
const diffResult = diffLines(oldCode, snippet.newString);
|
|
|
|
const oldHighlighted = highlightLines(oldCode, fileName);
|
|
const newHighlighted = highlightLines(snippet.newString, fileName);
|
|
|
|
const result: DiffLine[] = [];
|
|
let oldIdx = 0;
|
|
let newIdx = 0;
|
|
|
|
for (const part of diffResult) {
|
|
const lineCount = part.value.replace(/\n$/, '').split('\n').length;
|
|
for (let i = 0; i < lineCount; i++) {
|
|
if (part.removed) {
|
|
result.push({
|
|
type: 'removed',
|
|
html: oldHighlighted[oldIdx++] ?? '',
|
|
});
|
|
} else if (part.added) {
|
|
result.push({
|
|
type: 'added',
|
|
html: newHighlighted[newIdx++] ?? '',
|
|
});
|
|
} else {
|
|
result.push({
|
|
type: 'unchanged',
|
|
html: oldHighlighted[oldIdx++] ?? '',
|
|
});
|
|
newIdx++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// =============================================================================
|
|
// SnippetDiffView
|
|
// =============================================================================
|
|
|
|
const SnippetDiffView = ({
|
|
snippet,
|
|
index,
|
|
fileName,
|
|
}: {
|
|
snippet: SnippetDiff;
|
|
index: number;
|
|
fileName: string;
|
|
}) => {
|
|
const lines = useMemo(() => buildHighlightedDiffLines(snippet, fileName), [snippet, fileName]);
|
|
|
|
const toolLabel =
|
|
snippet.type === 'write-new'
|
|
? 'New file'
|
|
: snippet.type === 'write-update'
|
|
? 'Full rewrite'
|
|
: snippet.type === 'multi-edit'
|
|
? 'Multi-edit'
|
|
: 'Edit';
|
|
|
|
return (
|
|
<div className="overflow-hidden rounded-lg border border-border">
|
|
{/* Snippet header */}
|
|
<div className="flex items-center justify-between border-b border-border bg-surface-raised px-3 py-1.5">
|
|
<span className="text-xs text-text-muted">
|
|
#{index + 1} {toolLabel}
|
|
</span>
|
|
<span className="text-xs text-text-muted">
|
|
{snippet.timestamp ? new Date(snippet.timestamp).toLocaleTimeString() : ''}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Diff lines with syntax highlighting (hljs HTML — safe, all input is escaped) */}
|
|
<div className="overflow-x-auto font-mono text-xs leading-5">
|
|
{lines.map((line, i) => {
|
|
let bgClass = '';
|
|
let prefix = ' ';
|
|
|
|
if (line.type === 'added') {
|
|
bgClass = 'bg-[var(--diff-added-bg,rgba(46,160,67,0.15))]';
|
|
prefix = '+';
|
|
} else if (line.type === 'removed') {
|
|
bgClass = 'bg-[var(--diff-removed-bg,rgba(248,81,73,0.15))]';
|
|
prefix = '-';
|
|
}
|
|
|
|
return (
|
|
<div key={i} className={`flex px-3 ${bgClass}`}>
|
|
<span className="inline-block w-4 shrink-0 select-none text-text-muted opacity-50">
|
|
{prefix}
|
|
</span>
|
|
{/* highlight.js escapes all input text — only produces <span class="hljs-*"> tags */}
|
|
<span
|
|
className="whitespace-pre text-text-secondary"
|
|
dangerouslySetInnerHTML={{ __html: line.html }}
|
|
/>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// =============================================================================
|
|
// ReviewDiffContent
|
|
// =============================================================================
|
|
|
|
export const ReviewDiffContent = ({ file }: ReviewDiffContentProps) => {
|
|
const nonErrorSnippets = useMemo(() => file.snippets.filter((s) => !s.isError), [file.snippets]);
|
|
|
|
return (
|
|
<div className="space-y-4 p-4">
|
|
{nonErrorSnippets.map((snippet, index) => (
|
|
<SnippetDiffView
|
|
key={`${snippet.toolUseId}-${index}`}
|
|
snippet={snippet}
|
|
index={index}
|
|
fileName={file.relativePath}
|
|
/>
|
|
))}
|
|
|
|
{nonErrorSnippets.length === 0 && (
|
|
<div className="py-8 text-center text-sm text-text-muted">No changes to display</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|