Skip to content

Commit 1df5f40

Browse files
backnotpropclaude
andauthored
feat: persist viewed files in code review drafts (#332) (#335)
Extend the draft auto-save system to include viewed file state alongside annotations. Viewed files are saved atomically in the same draft file, restored together via the same banner, and backward compatible with existing drafts (optional field, defaults to empty). Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d5b6bc7 commit 1df5f40

2 files changed

Lines changed: 29 additions & 12 deletions

File tree

packages/review-editor/App.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,13 +116,15 @@ const ReviewApp: React.FC = () => {
116116
// Auto-save code annotation drafts
117117
const { draftBanner, restoreDraft, dismissDraft } = useCodeAnnotationDraft({
118118
annotations,
119+
viewedFiles,
119120
isApiMode: !!origin,
120121
submitted: !!submitted,
121122
});
122123

123124
const handleRestoreDraft = useCallback(() => {
124125
const restored = restoreDraft();
125-
if (restored.length > 0) setAnnotations(restored);
126+
if (restored.annotations.length > 0) setAnnotations(restored.annotations);
127+
if (restored.viewedFiles.length > 0) setViewedFiles(new Set(restored.viewedFiles));
126128
}, [restoreDraft]);
127129

128130
// VS Code editor annotations (only polls when inside VS Code webview)
@@ -828,7 +830,12 @@ const ReviewApp: React.FC = () => {
828830
onClose={dismissDraft}
829831
onConfirm={handleRestoreDraft}
830832
title="Draft Recovered"
831-
message={draftBanner ? `Found ${draftBanner.count} annotation${draftBanner.count !== 1 ? 's' : ''} from ${draftBanner.timeAgo}. Would you like to restore them?` : ''}
833+
message={draftBanner ? (() => {
834+
const parts: string[] = [];
835+
if (draftBanner.count > 0) parts.push(`${draftBanner.count} annotation${draftBanner.count !== 1 ? 's' : ''}`);
836+
if (draftBanner.viewedCount > 0) parts.push(`${draftBanner.viewedCount} viewed file${draftBanner.viewedCount !== 1 ? 's' : ''}`);
837+
return `Found ${parts.join(' and ')} from ${draftBanner.timeAgo}. Would you like to restore them?`;
838+
})() : ''}
832839
confirmText="Restore"
833840
cancelText="Dismiss"
834841
showCancel

packages/ui/hooks/useCodeAnnotationDraft.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const DEBOUNCE_MS = 500;
1212

1313
interface DraftData {
1414
codeAnnotations: CodeAnnotation[];
15+
viewedFiles?: string[];
1516
ts: number;
1617
}
1718

@@ -28,22 +29,24 @@ function formatTimeAgo(ts: number): string {
2829

2930
interface UseCodeAnnotationDraftOptions {
3031
annotations: CodeAnnotation[];
32+
viewedFiles: Set<string>;
3133
isApiMode: boolean;
3234
submitted: boolean;
3335
}
3436

3537
interface UseCodeAnnotationDraftResult {
36-
draftBanner: { count: number; timeAgo: string } | null;
37-
restoreDraft: () => CodeAnnotation[];
38+
draftBanner: { count: number; viewedCount: number; timeAgo: string } | null;
39+
restoreDraft: () => { annotations: CodeAnnotation[]; viewedFiles: string[] };
3840
dismissDraft: () => void;
3941
}
4042

4143
export function useCodeAnnotationDraft({
4244
annotations,
45+
viewedFiles,
4346
isApiMode,
4447
submitted,
4548
}: UseCodeAnnotationDraftOptions): UseCodeAnnotationDraftResult {
46-
const [draftBanner, setDraftBanner] = useState<{ count: number; timeAgo: string } | null>(null);
49+
const [draftBanner, setDraftBanner] = useState<{ count: number; viewedCount: number; timeAgo: string } | null>(null);
4750
const draftDataRef = useRef<DraftData | null>(null);
4851
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
4952
const hasMountedRef = useRef(false);
@@ -58,11 +61,14 @@ export function useCodeAnnotationDraft({
5861
return res.json();
5962
})
6063
.then((data: DraftData | null) => {
61-
if (data?.codeAnnotations && Array.isArray(data.codeAnnotations) && data.codeAnnotations.length > 0) {
64+
const annotationCount = Array.isArray(data?.codeAnnotations) ? data.codeAnnotations.length : 0;
65+
const viewedCount = Array.isArray(data?.viewedFiles) ? data.viewedFiles.length : 0;
66+
if (annotationCount > 0 || viewedCount > 0) {
6267
draftDataRef.current = data;
6368
setDraftBanner({
64-
count: data.codeAnnotations.length,
65-
timeAgo: formatTimeAgo(data.ts || 0),
69+
count: annotationCount,
70+
viewedCount,
71+
timeAgo: formatTimeAgo(data?.ts || 0),
6672
});
6773
}
6874
hasMountedRef.current = true;
@@ -72,17 +78,18 @@ export function useCodeAnnotationDraft({
7278
});
7379
}, [isApiMode]);
7480

75-
// Debounced auto-save on annotation changes
81+
// Debounced auto-save on annotation/viewed changes
7682
useEffect(() => {
7783
if (!isApiMode || submitted) return;
7884
if (!hasMountedRef.current) return;
79-
if (annotations.length === 0) return;
85+
if (annotations.length === 0 && viewedFiles.size === 0) return;
8086

8187
if (timerRef.current) clearTimeout(timerRef.current);
8288

8389
timerRef.current = setTimeout(() => {
8490
const payload: DraftData = {
8591
codeAnnotations: annotations,
92+
viewedFiles: [...viewedFiles],
8693
ts: Date.now(),
8794
};
8895

@@ -98,13 +105,16 @@ export function useCodeAnnotationDraft({
98105
return () => {
99106
if (timerRef.current) clearTimeout(timerRef.current);
100107
};
101-
}, [annotations, isApiMode, submitted]);
108+
}, [annotations, viewedFiles, isApiMode, submitted]);
102109

103110
const restoreDraft = useCallback(() => {
104111
const data = draftDataRef.current;
105112
setDraftBanner(null);
106113
draftDataRef.current = null;
107-
return data?.codeAnnotations ?? [];
114+
return {
115+
annotations: data?.codeAnnotations ?? [],
116+
viewedFiles: data?.viewedFiles ?? [],
117+
};
108118
}, []);
109119

110120
const dismissDraft = useCallback(() => {

0 commit comments

Comments
 (0)