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:
@@ -21,6 +21,7 @@ import {
|
|||||||
DrizzleLogChunkRepository,
|
DrizzleLogChunkRepository,
|
||||||
DrizzleConversationRepository,
|
DrizzleConversationRepository,
|
||||||
DrizzleChatSessionRepository,
|
DrizzleChatSessionRepository,
|
||||||
|
DrizzleReviewCommentRepository,
|
||||||
} from './db/index.js';
|
} from './db/index.js';
|
||||||
import type { InitiativeRepository } from './db/repositories/initiative-repository.js';
|
import type { InitiativeRepository } from './db/repositories/initiative-repository.js';
|
||||||
import type { PhaseRepository } from './db/repositories/phase-repository.js';
|
import type { PhaseRepository } from './db/repositories/phase-repository.js';
|
||||||
@@ -34,6 +35,7 @@ import type { ChangeSetRepository } from './db/repositories/change-set-repositor
|
|||||||
import type { LogChunkRepository } from './db/repositories/log-chunk-repository.js';
|
import type { LogChunkRepository } from './db/repositories/log-chunk-repository.js';
|
||||||
import type { ConversationRepository } from './db/repositories/conversation-repository.js';
|
import type { ConversationRepository } from './db/repositories/conversation-repository.js';
|
||||||
import type { ChatSessionRepository } from './db/repositories/chat-session-repository.js';
|
import type { ChatSessionRepository } from './db/repositories/chat-session-repository.js';
|
||||||
|
import type { ReviewCommentRepository } from './db/repositories/review-comment-repository.js';
|
||||||
import type { EventBus } from './events/index.js';
|
import type { EventBus } from './events/index.js';
|
||||||
import { createEventBus } from './events/index.js';
|
import { createEventBus } from './events/index.js';
|
||||||
import { ProcessManager, ProcessRegistry } from './process/index.js';
|
import { ProcessManager, ProcessRegistry } from './process/index.js';
|
||||||
@@ -58,7 +60,7 @@ import type { ServerContextDeps } from './server/index.js';
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All 12 repository ports.
|
* All 13 repository ports.
|
||||||
*/
|
*/
|
||||||
export interface Repositories {
|
export interface Repositories {
|
||||||
initiativeRepository: InitiativeRepository;
|
initiativeRepository: InitiativeRepository;
|
||||||
@@ -73,10 +75,11 @@ export interface Repositories {
|
|||||||
logChunkRepository: LogChunkRepository;
|
logChunkRepository: LogChunkRepository;
|
||||||
conversationRepository: ConversationRepository;
|
conversationRepository: ConversationRepository;
|
||||||
chatSessionRepository: ChatSessionRepository;
|
chatSessionRepository: ChatSessionRepository;
|
||||||
|
reviewCommentRepository: ReviewCommentRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create all 12 Drizzle repository adapters from a database instance.
|
* Create all 13 Drizzle repository adapters from a database instance.
|
||||||
* Reusable by both the production server and the test harness.
|
* Reusable by both the production server and the test harness.
|
||||||
*/
|
*/
|
||||||
export function createRepositories(db: DrizzleDatabase): Repositories {
|
export function createRepositories(db: DrizzleDatabase): Repositories {
|
||||||
@@ -93,6 +96,7 @@ export function createRepositories(db: DrizzleDatabase): Repositories {
|
|||||||
logChunkRepository: new DrizzleLogChunkRepository(db),
|
logChunkRepository: new DrizzleLogChunkRepository(db),
|
||||||
conversationRepository: new DrizzleConversationRepository(db),
|
conversationRepository: new DrizzleConversationRepository(db),
|
||||||
chatSessionRepository: new DrizzleChatSessionRepository(db),
|
chatSessionRepository: new DrizzleChatSessionRepository(db),
|
||||||
|
reviewCommentRepository: new DrizzleReviewCommentRepository(db),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,3 +17,4 @@ export { DrizzleChangeSetRepository } from './change-set.js';
|
|||||||
export { DrizzleLogChunkRepository } from './log-chunk.js';
|
export { DrizzleLogChunkRepository } from './log-chunk.js';
|
||||||
export { DrizzleConversationRepository } from './conversation.js';
|
export { DrizzleConversationRepository } from './conversation.js';
|
||||||
export { DrizzleChatSessionRepository } from './chat-session.js';
|
export { DrizzleChatSessionRepository } from './chat-session.js';
|
||||||
|
export { DrizzleReviewCommentRepository } from './review-comment.js';
|
||||||
|
|||||||
78
apps/server/db/repositories/drizzle/review-comment.ts
Normal file
78
apps/server/db/repositories/drizzle/review-comment.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* Drizzle Review Comment Repository Adapter
|
||||||
|
*
|
||||||
|
* Implements ReviewCommentRepository interface using Drizzle ORM.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { eq, asc } from 'drizzle-orm';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import type { DrizzleDatabase } from '../../index.js';
|
||||||
|
import { reviewComments, type ReviewComment } from '../../schema.js';
|
||||||
|
import type { ReviewCommentRepository, CreateReviewCommentData } from '../review-comment-repository.js';
|
||||||
|
|
||||||
|
export class DrizzleReviewCommentRepository implements ReviewCommentRepository {
|
||||||
|
constructor(private db: DrizzleDatabase) {}
|
||||||
|
|
||||||
|
async create(data: CreateReviewCommentData): Promise<ReviewComment> {
|
||||||
|
const now = new Date();
|
||||||
|
const id = nanoid();
|
||||||
|
await this.db.insert(reviewComments).values({
|
||||||
|
id,
|
||||||
|
phaseId: data.phaseId,
|
||||||
|
filePath: data.filePath,
|
||||||
|
lineNumber: data.lineNumber,
|
||||||
|
lineType: data.lineType,
|
||||||
|
body: data.body,
|
||||||
|
author: data.author ?? 'you',
|
||||||
|
resolved: false,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
});
|
||||||
|
const rows = await this.db
|
||||||
|
.select()
|
||||||
|
.from(reviewComments)
|
||||||
|
.where(eq(reviewComments.id, id))
|
||||||
|
.limit(1);
|
||||||
|
return rows[0]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByPhaseId(phaseId: string): Promise<ReviewComment[]> {
|
||||||
|
return this.db
|
||||||
|
.select()
|
||||||
|
.from(reviewComments)
|
||||||
|
.where(eq(reviewComments.phaseId, phaseId))
|
||||||
|
.orderBy(asc(reviewComments.createdAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
async resolve(id: string): Promise<ReviewComment | null> {
|
||||||
|
await this.db
|
||||||
|
.update(reviewComments)
|
||||||
|
.set({ resolved: true, updatedAt: new Date() })
|
||||||
|
.where(eq(reviewComments.id, id));
|
||||||
|
const rows = await this.db
|
||||||
|
.select()
|
||||||
|
.from(reviewComments)
|
||||||
|
.where(eq(reviewComments.id, id))
|
||||||
|
.limit(1);
|
||||||
|
return rows[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async unresolve(id: string): Promise<ReviewComment | null> {
|
||||||
|
await this.db
|
||||||
|
.update(reviewComments)
|
||||||
|
.set({ resolved: false, updatedAt: new Date() })
|
||||||
|
.where(eq(reviewComments.id, id));
|
||||||
|
const rows = await this.db
|
||||||
|
.select()
|
||||||
|
.from(reviewComments)
|
||||||
|
.where(eq(reviewComments.id, id))
|
||||||
|
.limit(1);
|
||||||
|
return rows[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string): Promise<void> {
|
||||||
|
await this.db
|
||||||
|
.delete(reviewComments)
|
||||||
|
.where(eq(reviewComments.id, id));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,3 +78,8 @@ export type {
|
|||||||
CreateChatSessionData,
|
CreateChatSessionData,
|
||||||
CreateChatMessageData,
|
CreateChatMessageData,
|
||||||
} from './chat-session-repository.js';
|
} from './chat-session-repository.js';
|
||||||
|
|
||||||
|
export type {
|
||||||
|
ReviewCommentRepository,
|
||||||
|
CreateReviewCommentData,
|
||||||
|
} from './review-comment-repository.js';
|
||||||
|
|||||||
24
apps/server/db/repositories/review-comment-repository.ts
Normal file
24
apps/server/db/repositories/review-comment-repository.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Review Comment Repository Port Interface
|
||||||
|
*
|
||||||
|
* Port for persisting inline review comments on phase diffs.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ReviewComment } from '../schema.js';
|
||||||
|
|
||||||
|
export interface CreateReviewCommentData {
|
||||||
|
phaseId: string;
|
||||||
|
filePath: string;
|
||||||
|
lineNumber: number;
|
||||||
|
lineType: 'added' | 'removed' | 'context';
|
||||||
|
body: string;
|
||||||
|
author?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReviewCommentRepository {
|
||||||
|
create(data: CreateReviewCommentData): Promise<ReviewComment>;
|
||||||
|
findByPhaseId(phaseId: string): Promise<ReviewComment[]>;
|
||||||
|
resolve(id: string): Promise<ReviewComment | null>;
|
||||||
|
unresolve(id: string): Promise<ReviewComment | null>;
|
||||||
|
delete(id: string): Promise<void>;
|
||||||
|
}
|
||||||
@@ -604,3 +604,27 @@ export const chatMessagesRelations = relations(chatMessages, ({ one }) => ({
|
|||||||
|
|
||||||
export type ChatMessage = InferSelectModel<typeof chatMessages>;
|
export type ChatMessage = InferSelectModel<typeof chatMessages>;
|
||||||
export type NewChatMessage = InferInsertModel<typeof chatMessages>;
|
export type NewChatMessage = InferInsertModel<typeof chatMessages>;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// REVIEW COMMENTS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const reviewComments = sqliteTable('review_comments', {
|
||||||
|
id: text('id').primaryKey(),
|
||||||
|
phaseId: text('phase_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => phases.id, { onDelete: 'cascade' }),
|
||||||
|
filePath: text('file_path').notNull(),
|
||||||
|
lineNumber: integer('line_number').notNull(),
|
||||||
|
lineType: text('line_type', { enum: ['added', 'removed', 'context'] }).notNull(),
|
||||||
|
body: text('body').notNull(),
|
||||||
|
author: text('author').notNull().default('you'),
|
||||||
|
resolved: integer('resolved', { mode: 'boolean' }).notNull().default(false),
|
||||||
|
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
||||||
|
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
||||||
|
}, (table) => [
|
||||||
|
index('review_comments_phase_id_idx').on(table.phaseId),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type ReviewComment = InferSelectModel<typeof reviewComments>;
|
||||||
|
export type NewReviewComment = InferInsertModel<typeof reviewComments>;
|
||||||
|
|||||||
15
apps/server/drizzle/0028_add_review_comments.sql
Normal file
15
apps/server/drizzle/0028_add_review_comments.sql
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
-- Add review_comments table for persisting inline review comments on phase diffs
|
||||||
|
CREATE TABLE IF NOT EXISTS review_comments (
|
||||||
|
id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
phase_id TEXT NOT NULL REFERENCES phases(id) ON DELETE CASCADE,
|
||||||
|
file_path TEXT NOT NULL,
|
||||||
|
line_number INTEGER NOT NULL,
|
||||||
|
line_type TEXT NOT NULL,
|
||||||
|
body TEXT NOT NULL,
|
||||||
|
author TEXT NOT NULL DEFAULT 'you',
|
||||||
|
resolved INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS review_comments_phase_id_idx ON review_comments (phase_id);
|
||||||
@@ -21,6 +21,7 @@ import type { ChangeSetRepository } from '../db/repositories/change-set-reposito
|
|||||||
import type { LogChunkRepository } from '../db/repositories/log-chunk-repository.js';
|
import type { LogChunkRepository } from '../db/repositories/log-chunk-repository.js';
|
||||||
import type { ConversationRepository } from '../db/repositories/conversation-repository.js';
|
import type { ConversationRepository } from '../db/repositories/conversation-repository.js';
|
||||||
import type { ChatSessionRepository } from '../db/repositories/chat-session-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 { AccountCredentialManager } from '../agent/credentials/types.js';
|
||||||
import type { DispatchManager, PhaseDispatchManager } from '../dispatch/types.js';
|
import type { DispatchManager, PhaseDispatchManager } from '../dispatch/types.js';
|
||||||
import type { CoordinationManager } from '../coordination/types.js';
|
import type { CoordinationManager } from '../coordination/types.js';
|
||||||
@@ -76,6 +77,8 @@ export interface TrpcAdapterOptions {
|
|||||||
conversationRepository?: ConversationRepository;
|
conversationRepository?: ConversationRepository;
|
||||||
/** Chat session repository for iterative phase/task chat */
|
/** Chat session repository for iterative phase/task chat */
|
||||||
chatSessionRepository?: ChatSessionRepository;
|
chatSessionRepository?: ChatSessionRepository;
|
||||||
|
/** Review comment repository for inline review comments on phase diffs */
|
||||||
|
reviewCommentRepository?: ReviewCommentRepository;
|
||||||
/** Absolute path to the workspace root (.cwrc directory) */
|
/** Absolute path to the workspace root (.cwrc directory) */
|
||||||
workspaceRoot?: string;
|
workspaceRoot?: string;
|
||||||
}
|
}
|
||||||
@@ -158,6 +161,7 @@ export function createTrpcHandler(options: TrpcAdapterOptions) {
|
|||||||
previewManager: options.previewManager,
|
previewManager: options.previewManager,
|
||||||
conversationRepository: options.conversationRepository,
|
conversationRepository: options.conversationRepository,
|
||||||
chatSessionRepository: options.chatSessionRepository,
|
chatSessionRepository: options.chatSessionRepository,
|
||||||
|
reviewCommentRepository: options.reviewCommentRepository,
|
||||||
workspaceRoot: options.workspaceRoot,
|
workspaceRoot: options.workspaceRoot,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 { LogChunkRepository } from '../db/repositories/log-chunk-repository.js';
|
||||||
import type { ConversationRepository } from '../db/repositories/conversation-repository.js';
|
import type { ConversationRepository } from '../db/repositories/conversation-repository.js';
|
||||||
import type { ChatSessionRepository } from '../db/repositories/chat-session-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 { AccountCredentialManager } from '../agent/credentials/types.js';
|
||||||
import type { DispatchManager, PhaseDispatchManager } from '../dispatch/types.js';
|
import type { DispatchManager, PhaseDispatchManager } from '../dispatch/types.js';
|
||||||
import type { CoordinationManager } from '../coordination/types.js';
|
import type { CoordinationManager } from '../coordination/types.js';
|
||||||
@@ -76,6 +77,8 @@ export interface TRPCContext {
|
|||||||
conversationRepository?: ConversationRepository;
|
conversationRepository?: ConversationRepository;
|
||||||
/** Chat session repository for iterative phase/task chat */
|
/** Chat session repository for iterative phase/task chat */
|
||||||
chatSessionRepository?: ChatSessionRepository;
|
chatSessionRepository?: ChatSessionRepository;
|
||||||
|
/** Review comment repository for inline review comments on phase diffs */
|
||||||
|
reviewCommentRepository?: ReviewCommentRepository;
|
||||||
/** Absolute path to the workspace root (.cwrc directory) */
|
/** Absolute path to the workspace root (.cwrc directory) */
|
||||||
workspaceRoot?: string;
|
workspaceRoot?: string;
|
||||||
}
|
}
|
||||||
@@ -106,6 +109,7 @@ export interface CreateContextOptions {
|
|||||||
previewManager?: PreviewManager;
|
previewManager?: PreviewManager;
|
||||||
conversationRepository?: ConversationRepository;
|
conversationRepository?: ConversationRepository;
|
||||||
chatSessionRepository?: ChatSessionRepository;
|
chatSessionRepository?: ChatSessionRepository;
|
||||||
|
reviewCommentRepository?: ReviewCommentRepository;
|
||||||
workspaceRoot?: string;
|
workspaceRoot?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +143,7 @@ export function createContext(options: CreateContextOptions): TRPCContext {
|
|||||||
previewManager: options.previewManager,
|
previewManager: options.previewManager,
|
||||||
conversationRepository: options.conversationRepository,
|
conversationRepository: options.conversationRepository,
|
||||||
chatSessionRepository: options.chatSessionRepository,
|
chatSessionRepository: options.chatSessionRepository,
|
||||||
|
reviewCommentRepository: options.reviewCommentRepository,
|
||||||
workspaceRoot: options.workspaceRoot,
|
workspaceRoot: options.workspaceRoot,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { LogChunkRepository } from '../../db/repositories/log-chunk-repository.js';
|
||||||
import type { ConversationRepository } from '../../db/repositories/conversation-repository.js';
|
import type { ConversationRepository } from '../../db/repositories/conversation-repository.js';
|
||||||
import type { ChatSessionRepository } from '../../db/repositories/chat-session-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 { DispatchManager, PhaseDispatchManager } from '../../dispatch/types.js';
|
||||||
import type { CoordinationManager } from '../../coordination/types.js';
|
import type { CoordinationManager } from '../../coordination/types.js';
|
||||||
import type { BranchManager } from '../../git/branch-manager.js';
|
import type { BranchManager } from '../../git/branch-manager.js';
|
||||||
@@ -203,3 +204,13 @@ export function requireChatSessionRepository(ctx: TRPCContext): ChatSessionRepos
|
|||||||
}
|
}
|
||||||
return ctx.chatSessionRepository;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { TRPCError } from '@trpc/server';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import type { Phase } from '../../db/schema.js';
|
import type { Phase } from '../../db/schema.js';
|
||||||
import type { ProcedureBuilder } from '../trpc.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 { phaseBranchName } from '../../git/branch-naming.js';
|
||||||
import { ensureProjectClone } from '../../git/project-clones.js';
|
import { ensureProjectClone } from '../../git/project-clones.js';
|
||||||
|
|
||||||
@@ -298,5 +298,48 @@ export function phaseProcedures(publicProcedure: ProcedureBuilder) {
|
|||||||
|
|
||||||
return { rawDiff };
|
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;
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useCallback, useMemo, useRef } from "react";
|
import { useCallback, useMemo, useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { trpc } from "@/lib/trpc";
|
import { trpc } from "@/lib/trpc";
|
||||||
@@ -6,14 +6,13 @@ import { parseUnifiedDiff } from "./parse-diff";
|
|||||||
import { DiffViewer } from "./DiffViewer";
|
import { DiffViewer } from "./DiffViewer";
|
||||||
import { ReviewSidebar } from "./ReviewSidebar";
|
import { ReviewSidebar } from "./ReviewSidebar";
|
||||||
import { ReviewHeader } from "./ReviewHeader";
|
import { ReviewHeader } from "./ReviewHeader";
|
||||||
import type { ReviewComment, ReviewStatus, DiffLine } from "./types";
|
import type { ReviewStatus, DiffLine } from "./types";
|
||||||
|
|
||||||
interface ReviewTabProps {
|
interface ReviewTabProps {
|
||||||
initiativeId: string;
|
initiativeId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ReviewTab({ initiativeId }: ReviewTabProps) {
|
export function ReviewTab({ initiativeId }: ReviewTabProps) {
|
||||||
const [comments, setComments] = useState<ReviewComment[]>([]);
|
|
||||||
const [status, setStatus] = useState<ReviewStatus>("pending");
|
const [status, setStatus] = useState<ReviewStatus>("pending");
|
||||||
const [selectedCommit, setSelectedCommit] = useState<string | null>(null);
|
const [selectedCommit, setSelectedCommit] = useState<string | null>(null);
|
||||||
const fileRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
const fileRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||||
@@ -116,6 +115,44 @@ export function ReviewTab({ initiativeId }: ReviewTabProps) {
|
|||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
// Review comments — persisted to DB
|
||||||
|
const utils = trpc.useUtils();
|
||||||
|
const commentsQuery = trpc.listReviewComments.useQuery(
|
||||||
|
{ phaseId: activePhaseId! },
|
||||||
|
{ enabled: !!activePhaseId },
|
||||||
|
);
|
||||||
|
const comments = useMemo(() => {
|
||||||
|
return (commentsQuery.data ?? []).map((c) => ({
|
||||||
|
id: c.id,
|
||||||
|
filePath: c.filePath,
|
||||||
|
lineNumber: c.lineNumber,
|
||||||
|
lineType: c.lineType as "added" | "removed" | "context",
|
||||||
|
body: c.body,
|
||||||
|
author: c.author,
|
||||||
|
createdAt: c.createdAt instanceof Date ? c.createdAt.toISOString() : String(c.createdAt),
|
||||||
|
resolved: c.resolved,
|
||||||
|
}));
|
||||||
|
}, [commentsQuery.data]);
|
||||||
|
|
||||||
|
const createCommentMutation = trpc.createReviewComment.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
utils.listReviewComments.invalidate({ phaseId: activePhaseId! });
|
||||||
|
},
|
||||||
|
onError: (err) => toast.error(`Failed to save comment: ${err.message}`),
|
||||||
|
});
|
||||||
|
|
||||||
|
const resolveCommentMutation = trpc.resolveReviewComment.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
utils.listReviewComments.invalidate({ phaseId: activePhaseId! });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const unresolveCommentMutation = trpc.unresolveReviewComment.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
utils.listReviewComments.invalidate({ phaseId: activePhaseId! });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const approveMutation = trpc.approvePhaseReview.useMutation({
|
const approveMutation = trpc.approvePhaseReview.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setStatus("approved");
|
setStatus("approved");
|
||||||
@@ -141,33 +178,26 @@ export function ReviewTab({ initiativeId }: ReviewTabProps) {
|
|||||||
|
|
||||||
const handleAddComment = useCallback(
|
const handleAddComment = useCallback(
|
||||||
(filePath: string, lineNumber: number, lineType: DiffLine["type"], body: string) => {
|
(filePath: string, lineNumber: number, lineType: DiffLine["type"], body: string) => {
|
||||||
const newComment: ReviewComment = {
|
if (!activePhaseId) return;
|
||||||
id: `c${Date.now()}`,
|
createCommentMutation.mutate({
|
||||||
|
phaseId: activePhaseId,
|
||||||
filePath,
|
filePath,
|
||||||
lineNumber,
|
lineNumber,
|
||||||
lineType,
|
lineType,
|
||||||
body,
|
body,
|
||||||
author: "you",
|
});
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
resolved: false,
|
|
||||||
};
|
|
||||||
setComments((prev) => [...prev, newComment]);
|
|
||||||
toast.success("Comment added");
|
toast.success("Comment added");
|
||||||
},
|
},
|
||||||
[],
|
[activePhaseId, createCommentMutation],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleResolveComment = useCallback((commentId: string) => {
|
const handleResolveComment = useCallback((commentId: string) => {
|
||||||
setComments((prev) =>
|
resolveCommentMutation.mutate({ id: commentId });
|
||||||
prev.map((c) => (c.id === commentId ? { ...c, resolved: true } : c)),
|
}, [resolveCommentMutation]);
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleUnresolveComment = useCallback((commentId: string) => {
|
const handleUnresolveComment = useCallback((commentId: string) => {
|
||||||
setComments((prev) =>
|
unresolveCommentMutation.mutate({ id: commentId });
|
||||||
prev.map((c) => (c.id === commentId ? { ...c, resolved: false } : c)),
|
}, [unresolveCommentMutation]);
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleApprove = useCallback(() => {
|
const handleApprove = useCallback(() => {
|
||||||
if (!activePhaseId) return;
|
if (!activePhaseId) return;
|
||||||
@@ -192,7 +222,6 @@ export function ReviewTab({ initiativeId }: ReviewTabProps) {
|
|||||||
setSelectedPhaseId(id);
|
setSelectedPhaseId(id);
|
||||||
setSelectedCommit(null);
|
setSelectedCommit(null);
|
||||||
setStatus("pending");
|
setStatus("pending");
|
||||||
setComments([]);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const unresolvedCount = comments.filter((c) => !c.resolved).length;
|
const unresolvedCount = comments.filter((c) => !c.resolved).length;
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
- **Schema**: `apps/server/db/schema.ts` — all tables, columns, relations
|
- **Schema**: `apps/server/db/schema.ts` — all tables, columns, relations
|
||||||
- **Ports** (interfaces): `apps/server/db/repositories/*.ts` — 12 repository interfaces
|
- **Ports** (interfaces): `apps/server/db/repositories/*.ts` — 13 repository interfaces
|
||||||
- **Adapters** (implementations): `apps/server/db/repositories/drizzle/*.ts` — 12 Drizzle adapters
|
- **Adapters** (implementations): `apps/server/db/repositories/drizzle/*.ts` — 13 Drizzle adapters
|
||||||
- **Barrel exports**: `apps/server/db/index.ts` re-exports everything
|
- **Barrel exports**: `apps/server/db/index.ts` re-exports everything
|
||||||
|
|
||||||
All adapters use nanoid() for IDs, auto-manage timestamps, and use Drizzle's `.returning()` for atomic reads after writes.
|
All adapters use nanoid() for IDs, auto-manage timestamps, and use Drizzle's `.returning()` for atomic reads after writes.
|
||||||
@@ -195,9 +195,27 @@ Messages within a chat session.
|
|||||||
|
|
||||||
Index: `(chatSessionId)`.
|
Index: `(chatSessionId)`.
|
||||||
|
|
||||||
|
### review_comments
|
||||||
|
|
||||||
|
Inline review comments on phase diffs, persisted across page reloads.
|
||||||
|
|
||||||
|
| Column | Type | Notes |
|
||||||
|
|--------|------|-------|
|
||||||
|
| id | text PK | nanoid |
|
||||||
|
| phaseId | text FK → phases (cascade) | scopes comment to phase |
|
||||||
|
| filePath | text NOT NULL | file in diff |
|
||||||
|
| lineNumber | integer NOT NULL | line number (new-side or old-side for deletions) |
|
||||||
|
| lineType | text enum | 'added' \| 'removed' \| 'context' |
|
||||||
|
| body | text NOT NULL | comment text |
|
||||||
|
| author | text NOT NULL | default 'you' |
|
||||||
|
| resolved | integer/boolean | default false |
|
||||||
|
| createdAt, updatedAt | integer/timestamp | |
|
||||||
|
|
||||||
|
Index: `(phaseId)`.
|
||||||
|
|
||||||
## Repository Interfaces
|
## Repository Interfaces
|
||||||
|
|
||||||
12 repositories, each with standard CRUD plus domain-specific methods:
|
13 repositories, each with standard CRUD plus domain-specific methods:
|
||||||
|
|
||||||
| Repository | Key Methods |
|
| Repository | Key Methods |
|
||||||
|-----------|-------------|
|
|-----------|-------------|
|
||||||
@@ -213,6 +231,7 @@ Index: `(chatSessionId)`.
|
|||||||
| LogChunkRepository | insertChunk, findByAgentId, deleteByAgentId, getSessionCount |
|
| LogChunkRepository | insertChunk, findByAgentId, deleteByAgentId, getSessionCount |
|
||||||
| ConversationRepository | create, findById, findPendingForAgent, answer |
|
| ConversationRepository | create, findById, findPendingForAgent, answer |
|
||||||
| ChatSessionRepository | createSession, findActiveSession, findActiveSessionByAgentId, updateSession, createMessage, findMessagesBySessionId |
|
| ChatSessionRepository | createSession, findActiveSession, findActiveSessionByAgentId, updateSession, createMessage, findMessagesBySessionId |
|
||||||
|
| ReviewCommentRepository | create, findByPhaseId, resolve, unresolve, delete |
|
||||||
|
|
||||||
## Migrations
|
## Migrations
|
||||||
|
|
||||||
@@ -224,4 +243,4 @@ Key rules:
|
|||||||
- See [database-migrations.md](database-migrations.md) for full workflow
|
- See [database-migrations.md](database-migrations.md) for full workflow
|
||||||
- Snapshots stale after 0008; migrations 0008+ are hand-written
|
- Snapshots stale after 0008; migrations 0008+ are hand-written
|
||||||
|
|
||||||
Current migrations: 0000 through 0027 (28 total).
|
Current migrations: 0000 through 0028 (29 total).
|
||||||
|
|||||||
@@ -113,6 +113,10 @@ Each procedure uses `require*Repository(ctx)` helpers that throw `TRPCError(INTE
|
|||||||
| getPhaseReviewCommits | query | List commits between initiative and phase branch |
|
| getPhaseReviewCommits | query | List commits between initiative and phase branch |
|
||||||
| getCommitDiff | query | Diff for a single commit (by hash) in a phase |
|
| getCommitDiff | query | Diff for a single commit (by hash) in a phase |
|
||||||
| approvePhaseReview | mutation | Approve and merge phase branch |
|
| approvePhaseReview | mutation | Approve and merge phase branch |
|
||||||
|
| listReviewComments | query | List review comments by phaseId |
|
||||||
|
| createReviewComment | mutation | Create inline review comment on diff |
|
||||||
|
| resolveReviewComment | mutation | Mark review comment as resolved |
|
||||||
|
| unresolveReviewComment | mutation | Mark review comment as unresolved |
|
||||||
|
|
||||||
### Phase Dispatch
|
### Phase Dispatch
|
||||||
| Procedure | Type | Description |
|
| Procedure | Type | Description |
|
||||||
|
|||||||
Reference in New Issue
Block a user