- Diff viewer with inline comments and multi-line selection. -
-- Single line: Click the{' '} - + button on any line to add a - comment. -
-- Multi-line: Click and drag from one line to another to select a range, - then add your comment. -
-
+ {comment.codeContent}
+
+ )}
+
+ {/* Comment text */}
+ + {comment.annotation} +
+
+ {comment.codeContent}
+
+ )}
+
+ {/* Comment text */}
+ + {comment.annotation} +
+
diff --git a/apps/web/components/task/task-center-panel.tsx b/apps/web/components/task/task-center-panel.tsx
index d4855794d8..168ece2dac 100644
--- a/apps/web/components/task/task-center-panel.tsx
+++ b/apps/web/components/task/task-center-panel.tsx
@@ -308,6 +308,7 @@ export const TaskCenterPanel = memo(function TaskCenterPanel({
setSelectedDiff(null)}
+ onOpenFile={handleOpenFileFromChat}
/>
@@ -323,6 +324,10 @@ export const TaskCenterPanel = memo(function TaskCenterPanel({
onOpenFile={handleOpenFileFromChat}
showRequestChangesTooltip={showRequestChangesTooltip}
onRequestChangesTooltipDismiss={() => setShowRequestChangesTooltip(false)}
+ onSelectDiff={(path) => {
+ setSelectedDiff({ path });
+ setLeftTab('changes');
+ }}
/>
)
) : (
diff --git a/apps/web/components/task/task-changes-panel.tsx b/apps/web/components/task/task-changes-panel.tsx
index 0b09a00222..8dbfdaaf11 100644
--- a/apps/web/components/task/task-changes-panel.tsx
+++ b/apps/web/components/task/task-changes-panel.tsx
@@ -1,14 +1,7 @@
'use client';
import { memo, useMemo, useState } from 'react';
-import { DiffModeEnum, DiffView } from '@git-diff-view/react';
-import {
- IconArrowBackUp,
- IconCopy,
- IconLayoutColumns,
- IconLayoutRows,
-} from '@tabler/icons-react';
-import { useTheme } from 'next-themes';
+import { IconArrowBackUp } from '@tabler/icons-react';
import { Badge } from '@kandev/ui/badge';
import { Button } from '@kandev/ui/button';
import { SessionPanelContent } from '@kandev/ui/pannel-session';
@@ -23,71 +16,52 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from '@kandev/ui/alert-dialog';
-import { cn } from '@/lib/utils';
import { useToast } from '@/components/toast-provider';
-import { getLanguageFromPath } from '@/lib/languages';
-import { getLocalStorage, setLocalStorage } from '@/lib/local-storage';
import { useAppStore } from '@/components/state-provider';
import { useSessionGitStatus } from '@/hooks/domains/session/use-session-git-status';
import { useCumulativeDiff } from '@/hooks/domains/session/use-cumulative-diff';
import { useGitOperations } from '@/hooks/use-git-operations';
+import { FileDiffViewer } from '@/components/diff';
import type { FileInfo } from '@/lib/state/slices';
import type { SelectedDiff } from './task-layout';
type TaskChangesPanelProps = {
selectedDiff: SelectedDiff | null;
onClearSelected: () => void;
+ /** Callback to open file in editor */
+ onOpenFile?: (filePath: string) => void;
};
-const DEFAULT_DIFF_MODE: 'unified' | 'split' = 'unified';
-
/**
- * Normalize diff content for the @git-diff-view/react library.
+ * Normalize diff content by handling edge cases from the backend.
*
- * Handles two edge cases from the backend:
+ * Handles:
* 1. Multiple diffs concatenated with "--- Staged changes ---" separator
- * (we take the staged diff as it's what matters for the commit)
- * 2. Diffs missing the "diff --git" header line (for untracked files)
- *
- * The library expects each hunk to be a complete diff string with headers.
+ * 2. Diffs missing headers
*/
-function parseDiffIntoHunks(diffContent: string, filePath?: string): string[] {
- if (!diffContent || typeof diffContent !== 'string') return [];
+function normalizeDiffContent(diffContent: string): string {
+ if (!diffContent || typeof diffContent !== 'string') return '';
let trimmed = diffContent.trim();
- if (!trimmed) return [];
+ if (!trimmed) return '';
// Handle concatenated diffs with "--- Staged changes ---" separator
- // Take the staged diff (after the separator) as it's the authoritative one
const stagedSeparator = '--- Staged changes ---';
if (trimmed.includes(stagedSeparator)) {
const parts = trimmed.split(stagedSeparator);
- // Use the staged diff (second part) if available
trimmed = (parts[1] || parts[0]).trim();
}
- // If diff is missing the "diff --git" header, add it
- // This happens for untracked files where backend generates incomplete diff
- if (!trimmed.startsWith('diff --git') && trimmed.includes('@@')) {
- const path = filePath || 'file';
- const header = `diff --git a/${path} b/${path}\nnew file mode 100644\nindex 0000000..0000000\n`;
- trimmed = header + trimmed;
- }
-
- // Return as single hunk - the library handles parsing internally
- return [trimmed];
+ return trimmed;
}
const TaskChangesPanel = memo(function TaskChangesPanel({
selectedDiff,
onClearSelected,
+ onOpenFile,
}: TaskChangesPanelProps) {
- const [diffViewMode, setDiffViewMode] = useState<'unified' | 'split'>(() =>
- getLocalStorage('task-diff-view-mode', DEFAULT_DIFF_MODE)
- );
const [showDiscardDialog, setShowDiscardDialog] = useState(false);
- const { resolvedTheme } = useTheme();
const { toast } = useToast();
const activeSessionId = useAppStore((state) => state.tasks.activeSessionId);
const gitStatus = useSessionGitStatus(activeSessionId);
@@ -124,8 +98,6 @@ const TaskChangesPanel = memo(function TaskChangesPanel({
}));
}, [cumulativeDiff]);
- const diffModeEnum = diffViewMode === 'split' ? DiffModeEnum.Split : DiffModeEnum.Unified;
- const diffTheme = resolvedTheme === 'dark' ? 'dark' : 'light';
const selectedFile = selectedDiffPath && gitStatus?.files ? gitStatus.files[selectedDiffPath] : null;
// Use provided content if available, otherwise fall back to git status
const selectedDiffContent = selectedDiff?.content ?? selectedFile?.diff ?? '';
@@ -138,20 +110,16 @@ const TaskChangesPanel = memo(function TaskChangesPanel({
const hasAnyChanges = hasUncommitted || hasCommitted;
const handleDiscardClick = () => {
- console.log('Discard button clicked', { selectedDiffPath, canDiscard });
setShowDiscardDialog(true);
};
const handleDiscardConfirm = async () => {
if (!selectedDiffPath) {
- console.error('No selectedDiffPath');
return;
}
- console.log('Discarding changes for:', selectedDiffPath);
try {
const result = await discard([selectedDiffPath]);
- console.log('Discard result:', result);
if (result.success) {
toast({
title: 'Changes discarded',
@@ -167,7 +135,6 @@ const TaskChangesPanel = memo(function TaskChangesPanel({
});
}
} catch (error) {
- console.error('Discard error:', error);
toast({
title: 'Failed to discard changes',
description: error instanceof Error ? error.message : 'An unknown error occurred',
@@ -180,91 +147,23 @@ const TaskChangesPanel = memo(function TaskChangesPanel({
return (
- {/* Header toolbar */}
+ {/* Header */}
-
-
- {selectedDiffPath ?? 'All files'}
-
-
-
- {selectedDiffPath && (
+ {selectedDiffPath && (
+ <>
- Show all uncommitted changes
-
- )}
-
-
-
-
-
-
- Copy diff
-
-
-
-
-
-
-
-
- {canDiscard ? 'Discard changes' : hasProvidedContent ? 'Cannot discard committed changes' : 'Select an uncommitted file'}
-
-
-
-
-
-
-
-
-
- Unified view
+ Show all changes
@@ -273,39 +172,34 @@ const TaskChangesPanel = memo(function TaskChangesPanel({
type="button"
variant="ghost"
size="sm"
- className={cn(
- 'h-7 px-2 text-xs rounded-none cursor-pointer',
- diffViewMode === 'split' && 'bg-muted'
- )}
- onClick={() => {
- setDiffViewMode('split');
- setLocalStorage('task-diff-view-mode', 'split');
- }}
+ className="h-7 px-2 text-xs cursor-pointer"
+ disabled={!canDiscard || isDiscarding}
+ onClick={handleDiscardClick}
>
-
+
- Split view
+
+ {canDiscard ? 'Discard changes' : hasProvidedContent ? 'Cannot discard committed changes' : 'Select an uncommitted file'}
+
-
-
+ >
+ )}
{/* Diff content */}
{isSingleDiffSelected && selectedDiffContent ? (
// Render single file diff (from commit, snapshot, or uncommitted)
- // Key includes content length to force re-render when diff updates
-
) : cumulativeLoading ? (
@@ -326,29 +220,17 @@ const TaskChangesPanel = memo(function TaskChangesPanel({
{uncommittedFiles.map((file) => {
if (!file.diff) return null;
- const fileLang = getLanguageFromPath(file.path);
- // Include diff length in key to force re-render when content changes
- const diffKey = `uncommitted-${file.path}-${file.diff.length}`;
+ const normalizedDiff = normalizeDiffContent(file.diff);
+ const diffKey = `uncommitted-${file.path}-${normalizedDiff.length}`;
return (
-
-
- {file.path}
-
-
- +{file.additions}
- {' / '}
- -{file.deletions}
-
-
-
);
@@ -364,9 +246,8 @@ const TaskChangesPanel = memo(function TaskChangesPanel({