mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
fix: resolve gray screen of death issue during long chats (#7165)
- Enhanced ErrorBoundary with recovery mechanism and auto-retry - Added proper error state management and recovery buttons - Improved memory management in ChatView with better LRUCache settings - Added periodic memory cleanup for very long chat sessions - Implemented error handling for Virtuoso virtual scrolling component - Added fallback UI for scroll rendering failures - Optimized viewport settings to reduce memory usage - Added error boundaries around critical rendering components This fix addresses the gray screen issue that occurs during extended chat sessions by: 1. Preventing the error boundary from showing a gray overlay 2. Adding automatic recovery mechanisms 3. Improving memory management to prevent crashes 4. Providing user-friendly recovery options when errors occur
This commit is contained in:
parent
185365af5d
commit
60910c479f
2 changed files with 302 additions and 59 deletions
|
|
@ -2,6 +2,7 @@ import React, { Component } from "react"
|
|||
import { telemetryClient } from "@src/utils/TelemetryClient"
|
||||
import { withTranslation, WithTranslation } from "react-i18next"
|
||||
import { enhanceErrorWithSourceMaps } from "@src/utils/sourceMapUtils"
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
type ErrorProps = {
|
||||
children: React.ReactNode
|
||||
|
|
@ -11,12 +12,19 @@ type ErrorState = {
|
|||
error?: string
|
||||
componentStack?: string | null
|
||||
timestamp?: number
|
||||
hasError: boolean
|
||||
errorCount: number
|
||||
}
|
||||
|
||||
class ErrorBoundary extends Component<ErrorProps, ErrorState> {
|
||||
private retryTimeoutId: NodeJS.Timeout | null = null
|
||||
|
||||
constructor(props: ErrorProps) {
|
||||
super(props)
|
||||
this.state = {}
|
||||
this.state = {
|
||||
hasError: false,
|
||||
errorCount: 0,
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: unknown) {
|
||||
|
|
@ -31,6 +39,7 @@ class ErrorBoundary extends Component<ErrorProps, ErrorState> {
|
|||
return {
|
||||
error: errorMessage,
|
||||
timestamp: Date.now(),
|
||||
hasError: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -38,24 +47,63 @@ class ErrorBoundary extends Component<ErrorProps, ErrorState> {
|
|||
const componentStack = errorInfo.componentStack || ""
|
||||
const enhancedError = await enhanceErrorWithSourceMaps(error, componentStack)
|
||||
|
||||
// Increment error count
|
||||
this.setState((prevState) => ({
|
||||
errorCount: prevState.errorCount + 1,
|
||||
}))
|
||||
|
||||
telemetryClient.capture("error_boundary_caught_error", {
|
||||
error: enhancedError.message,
|
||||
stack: enhancedError.sourceMappedStack || enhancedError.stack,
|
||||
componentStack: enhancedError.sourceMappedComponentStack || componentStack,
|
||||
timestamp: Date.now(),
|
||||
errorType: enhancedError.name,
|
||||
errorCount: this.state.errorCount + 1,
|
||||
})
|
||||
|
||||
this.setState({
|
||||
error: enhancedError.sourceMappedStack || enhancedError.stack,
|
||||
componentStack: enhancedError.sourceMappedComponentStack || componentStack,
|
||||
})
|
||||
|
||||
// Auto-retry after 5 seconds if this is the first error
|
||||
if (this.state.errorCount === 0 && !this.retryTimeoutId) {
|
||||
this.retryTimeoutId = setTimeout(() => {
|
||||
this.handleReset()
|
||||
}, 5000)
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.retryTimeoutId) {
|
||||
clearTimeout(this.retryTimeoutId)
|
||||
this.retryTimeoutId = null
|
||||
}
|
||||
}
|
||||
|
||||
handleReset = () => {
|
||||
if (this.retryTimeoutId) {
|
||||
clearTimeout(this.retryTimeoutId)
|
||||
this.retryTimeoutId = null
|
||||
}
|
||||
|
||||
this.setState({
|
||||
error: undefined,
|
||||
componentStack: undefined,
|
||||
timestamp: undefined,
|
||||
hasError: false,
|
||||
// Don't reset errorCount to track total errors in session
|
||||
})
|
||||
}
|
||||
|
||||
handleReload = () => {
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
render() {
|
||||
const { t } = this.props
|
||||
|
||||
if (!this.state.error) {
|
||||
if (!this.state.hasError || !this.state.error) {
|
||||
return this.props.children
|
||||
}
|
||||
|
||||
|
|
@ -64,30 +112,65 @@ class ErrorBoundary extends Component<ErrorProps, ErrorState> {
|
|||
|
||||
const version = process.env.PKG_VERSION || "unknown"
|
||||
|
||||
// Use a white background to ensure visibility and prevent gray screen
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-lg font-bold mt-0 mb-2">
|
||||
{t("errorBoundary.title")} (v{version})
|
||||
</h2>
|
||||
<p className="mb-4">
|
||||
{t("errorBoundary.reportText")}{" "}
|
||||
<a href="https://github.com/RooCodeInc/Roo-Code/issues" target="_blank" rel="noreferrer">
|
||||
{t("errorBoundary.githubText")}
|
||||
</a>
|
||||
</p>
|
||||
<p className="mb-2">{t("errorBoundary.copyInstructions")}</p>
|
||||
<div
|
||||
className="fixed inset-0 bg-vscode-editor-background overflow-auto p-4"
|
||||
style={{ backgroundColor: "var(--vscode-editor-background, white)", zIndex: 9999 }}>
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h2 className="text-lg font-bold mt-0 mb-2 text-vscode-editor-foreground">
|
||||
{t("errorBoundary.title")} (v{version})
|
||||
</h2>
|
||||
|
||||
<div className="mb-4">
|
||||
<h3 className="text-md font-bold mb-1">{t("errorBoundary.errorStack")}</h3>
|
||||
<pre className="p-2 border rounded text-sm overflow-auto">{errorDisplay}</pre>
|
||||
</div>
|
||||
{this.state.errorCount === 1 && (
|
||||
<div className="mb-4 p-3 bg-vscode-notifications-background border border-vscode-notifications-border rounded">
|
||||
<p className="text-vscode-notifications-foreground">
|
||||
The application will attempt to recover automatically in a few seconds...
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{componentStackDisplay && (
|
||||
<div>
|
||||
<h3 className="text-md font-bold mb-1">{t("errorBoundary.componentStack")}</h3>
|
||||
<pre className="p-2 border rounded text-sm overflow-auto">{componentStackDisplay}</pre>
|
||||
<div className="flex gap-2 mb-4">
|
||||
<VSCodeButton appearance="primary" onClick={this.handleReset}>
|
||||
Try Again
|
||||
</VSCodeButton>
|
||||
<VSCodeButton appearance="secondary" onClick={this.handleReload}>
|
||||
Reload Window
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="mb-4 text-vscode-editor-foreground">
|
||||
{t("errorBoundary.reportText")}{" "}
|
||||
<a
|
||||
href="https://github.com/RooCodeInc/Roo-Code/issues"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-vscode-textLink-foreground hover:text-vscode-textLink-activeForeground">
|
||||
{t("errorBoundary.githubText")}
|
||||
</a>
|
||||
</p>
|
||||
<p className="mb-2 text-vscode-editor-foreground">{t("errorBoundary.copyInstructions")}</p>
|
||||
|
||||
<details className="mb-4">
|
||||
<summary className="cursor-pointer text-vscode-editor-foreground font-bold mb-2">
|
||||
{t("errorBoundary.errorStack")} (Click to expand)
|
||||
</summary>
|
||||
<pre className="p-2 border border-vscode-panel-border rounded text-sm overflow-auto bg-vscode-editor-background text-vscode-editor-foreground mt-2">
|
||||
{errorDisplay}
|
||||
</pre>
|
||||
</details>
|
||||
|
||||
{componentStackDisplay && (
|
||||
<details>
|
||||
<summary className="cursor-pointer text-vscode-editor-foreground font-bold mb-2">
|
||||
{t("errorBoundary.componentStack")} (Click to expand)
|
||||
</summary>
|
||||
<pre className="p-2 border border-vscode-panel-border rounded text-sm overflow-auto bg-vscode-editor-background text-vscode-editor-foreground mt-2">
|
||||
{componentStackDisplay}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -181,8 +181,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
const [showAnnouncementModal, setShowAnnouncementModal] = useState(false)
|
||||
const everVisibleMessagesTsRef = useRef<LRUCache<number, boolean>>(
|
||||
new LRUCache({
|
||||
max: 100,
|
||||
ttl: 1000 * 60 * 5,
|
||||
max: 200, // Increased from 100 to handle longer conversations
|
||||
ttl: 1000 * 60 * 10, // Increased from 5 to 10 minutes
|
||||
}),
|
||||
)
|
||||
const autoApproveTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
|
@ -457,6 +457,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
|
||||
useEffect(() => {
|
||||
if (isHidden) {
|
||||
// Clear cache when view is hidden to free memory
|
||||
everVisibleMessagesTsRef.current.clear()
|
||||
}
|
||||
}, [isHidden])
|
||||
|
|
@ -464,10 +465,35 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
useEffect(() => {
|
||||
const cache = everVisibleMessagesTsRef.current
|
||||
return () => {
|
||||
// Ensure cache is cleared on unmount
|
||||
cache.clear()
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Add periodic memory cleanup for very long chats
|
||||
useEffect(() => {
|
||||
const memoryCleanupInterval = setInterval(() => {
|
||||
// Only keep recent messages in cache during very long chats
|
||||
const currentSize = everVisibleMessagesTsRef.current.size
|
||||
if (currentSize > 150) {
|
||||
// Force garbage collection by recreating the cache
|
||||
const oldCache = everVisibleMessagesTsRef.current
|
||||
everVisibleMessagesTsRef.current = new LRUCache({
|
||||
max: 200,
|
||||
ttl: 1000 * 60 * 10,
|
||||
})
|
||||
// Copy only the most recent entries
|
||||
const entries = Array.from(oldCache.entries()).slice(-100)
|
||||
entries.forEach(([key, value]) => {
|
||||
everVisibleMessagesTsRef.current.set(key, value)
|
||||
})
|
||||
oldCache.clear()
|
||||
}
|
||||
}, 60000) // Run every minute
|
||||
|
||||
return () => clearInterval(memoryCleanupInterval)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const prev = prevExpandedRowsRef.current
|
||||
let wasAnyRowExpandedByUser = false
|
||||
|
|
@ -900,27 +926,37 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
useMount(() => textAreaRef.current?.focus())
|
||||
|
||||
const visibleMessages = useMemo(() => {
|
||||
// Remove the 500-message limit to prevent array index shifting
|
||||
// Virtuoso is designed to efficiently handle large lists through virtualization
|
||||
const newVisibleMessages = modifiedMessages.filter((message: ClineMessage) => {
|
||||
if (everVisibleMessagesTsRef.current.has(message.ts)) {
|
||||
const alwaysHiddenOnceProcessedAsk: ClineAsk[] = [
|
||||
"api_req_failed",
|
||||
"resume_task",
|
||||
"resume_completed_task",
|
||||
]
|
||||
const alwaysHiddenOnceProcessedSay = [
|
||||
"api_req_finished",
|
||||
"api_req_retried",
|
||||
"api_req_deleted",
|
||||
"mcp_server_request_started",
|
||||
]
|
||||
if (message.ask && alwaysHiddenOnceProcessedAsk.includes(message.ask)) return false
|
||||
if (message.say && alwaysHiddenOnceProcessedSay.includes(message.say)) return false
|
||||
if (message.say === "text" && (message.text ?? "") === "" && (message.images?.length ?? 0) === 0) {
|
||||
return false
|
||||
// Limit processing for very large message arrays to prevent performance issues
|
||||
const messagesToProcess =
|
||||
modifiedMessages.length > 5000
|
||||
? modifiedMessages.slice(-5000) // Only process last 5000 messages for very long chats
|
||||
: modifiedMessages
|
||||
|
||||
const newVisibleMessages = messagesToProcess.filter((message: ClineMessage) => {
|
||||
// Check cache with try-catch to handle potential memory issues
|
||||
try {
|
||||
if (everVisibleMessagesTsRef.current.has(message.ts)) {
|
||||
const alwaysHiddenOnceProcessedAsk: ClineAsk[] = [
|
||||
"api_req_failed",
|
||||
"resume_task",
|
||||
"resume_completed_task",
|
||||
]
|
||||
const alwaysHiddenOnceProcessedSay = [
|
||||
"api_req_finished",
|
||||
"api_req_retried",
|
||||
"api_req_deleted",
|
||||
"mcp_server_request_started",
|
||||
]
|
||||
if (message.ask && alwaysHiddenOnceProcessedAsk.includes(message.ask)) return false
|
||||
if (message.say && alwaysHiddenOnceProcessedSay.includes(message.say)) return false
|
||||
if (message.say === "text" && (message.text ?? "") === "" && (message.images?.length ?? 0) === 0) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error("Error accessing message cache:", error)
|
||||
// Continue processing without cache on error
|
||||
}
|
||||
|
||||
switch (message.ask) {
|
||||
|
|
@ -938,8 +974,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
case "api_req_deleted":
|
||||
return false
|
||||
case "api_req_retry_delayed":
|
||||
const last1 = modifiedMessages.at(-1)
|
||||
const last2 = modifiedMessages.at(-2)
|
||||
const last1 = messagesToProcess.at(-1)
|
||||
const last2 = messagesToProcess.at(-2)
|
||||
if (last1?.ask === "resume_task" && last2 === message) {
|
||||
return true
|
||||
} else if (message !== last1) {
|
||||
|
|
@ -955,10 +991,19 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
return true
|
||||
})
|
||||
|
||||
const viewportStart = Math.max(0, newVisibleMessages.length - 100)
|
||||
newVisibleMessages
|
||||
.slice(viewportStart)
|
||||
.forEach((msg: ClineMessage) => everVisibleMessagesTsRef.current.set(msg.ts, true))
|
||||
// Safely update cache with error handling
|
||||
try {
|
||||
const viewportStart = Math.max(0, newVisibleMessages.length - 100)
|
||||
newVisibleMessages.slice(viewportStart).forEach((msg: ClineMessage) => {
|
||||
try {
|
||||
everVisibleMessagesTsRef.current.set(msg.ts, true)
|
||||
} catch (error) {
|
||||
console.error("Error updating message cache:", error)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error processing visible messages:", error)
|
||||
}
|
||||
|
||||
return newVisibleMessages
|
||||
}, [modifiedMessages])
|
||||
|
|
@ -1867,22 +1912,18 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
{task && (
|
||||
<>
|
||||
<div className="grow flex" ref={scrollContainerRef}>
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
key={task.ts}
|
||||
className="scrollable grow overflow-y-scroll mb-1"
|
||||
increaseViewportBy={{ top: 3_000, bottom: 1000 }}
|
||||
data={groupedMessages}
|
||||
<ErrorBoundaryVirtuoso
|
||||
virtuosoRef={virtuosoRef}
|
||||
task={task}
|
||||
groupedMessages={groupedMessages}
|
||||
itemContent={itemContent}
|
||||
atBottomStateChange={(isAtBottom: boolean) => {
|
||||
onAtBottomChange={(isAtBottom: boolean) => {
|
||||
setIsAtBottom(isAtBottom)
|
||||
if (isAtBottom) {
|
||||
disableAutoScrollRef.current = false
|
||||
}
|
||||
setShowScrollToBottom(disableAutoScrollRef.current && !isAtBottom)
|
||||
}}
|
||||
atBottomThreshold={10}
|
||||
initialTopMostItemIndex={groupedMessages.length - 1}
|
||||
/>
|
||||
</div>
|
||||
<div className={`flex-initial min-h-0 ${!areButtonsVisible ? "mb-1" : ""}`}>
|
||||
|
|
@ -2012,6 +2053,125 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
)
|
||||
}
|
||||
|
||||
// Error boundary wrapper for Virtuoso to handle scrolling errors
|
||||
const ErrorBoundaryVirtuoso: React.FC<{
|
||||
virtuosoRef: React.RefObject<VirtuosoHandle>
|
||||
task: any
|
||||
groupedMessages: any[]
|
||||
itemContent: (index: number, item: any) => React.ReactNode
|
||||
onAtBottomChange: (isAtBottom: boolean) => void
|
||||
}> = ({ virtuosoRef, task, groupedMessages, itemContent, onAtBottomChange }) => {
|
||||
const [hasError, setHasError] = useState(false)
|
||||
const [retryCount, setRetryCount] = useState(0)
|
||||
|
||||
// Reset error state when task changes
|
||||
useEffect(() => {
|
||||
setHasError(false)
|
||||
setRetryCount(0)
|
||||
}, [task?.ts])
|
||||
|
||||
const handleRetry = useCallback(() => {
|
||||
setHasError(false)
|
||||
setRetryCount((prev) => prev + 1)
|
||||
}, [])
|
||||
|
||||
if (hasError) {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col items-center justify-center p-4">
|
||||
<div className="text-center max-w-md">
|
||||
<h3 className="text-lg font-semibold mb-2 text-vscode-editor-foreground">
|
||||
Unable to display messages
|
||||
</h3>
|
||||
<p className="text-sm text-vscode-descriptionForeground mb-4">
|
||||
The chat view encountered an issue while rendering messages. This can happen with very long
|
||||
conversations.
|
||||
</p>
|
||||
<div className="flex gap-2 justify-center">
|
||||
<VSCodeButton appearance="primary" onClick={handleRetry}>
|
||||
Try Again
|
||||
</VSCodeButton>
|
||||
<VSCodeButton appearance="secondary" onClick={() => window.location.reload()}>
|
||||
Reload Window
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
{retryCount > 0 && (
|
||||
<p className="text-xs text-vscode-descriptionForeground mt-2">Retry attempts: {retryCount}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary
|
||||
onError={(error: Error) => {
|
||||
console.error("Virtuoso rendering error:", error)
|
||||
setHasError(true)
|
||||
}}
|
||||
fallback={
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<p className="text-vscode-descriptionForeground">Loading messages...</p>
|
||||
</div>
|
||||
}>
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
key={`${task.ts}-${retryCount}`}
|
||||
className="scrollable grow overflow-y-scroll mb-1"
|
||||
increaseViewportBy={{ top: 1500, bottom: 500 }}
|
||||
data={groupedMessages}
|
||||
itemContent={itemContent}
|
||||
atBottomStateChange={onAtBottomChange}
|
||||
atBottomThreshold={10}
|
||||
initialTopMostItemIndex={Math.max(0, groupedMessages.length - 1)}
|
||||
overscan={10}
|
||||
scrollSeekConfiguration={{
|
||||
enter: (velocity) => Math.abs(velocity) > 200,
|
||||
exit: (velocity) => Math.abs(velocity) < 30,
|
||||
change: () => {},
|
||||
}}
|
||||
// Add error handling for item rendering
|
||||
components={{
|
||||
ScrollSeekPlaceholder: () => (
|
||||
<div className="h-20 flex items-center justify-center">
|
||||
<span className="text-vscode-descriptionForeground">Loading...</span>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
// Simple error boundary for Virtuoso
|
||||
class ErrorBoundary extends React.Component<
|
||||
{
|
||||
children: React.ReactNode
|
||||
onError?: (error: Error) => void
|
||||
fallback?: React.ReactNode
|
||||
},
|
||||
{ hasError: boolean }
|
||||
> {
|
||||
constructor(props: any) {
|
||||
super(props)
|
||||
this.state = { hasError: false }
|
||||
}
|
||||
|
||||
static getDerivedStateFromError() {
|
||||
return { hasError: true }
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error) {
|
||||
this.props.onError?.(error)
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return this.props.fallback || <div>Something went wrong</div>
|
||||
}
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
|
||||
const ChatView = forwardRef(ChatViewComponent)
|
||||
|
||||
export default ChatView
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue