}
badge={taskChangesFiles ? taskChangesFiles.length : undefined}
defaultOpen={!!taskChangesFiles && taskChangesFiles.length > 0}
>
@@ -296,7 +324,7 @@ export const TaskDetailDialog = ({
type="button"
className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs transition-colors hover:bg-[var(--color-surface-raised)]"
onClick={() => {
- onClose();
+ handleClose();
onViewChanges(currentTask.id, file.filePath);
}}
>
@@ -447,6 +475,7 @@ export const TaskDetailDialog = ({
{/* Comments */}
}
badge={
(currentTask.comments?.length ?? 0) > 0
? (currentTask.comments?.length ?? 0)
@@ -460,9 +489,20 @@ export const TaskDetailDialog = ({
comments={currentTask.comments ?? []}
members={members}
hideHeader
+ hideInput
+ onReply={handleReply}
/>
+ {/* Comment input — always visible outside the collapsible section */}
+
+
{onDeleteTask && currentTask ? (
diff --git a/src/renderer/components/team/review/ChangeReviewDialog.tsx b/src/renderer/components/team/review/ChangeReviewDialog.tsx
index bc639052..320e697d 100644
--- a/src/renderer/components/team/review/ChangeReviewDialog.tsx
+++ b/src/renderer/components/team/review/ChangeReviewDialog.tsx
@@ -539,6 +539,7 @@ export const ChangeReviewDialog = ({
fileContentsLoading={fileContentsLoading}
viewedSet={viewedSet}
editedContents={editedContents}
+ hunkDecisions={hunkDecisions}
fileDecisions={fileDecisions}
collapseUnchanged={collapseUnchanged}
applying={applying}
diff --git a/src/renderer/components/team/review/CodeMirrorDiffUtils.ts b/src/renderer/components/team/review/CodeMirrorDiffUtils.ts
index 5b876ae0..aed9aa95 100644
--- a/src/renderer/components/team/review/CodeMirrorDiffUtils.ts
+++ b/src/renderer/components/team/review/CodeMirrorDiffUtils.ts
@@ -91,4 +91,43 @@ export const mirrorEditsAfterResolve = EditorState.transactionExtender.of((tr) =
return { effects: originalDocChangeEffect(tr.startState, tr.changes) };
});
+/**
+ * Replay persisted per-hunk decisions on a freshly mounted editor.
+ * Processes chunks in reverse order to preserve earlier chunk positions.
+ */
+export function replayHunkDecisions(
+ view: EditorView,
+ filePath: string,
+ hunkDecisions: Record
+): void {
+ const result = getChunks(view.state);
+ if (!result || result.chunks.length === 0) return;
+
+ // Collect decisions that need replaying
+ const toReplay: { index: number; decision: 'accepted' | 'rejected' }[] = [];
+ for (let i = 0; i < result.chunks.length; i++) {
+ const key = `${filePath}:${i}`;
+ const d = hunkDecisions[key];
+ if (d === 'accepted' || d === 'rejected') {
+ toReplay.push({ index: i, decision: d });
+ }
+ }
+
+ if (toReplay.length === 0) return;
+
+ // Process in reverse order — removing a later chunk doesn't shift earlier positions
+ for (let i = toReplay.length - 1; i >= 0; i--) {
+ const { index, decision } = toReplay[i];
+ const currentChunks = getChunks(view.state);
+ if (!currentChunks || index >= currentChunks.chunks.length) continue;
+
+ const chunk = currentChunks.chunks[index];
+ if (decision === 'accepted') {
+ acceptChunk(view, chunk.fromB);
+ } else {
+ rejectChunk(view, chunk.fromB);
+ }
+ }
+}
+
export { acceptChunk, getChunks, rejectChunk };
diff --git a/src/renderer/components/team/review/ContinuousScrollView.tsx b/src/renderer/components/team/review/ContinuousScrollView.tsx
index f1bab01e..9c604841 100644
--- a/src/renderer/components/team/review/ContinuousScrollView.tsx
+++ b/src/renderer/components/team/review/ContinuousScrollView.tsx
@@ -3,7 +3,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useLazyFileContent } from '@renderer/hooks/useLazyFileContent';
import { useVisibleFileSection } from '@renderer/hooks/useVisibleFileSection';
-import { acceptAllChunks, rejectAllChunks } from './CodeMirrorDiffUtils';
+import { acceptAllChunks, rejectAllChunks, replayHunkDecisions } from './CodeMirrorDiffUtils';
import { FileSectionDiff } from './FileSectionDiff';
import { FileSectionHeader } from './FileSectionHeader';
import { FileSectionPlaceholder } from './FileSectionPlaceholder';
@@ -18,6 +18,7 @@ interface ContinuousScrollViewProps {
fileContentsLoading: Record;
viewedSet: Set;
editedContents: Record;
+ hunkDecisions: Record;
fileDecisions: Record;
collapseUnchanged: boolean;
applying: boolean;
@@ -48,6 +49,7 @@ export const ContinuousScrollView = ({
fileContentsLoading,
viewedSet,
editedContents,
+ hunkDecisions,
fileDecisions,
collapseUnchanged,
applying,
@@ -114,10 +116,12 @@ export const ContinuousScrollView = ({
[registerFileSectionRef, registerLazyRef]
);
- // Ref to avoid stale closure — fileDecisions changes frequently
+ // Refs to avoid stale closures — decisions change frequently
const fileDecisionsRef = useRef(fileDecisions);
+ const hunkDecisionsRef = useRef(hunkDecisions);
useEffect(() => {
fileDecisionsRef.current = fileDecisions;
+ hunkDecisionsRef.current = hunkDecisions;
});
const handleEditorViewReady = useCallback(
@@ -125,18 +129,21 @@ export const ContinuousScrollView = ({
if (view) {
editorViewMapRef.current.set(filePath, view);
- // Sync pre-existing "Accept All" / "Reject All" decisions to newly mounted editors.
- // When Accept All runs, store is updated for ALL files, but CM only updates mounted ones.
- // Lazily-loaded files mount later and need their CM state synced with the store.
- const decision = fileDecisionsRef.current[filePath];
- if (decision === 'accepted' || decision === 'rejected') {
+ const fileDecision = fileDecisionsRef.current[filePath];
+ if (fileDecision === 'accepted' || fileDecision === 'rejected') {
+ // Sync file-level "Accept All" / "Reject All" decisions
requestAnimationFrame(() => {
- if (decision === 'accepted') {
+ if (fileDecision === 'accepted') {
acceptAllChunks(view);
} else {
rejectAllChunks(view);
}
});
+ } else {
+ // Replay individual per-hunk decisions persisted from previous session
+ requestAnimationFrame(() => {
+ replayHunkDecisions(view, filePath, hunkDecisionsRef.current);
+ });
}
} else {
editorViewMapRef.current.delete(filePath);
diff --git a/src/renderer/components/team/review/ReviewFileTree.tsx b/src/renderer/components/team/review/ReviewFileTree.tsx
index 44fb38d7..dfb7a831 100644
--- a/src/renderer/components/team/review/ReviewFileTree.tsx
+++ b/src/renderer/components/team/review/ReviewFileTree.tsx
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { cn } from '@renderer/lib/utils';
import { useStore } from '@renderer/store';
import {
@@ -7,6 +8,7 @@ import {
ChevronRight,
Circle,
CircleDot,
+ Eye,
File,
Folder,
FolderOpen,
@@ -101,18 +103,36 @@ function getFileStatus(
return 'mixed';
}
+const statusLabels: Record = {
+ accepted: 'All changes accepted',
+ rejected: 'All changes rejected',
+ mixed: 'Partially reviewed',
+ pending: 'Pending review',
+};
+
const FileStatusIcon = ({ status }: { status: FileStatus }): JSX.Element => {
- switch (status) {
- case 'accepted':
- return ;
- case 'rejected':
- return ;
- case 'mixed':
- return ;
- case 'pending':
- default:
- return ;
- }
+ const icon = (() => {
+ switch (status) {
+ case 'accepted':
+ return ;
+ case 'rejected':
+ return ;
+ case 'mixed':
+ return ;
+ case 'pending':
+ default:
+ return ;
+ }
+ })();
+
+ return (
+
+
+ {icon}
+
+ {statusLabels[status]}
+
+ );
};
const TreeItem = ({
@@ -123,8 +143,6 @@ const TreeItem = ({
depth,
hunkDecisions,
viewedSet,
- onMarkViewed,
- onUnmarkViewed,
collapsedFolders,
onToggleFolder,
}: {
@@ -135,8 +153,6 @@ const TreeItem = ({
depth: number;
hunkDecisions: Record;
viewedSet?: Set;
- onMarkViewed?: (filePath: string) => void;
- onUnmarkViewed?: (filePath: string) => void;
collapsedFolders: Set;
onToggleFolder: (fullPath: string) => void;
}): JSX.Element => {
@@ -160,22 +176,15 @@ const TreeItem = ({
>
- {viewedSet && (
- {
- e.stopPropagation();
- if (e.target.checked) {
- onMarkViewed?.(node.file!.filePath);
- } else {
- onUnmarkViewed?.(node.file!.filePath);
- }
- }}
- onClick={(e) => e.stopPropagation()}
- className="size-3 shrink-0 rounded border-zinc-600 accent-green-500"
- aria-label={`Mark ${node.name} as viewed`}
- />
+ {viewedSet && viewedSet.has(node.file.filePath) && (
+
+
+
+
+
+
+ Viewed
+
)}
@@ -277,8 +284,6 @@ export const ReviewFileTree = ({
selectedFilePath,
onSelectFile,
viewedSet,
- onMarkViewed,
- onUnmarkViewed,
activeFilePath,
}: ReviewFileTreeProps): JSX.Element => {
const hunkDecisions = useStore((state) => state.hunkDecisions);
@@ -343,8 +348,6 @@ export const ReviewFileTree = ({
depth={0}
hunkDecisions={hunkDecisions}
viewedSet={viewedSet}
- onMarkViewed={onMarkViewed}
- onUnmarkViewed={onUnmarkViewed}
collapsedFolders={collapsedFolders}
onToggleFolder={toggleFolder}
/>