From c528b07fecde17aee8c19fcdd9ca3ad1592ed652 Mon Sep 17 00:00:00 2001 From: iliya Date: Sat, 28 Feb 2026 22:48:08 +0200 Subject: [PATCH] feat: enhance MarkdownViewer and task dialogs with loading state management and performance logging - Introduced character limits for Markdown content in MarkdownViewer to prevent UI freezes with large content. - Added state management for raw content display in MarkdownViewer, allowing users to expand and view large markdown files. - Implemented performance logging in GlobalTaskDetailDialog and TaskDetailDialog to track loading states and improve debugging. - Updated loading state handling in TaskDetailDialog to ensure accurate representation of loading conditions. --- package.json | 1 - .../chat/viewers/MarkdownViewer.tsx | 130 ++++++++++++++++++ .../team/dialogs/GlobalTaskDetailDialog.tsx | 20 ++- .../team/dialogs/TaskDetailDialog.tsx | 8 ++ 4 files changed, 157 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 5c0679b6..cdc24670 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,6 @@ }, "lint-staged": { "src/**/*.{ts,tsx,js,jsx}": [ - "eslint --fix", "prettier --write" ], "src/**/*.{json,css}": [ diff --git a/src/renderer/components/chat/viewers/MarkdownViewer.tsx b/src/renderer/components/chat/viewers/MarkdownViewer.tsx index 17936550..83ec765d 100644 --- a/src/renderer/components/chat/viewers/MarkdownViewer.tsx +++ b/src/renderer/components/chat/viewers/MarkdownViewer.tsx @@ -268,6 +268,9 @@ function createViewerMarkdownComponents(searchCtx: SearchContext | null): Compon /** Default components without search highlighting */ const defaultComponents = createViewerMarkdownComponents(null); +const MAX_MARKDOWN_CHARS = 200_000; +const LARGE_PREVIEW_CHARS = 50_000; + // ============================================================================= // Component // ============================================================================= @@ -281,6 +284,11 @@ export const MarkdownViewer: React.FC = ({ copyable = false, bare = false, }) => { + const [showRaw, setShowRaw] = React.useState(false); + const [rawLimit, setRawLimit] = React.useState(LARGE_PREVIEW_CHARS); + + const isTooLarge = content.length > MAX_MARKDOWN_CHARS; + // Only subscribe to search store when itemId is provided const { searchQuery, searchMatches, currentSearchIndex } = useStore( useShallow((s) => ({ @@ -290,6 +298,128 @@ export const MarkdownViewer: React.FC = ({ })) ); + // Guard: very large markdown can freeze the renderer (remark/rehype + highlighting). + // For large content, default to a lightweight raw preview with manual expansion. + if (isTooLarge || showRaw) { + const shown = content.slice(0, Math.min(rawLimit, content.length)); + const isTruncated = shown.length < content.length; + return ( +
+ {copyable && !label && ( + + )} + + {label && ( +
+ + + {label} + + + Raw + + + + {copyable && } +
+ )} + + {!label && ( +
+ Raw preview + +
+ )} + + {isTooLarge && ( +
+ Content is very large ({content.length.toLocaleString()} chars). Showing raw preview to + keep the UI responsive. +
+ )} + +
+
+            {shown}
+          
+ {isTruncated && ( +
+ + Showing {shown.length.toLocaleString()} / {content.length.toLocaleString()} chars + +
+ + +
+
+ )} +
+
+ ); + } + // Create search context (fresh each render so counter starts at 0) const searchCtx = searchQuery && itemId diff --git a/src/renderer/components/team/dialogs/GlobalTaskDetailDialog.tsx b/src/renderer/components/team/dialogs/GlobalTaskDetailDialog.tsx index 21b9ac9e..afdf97f4 100644 --- a/src/renderer/components/team/dialogs/GlobalTaskDetailDialog.tsx +++ b/src/renderer/components/team/dialogs/GlobalTaskDetailDialog.tsx @@ -65,6 +65,24 @@ export const GlobalTaskDetailDialog = (): React.JSX.Element | null => { ]); const isFullTeamLoaded = selectedTeamName === teamName && !!selectedTeamData; + const isThisTeamLoading = + selectedTeamName === teamName && selectedTeamLoading && !selectedTeamData; + + useEffect(() => { + if (!globalTaskDetail) return; + console.warn( + `[GlobalTaskDetailDialog] team=${teamName} taskId=${taskId} selectedTeamName=${selectedTeamName ?? ''} loading=${selectedTeamLoading} hasData=${!!selectedTeamData} isFull=${isFullTeamLoaded} isThisTeamLoading=${isThisTeamLoading}` + ); + }, [ + globalTaskDetail, + isFullTeamLoaded, + isThisTeamLoading, + selectedTeamData, + selectedTeamLoading, + selectedTeamName, + taskId, + teamName, + ]); const taskMap = useMemo(() => { const map = new Map(); @@ -111,7 +129,7 @@ export const GlobalTaskDetailDialog = (): React.JSX.Element | null => { { + if (!open) return; + if (!loading) return; + console.warn( + `[TaskDetailDialog] loading=true variant=${variant} team=${teamName} taskId=${task?.id ?? ''}` + ); + }, [loading, open, task?.id, teamName, variant]); + if (loading) { return ( !v && onClose()}>