mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
feat: add webview heartbeat monitoring for gray screen detection and recovery
Implements a ping-pong heartbeat system to detect when the webview becomes unresponsive (gray screen issue) and provide auto-recovery options. Changes: - Add ping/pong message types to ExtensionMessage and WebviewMessage - Implement heartbeat sender and monitoring in ClineProvider - Add automatic detection of unresponsive webviews after 3 missed heartbeats - Show notification with option to reload webview on detection - Handle pong messages in webviewMessageHandler - Respond to ping messages in webview ExtensionStateContext Fixes #10737
This commit is contained in:
parent
dbf206f06b
commit
122d28f2e0
4 changed files with 149 additions and 0 deletions
|
|
@ -94,6 +94,7 @@ export interface ExtensionMessage {
|
|||
| "claudeCodeRateLimits"
|
||||
| "customToolsResult"
|
||||
| "modes"
|
||||
| "ping"
|
||||
text?: string
|
||||
payload?: any // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
checkpointWarning?: {
|
||||
|
|
@ -509,6 +510,7 @@ export interface WebviewMessage {
|
|||
| "requestModes"
|
||||
| "switchMode"
|
||||
| "debugSetting"
|
||||
| "pong"
|
||||
text?: string
|
||||
editedMessageContent?: string
|
||||
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
|
||||
|
|
|
|||
|
|
@ -147,6 +147,14 @@ export class ClineProvider
|
|||
private taskEventListeners: WeakMap<Task, Array<() => void>> = new WeakMap()
|
||||
private currentWorkspacePath: string | undefined
|
||||
|
||||
// Webview heartbeat monitoring for detecting unresponsive webviews
|
||||
private heartbeatInterval?: ReturnType<typeof setInterval>
|
||||
private lastPongTimestamp: number = 0
|
||||
private heartbeatMissCount: number = 0
|
||||
private static readonly HEARTBEAT_INTERVAL_MS = 5000 // Send ping every 5 seconds
|
||||
private static readonly HEARTBEAT_TIMEOUT_MS = 15000 // Consider unresponsive after 15 seconds (3 missed pings)
|
||||
private static readonly MAX_HEARTBEAT_MISSES = 3 // Number of missed heartbeats before showing recovery option
|
||||
|
||||
private recentTasksCache?: string[]
|
||||
private pendingOperations: Map<string, PendingEditOperation> = new Map()
|
||||
private static readonly PENDING_OPERATION_TIMEOUT_MS = 30000 // 30 seconds
|
||||
|
|
@ -578,6 +586,9 @@ export class ClineProvider
|
|||
async dispose() {
|
||||
this.log("Disposing ClineProvider...")
|
||||
|
||||
// Stop heartbeat monitoring
|
||||
this.stopHeartbeatMonitoring()
|
||||
|
||||
// Clear all tasks from the stack.
|
||||
while (this.clineStack.length > 0) {
|
||||
await this.removeClineFromStack()
|
||||
|
|
@ -630,6 +641,131 @@ export class ClineProvider
|
|||
return findLast(Array.from(this.activeInstances), (instance) => instance.view?.visible === true)
|
||||
}
|
||||
|
||||
// Webview Heartbeat Monitoring Methods
|
||||
|
||||
/**
|
||||
* Starts heartbeat monitoring for the webview.
|
||||
* Sends periodic ping messages to the webview and monitors for pong responses.
|
||||
* If the webview becomes unresponsive (no pong received), offers recovery options.
|
||||
*/
|
||||
private startHeartbeatMonitoring(): void {
|
||||
// Clear any existing heartbeat interval
|
||||
this.stopHeartbeatMonitoring()
|
||||
|
||||
// Initialize heartbeat state
|
||||
this.lastPongTimestamp = Date.now()
|
||||
this.heartbeatMissCount = 0
|
||||
|
||||
this.heartbeatInterval = setInterval(() => {
|
||||
// Only monitor when the webview is visible
|
||||
if (!this.view?.visible) {
|
||||
// Reset miss count when not visible - the webview might be hidden
|
||||
this.heartbeatMissCount = 0
|
||||
return
|
||||
}
|
||||
|
||||
// Send ping to webview
|
||||
this.postMessageToWebview({ type: "ping" })
|
||||
|
||||
// Check if we've received a pong recently
|
||||
const timeSinceLastPong = Date.now() - this.lastPongTimestamp
|
||||
|
||||
if (timeSinceLastPong > ClineProvider.HEARTBEAT_TIMEOUT_MS) {
|
||||
this.heartbeatMissCount++
|
||||
this.log(
|
||||
`[Heartbeat] Webview unresponsive - no pong received for ${Math.round(timeSinceLastPong / 1000)}s (miss count: ${this.heartbeatMissCount})`,
|
||||
)
|
||||
|
||||
// After MAX_HEARTBEAT_MISSES, offer recovery option
|
||||
if (this.heartbeatMissCount >= ClineProvider.MAX_HEARTBEAT_MISSES) {
|
||||
this.handleWebviewUnresponsive()
|
||||
}
|
||||
}
|
||||
}, ClineProvider.HEARTBEAT_INTERVAL_MS)
|
||||
|
||||
this.log("[Heartbeat] Monitoring started")
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops heartbeat monitoring for the webview.
|
||||
*/
|
||||
private stopHeartbeatMonitoring(): void {
|
||||
if (this.heartbeatInterval) {
|
||||
clearInterval(this.heartbeatInterval)
|
||||
this.heartbeatInterval = undefined
|
||||
this.log("[Heartbeat] Monitoring stopped")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles pong response from the webview, indicating it's still responsive.
|
||||
* Called by webviewMessageHandler when a "pong" message is received.
|
||||
*/
|
||||
public handlePong(): void {
|
||||
this.lastPongTimestamp = Date.now()
|
||||
this.heartbeatMissCount = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the case when the webview becomes unresponsive.
|
||||
* Shows a notification with options to reload the webview.
|
||||
*/
|
||||
private handleWebviewUnresponsive(): void {
|
||||
// Stop sending more pings while handling the unresponsive state
|
||||
this.stopHeartbeatMonitoring()
|
||||
|
||||
this.log("[Heartbeat] Webview detected as unresponsive, showing recovery notification")
|
||||
|
||||
// Show notification with reload option
|
||||
vscode.window
|
||||
.showWarningMessage(
|
||||
"Roo Code panel has become unresponsive. This can happen during long-running tasks with auto-approval enabled.",
|
||||
"Reload Panel",
|
||||
"Dismiss",
|
||||
)
|
||||
.then((selection) => {
|
||||
if (selection === "Reload Panel") {
|
||||
this.reloadWebview()
|
||||
} else {
|
||||
// If dismissed, restart monitoring in case the user wants to continue
|
||||
this.startHeartbeatMonitoring()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the webview to recover from an unresponsive state.
|
||||
* This preserves the current task and state.
|
||||
*/
|
||||
public async reloadWebview(): Promise<void> {
|
||||
this.log("[Heartbeat] Reloading webview...")
|
||||
|
||||
if (!this.view?.webview) {
|
||||
this.log("[Heartbeat] Cannot reload - no webview available")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Regenerate and set the HTML content
|
||||
this.view.webview.html =
|
||||
this.contextProxy.extensionMode === vscode.ExtensionMode.Development
|
||||
? await this.getHMRHtmlContent(this.view.webview)
|
||||
: await this.getHtmlContent(this.view.webview)
|
||||
|
||||
// The webview will re-initialize and request state via "webviewDidLaunch"
|
||||
// which will trigger postStateToWebview() and restore the UI
|
||||
|
||||
// Restart heartbeat monitoring after reload
|
||||
this.startHeartbeatMonitoring()
|
||||
|
||||
this.log("[Heartbeat] Webview reloaded successfully")
|
||||
vscode.window.showInformationMessage("Roo Code panel has been reloaded.")
|
||||
} catch (error) {
|
||||
this.log(`[Heartbeat] Failed to reload webview: ${error instanceof Error ? error.message : String(error)}`)
|
||||
vscode.window.showErrorMessage("Failed to reload Roo Code panel. Please try reopening the panel manually.")
|
||||
}
|
||||
}
|
||||
|
||||
public static async getInstance(): Promise<ClineProvider | undefined> {
|
||||
let visibleProvider = ClineProvider.getVisibleInstance()
|
||||
|
||||
|
|
@ -861,6 +997,9 @@ export class ClineProvider
|
|||
|
||||
// If the extension is starting a new session, clear previous task state.
|
||||
await this.removeClineFromStack()
|
||||
|
||||
// Start heartbeat monitoring to detect unresponsive webviews
|
||||
this.startHeartbeatMonitoring()
|
||||
}
|
||||
|
||||
public async createTaskWithHistoryItem(
|
||||
|
|
|
|||
|
|
@ -447,6 +447,10 @@ export const webviewMessageHandler = async (
|
|||
}
|
||||
|
||||
switch (message.type) {
|
||||
case "pong":
|
||||
// Heartbeat response from webview - delegate to provider
|
||||
provider.handlePong()
|
||||
break
|
||||
case "webviewDidLaunch":
|
||||
// Load custom modes first
|
||||
const customModes = await provider.customModesManager.getCustomModes()
|
||||
|
|
|
|||
|
|
@ -321,6 +321,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
(event: MessageEvent) => {
|
||||
const message: ExtensionMessage = event.data
|
||||
switch (message.type) {
|
||||
case "ping":
|
||||
// Respond to heartbeat ping immediately
|
||||
vscode.postMessage({ type: "pong" })
|
||||
break
|
||||
case "state": {
|
||||
const newState = message.state!
|
||||
setState((prevState) => mergeExtensionState(prevState, newState))
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue