feat: Persist review comments to database

Review comments on phase diffs now survive page reloads and phase
switches. Adds review_comments table (migration 0028), repository
port/adapter (13th repo), tRPC procedures (listReviewComments,
createReviewComment, resolveReviewComment, unresolveReviewComment),
and replaces useState-based comments in ReviewTab with tRPC queries
and mutations.
This commit is contained in:
Lukas May
2026-03-05 11:16:54 +01:00
parent 69d2543995
commit 173c7f7916
14 changed files with 293 additions and 27 deletions

View File

@@ -18,6 +18,7 @@ import type { ChangeSetRepository } from '../db/repositories/change-set-reposito
import type { LogChunkRepository } from '../db/repositories/log-chunk-repository.js';
import type { ConversationRepository } from '../db/repositories/conversation-repository.js';
import type { ChatSessionRepository } from '../db/repositories/chat-session-repository.js';
import type { ReviewCommentRepository } from '../db/repositories/review-comment-repository.js';
import type { AccountCredentialManager } from '../agent/credentials/types.js';
import type { DispatchManager, PhaseDispatchManager } from '../dispatch/types.js';
import type { CoordinationManager } from '../coordination/types.js';
@@ -76,6 +77,8 @@ export interface TRPCContext {
conversationRepository?: ConversationRepository;
/** Chat session repository for iterative phase/task chat */
chatSessionRepository?: ChatSessionRepository;
/** Review comment repository for inline review comments on phase diffs */
reviewCommentRepository?: ReviewCommentRepository;
/** Absolute path to the workspace root (.cwrc directory) */
workspaceRoot?: string;
}
@@ -106,6 +109,7 @@ export interface CreateContextOptions {
previewManager?: PreviewManager;
conversationRepository?: ConversationRepository;
chatSessionRepository?: ChatSessionRepository;
reviewCommentRepository?: ReviewCommentRepository;
workspaceRoot?: string;
}
@@ -139,6 +143,7 @@ export function createContext(options: CreateContextOptions): TRPCContext {
previewManager: options.previewManager,
conversationRepository: options.conversationRepository,
chatSessionRepository: options.chatSessionRepository,
reviewCommentRepository: options.reviewCommentRepository,
workspaceRoot: options.workspaceRoot,
};
}

View File

@@ -18,6 +18,7 @@ import type { ChangeSetRepository } from '../../db/repositories/change-set-repos
import type { LogChunkRepository } from '../../db/repositories/log-chunk-repository.js';
import type { ConversationRepository } from '../../db/repositories/conversation-repository.js';
import type { ChatSessionRepository } from '../../db/repositories/chat-session-repository.js';
import type { ReviewCommentRepository } from '../../db/repositories/review-comment-repository.js';
import type { DispatchManager, PhaseDispatchManager } from '../../dispatch/types.js';
import type { CoordinationManager } from '../../coordination/types.js';
import type { BranchManager } from '../../git/branch-manager.js';
@@ -203,3 +204,13 @@ export function requireChatSessionRepository(ctx: TRPCContext): ChatSessionRepos
}
return ctx.chatSessionRepository;
}
export function requireReviewCommentRepository(ctx: TRPCContext): ReviewCommentRepository {
if (!ctx.reviewCommentRepository) {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Review comment repository not available',
});
}
return ctx.reviewCommentRepository;
}

View File

@@ -6,7 +6,7 @@ import { TRPCError } from '@trpc/server';
import { z } from 'zod';
import type { Phase } from '../../db/schema.js';
import type { ProcedureBuilder } from '../trpc.js';
import { requirePhaseRepository, requireTaskRepository, requireBranchManager, requireInitiativeRepository, requireProjectRepository, requireExecutionOrchestrator } from './_helpers.js';
import { requirePhaseRepository, requireTaskRepository, requireBranchManager, requireInitiativeRepository, requireProjectRepository, requireExecutionOrchestrator, requireReviewCommentRepository } from './_helpers.js';
import { phaseBranchName } from '../../git/branch-naming.js';
import { ensureProjectClone } from '../../git/project-clones.js';
@@ -298,5 +298,48 @@ export function phaseProcedures(publicProcedure: ProcedureBuilder) {
return { rawDiff };
}),
listReviewComments: publicProcedure
.input(z.object({ phaseId: z.string().min(1) }))
.query(async ({ ctx, input }) => {
const repo = requireReviewCommentRepository(ctx);
return repo.findByPhaseId(input.phaseId);
}),
createReviewComment: publicProcedure
.input(z.object({
phaseId: z.string().min(1),
filePath: z.string().min(1),
lineNumber: z.number().int(),
lineType: z.enum(['added', 'removed', 'context']),
body: z.string().min(1),
author: z.string().optional(),
}))
.mutation(async ({ ctx, input }) => {
const repo = requireReviewCommentRepository(ctx);
return repo.create(input);
}),
resolveReviewComment: publicProcedure
.input(z.object({ id: z.string().min(1) }))
.mutation(async ({ ctx, input }) => {
const repo = requireReviewCommentRepository(ctx);
const comment = await repo.resolve(input.id);
if (!comment) {
throw new TRPCError({ code: 'NOT_FOUND', message: `Review comment '${input.id}' not found` });
}
return comment;
}),
unresolveReviewComment: publicProcedure
.input(z.object({ id: z.string().min(1) }))
.mutation(async ({ ctx, input }) => {
const repo = requireReviewCommentRepository(ctx);
const comment = await repo.unresolve(input.id);
if (!comment) {
throw new TRPCError({ code: 'NOT_FOUND', message: `Review comment '${input.id}' not found` });
}
return comment;
}),
};
}