Skip to content

Commit cc44f6a

Browse files
authored
feat: Add code suggestions, inline editing, and refactor DiffViewer (#198)
* ✨ Add code suggestions, inline editing, and refactor DiffViewer - GitHub-style suggestion blocks with syntax-highlighted diff view - Inline annotation editing via toolbar (edit button on inline comments) - Expandable two-pane code modal for writing suggestions - Inline markdown rendering (bold, italic, code) in review comments - Original code extraction from unified diff patches - Decompose DiffViewer (615→200 lines) into focused components: FileHeader, InlineAnnotation, SuggestionBlock, SuggestionDiff, SuggestionModal, AnnotationToolbar, HighlightedCode - Extract hooks: useAnnotationToolbar, useTabIndent - Extract utils: detectLanguage, formatLineRange, patchParser, renderInlineMarkdown - Fix suggestedCode trim stripping meaningful indentation - Fix stale closure in useTabIndent consumers * 🐛 Fix annotation edit field overwrite and add IME composition guard - Conditionally spread only defined fields in handleEditAnnotation to prevent undefined values from wiping sibling fields - Add !e.nativeEvent.isComposing guard to Cmd/Ctrl+Enter handlers in comment and suggested-code textareas for CJK input support
1 parent c4f3479 commit cc44f6a

20 files changed

Lines changed: 1215 additions & 269 deletions

bun.lock

Lines changed: 5 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/review-editor/App.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { storage } from '@plannotator/ui/utils/storage';
88
import { CompletionOverlay } from '@plannotator/ui/components/CompletionOverlay';
99
import { getIdentity } from '@plannotator/ui/utils/identity';
1010
import { getAgentSwitchSettings, getEffectiveAgentName } from '@plannotator/ui/utils/agentSwitch';
11-
import { CodeAnnotation, CodeAnnotationType, SelectedLineRange, DiffAnnotationMetadata } from '@plannotator/ui/types';
11+
import { CodeAnnotation, CodeAnnotationType, SelectedLineRange } from '@plannotator/ui/types';
1212
import { useResizablePanel } from '@plannotator/ui/hooks/useResizablePanel';
1313
import { ResizeHandle } from '@plannotator/ui/components/ResizeHandle';
1414
import { DiffViewer } from './components/DiffViewer';
@@ -255,7 +255,8 @@ const ReviewApp: React.FC = () => {
255255
const handleAddAnnotation = useCallback((
256256
type: CodeAnnotationType,
257257
text?: string,
258-
suggestedCode?: string
258+
suggestedCode?: string,
259+
originalCode?: string
259260
) => {
260261
if (!pendingSelection || !files[activeFileIndex]) return;
261262

@@ -272,6 +273,7 @@ const ReviewApp: React.FC = () => {
272273
side: pendingSelection.side === 'additions' ? 'new' : 'old',
273274
text,
274275
suggestedCode,
276+
originalCode,
275277
createdAt: Date.now(),
276278
author: identity,
277279
};
@@ -280,6 +282,23 @@ const ReviewApp: React.FC = () => {
280282
setPendingSelection(null);
281283
}, [pendingSelection, files, activeFileIndex, identity]);
282284

285+
// Edit annotation
286+
const handleEditAnnotation = useCallback((
287+
id: string,
288+
text?: string,
289+
suggestedCode?: string,
290+
originalCode?: string
291+
) => {
292+
setAnnotations(prev => prev.map(ann =>
293+
ann.id === id ? {
294+
...ann,
295+
...(text !== undefined && { text }),
296+
...(suggestedCode !== undefined && { suggestedCode }),
297+
...(originalCode !== undefined && { originalCode }),
298+
} : ann
299+
));
300+
}, []);
301+
283302
// Delete annotation
284303
const handleDeleteAnnotation = useCallback((id: string) => {
285304
setAnnotations(prev => prev.filter(a => a.id !== id));
@@ -755,6 +774,7 @@ const ReviewApp: React.FC = () => {
755774
pendingSelection={pendingSelection}
756775
onLineSelection={handleLineSelection}
757776
onAddAnnotation={handleAddAnnotation}
777+
onEditAnnotation={handleEditAnnotation}
758778
onSelectAnnotation={handleSelectAnnotation}
759779
onDeleteAnnotation={handleDeleteAnnotation}
760780
isViewed={viewedFiles.has(activeFile.path)}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import React from 'react';
2+
import { ToolbarState } from '../hooks/useAnnotationToolbar';
3+
import { useTabIndent } from '../hooks/useTabIndent';
4+
import { formatLineRange } from '../utils/formatLineRange';
5+
6+
interface AnnotationToolbarProps {
7+
toolbarState: ToolbarState;
8+
toolbarRef: React.RefObject<HTMLDivElement>;
9+
commentText: string;
10+
setCommentText: (text: string) => void;
11+
suggestedCode: string;
12+
setSuggestedCode: React.Dispatch<React.SetStateAction<string>>;
13+
showSuggestedCode: boolean;
14+
setShowSuggestedCode: (show: boolean) => void;
15+
isEditing?: boolean;
16+
setShowCodeModal: (show: boolean) => void;
17+
onSubmit: () => void;
18+
onDismiss: () => void;
19+
onCancel: () => void;
20+
}
21+
22+
/** Floating comment input form that appears after line selection */
23+
export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
24+
toolbarState,
25+
toolbarRef,
26+
commentText,
27+
setCommentText,
28+
suggestedCode,
29+
setSuggestedCode,
30+
showSuggestedCode,
31+
setShowSuggestedCode,
32+
isEditing = false,
33+
setShowCodeModal,
34+
onSubmit,
35+
onDismiss,
36+
onCancel,
37+
}) => {
38+
const handleTabIndent = useTabIndent(setSuggestedCode);
39+
40+
return (
41+
<div
42+
ref={toolbarRef}
43+
className="review-toolbar"
44+
style={{
45+
position: 'fixed',
46+
top: Math.min(toolbarState.position.top, window.innerHeight - 200),
47+
left: Math.max(150, Math.min(toolbarState.position.left, window.innerWidth - 150)),
48+
transform: 'translateX(-50%)',
49+
zIndex: 1000,
50+
}}
51+
>
52+
<div className="w-80">
53+
<div className="flex items-center justify-between mb-2">
54+
<span className="text-xs text-muted-foreground">
55+
{isEditing ? 'Edit annotation' : formatLineRange(toolbarState.range.start, toolbarState.range.end)}
56+
</span>
57+
<button
58+
onClick={onCancel}
59+
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
60+
title="Cancel"
61+
>
62+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
63+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
64+
</svg>
65+
</button>
66+
</div>
67+
68+
<textarea
69+
value={commentText}
70+
onChange={(e) => setCommentText(e.target.value)}
71+
placeholder="Leave feedback..."
72+
className="w-full px-3 py-2 bg-muted rounded-lg text-xs resize-none focus:outline-none focus:ring-1 focus:ring-primary/50"
73+
rows={3}
74+
autoFocus
75+
onKeyDown={(e) => {
76+
if (e.key === 'Escape') {
77+
onDismiss();
78+
} else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && !e.nativeEvent.isComposing) {
79+
onSubmit();
80+
}
81+
}}
82+
/>
83+
84+
{/* Optional suggested code section */}
85+
{showSuggestedCode ? (
86+
<div className="mt-2">
87+
<div className="flex items-center justify-between mb-1">
88+
<span className="text-[10px] text-muted-foreground">Suggested code</span>
89+
<button
90+
onClick={() => setShowCodeModal(true)}
91+
className="p-0.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
92+
title="Expand editor"
93+
>
94+
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
95+
<path strokeLinecap="round" strokeLinejoin="round" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5v-4m0 4h-4m4 0l-5-5" />
96+
</svg>
97+
</button>
98+
</div>
99+
<textarea
100+
value={suggestedCode}
101+
onChange={(e) => setSuggestedCode(e.target.value)}
102+
placeholder="Enter code suggestion..."
103+
className="suggested-code-input"
104+
rows={4}
105+
autoFocus
106+
spellCheck={false}
107+
onKeyDown={(e) => {
108+
if (e.key === 'Tab') {
109+
handleTabIndent(e);
110+
} else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && !e.nativeEvent.isComposing) {
111+
onSubmit();
112+
}
113+
}}
114+
/>
115+
</div>
116+
) : (
117+
<button
118+
onClick={() => setShowSuggestedCode(true)}
119+
className="mt-2 text-xs text-muted-foreground hover:text-foreground flex items-center gap-1 transition-colors"
120+
>
121+
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
122+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
123+
</svg>
124+
Add suggested code
125+
</button>
126+
)}
127+
128+
<div className="flex justify-end gap-2 mt-3">
129+
<button
130+
onClick={onSubmit}
131+
disabled={!commentText.trim() && !suggestedCode.trim()}
132+
className="review-toolbar-btn primary disabled:opacity-50 disabled:cursor-not-allowed"
133+
>
134+
{isEditing ? 'Update' : 'Add Comment'}
135+
</button>
136+
</div>
137+
</div>
138+
</div>
139+
);
140+
};

0 commit comments

Comments
 (0)