Skip to content

Commit e82d9eb

Browse files
committed
Add a tool for adding comments
1 parent 6eff12f commit e82d9eb

11 files changed

Lines changed: 99 additions & 33 deletions

File tree

backend/src/entities/user.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export class User extends BaseEntity {
6969
firstName: user.firstName,
7070
email: user.isBordly ? '' : user.email,
7171
photoUrl: user.photoUrl,
72+
isBordly: user.isBordly,
7273
};
7374
}
7475

backend/src/services/agent.service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { BoardMember } from '@/entities/board-member';
99
import { BORDLY_USER_ID } from '@/entities/user';
1010
import { BoardMemberService } from '@/services/board-member.service';
1111
import { boardCardReadTool } from '@/tools/board-card-read.tool';
12+
import { commentAddTool } from '@/tools/comment-add.tool';
1213
import { emailDraftUpsertTool } from '@/tools/email-draft-upsert.tool';
1314
import { gmailAttachmentReadTool } from '@/tools/gmail-attachment-read.tool';
1415
import { ENV } from '@/utils/env';
@@ -30,6 +31,7 @@ const BORDLY_AGENT = {
3031
3132
- Treat the user's prompt as a simplified request, not a word-for-word instruction.
3233
- Never make assumptions about the board card's state without using the tools to verify.
34+
- When a prompt asks for information, add a comment instead of outputting the information directly.
3335
3436
# Writing emails
3537
@@ -39,6 +41,7 @@ const BORDLY_AGENT = {
3941
model: ENV.LLM_THINKING_MODEL,
4042
tools: {
4143
'board-card-read': boardCardReadTool,
44+
'comment-add': commentAddTool,
4245
'email-draft-upsert': emailDraftUpsertTool,
4346
'gmail-attachment-read': gmailAttachmentReadTool,
4447
} as Record<string, ToolAction<unknown, unknown>>,

backend/src/services/comment.service.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { AgentService } from '@/services/agent.service';
77
import { BoardCardService } from '@/services/board-card.service';
88
import { BoardMemberService } from '@/services/board-member.service';
99
import { orm } from '@/utils/orm';
10-
import { isBordlyComment } from '@/utils/shared';
10+
import { isCommentForBordly } from '@/utils/shared';
1111

1212
export class CommentService {
1313
static async create(
@@ -34,14 +34,17 @@ export class CommentService {
3434
boardCard.setSnippet(`${user.firstName}: ${contentText}`);
3535
boardCard.setLastEventAt(comment.createdAt);
3636

37-
const userBoardCardReadPosition = boardCard.boardCardReadPositions.find((pos) => pos.user.id === user.id)!;
38-
userBoardCardReadPosition.setLastReadAt(boardCard.lastEventAt);
37+
if (!user.isBordly) {
38+
const userBoardCardReadPosition = boardCard.boardCardReadPositions.find((pos) => pos.user.id === user.id)!;
39+
userBoardCardReadPosition.setLastReadAt(boardCard.lastEventAt);
40+
orm.em.persist(userBoardCardReadPosition);
41+
}
3942

40-
orm.em.persist([comment, boardCard, userBoardCardReadPosition]);
43+
orm.em.persist([comment, boardCard]);
4144

4245
await orm.em.flush();
4346

44-
if (isBordlyComment(contentText)) {
47+
if (isCommentForBordly(contentText) && !user.isBordly) {
4548
const userBoardMember = await BoardMemberService.findById(board, {
4649
boardMemberId: boardMember.id,
4750
populate: ['user', 'memory'],
@@ -80,7 +83,12 @@ export class CommentService {
8083
populate?: Populate<Comment, Hint>;
8184
},
8285
) {
83-
const comment = await CommentService.findById(boardCard, { commentId, populate });
86+
const comment = await CommentService.findById(boardCard, {
87+
commentId,
88+
populate: [...(populate || []), 'user'] as Populate<Comment, Hint>,
89+
});
90+
if (comment.user.id !== user.id) throw new Error('You can only edit your own comments');
91+
8492
const boardMember = user.boardMembers.find((bm) => bm.board.id === board.id)!;
8593
const wasLastBoardCardEvent = comment.createdAt.getTime() === boardCard.lastEventAt.getTime();
8694

@@ -93,7 +101,7 @@ export class CommentService {
93101

94102
await orm.em.flush();
95103

96-
if (isBordlyComment(contentText)) {
104+
if (isCommentForBordly(contentText)) {
97105
const userBoardMember = await BoardMemberService.findById(board, {
98106
boardMemberId: boardMember.id,
99107
populate: ['user', 'memory'],
@@ -113,8 +121,12 @@ export class CommentService {
113121
return { boardCard, comment };
114122
}
115123

116-
static async delete(boardCard: BoardCard, { commentId }: { commentId: string }) {
117-
const comment = await CommentService.findById(boardCard, { commentId });
124+
static async delete(boardCard: BoardCard, { user, commentId }: { user: Loaded<User>; commentId: string }) {
125+
const comment = await CommentService.findById(boardCard, { commentId, populate: ['user'] });
126+
if (comment.user.id !== user.id && !comment.loadedUser.isBordly) {
127+
throw new Error('You can only delete your own comments or comments from Bordly');
128+
}
129+
118130
orm.em.remove(comment);
119131

120132
await BoardCardService.rebuildLastEventAtAndSnippet(boardCard, { ignoreLastEventAt: comment.createdAt });

backend/src/services/email-draft.service.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,8 @@ export class EmailDraftService {
102102
boardCard.setSnippet(htmlToText(boardCard.emailDraft.bodyHtml || ''));
103103
orm.em.persist([boardCard.emailDraft, boardCard]);
104104

105-
const userBoardCardReadPosition = boardCard.boardCardReadPositions.find((pos) => pos.user.id === user.id);
106-
if (userBoardCardReadPosition) {
105+
if (!user.isBordly) {
106+
const userBoardCardReadPosition = boardCard.boardCardReadPositions.find((pos) => pos.user.id === user.id)!;
107107
userBoardCardReadPosition.setLastReadAt(boardCard.lastEventAt);
108108
orm.em.persist(userBoardCardReadPosition);
109109
}
@@ -218,10 +218,8 @@ export class EmailDraftService {
218218
orm.em.persist(rebuiltBoardCard);
219219

220220
const userBoardCardReadPosition = rebuiltBoardCard.boardCardReadPositions.find((pos) => pos.user.id === user.id)!;
221-
if (userBoardCardReadPosition) {
222-
userBoardCardReadPosition.setLastReadAt(rebuiltBoardCard.lastEventAt);
223-
orm.em.persist(userBoardCardReadPosition);
224-
}
221+
userBoardCardReadPosition.setLastReadAt(rebuiltBoardCard.lastEventAt);
222+
orm.em.persist(userBoardCardReadPosition);
225223

226224
if (rebuiltBoardCard.emailDraft) {
227225
const { emailDraft } = rebuiltBoardCard;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { RequestContext } from '@mastra/core/request-context';
2+
import { createTool } from '@mastra/core/tools';
3+
import type { Loaded } from '@mikro-orm/postgresql';
4+
import { z } from 'zod';
5+
import type { User } from '@/entities/user';
6+
import type { Context } from '@/services/agent.service';
7+
import { BoardCardService } from '@/services/board-card.service';
8+
import { BoardMemberService } from '@/services/board-member.service';
9+
import { CommentService } from '@/services/comment.service';
10+
11+
export const commentAddTool = createTool({
12+
id: 'comment-add',
13+
description: 'Add a comment to the board card',
14+
inputSchema: z.object({
15+
contentHtml: z.string().min(1).describe('Comment content in HTML format'),
16+
contentText: z.string().min(1).describe('Comment content in plain text format'),
17+
}),
18+
execute: async (data, context) => {
19+
const { requestContext } = context as { requestContext: RequestContext<Context> };
20+
const initialBoardCard = requestContext.get('boardCard');
21+
console.log(`[AGENT] Executing comment-add for board card ${initialBoardCard.id}: ${JSON.stringify(data)}`);
22+
23+
const bordlyBoardMember = requestContext.get('bordlyBoardMember');
24+
if (!bordlyBoardMember) throw new Error('Board member context is required');
25+
26+
await BoardMemberService.populate(bordlyBoardMember, ['user.boardMembers']);
27+
const user = bordlyBoardMember.loadedUser as Loaded<User, 'boardMembers'>;
28+
29+
const boardCard = await BoardCardService.populate(initialBoardCard, ['boardCardReadPositions']);
30+
31+
const { boardCard: updatedBoardCard } = await CommentService.create(boardCard, {
32+
board: bordlyBoardMember.board,
33+
user,
34+
contentHtml: data.contentHtml,
35+
contentText: data.contentText,
36+
});
37+
38+
requestContext.set('boardCard', updatedBoardCard);
39+
return { success: true };
40+
},
41+
});

backend/src/tools/email-draft-upsert.tool.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@ export const emailDraftUpsertTool = createTool({
2828
const { requestContext } = context as { requestContext: RequestContext<Context> };
2929
const initialBoardCard = requestContext.get('boardCard');
3030
console.log(`[AGENT] Executing email-draft-upsert for board card ${initialBoardCard.id}: ${JSON.stringify(data)}`);
31-
const bordlyBoardMember = requestContext.get('bordlyBoardMember');
31+
const userBoardMember = requestContext.get('userBoardMember');
3232
const userTimeZone = requestContext.get('userTimeZone');
33-
if (!bordlyBoardMember) throw new Error('Board member context is required');
33+
if (!userBoardMember) throw new Error('Board member context is required');
3434

35-
await BoardMemberService.populate(bordlyBoardMember, ['user.boardMembers']);
36-
const user = bordlyBoardMember.loadedUser as Loaded<User, 'boardMembers'>;
35+
await BoardMemberService.populate(userBoardMember, ['user.boardMembers']);
36+
const user = userBoardMember.loadedUser as Loaded<User, 'boardMembers'>;
3737

3838
const boardCard = await BoardCardService.populate(initialBoardCard, [
3939
'emailDraft',

backend/src/trpc-routes/comment.routes.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,10 @@ export const COMMENT_ROUTES = {
7777
boardCardId: input.boardCardId,
7878
populate: BOARD_CARD_POPULATE,
7979
});
80-
const boardCard = await CommentService.delete(initialBoardCard, { commentId: input.commentId });
80+
const boardCard = await CommentService.delete(initialBoardCard, {
81+
user: ctx.user!,
82+
commentId: input.commentId,
83+
});
8184
return { boardCard: boardCardToJson(boardCard as typeof initialBoardCard, ctx) };
8285
}),
8386
} satisfies TRPCRouterRecord,

backend/src/utils/shared.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export interface Participant {
3939

4040
export const participantToString = (p: Participant) => (p.name ? `${p.name} <${p.email}>` : p.email);
4141

42-
export const isBordlyComment = (text: string) => text.trim().toLowerCase().startsWith('@bordly');
42+
export const isCommentForBordly = (text: string) => text.trim().toLowerCase().startsWith('@bordly');
4343

4444
export const createQuotedHtml = ({
4545
from,

frontend/src/components/board-card/comment-input.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useMutation } from '@tanstack/react-query';
2-
import { isBordlyComment } from 'bordly-backend/utils/shared';
2+
import { isCommentForBordly } from 'bordly-backend/utils/shared';
33
import { ArrowDown } from 'lucide-react';
44
import { useState } from 'react';
55
import { Button } from '@/components/ui/button';
@@ -40,7 +40,7 @@ export const CommentInput = ({
4040
queryKey: boardCardQueryKey,
4141
onExecute: (params) => {
4242
addFakeCommentData({ trpc, queryClient, params });
43-
if (isBordlyComment(params.contentText)) {
43+
if (isCommentForBordly(params.contentText)) {
4444
const bordlyUser = boardMembers.find((member) => member.isAgent);
4545
if (bordlyUser) {
4646
addBordlyThinkingComment({

frontend/src/components/board-card/mention-textarea.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,11 @@ export const MentionTextarea = ({
288288
};
289289

290290
export const renderCommentHtml = (html: string) => {
291-
// biome-ignore lint/security/noDangerouslySetInnerHtml: HTML is generated by Tiptap editor
292-
return <div className="whitespace-pre-wrap" dangerouslySetInnerHTML={{ __html: html }} />;
291+
return (
292+
<div
293+
className="[&_p]:mb-1 [&_p:last-child]:mb-0 [&_ul]:my-1 [&_ul]:list-disc [&_ul]:pl-6 [&_ol]:my-1 [&_ol]:list-decimal [&_ol]:pl-6 [&_li]:my-0 text-1.5"
294+
// biome-ignore lint/security/noDangerouslySetInnerHtml: HTML is generated by Tiptap editor
295+
dangerouslySetInnerHTML={{ __html: html }}
296+
/>
297+
);
293298
};

0 commit comments

Comments
 (0)