review-store.ts

  1import type { ReviewThread, ReviewComment, Review } from '$lib/types/index';
  2import { randomUUID } from 'crypto';
  3
  4const threads = new Map<string, ReviewThread>();
  5let reviewStatus: 'pending' | 'approved' | 'changes_requested' = 'pending';
  6let reviewSummary: string | undefined;
  7const contextFiles = new Set<string>();
  8const worktreeApprovals = new Set<string>();
  9// jj only: the change currently being viewed (null = working copy @).
 10let selectedChangeId: string | null = null;
 11
 12export function addContextFile(filePath: string): void {
 13	contextFiles.add(filePath);
 14}
 15
 16export function removeContextFile(filePath: string): void {
 17	contextFiles.delete(filePath);
 18}
 19
 20export function getContextFiles(): string[] {
 21	return Array.from(contextFiles);
 22}
 23
 24export function addComment(
 25	filePath: string,
 26	lineNumber: number,
 27	side: 'old' | 'new',
 28	body: string,
 29	type: 'comment' | 'suggestion' | 'question' = 'comment',
 30	threadId?: string,
 31	originalLineContent?: string
 32): ReviewThread {
 33	const comment: ReviewComment = {
 34		id: randomUUID(),
 35		filePath,
 36		lineNumber,
 37		side,
 38		body,
 39		type,
 40		createdAt: new Date().toISOString()
 41	};
 42
 43	if (threadId && threads.has(threadId)) {
 44		const thread = threads.get(threadId)!;
 45		thread.comments.push(comment);
 46		return thread;
 47	}
 48
 49	const thread: ReviewThread = {
 50		id: randomUUID(),
 51		filePath,
 52		lineNumber,
 53		side,
 54		comments: [comment],
 55		resolved: false,
 56		outdated: false,
 57		originalLineContent
 58	};
 59	threads.set(thread.id, thread);
 60	return thread;
 61}
 62
 63export function getThreads(filePath?: string): ReviewThread[] {
 64	const all = Array.from(threads.values());
 65	if (filePath) return all.filter((t) => t.filePath === filePath);
 66	return all;
 67}
 68
 69export function getThread(threadId: string): ReviewThread | undefined {
 70	return threads.get(threadId);
 71}
 72
 73export function resolveThread(threadId: string): boolean {
 74	const thread = threads.get(threadId);
 75	if (!thread) return false;
 76	thread.resolved = !thread.resolved;
 77	return true;
 78}
 79
 80export function deleteComment(commentId: string): boolean {
 81	for (const [threadId, thread] of threads) {
 82		const idx = thread.comments.findIndex((c) => c.id === commentId);
 83		if (idx !== -1) {
 84			thread.comments.splice(idx, 1);
 85			if (thread.comments.length === 0) {
 86				threads.delete(threadId);
 87			}
 88			return true;
 89		}
 90	}
 91	return false;
 92}
 93
 94export function deleteThread(threadId: string): boolean {
 95	return threads.delete(threadId);
 96}
 97
 98export function getReview(): Review {
 99	return {
100		id: 'current',
101		threads: Array.from(threads.values()),
102		status: reviewStatus,
103		createdAt: new Date().toISOString(),
104		summary: reviewSummary
105	};
106}
107
108export function setReviewStatus(status: 'pending' | 'approved' | 'changes_requested'): void {
109	reviewStatus = status;
110}
111
112export function setReviewSummary(summary: string): void {
113	reviewSummary = summary;
114}
115
116export function approveWorktreeFile(filePath: string): void {
117	worktreeApprovals.add(filePath);
118}
119
120export function unapproveWorktreeFile(filePath: string): void {
121	worktreeApprovals.delete(filePath);
122}
123
124export function getWorktreeApprovals(): Set<string> {
125	return worktreeApprovals;
126}
127
128export function clearApprovals(): void {
129	worktreeApprovals.clear();
130}
131
132export function getSelectedChangeId(): string | null {
133	return selectedChangeId;
134}
135
136export function setSelectedChangeId(changeId: string | null): void {
137	selectedChangeId = changeId;
138}
139
140export function clearAll(): void {
141	threads.clear();
142	reviewStatus = 'pending';
143	reviewSummary = undefined;
144	contextFiles.clear();
145	worktreeApprovals.clear();
146	selectedChangeId = null;
147}
148
149export function markOutdated(threadId: string, outdated: boolean): boolean {
150	const thread = threads.get(threadId);
151	if (!thread) return false;
152	thread.outdated = outdated;
153	return true;
154}
155
156export function getStats(): { totalThreads: number; totalComments: number; filesWithComments: number } {
157	const all = Array.from(threads.values());
158	const files = new Set(all.map((t) => t.filePath));
159	return {
160		totalThreads: all.length,
161		totalComments: all.reduce((sum, t) => sum + t.comments.length, 0),
162		filesWithComments: files.size
163	};
164}