Repository navigation
Description
Context
The chat webview turns gray (crashes) during long-running tasks. User-reported diagnostics from logWebviewHiddenDiagnostics confirm 3,525 messages at crash time. This is tracked upstream as a recurring user complaint — PR #153 reduced the react-virtuoso pre-render buffer but did not address the root cause.
Related: Part of the broader webview message architecture addressed by #355 (Stories 3.2c + 3.2d), but scoped as an independent fix that doesn't touch the message protocol or task lifecycle.
Developer Notes
Problem
addToClineMessages (Task.ts:1015) calls postStateToWebviewWithoutTaskHistory() on every single message addition, serializing and posting the entire clineMessages array to the webview. With 3,525 messages averaging ~2KB of tool output each, every state push is ~7MB. During active tool use with checkpoint saves, this fires dozens of times per minute. The webview must deserialize, merge into React state, and re-render each time — eventually exceeding the V8 heap limit and crashing the renderer (gray screen).
Secondary issue: aggregatedCostsMap in ChatView.tsx is never cleared on task change (line 480-491 cleanup effect), causing unbounded growth across task switches with subtasks.
Proposed Fix
Throttle the hot-path state pushes — no new message types, no protocol changes.
ClineProvider.ts — add throttled variant (~10 lines)
private _postStateToWebviewThrottled = debounce(
() => this.postStateToWebviewWithoutTaskHistory(),
500,
{ leading: true, trailing: true, maxWait: 1000 }
)
async postStateToWebviewThrottled(): Promise<void> {
this._postStateToWebviewThrottled()
}Task.ts — swap 2 hot-path call sites (2 line changes)
| Line | Context | Change |
|---|---|---|
| 1015 | addToClineMessages — every new message |
→ postStateToWebviewThrottled |
| 527 | messageQueueService.stateChanged |
→ postStateToWebviewThrottled |
| 1828 | Task start (clear state) | Keep immediate |
| 2551 | Before API request | Keep immediate |
| 3291 | After streaming complete | Keep immediate |
Partial message streaming is unaffected — updateClineMessage (Task.ts:1048) already uses the lightweight { type: "messageUpdated" } path, bypassing the full state push entirely.
ChatView.tsx:481 — clear aggregatedCostsMap on task change (1 line)
setAggregatedCostsMap(new Map())Task.abortTask — flush debounce on abort (1 line)
Ensures the final state is delivered when a task is stopped.
Impact
At 3,525 messages:
- Before: ~7MB × dozens/sec during rapid tool use → OOM → gray screen
- After: ~7MB × max 2/sec (throttled) → webview stays alive
Risk Assessment
- Low risk: No new message types, no protocol changes, no task lifecycle changes
- UI delay: New message bubbles may appear up to 500ms late; streaming text is unaffected (uses
messageUpdatedpath) - Final state guaranteed:
trailing: true+ abort flush ensures last state is always delivered - No fix(task-lifecycle): preserve parent-child link when delegated subtask is interrupted #560-style edge cases: This doesn't add states, transitions, or concurrency primitives — it just coalesces rapid calls to an existing method
Acceptance Criteria
- Chat remains responsive at 3,500+ messages (no gray screen)
- Streaming text still appears in real-time (messageUpdated path unchanged)
- Task start shows clean state immediately (line 1828 not throttled)
- Task abort delivers final state (debounce flushed)
-
aggregatedCostsMapis empty after task switch - Existing test suite passes unchanged
Activity
JunyongParkDev commentedon Jul 31, 2026
Claiming