Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 19 additions & 9 deletions packages/review-editor/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ const ReviewApp: React.FC = () => {
const [isPanelOpen, setIsPanelOpen] = useState(true);
const [copyFeedback, setCopyFeedback] = useState<string | null>(null);
const [viewedFiles, setViewedFiles] = useState<Set<string>>(new Set());
const [hideViewedFiles, setHideViewedFiles] = useState(false);
const [origin, setOrigin] = useState<'opencode' | 'claude-code' | null>(null);
const [diffType, setDiffType] = useState<string>('uncommitted');
const [gitContext, setGitContext] = useState<GitContext | null>(null);
Expand All @@ -154,14 +155,6 @@ const ReviewApp: React.FC = () => {

const identity = useMemo(() => getIdentity(), []);

// Mark file as viewed when it becomes active
useEffect(() => {
const activeFile = files[activeFileIndex];
if (activeFile && !viewedFiles.has(activeFile.path)) {
setViewedFiles(prev => new Set([...prev, activeFile.path]));
}
}, [activeFileIndex, files]);

// Global keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
Expand Down Expand Up @@ -294,11 +287,23 @@ const ReviewApp: React.FC = () => {
// Switch file - clears pending selection to avoid invalid line ranges
const handleFileSwitch = useCallback((index: number) => {
if (index !== activeFileIndex) {
setPendingSelection(null); // Clear selection when switching files
setPendingSelection(null);
setActiveFileIndex(index);
}
}, [activeFileIndex]);

const handleToggleViewed = useCallback((filePath: string) => {
setViewedFiles(prev => {
const next = new Set(prev);
if (next.has(filePath)) {
next.delete(filePath);
} else {
next.add(filePath);
}
return next;
});
}, []);

// Switch diff type (uncommitted, staged, last-commit, branch, etc.)
const handleDiffSwitch = useCallback(async (newDiffType: string) => {
if (newDiffType === diffType || newDiffType === 'separator') return;
Expand Down Expand Up @@ -712,6 +717,9 @@ const ReviewApp: React.FC = () => {
onSelectFile={handleFileSwitch}
annotations={annotations}
viewedFiles={viewedFiles}
onToggleViewed={handleToggleViewed}
hideViewedFiles={hideViewedFiles}
onToggleHideViewed={() => setHideViewedFiles(prev => !prev)}
enableKeyboardNav={!showExportModal}
diffOptions={gitContext?.diffOptions}
activeDiffType={diffType}
Expand All @@ -734,6 +742,8 @@ const ReviewApp: React.FC = () => {
onAddAnnotation={handleAddAnnotation}
onSelectAnnotation={handleSelectAnnotation}
onDeleteAnnotation={handleDeleteAnnotation}
isViewed={viewedFiles.has(activeFile.path)}
onToggleViewed={() => handleToggleViewed(activeFile.path)}
/>
) : (
<div className="h-full flex items-center justify-center">
Expand Down
84 changes: 56 additions & 28 deletions packages/review-editor/components/DiffViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ interface DiffViewerProps {
onAddAnnotation: (type: CodeAnnotationType, text?: string, suggestedCode?: string) => void;
onSelectAnnotation: (id: string | null) => void;
onDeleteAnnotation: (id: string) => void;
isViewed?: boolean;
onToggleViewed?: () => void;
}

interface ToolbarState {
Expand All @@ -32,6 +34,8 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
onAddAnnotation,
onSelectAnnotation,
onDeleteAnnotation,
isViewed = false,
onToggleViewed,
}) => {
const { theme } = useTheme();
const containerRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -211,35 +215,59 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
{/* File header */}
<div className="sticky top-0 z-10 px-4 py-2 bg-card/95 backdrop-blur border-b border-border flex items-center justify-between">
<span className="font-mono text-sm text-foreground">{filePath}</span>
<button
onClick={async () => {
try {
await navigator.clipboard.writeText(patch);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
}}
className="text-xs text-muted-foreground hover:text-foreground px-2 py-1 rounded hover:bg-muted transition-colors flex items-center gap-1"
title="Copy this file's diff"
>
{copied ? (
<>
<svg className="w-3.5 h-3.5 text-success" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
Copied!
</>
) : (
<>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
Copy Diff
</>
<div className="flex items-center gap-2">
{onToggleViewed && (
<button
onClick={onToggleViewed}
className={`text-xs px-2 py-1 rounded transition-colors flex items-center gap-1 ${
isViewed
? 'bg-success/15 text-success'
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
}`}
title={isViewed ? "Mark as not viewed" : "Mark as viewed"}
>
{isViewed ? (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
) : (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<circle cx="12" cy="12" r="9" />
</svg>
)}
Viewed
</button>
)}
</button>
<button
onClick={async () => {
try {
await navigator.clipboard.writeText(patch);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
}}
className="text-xs text-muted-foreground hover:text-foreground px-2 py-1 rounded hover:bg-muted transition-colors flex items-center gap-1"
title="Copy this file's diff"
>
{copied ? (
<>
<svg className="w-3.5 h-3.5 text-success" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
Copied!
</>
) : (
<>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
Copy Diff
</>
)}
</button>
</div>
</div>

{/* Diff content */}
Expand Down
67 changes: 53 additions & 14 deletions packages/review-editor/components/FileTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,13 @@ interface FileTreeProps {
onSelectFile: (index: number) => void;
annotations: CodeAnnotation[];
viewedFiles: Set<string>;
onToggleViewed?: (filePath: string) => void;
hideViewedFiles?: boolean;
onToggleHideViewed?: () => void;
enableKeyboardNav?: boolean;
/** Available diff options for the dropdown */
diffOptions?: DiffOption[];
/** Currently selected diff type */
activeDiffType?: string;
/** Callback when user selects a different diff */
onSelectDiff?: (diffType: string) => void;
/** Whether diff is currently loading */
isLoadingDiff?: boolean;
}

Expand All @@ -37,6 +36,9 @@ export const FileTree: React.FC<FileTreeProps> = ({
onSelectFile,
annotations,
viewedFiles,
onToggleViewed,
hideViewedFiles = false,
onToggleHideViewed,
enableKeyboardNav = true,
diffOptions,
activeDiffType,
Expand Down Expand Up @@ -87,9 +89,29 @@ export const FileTree: React.FC<FileTreeProps> = ({
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
Files
</span>
<span className="text-xs text-muted-foreground bg-muted px-1.5 py-0.5 rounded">
{files.length}
</span>
<div className="flex items-center gap-1.5">
<span className="text-xs text-muted-foreground">
{viewedFiles.size}/{files.length}
</span>
{onToggleHideViewed && (
<button
onClick={onToggleHideViewed}
className={`p-1 rounded transition-colors ${hideViewedFiles ? 'bg-primary/15 text-primary' : 'hover:bg-muted text-muted-foreground'}`}
title={hideViewedFiles ? "Show viewed files" : "Hide viewed files"}
>
{hideViewedFiles ? (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
</svg>
) : (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
)}
</button>
)}
</div>
</div>
</div>

Expand Down Expand Up @@ -140,20 +162,37 @@ export const FileTree: React.FC<FileTreeProps> = ({
const isViewed = viewedFiles.has(file.path);
const fileName = file.path.split('/').pop() || file.path;

if (hideViewedFiles && isViewed && !isActive) {
return null;
}

return (
<button
key={file.path}
onClick={() => onSelectFile(index)}
className={`file-tree-item w-full text-left group ${isActive ? 'active' : ''} ${annotationCount > 0 ? 'has-annotations' : ''}`}
>
<div className="flex items-center gap-1 flex-1 min-w-0">
<div className="flex items-center gap-1.5 flex-1 min-w-0">
<span
role="checkbox"
aria-checked={isViewed}
onClick={(e) => {
e.stopPropagation();
onToggleViewed?.(file.path);
}}
className="flex-shrink-0 p-0.5 rounded hover:bg-muted/50 cursor-pointer"
>
{isViewed ? (
<svg className="w-3.5 h-3.5 text-success" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
) : (
<svg className="w-3.5 h-3.5 text-muted-foreground opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<circle cx="12" cy="12" r="9" />
</svg>
)}
</span>
<span className="truncate">{fileName}</span>
{isViewed && (
<svg className="w-3 h-3 flex-shrink-0 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
)}
</div>
<div className="flex items-center gap-1.5 flex-shrink-0 text-[10px]">
{annotationCount > 0 && (
Expand Down