diff --git a/src/renderer/components/team/dialogs/TaskCommentsSection.tsx b/src/renderer/components/team/dialogs/TaskCommentsSection.tsx index 22e69cd5..426ec636 100644 --- a/src/renderer/components/team/dialogs/TaskCommentsSection.tsx +++ b/src/renderer/components/team/dialogs/TaskCommentsSection.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer'; import { ReplyQuoteBlock } from '@renderer/components/team/activity/ReplyQuoteBlock'; @@ -20,6 +20,9 @@ import type { MentionSuggestion } from '@renderer/types/mention'; import type { ResolvedTeamMember, TaskComment } from '@shared/types'; const MAX_COMMENT_LENGTH = 2000; +const INITIAL_VISIBLE_COMMENTS = 30; +const VISIBLE_COMMENTS_STEP = 50; +const MAX_COMMENTS_TO_RENDER = 2000; interface TaskCommentsSectionProps { teamName: string; @@ -49,6 +52,13 @@ export const TaskCommentsSection = ({ const [replyTo, setReplyTo] = useState<{ author: string; text: string } | null>(null); const [expandedCommentIds, setExpandedCommentIds] = useState>(new Set()); + const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COMMENTS); + + useEffect(() => { + setVisibleCount(INITIAL_VISIBLE_COMMENTS); + setExpandedCommentIds(new Set()); + setReplyTo(null); + }, [teamIdKey(teamName, taskId)]); const toggleCommentExpanded = useCallback((commentId: string) => { setExpandedCommentIds((prev) => { @@ -62,6 +72,24 @@ export const TaskCommentsSection = ({ const draft = useDraftPersistence({ key: `taskComment:${teamName}:${taskId}` }); const colorMap = useMemo(() => buildMemberColorMap(members), [members]); + const cappedComments = useMemo(() => { + if (comments.length <= MAX_COMMENTS_TO_RENDER) return comments; + // In extreme cases, rendering thousands of markdown blocks can freeze the renderer. + // Keep the UI responsive by showing only the most recent subset. + return comments.slice(-MAX_COMMENTS_TO_RENDER); + }, [comments]); + + const sortedComments = useMemo(() => { + const list = [...cappedComments]; + list.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); + return list; + }, [cappedComments]); + + const visibleComments = useMemo( + () => sortedComments.slice(0, Math.min(visibleCount, sortedComments.length)), + [sortedComments, visibleCount] + ); + const mentionSuggestions = useMemo( () => members.map((m) => ({ @@ -105,130 +133,150 @@ export const TaskCommentsSection = ({ {comments.length > 0 ? (
- {[...comments] - .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) - .map((comment) => ( -
-
- { - const rc = colorMap.get(comment.author); - return rc ? getTeamColorSet(rc).text : 'var(--color-text-secondary)'; - })(), - }} - > - {comment.author} - - - {(() => { - const date = new Date(comment.createdAt); - return isNaN(date.getTime()) - ? 'unknown time' - : formatDistanceToNow(date, { addSuffix: true }); - })()} - - - - - - Reply to comment - -
- {(() => { - const reply = parseMessageReply(comment.text); - const rawForDisplay = reply ? reply.replyText : comment.text; - const displayText = stripAgentBlocks(rawForDisplay); - const needsExpandCollapse = displayText.includes('\n'); - const expanded = expandedCommentIds.has(comment.id); - const collapsedHeight = 'max-h-[120px]'; - const showCollapsed = needsExpandCollapse && !expanded; - const showExpandedButton = needsExpandCollapse && expanded; - return ( -
-
MAX_COMMENTS_TO_RENDER ? ( +
+ Showing the most recent {MAX_COMMENTS_TO_RENDER.toLocaleString()} comments to keep the + UI responsive. +
+ ) : null} + + {visibleComments.map((comment) => ( +
+
+ { + const rc = colorMap.get(comment.author); + return rc ? getTeamColorSet(rc).text : 'var(--color-text-secondary)'; + })(), + }} + > + {comment.author} + + + {(() => { + const date = new Date(comment.createdAt); + return isNaN(date.getTime()) + ? 'unknown time' + : formatDistanceToNow(date, { addSuffix: true }); + })()} + + + + + + Reply to comment + +
+ {(() => { + const reply = parseMessageReply(comment.text); + const rawForDisplay = reply ? reply.replyText : comment.text; + const displayText = stripAgentBlocks(rawForDisplay); + const needsExpandCollapse = displayText.includes('\n'); + const expanded = expandedCommentIds.has(comment.id); + const collapsedHeight = 'max-h-[120px]'; + const showCollapsed = needsExpandCollapse && !expanded; + const showExpandedButton = needsExpandCollapse && expanded; + return ( +
+
+ {reply ? ( + + ) : ( + + )} + {showCollapsed && ( + <> +
- ) : ( - - )} - {showCollapsed && ( - <> -
-
- -
- - )} -
- {showExpandedButton && ( -
- -
+
+ +
+ )}
- ); - })()} -
- ))} + {showExpandedButton && ( +
+ +
+ )} +
+ ); + })()} +
+ ))} + + {sortedComments.length > visibleComments.length ? ( +
+ +
+ ) : null}
) : null} @@ -313,3 +361,7 @@ export const TaskCommentsSection = ({
); }; + +function teamIdKey(teamName: string, taskId: string): string { + return `${teamName}::${taskId}`; +}