Adds DiffSplitView component for side-by-side diff mode, whitespace-only change filtering, inline review comments with thread/gutter cell UI, diff preferences persistence, and write-file API support for in-browser editing. Backend: hideWhitespace param on git diff endpoint, write_file route.
93 lines
2.3 KiB
TypeScript
93 lines
2.3 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
|
|
|
export interface DiffComment {
|
|
id: string;
|
|
body: string;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
}
|
|
|
|
export interface DiffCommentTarget {
|
|
filePath: string;
|
|
side: 'old' | 'new';
|
|
lineNumber: number;
|
|
}
|
|
|
|
function loadFromStorage(key: string): Map<string, DiffComment[]> {
|
|
try {
|
|
const raw = localStorage.getItem(key);
|
|
if (!raw) return new Map();
|
|
const parsed = JSON.parse(raw);
|
|
return new Map(Object.entries(parsed));
|
|
} catch {
|
|
return new Map();
|
|
}
|
|
}
|
|
|
|
function saveToStorage(key: string, map: Map<string, DiffComment[]>) {
|
|
const obj: Record<string, DiffComment[]> = {};
|
|
for (const [k, v] of map) obj[k] = v;
|
|
localStorage.setItem(key, JSON.stringify(obj));
|
|
}
|
|
|
|
export function useDiffComments(sessionId: string, mode: string) {
|
|
const storageKey = `boocode.diff.comments.${sessionId}.${mode}`;
|
|
const [comments, setComments] = useState<Map<string, DiffComment[]>>(() =>
|
|
loadFromStorage(storageKey)
|
|
);
|
|
|
|
useEffect(() => {
|
|
saveToStorage(storageKey, comments);
|
|
}, [storageKey, comments]);
|
|
|
|
const addComment = useCallback(
|
|
(key: string, comment: DiffComment) => {
|
|
setComments((prev) => {
|
|
const next = new Map(prev);
|
|
const list = next.get(key) ?? [];
|
|
next.set(key, [...list, comment]);
|
|
return next;
|
|
});
|
|
},
|
|
[]
|
|
);
|
|
|
|
const updateComment = useCallback(
|
|
(key: string, id: string, body: string) => {
|
|
setComments((prev) => {
|
|
const next = new Map(prev);
|
|
const list = next.get(key);
|
|
if (!list) return prev;
|
|
next.set(
|
|
key,
|
|
list.map((c) =>
|
|
c.id === id ? { ...c, body, updatedAt: Date.now() } : c
|
|
)
|
|
);
|
|
return next;
|
|
});
|
|
},
|
|
[]
|
|
);
|
|
|
|
const deleteComment = useCallback(
|
|
(key: string, id: string) => {
|
|
setComments((prev) => {
|
|
const next = new Map(prev);
|
|
const list = next.get(key);
|
|
if (!list) return prev;
|
|
const filtered = list.filter((c) => c.id !== id);
|
|
if (filtered.length === 0) {
|
|
next.delete(key);
|
|
} else {
|
|
next.set(key, filtered);
|
|
}
|
|
return next;
|
|
});
|
|
},
|
|
[]
|
|
);
|
|
|
|
return { comments, addComment, updateComment, deleteComment };
|
|
}
|