feat: Add threaded review comments + agent comment responses
Introduces GitHub-style threaded comments via parentCommentId self-reference. Users and agents can reply within comment threads, and review agents receive comment IDs so they can post targeted responses via comment-responses.json. - Migration 0032: parentCommentId column + index on review_comments - Repository: createReply() copies parent context, default author 'you' → 'user' - tRPC: replyToReviewComment procedure, requestPhaseChanges passes threaded comments - Orchestrator: formats [comment:ID] tags with full reply threads in task description - Agent IO: readCommentResponses() reads .cw/output/comment-responses.json - OutputHandler: processes agent comment responses (creates replies, resolves threads) - Execute prompt: conditional <review_comments> block when task has [comment:] markers - Frontend: CommentThread renders root+replies with agent-specific styling + reply form - Sidebar/ReviewTab: root-only comment counts, reply mutation plumbing through DiffViewer chain
This commit is contained in:
@@ -344,7 +344,14 @@ export class ExecutionOrchestrator {
|
||||
*/
|
||||
async requestChangesOnPhase(
|
||||
phaseId: string,
|
||||
unresolvedComments: Array<{ filePath: string; lineNumber: number; body: string }>,
|
||||
unresolvedThreads: Array<{
|
||||
id: string;
|
||||
filePath: string;
|
||||
lineNumber: number;
|
||||
body: string;
|
||||
author: string;
|
||||
replies: Array<{ id: string; body: string; author: string }>;
|
||||
}>,
|
||||
summary?: string,
|
||||
): Promise<{ taskId: string }> {
|
||||
const phase = await this.phaseRepository.findById(phaseId);
|
||||
@@ -365,16 +372,16 @@ export class ExecutionOrchestrator {
|
||||
return { taskId: activeReview.id };
|
||||
}
|
||||
|
||||
// Build revision task description from comments + summary
|
||||
// Build revision task description from threaded comments + summary
|
||||
const lines: string[] = [];
|
||||
if (summary) {
|
||||
lines.push(`## Summary\n\n${summary}\n`);
|
||||
}
|
||||
if (unresolvedComments.length > 0) {
|
||||
if (unresolvedThreads.length > 0) {
|
||||
lines.push('## Review Comments\n');
|
||||
// Group comments by file
|
||||
const byFile = new Map<string, typeof unresolvedComments>();
|
||||
for (const c of unresolvedComments) {
|
||||
const byFile = new Map<string, typeof unresolvedThreads>();
|
||||
for (const c of unresolvedThreads) {
|
||||
const arr = byFile.get(c.filePath) ?? [];
|
||||
arr.push(c);
|
||||
byFile.set(c.filePath, arr);
|
||||
@@ -382,9 +389,13 @@ export class ExecutionOrchestrator {
|
||||
for (const [filePath, fileComments] of byFile) {
|
||||
lines.push(`### ${filePath}\n`);
|
||||
for (const c of fileComments) {
|
||||
lines.push(`- **Line ${c.lineNumber}**: ${c.body}`);
|
||||
lines.push(`#### Line ${c.lineNumber} [comment:${c.id}]`);
|
||||
lines.push(`**${c.author}**: ${c.body}`);
|
||||
for (const r of c.replies) {
|
||||
lines.push(`> **${r.author}**: ${r.body}`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,12 +425,12 @@ export class ExecutionOrchestrator {
|
||||
phaseId,
|
||||
initiativeId: phase.initiativeId,
|
||||
taskId: task.id,
|
||||
commentCount: unresolvedComments.length,
|
||||
commentCount: unresolvedThreads.length,
|
||||
},
|
||||
};
|
||||
this.eventBus.emit(event);
|
||||
|
||||
log.info({ phaseId, taskId: task.id, commentCount: unresolvedComments.length }, 'changes requested on phase');
|
||||
log.info({ phaseId, taskId: task.id, commentCount: unresolvedThreads.length }, 'changes requested on phase');
|
||||
|
||||
// Kick off dispatch
|
||||
this.scheduleDispatch();
|
||||
|
||||
Reference in New Issue
Block a user