Skip to content

fix: prevent endless rAF loop in layout when widget is hidden#312183

Merged
joshspicer merged 2 commits intomainfrom
agents/fix-endless-loop-in-layout-function
Apr 23, 2026
Merged

fix: prevent endless rAF loop in layout when widget is hidden#312183
joshspicer merged 2 commits intomainfrom
agents/fix-endless-loop-in-layout-function

Conversation

@joshspicer
Copy link
Copy Markdown
Member

The layout() methods in mcpListWidget, pluginListWidget, and aiCustomizationListWidget defer to requestAnimationFrame when searchAndButtonContainer.offsetHeight is 0, assuming the browser hasn't reflowed yet after a display:none → visible transition.

When the widget is created while permanently hidden (e.g. in component explorer, or when the MCP servers view is created but not visible), offsetHeight stays 0 forever, causing an infinite rAF loop.

Fix

Add a _layoutDeferred flag so the deferral happens at most once. If offsetHeight is still 0 after the single retry, we proceed with zero heights instead of looping — the next real layout() call when the widget becomes visible will compute correct dimensions.

Copilot AI review requested due to automatic review settings April 23, 2026 17:36
The layout methods in mcpListWidget, pluginListWidget, and
aiCustomizationListWidget would defer to requestAnimationFrame when
searchAndButtonContainer.offsetHeight was 0, assuming the browser
hadn't reflowed yet. When the widget is created while permanently
hidden (e.g. in component explorer or when the view isn't visible),
offsetHeight stays 0 forever, causing an infinite rAF loop.

Add a _layoutDeferred flag so the deferral happens at most once. If
offsetHeight is still 0 after the retry, proceed with zero heights
instead of  the next real layout call when the widgetlooping
becomes visible will compute correct dimensions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@joshspicer joshspicer force-pushed the agents/fix-endless-loop-in-layout-function branch from 084ff82 to 6059ed4 Compare April 23, 2026 17:38
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a UI performance/behavior issue in the AI Customization-related list widgets where layout() could get stuck in an endless requestAnimationFrame retry loop when the widget is created while permanently hidden (so offsetHeight remains 0).

Changes:

  • Add a _layoutDeferred guard flag to layout() in three widgets.
  • Update the inline comments to explain the single-retry intent and the “permanently hidden” scenario.
Show a summary per file
File Description
src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts Adds _layoutDeferred gating around the rAF-based layout deferral.
src/vs/workbench/contrib/chat/browser/aiCustomization/mcpListWidget.ts Adds _layoutDeferred gating around the rAF-based layout deferral.
src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts Adds _layoutDeferred gating around the rAF-based layout deferral.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 3

if (searchBarHeight === 0 && !this._layoutDeferred) {
this._layoutDeferred = true;
DOM.getWindow(this.element).requestAnimationFrame(() => {
this._layoutDeferred = false;
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _layoutDeferred flag is reset to false inside the rAF callback before calling layout() again. If offsetHeight stays 0 (e.g. widget remains hidden), the next layout() call will see !this._layoutDeferred and schedule another rAF, so the endless loop still happens. Keep the flag set across the retried layout() call and only clear it after the second attempt has proceeded without deferring.

Suggested change
this._layoutDeferred = false;

Copilot uses AI. Check for mistakes.
if (searchBarHeight === 0 && !this._layoutDeferred) {
this._layoutDeferred = true;
DOM.getWindow(this.element).requestAnimationFrame(() => {
this._layoutDeferred = false;
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rAF retry resets _layoutDeferred to false before calling layout() again. If searchAndButtonContainer.offsetHeight remains 0, the next layout() invocation will defer again, so this does not actually cap retries to one and can still spin forever. Consider leaving _layoutDeferred set for the retried call and clearing it only after that call has proceeded without scheduling another rAF.

Suggested change
this._layoutDeferred = false;

Copilot uses AI. Check for mistakes.
Comment on lines +1508 to +1509
this._layoutDeferred = false;
this.layout(height, width);
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_layoutDeferred is cleared inside the rAF callback before calling layout(height, width) again. If the widget remains hidden and offsetHeight stays 0, the retried layout() call will see !this._layoutDeferred and schedule another rAF, so this can still loop indefinitely. Keep _layoutDeferred set across the retried call (and clear it only after the second attempt continues) so the deferral truly happens at most once.

Suggested change
this._layoutDeferred = false;
this.layout(height, width);
try {
this.layout(height, width);
} finally {
this._layoutDeferred = false;
}

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Copy Markdown
Contributor

Screenshot Changes

Base: d75dc0e5 Current: 45cc4325

Changed (34)

chat/aiCustomizations/aiCustomizationManagementEditor/LocalHarness/Dark
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/LocalHarness/Light
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/CliHarness/Light
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/CliHarness/Dark
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/Sessions/Dark
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/SessionsSkillsTab/Dark
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/McpServersTab/Dark
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/AgentsTab/Dark
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/SkillsTab/Dark
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/Sessions/Light
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/SessionsSkillsTab/Light
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/McpServersTab/Light
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/AgentsTab/Light
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/SkillsTab/Light
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/InstructionsTab/Light
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/InstructionsTab/Dark
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/HooksTab/Dark
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/PromptsTab/Dark
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/PromptsTabScrolled/Dark
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/HooksTab/Light
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/PromptsTab/Light
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/PromptsTabScrolled/Light
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/McpServersTabScrolled/Light
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/McpServersTabNarrow/Light
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/AgentsTabNarrow/Light
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/AgentsItemEditor/Light
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/McpServersTabScrolled/Dark
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/McpServersTabNarrow/Dark
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/AgentsTabNarrow/Dark
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/AgentsItemEditor/Dark
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/McpServerDetail/Dark
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/McpServerDetail/Light
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/PluginDetail/Light
Before After
before after
chat/aiCustomizations/aiCustomizationManagementEditor/PluginDetail/Dark
Before After
before after

Keep _layoutDeferred set while the deferred layout call runs so
a second offsetHeight === 0 measurement cannot schedule another
requestAnimationFrame. This makes the retry truly one-shot in all
three AI customization list widgets.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@joshspicer joshspicer merged commit 208150f into main Apr 23, 2026
26 checks passed
@joshspicer joshspicer deleted the agents/fix-endless-loop-in-layout-function branch April 23, 2026 20:25
@vs-code-engineering vs-code-engineering Bot added this to the 1.118.0 milestone Apr 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants